openclaw-vainplex/src/agents/tools/message-tool.ts
2026-01-13 01:19:14 +00:00

218 lines
7.2 KiB
TypeScript

import { Type } from "@sinclair/typebox";
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import {
GATEWAY_CLIENT_IDS,
GATEWAY_CLIENT_MODES,
} from "../../gateway/protocol/client-info.js";
import { runMessageAction } from "../../infra/outbound/message-action-runner.js";
import {
listProviderMessageActions,
supportsProviderMessageButtons,
} from "../../providers/plugins/message-actions.js";
import {
PROVIDER_MESSAGE_ACTION_NAMES,
type ProviderMessageActionName,
} from "../../providers/plugins/types.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
const AllMessageActions = PROVIDER_MESSAGE_ACTION_NAMES;
const MessageToolCommonSchema = {
provider: Type.Optional(Type.String()),
to: Type.Optional(Type.String()),
message: Type.Optional(Type.String()),
media: Type.Optional(Type.String()),
buttons: Type.Optional(
Type.Array(
Type.Array(
Type.Object({
text: Type.String(),
callback_data: Type.String(),
}),
),
{
description: "Telegram inline keyboard buttons (array of button rows)",
},
),
),
messageId: Type.Optional(Type.String()),
replyTo: Type.Optional(Type.String()),
threadId: Type.Optional(Type.String()),
accountId: Type.Optional(Type.String()),
dryRun: Type.Optional(Type.Boolean()),
bestEffort: Type.Optional(Type.Boolean()),
gifPlayback: Type.Optional(Type.Boolean()),
emoji: Type.Optional(Type.String()),
remove: Type.Optional(Type.Boolean()),
limit: Type.Optional(Type.Number()),
before: Type.Optional(Type.String()),
after: Type.Optional(Type.String()),
around: Type.Optional(Type.String()),
pollQuestion: Type.Optional(Type.String()),
pollOption: Type.Optional(Type.Array(Type.String())),
pollDurationHours: Type.Optional(Type.Number()),
pollMulti: Type.Optional(Type.Boolean()),
channelId: Type.Optional(Type.String()),
channelIds: Type.Optional(Type.Array(Type.String())),
guildId: Type.Optional(Type.String()),
userId: Type.Optional(Type.String()),
authorId: Type.Optional(Type.String()),
authorIds: Type.Optional(Type.Array(Type.String())),
roleId: Type.Optional(Type.String()),
roleIds: Type.Optional(Type.Array(Type.String())),
emojiName: Type.Optional(Type.String()),
stickerId: Type.Optional(Type.Array(Type.String())),
stickerName: Type.Optional(Type.String()),
stickerDesc: Type.Optional(Type.String()),
stickerTags: Type.Optional(Type.String()),
threadName: Type.Optional(Type.String()),
autoArchiveMin: Type.Optional(Type.Number()),
query: Type.Optional(Type.String()),
eventName: Type.Optional(Type.String()),
eventType: Type.Optional(Type.String()),
startTime: Type.Optional(Type.String()),
endTime: Type.Optional(Type.String()),
desc: Type.Optional(Type.String()),
location: Type.Optional(Type.String()),
durationMin: Type.Optional(Type.Number()),
until: Type.Optional(Type.String()),
reason: Type.Optional(Type.String()),
deleteDays: Type.Optional(Type.Number()),
includeArchived: Type.Optional(Type.Boolean()),
participant: Type.Optional(Type.String()),
fromMe: Type.Optional(Type.Boolean()),
gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
name: Type.Optional(Type.String()),
type: Type.Optional(Type.Number()),
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
topic: Type.Optional(Type.String()),
position: Type.Optional(Type.Number()),
nsfw: Type.Optional(Type.Boolean()),
rateLimitPerUser: Type.Optional(Type.Number()),
categoryId: Type.Optional(Type.String()),
};
function buildMessageToolSchemaFromActions(
actions: readonly string[],
options: { includeButtons: boolean },
) {
const props: Record<string, unknown> = { ...MessageToolCommonSchema };
if (!options.includeButtons) delete props.buttons;
const schemas: Array<ReturnType<typeof Type.Object>> = [];
if (actions.includes("send")) {
schemas.push(
Type.Object({
action: Type.Literal("send"),
to: Type.String(),
message: Type.String(),
...props,
}),
);
}
const nonSendActions = actions.filter((action) => action !== "send");
if (nonSendActions.length > 0) {
schemas.push(
Type.Object({
action: Type.Union(
nonSendActions.map((action) => Type.Literal(action)),
),
...props,
}),
);
}
return schemas.length === 1 ? schemas[0] : Type.Union(schemas);
}
const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, {
includeButtons: true,
});
type MessageToolOptions = {
agentAccountId?: string;
config?: ClawdbotConfig;
currentChannelId?: string;
currentThreadTs?: string;
replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean };
};
function buildMessageToolSchema(cfg: ClawdbotConfig) {
const actions = listProviderMessageActions(cfg);
const includeButtons = supportsProviderMessageButtons(cfg);
return buildMessageToolSchemaFromActions(
actions.length > 0 ? actions : ["send"],
{ includeButtons },
);
}
function resolveAgentAccountId(value?: string): string | undefined {
const trimmed = value?.trim();
if (!trimmed) return undefined;
return normalizeAccountId(trimmed);
}
export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
const schema = options?.config
? buildMessageToolSchema(options.config)
: MessageToolSchema;
return {
label: "Message",
name: "message",
description:
"Send messages and provider actions (polls, reactions, pins, threads, etc.) via configured provider plugins.",
parameters: schema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = options?.config ?? loadConfig();
const action = readStringParam(params, "action", {
required: true,
}) as ProviderMessageActionName;
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
const gateway = {
url: readStringParam(params, "gatewayUrl", { trim: false }),
token: readStringParam(params, "gatewayToken", { trim: false }),
timeoutMs: readNumberParam(params, "timeoutMs"),
clientName: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
clientDisplayName: "agent",
mode: GATEWAY_CLIENT_MODES.BACKEND,
};
const toolContext =
options?.currentChannelId ||
options?.currentThreadTs ||
options?.replyToMode ||
options?.hasRepliedRef
? {
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
}
: undefined;
const result = await runMessageAction({
cfg,
action,
params,
defaultAccountId: accountId ?? undefined,
gateway,
toolContext,
});
if (result.toolResult) return result.toolResult;
return jsonResult(result.payload);
},
};
}