refactor: require target for message actions
This commit is contained in:
@@ -1,9 +1,41 @@
|
||||
import { MESSAGE_ACTION_TARGET_MODE } from "./message-action-spec.js";
|
||||
|
||||
export const CHANNEL_TARGET_DESCRIPTION =
|
||||
"Recipient/channel: E.164 for WhatsApp/Signal, Telegram chat id/@username, Discord/Slack channel/user, or iMessage handle/chat_id";
|
||||
|
||||
export const CHANNEL_TARGETS_DESCRIPTION =
|
||||
"Recipient/channel targets (same format as --to); accepts ids or names when the directory is available.";
|
||||
"Recipient/channel targets (same format as --target); accepts ids or names when the directory is available.";
|
||||
|
||||
export function normalizeChannelTargetInput(raw: string): string {
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
export function applyTargetToParams(params: {
|
||||
action: string;
|
||||
args: Record<string, unknown>;
|
||||
}): void {
|
||||
const target = typeof params.args.target === "string" ? params.args.target.trim() : "";
|
||||
const hasLegacyTo = typeof params.args.to === "string";
|
||||
const hasLegacyChannelId = typeof params.args.channelId === "string";
|
||||
const mode =
|
||||
MESSAGE_ACTION_TARGET_MODE[params.action as keyof typeof MESSAGE_ACTION_TARGET_MODE] ?? "none";
|
||||
|
||||
if (mode !== "none") {
|
||||
if (hasLegacyTo || hasLegacyChannelId) {
|
||||
throw new Error("Use `target` instead of `to`/`channelId`.");
|
||||
}
|
||||
} else if (hasLegacyTo) {
|
||||
throw new Error("Use `target` for actions that accept a destination.");
|
||||
}
|
||||
|
||||
if (!target) return;
|
||||
if (mode === "channelId") {
|
||||
params.args.channelId = target;
|
||||
return;
|
||||
}
|
||||
if (mode === "to") {
|
||||
params.args.to = target;
|
||||
return;
|
||||
}
|
||||
throw new Error(`Action ${params.action} does not accept a target.`);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,12 @@ export class DirectoryCache<T> {
|
||||
this.cache.set(key, { value, fetchedAt: Date.now() });
|
||||
}
|
||||
|
||||
clearMatching(match: (key: string) => boolean): void {
|
||||
for (const key of this.cache.keys()) {
|
||||
if (match(key)) this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
clear(cfg?: ClawdbotConfig): void {
|
||||
this.cache.clear();
|
||||
if (cfg) this.lastConfigRef = cfg;
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "slack",
|
||||
to: "#C12345678",
|
||||
target: "#C12345678",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
@@ -43,7 +43,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "slack",
|
||||
to: "#C12345678",
|
||||
target: "#C12345678",
|
||||
media: "https://example.com/note.ogg",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
@@ -60,7 +60,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "slack",
|
||||
to: "#C12345678",
|
||||
target: "#C12345678",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
dryRun: true,
|
||||
@@ -74,7 +74,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "slack",
|
||||
to: "channel:C99999999",
|
||||
target: "channel:C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
@@ -90,7 +90,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "thread-reply",
|
||||
params: {
|
||||
channel: "slack",
|
||||
channelId: "C99999999",
|
||||
target: "C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
@@ -106,7 +106,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "whatsapp",
|
||||
to: "group:123@g.us",
|
||||
target: "group:123@g.us",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "123@g.us" },
|
||||
@@ -122,7 +122,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "whatsapp",
|
||||
to: "456@g.us",
|
||||
target: "456@g.us",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "123@g.us", currentChannelProvider: "whatsapp" },
|
||||
@@ -138,7 +138,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "imessage",
|
||||
to: "imessage:+15551234567",
|
||||
target: "imessage:+15551234567",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "imessage:+15551234567" },
|
||||
@@ -154,7 +154,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "imessage",
|
||||
to: "imessage:+15551230000",
|
||||
target: "imessage:+15551230000",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: {
|
||||
@@ -174,7 +174,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "telegram",
|
||||
to: "telegram:@ops",
|
||||
target: "telegram:@ops",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
@@ -201,7 +201,7 @@ describe("runMessageAction context isolation", () => {
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "slack",
|
||||
to: "channel:C99999999",
|
||||
target: "channel:C99999999",
|
||||
message: "hi",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
|
||||
@@ -13,13 +13,10 @@ import type {
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||
import {
|
||||
listConfiguredMessageChannels,
|
||||
resolveMessageChannelSelection,
|
||||
} from "./channel-selection.js";
|
||||
import { listConfiguredMessageChannels, resolveMessageChannelSelection } from "./channel-selection.js";
|
||||
import { applyTargetToParams } from "./channel-target.js";
|
||||
import type { OutboundSendDeps } from "./deliver.js";
|
||||
import type { MessagePollResult, MessageSendResult } from "./message.js";
|
||||
import { sendMessage, sendPoll } from "./message.js";
|
||||
import {
|
||||
applyCrossContextDecoration,
|
||||
buildCrossContextDecoration,
|
||||
@@ -27,7 +24,9 @@ import {
|
||||
enforceCrossContextPolicy,
|
||||
shouldApplyCrossContextMarker,
|
||||
} from "./outbound-policy.js";
|
||||
import { resolveMessagingTarget } from "./target-resolver.js";
|
||||
import { executePollAction, executeSendAction } from "./outbound-send-service.js";
|
||||
import { actionRequiresTarget } from "./message-action-spec.js";
|
||||
import { resolveChannelTarget } from "./target-resolver.js";
|
||||
|
||||
export type MessageActionRunnerGateway = {
|
||||
url?: string;
|
||||
@@ -195,7 +194,7 @@ async function resolveActionTarget(params: {
|
||||
}): Promise<void> {
|
||||
const toRaw = typeof params.args.to === "string" ? params.args.to.trim() : "";
|
||||
if (toRaw) {
|
||||
const resolved = await resolveMessagingTarget({
|
||||
const resolved = await resolveChannelTarget({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
input: toRaw,
|
||||
@@ -210,7 +209,7 @@ async function resolveActionTarget(params: {
|
||||
const channelIdRaw =
|
||||
typeof params.args.channelId === "string" ? params.args.channelId.trim() : "";
|
||||
if (channelIdRaw) {
|
||||
const resolved = await resolveMessagingTarget({
|
||||
const resolved = await resolveChannelTarget({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
input: channelIdRaw,
|
||||
@@ -237,7 +236,6 @@ type ResolvedActionContext = {
|
||||
gateway?: MessageActionRunnerGateway;
|
||||
input: RunMessageActionParams;
|
||||
};
|
||||
|
||||
function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined {
|
||||
if (!input.gateway) return undefined;
|
||||
return {
|
||||
@@ -281,7 +279,7 @@ async function handleBroadcastAction(
|
||||
for (const targetChannel of targetChannels) {
|
||||
for (const target of rawTargets) {
|
||||
try {
|
||||
const resolved = await resolveMessagingTarget({
|
||||
const resolved = await resolveChannelTarget({
|
||||
cfg: input.cfg,
|
||||
channel: targetChannel,
|
||||
input: target,
|
||||
@@ -293,7 +291,7 @@ async function handleBroadcastAction(
|
||||
params: {
|
||||
...params,
|
||||
channel: targetChannel,
|
||||
to: resolved.target.to,
|
||||
target: resolved.target.to,
|
||||
},
|
||||
});
|
||||
results.push({
|
||||
@@ -326,11 +324,10 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
const { cfg, params, channel, accountId, dryRun, gateway, input } = ctx;
|
||||
const action: ChannelMessageActionName = "send";
|
||||
const to = readStringParam(params, "to", { required: true });
|
||||
// Allow message to be omitted when sending media-only (e.g., voice notes)
|
||||
const mediaHint = readStringParam(params, "media", { trim: false });
|
||||
let message =
|
||||
readStringParam(params, "message", {
|
||||
required: !mediaHint, // Only require message if no media hint
|
||||
required: !mediaHint,
|
||||
allowEmpty: true,
|
||||
}) ?? "";
|
||||
|
||||
@@ -364,50 +361,29 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
const mediaUrl = readStringParam(params, "media", { trim: false });
|
||||
const gifPlayback = readBooleanParam(params, "gifPlayback") ?? false;
|
||||
const bestEffort = readBooleanParam(params, "bestEffort");
|
||||
if (!dryRun) {
|
||||
const handled = await dispatchChannelMessageAction({
|
||||
channel,
|
||||
action,
|
||||
const send = await executeSendAction({
|
||||
ctx: {
|
||||
cfg,
|
||||
channel,
|
||||
params,
|
||||
accountId: accountId ?? undefined,
|
||||
gateway,
|
||||
toolContext: input.toolContext,
|
||||
deps: input.deps,
|
||||
dryRun,
|
||||
});
|
||||
if (handled) {
|
||||
return {
|
||||
kind: "send",
|
||||
channel,
|
||||
action,
|
||||
to,
|
||||
handledBy: "plugin",
|
||||
payload: extractToolPayload(handled),
|
||||
toolResult: handled,
|
||||
dryRun,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result: MessageSendResult = await sendMessage({
|
||||
cfg,
|
||||
mirror:
|
||||
input.sessionKey && !dryRun
|
||||
? {
|
||||
sessionKey: input.sessionKey,
|
||||
agentId: input.agentId,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
to,
|
||||
content: message,
|
||||
message,
|
||||
mediaUrl: mediaUrl || undefined,
|
||||
channel: channel || undefined,
|
||||
accountId: accountId ?? undefined,
|
||||
gifPlayback,
|
||||
dryRun,
|
||||
bestEffort: bestEffort ?? undefined,
|
||||
deps: input.deps,
|
||||
gateway,
|
||||
mirror:
|
||||
input.sessionKey && !dryRun
|
||||
? {
|
||||
sessionKey: input.sessionKey,
|
||||
agentId: input.agentId,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -415,9 +391,10 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
channel,
|
||||
action,
|
||||
to,
|
||||
handledBy: "core",
|
||||
payload: result,
|
||||
sendResult: result,
|
||||
handledBy: send.handledBy,
|
||||
payload: send.payload,
|
||||
toolResult: send.toolResult,
|
||||
sendResult: send.sendResult,
|
||||
dryRun,
|
||||
};
|
||||
}
|
||||
@@ -458,41 +435,21 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
});
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
const handled = await dispatchChannelMessageAction({
|
||||
channel,
|
||||
action,
|
||||
const poll = await executePollAction({
|
||||
ctx: {
|
||||
cfg,
|
||||
channel,
|
||||
params,
|
||||
accountId: accountId ?? undefined,
|
||||
gateway,
|
||||
toolContext: input.toolContext,
|
||||
dryRun,
|
||||
});
|
||||
if (handled) {
|
||||
return {
|
||||
kind: "poll",
|
||||
channel,
|
||||
action,
|
||||
to,
|
||||
handledBy: "plugin",
|
||||
payload: extractToolPayload(handled),
|
||||
toolResult: handled,
|
||||
dryRun,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result: MessagePollResult = await sendPoll({
|
||||
cfg,
|
||||
},
|
||||
to,
|
||||
question,
|
||||
options,
|
||||
maxSelections,
|
||||
durationHours: durationHours ?? undefined,
|
||||
channel,
|
||||
dryRun,
|
||||
gateway,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -500,9 +457,10 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
|
||||
channel,
|
||||
action,
|
||||
to,
|
||||
handledBy: "core",
|
||||
payload: result,
|
||||
pollResult: result,
|
||||
handledBy: poll.handledBy,
|
||||
payload: poll.payload,
|
||||
toolResult: poll.toolResult,
|
||||
pollResult: poll.pollResult,
|
||||
dryRun,
|
||||
};
|
||||
}
|
||||
@@ -560,6 +518,16 @@ export async function runMessageAction(
|
||||
return handleBroadcastAction(input, params);
|
||||
}
|
||||
|
||||
applyTargetToParams({ action, args: params });
|
||||
if (actionRequiresTarget(action)) {
|
||||
const hasTarget =
|
||||
(typeof params.to === "string" && params.to.trim()) ||
|
||||
(typeof params.channelId === "string" && params.channelId.trim());
|
||||
if (!hasTarget) {
|
||||
throw new Error(`Action ${action} requires a target.`);
|
||||
}
|
||||
}
|
||||
|
||||
const channel = await resolveChannel(cfg, params);
|
||||
const accountId = readStringParam(params, "accountId") ?? input.defaultAccountId;
|
||||
const dryRun = Boolean(input.dryRun ?? readBooleanParam(params, "dryRun"));
|
||||
|
||||
50
src/infra/outbound/message-action-spec.ts
Normal file
50
src/infra/outbound/message-action-spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { ChannelMessageActionName } from "../../channels/plugins/types.js";
|
||||
|
||||
export type MessageActionTargetMode = "to" | "channelId" | "none";
|
||||
|
||||
export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, MessageActionTargetMode> =
|
||||
{
|
||||
send: "to",
|
||||
broadcast: "none",
|
||||
poll: "to",
|
||||
react: "to",
|
||||
reactions: "to",
|
||||
read: "to",
|
||||
edit: "to",
|
||||
delete: "to",
|
||||
pin: "to",
|
||||
unpin: "to",
|
||||
"list-pins": "to",
|
||||
permissions: "to",
|
||||
"thread-create": "to",
|
||||
"thread-list": "none",
|
||||
"thread-reply": "to",
|
||||
search: "none",
|
||||
sticker: "to",
|
||||
"member-info": "none",
|
||||
"role-info": "none",
|
||||
"emoji-list": "none",
|
||||
"emoji-upload": "none",
|
||||
"sticker-upload": "none",
|
||||
"role-add": "none",
|
||||
"role-remove": "none",
|
||||
"channel-info": "channelId",
|
||||
"channel-list": "none",
|
||||
"channel-create": "none",
|
||||
"channel-edit": "channelId",
|
||||
"channel-delete": "channelId",
|
||||
"channel-move": "channelId",
|
||||
"category-create": "none",
|
||||
"category-edit": "none",
|
||||
"category-delete": "none",
|
||||
"voice-status": "none",
|
||||
"event-list": "none",
|
||||
"event-create": "none",
|
||||
timeout: "none",
|
||||
kick: "none",
|
||||
ban: "none",
|
||||
};
|
||||
|
||||
export function actionRequiresTarget(action: ChannelMessageActionName): boolean {
|
||||
return MESSAGE_ACTION_TARGET_MODE[action] !== "none";
|
||||
}
|
||||
93
src/infra/outbound/outbound-policy.test.ts
Normal file
93
src/infra/outbound/outbound-policy.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
applyCrossContextDecoration,
|
||||
buildCrossContextDecoration,
|
||||
enforceCrossContextPolicy,
|
||||
} from "./outbound-policy.js";
|
||||
|
||||
const slackConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
botToken: "xoxb-test",
|
||||
appToken: "xapp-test",
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
const discordConfig = {
|
||||
channels: {
|
||||
discord: {},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
describe("outbound policy", () => {
|
||||
it("blocks cross-provider sends by default", () => {
|
||||
expect(() =>
|
||||
enforceCrossContextPolicy({
|
||||
cfg: slackConfig,
|
||||
channel: "telegram",
|
||||
action: "send",
|
||||
args: { to: "telegram:@ops" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
}),
|
||||
).toThrow(/Cross-context messaging denied/);
|
||||
});
|
||||
|
||||
it("allows cross-provider sends when enabled", () => {
|
||||
const cfg = {
|
||||
...slackConfig,
|
||||
tools: {
|
||||
message: { crossContext: { allowAcrossProviders: true } },
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
|
||||
expect(() =>
|
||||
enforceCrossContextPolicy({
|
||||
cfg,
|
||||
channel: "telegram",
|
||||
action: "send",
|
||||
args: { to: "telegram:@ops" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("blocks same-provider cross-context when disabled", () => {
|
||||
const cfg = {
|
||||
...slackConfig,
|
||||
tools: { message: { crossContext: { allowWithinProvider: false } } },
|
||||
} as ClawdbotConfig;
|
||||
|
||||
expect(() =>
|
||||
enforceCrossContextPolicy({
|
||||
cfg,
|
||||
channel: "slack",
|
||||
action: "send",
|
||||
args: { to: "C99999999" },
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "slack" },
|
||||
}),
|
||||
).toThrow(/Cross-context messaging denied/);
|
||||
});
|
||||
|
||||
it("uses embeds when available and preferred", async () => {
|
||||
const decoration = await buildCrossContextDecoration({
|
||||
cfg: discordConfig,
|
||||
channel: "discord",
|
||||
target: "123",
|
||||
toolContext: { currentChannelId: "C12345678", currentChannelProvider: "discord" },
|
||||
});
|
||||
|
||||
expect(decoration).not.toBeNull();
|
||||
const applied = applyCrossContextDecoration({
|
||||
message: "hello",
|
||||
decoration: decoration!,
|
||||
preferEmbeds: true,
|
||||
});
|
||||
|
||||
expect(applied.usedEmbeds).toBe(true);
|
||||
expect(applied.embeds?.length).toBeGreaterThan(0);
|
||||
expect(applied.message).toBe("hello");
|
||||
});
|
||||
});
|
||||
164
src/infra/outbound/outbound-send-service.ts
Normal file
164
src/infra/outbound/outbound-send-service.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
|
||||
import type {
|
||||
ChannelId,
|
||||
ChannelThreadingToolContext,
|
||||
} from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
|
||||
import type { OutboundSendDeps } from "./deliver.js";
|
||||
import type { MessagePollResult, MessageSendResult } from "./message.js";
|
||||
import { sendMessage, sendPoll } from "./message.js";
|
||||
|
||||
export type OutboundGatewayContext = {
|
||||
url?: string;
|
||||
token?: string;
|
||||
timeoutMs?: number;
|
||||
clientName: GatewayClientName;
|
||||
clientDisplayName?: string;
|
||||
mode: GatewayClientMode;
|
||||
};
|
||||
|
||||
export type OutboundSendContext = {
|
||||
cfg: ClawdbotConfig;
|
||||
channel: ChannelId;
|
||||
params: Record<string, unknown>;
|
||||
accountId?: string | null;
|
||||
gateway?: OutboundGatewayContext;
|
||||
toolContext?: ChannelThreadingToolContext;
|
||||
deps?: OutboundSendDeps;
|
||||
dryRun: boolean;
|
||||
mirror?: {
|
||||
sessionKey: string;
|
||||
agentId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function extractToolPayload(result: AgentToolResult<unknown>): unknown {
|
||||
if (result.details !== undefined) return result.details;
|
||||
const textBlock = Array.isArray(result.content)
|
||||
? result.content.find(
|
||||
(block) =>
|
||||
block &&
|
||||
typeof block === "object" &&
|
||||
(block as { type?: unknown }).type === "text" &&
|
||||
typeof (block as { text?: unknown }).text === "string",
|
||||
)
|
||||
: undefined;
|
||||
const text = (textBlock as { text?: string } | undefined)?.text;
|
||||
if (text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return result.content ?? result;
|
||||
}
|
||||
|
||||
export async function executeSendAction(params: {
|
||||
ctx: OutboundSendContext;
|
||||
to: string;
|
||||
message: string;
|
||||
mediaUrl?: string;
|
||||
gifPlayback?: boolean;
|
||||
bestEffort?: boolean;
|
||||
}): Promise<{
|
||||
handledBy: "plugin" | "core";
|
||||
payload: unknown;
|
||||
toolResult?: AgentToolResult<unknown>;
|
||||
sendResult?: MessageSendResult;
|
||||
}> {
|
||||
if (!params.ctx.dryRun) {
|
||||
const handled = await dispatchChannelMessageAction({
|
||||
channel: params.ctx.channel,
|
||||
action: "send",
|
||||
cfg: params.ctx.cfg,
|
||||
params: params.ctx.params,
|
||||
accountId: params.ctx.accountId ?? undefined,
|
||||
gateway: params.ctx.gateway,
|
||||
toolContext: params.ctx.toolContext,
|
||||
dryRun: params.ctx.dryRun,
|
||||
});
|
||||
if (handled) {
|
||||
return {
|
||||
handledBy: "plugin",
|
||||
payload: extractToolPayload(handled),
|
||||
toolResult: handled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result: MessageSendResult = await sendMessage({
|
||||
cfg: params.ctx.cfg,
|
||||
to: params.to,
|
||||
content: params.message,
|
||||
mediaUrl: params.mediaUrl || undefined,
|
||||
channel: params.ctx.channel || undefined,
|
||||
accountId: params.ctx.accountId ?? undefined,
|
||||
gifPlayback: params.gifPlayback,
|
||||
dryRun: params.ctx.dryRun,
|
||||
bestEffort: params.bestEffort ?? undefined,
|
||||
deps: params.ctx.deps,
|
||||
gateway: params.ctx.gateway,
|
||||
mirror: params.ctx.mirror,
|
||||
});
|
||||
|
||||
return {
|
||||
handledBy: "core",
|
||||
payload: result,
|
||||
sendResult: result,
|
||||
};
|
||||
}
|
||||
|
||||
export async function executePollAction(params: {
|
||||
ctx: OutboundSendContext;
|
||||
to: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
maxSelections: number;
|
||||
durationHours?: number;
|
||||
}): Promise<{
|
||||
handledBy: "plugin" | "core";
|
||||
payload: unknown;
|
||||
toolResult?: AgentToolResult<unknown>;
|
||||
pollResult?: MessagePollResult;
|
||||
}> {
|
||||
if (!params.ctx.dryRun) {
|
||||
const handled = await dispatchChannelMessageAction({
|
||||
channel: params.ctx.channel,
|
||||
action: "poll",
|
||||
cfg: params.ctx.cfg,
|
||||
params: params.ctx.params,
|
||||
accountId: params.ctx.accountId ?? undefined,
|
||||
gateway: params.ctx.gateway,
|
||||
toolContext: params.ctx.toolContext,
|
||||
dryRun: params.ctx.dryRun,
|
||||
});
|
||||
if (handled) {
|
||||
return {
|
||||
handledBy: "plugin",
|
||||
payload: extractToolPayload(handled),
|
||||
toolResult: handled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result: MessagePollResult = await sendPoll({
|
||||
cfg: params.ctx.cfg,
|
||||
to: params.to,
|
||||
question: params.question,
|
||||
options: params.options,
|
||||
maxSelections: params.maxSelections,
|
||||
durationHours: params.durationHours ?? undefined,
|
||||
channel: params.ctx.channel,
|
||||
dryRun: params.ctx.dryRun,
|
||||
gateway: params.ctx.gateway,
|
||||
});
|
||||
|
||||
return {
|
||||
handledBy: "core",
|
||||
payload: result,
|
||||
pollResult: result,
|
||||
};
|
||||
}
|
||||
@@ -23,9 +23,34 @@ export type ResolveMessagingTargetResult =
|
||||
| { ok: true; target: ResolvedMessagingTarget }
|
||||
| { ok: false; error: Error; candidates?: ChannelDirectoryEntry[] };
|
||||
|
||||
export async function resolveChannelTarget(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
channel: ChannelId;
|
||||
input: string;
|
||||
accountId?: string | null;
|
||||
preferredKind?: TargetResolveKind;
|
||||
runtime?: RuntimeEnv;
|
||||
}): Promise<ResolveMessagingTargetResult> {
|
||||
return resolveMessagingTarget(params);
|
||||
}
|
||||
|
||||
const CACHE_TTL_MS = 30 * 60 * 1000;
|
||||
const directoryCache = new DirectoryCache<ChannelDirectoryEntry[]>(CACHE_TTL_MS);
|
||||
|
||||
export function resetDirectoryCache(params?: { channel?: ChannelId; accountId?: string | null }) {
|
||||
if (!params?.channel) {
|
||||
directoryCache.clear();
|
||||
return;
|
||||
}
|
||||
const channelKey = params.channel;
|
||||
const accountKey = params.accountId ?? "default";
|
||||
directoryCache.clearMatching((key) => {
|
||||
if (!key.startsWith(`${channelKey}:`)) return false;
|
||||
if (!params.accountId) return true;
|
||||
return key.startsWith(`${channelKey}:${accountKey}:`);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeQuery(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export function resolveOutboundTarget(params: {
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error(`Delivering to ${plugin.meta.label} requires --to`),
|
||||
error: new Error(`Delivering to ${plugin.meta.label} requires a destination`),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user