mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 19:50:22 +00:00
refactor: move channel delivery and ACP seams into plugins
This commit is contained in:
@@ -1,31 +1,125 @@
|
||||
import { markdownToSignalTextChunks } from "../../../../extensions/signal/src/format.js";
|
||||
import { sendMessageSignal } from "../../../../extensions/signal/src/send.js";
|
||||
import { resolveTextChunkLimit } from "../../../auto-reply/chunk.js";
|
||||
import { resolveMarkdownTableMode } from "../../../config/markdown-tables.js";
|
||||
import {
|
||||
resolveOutboundSendDep,
|
||||
type OutboundSendDeps,
|
||||
} from "../../../infra/outbound/send-deps.js";
|
||||
import {
|
||||
createScopedChannelMediaMaxBytesResolver,
|
||||
createDirectTextMediaOutbound,
|
||||
} from "./direct-text-media.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { createScopedChannelMediaMaxBytesResolver } from "./direct-text-media.js";
|
||||
|
||||
function resolveSignalSender(deps: OutboundSendDeps | undefined) {
|
||||
return resolveOutboundSendDep<typeof sendMessageSignal>(deps, "signal") ?? sendMessageSignal;
|
||||
}
|
||||
|
||||
export const signalOutbound = createDirectTextMediaOutbound({
|
||||
channel: "signal",
|
||||
resolveSender: resolveSignalSender,
|
||||
resolveMaxBytes: createScopedChannelMediaMaxBytesResolver("signal"),
|
||||
buildTextOptions: ({ cfg, maxBytes, accountId }) => ({
|
||||
cfg,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
}),
|
||||
buildMediaOptions: ({ cfg, mediaUrl, maxBytes, accountId, mediaLocalRoots }) => ({
|
||||
const resolveSignalMaxBytes = createScopedChannelMediaMaxBytesResolver("signal");
|
||||
type SignalSendOpts = NonNullable<Parameters<typeof sendMessageSignal>[2]>;
|
||||
|
||||
function inferSignalTableMode(params: { cfg: SignalSendOpts["cfg"]; accountId?: string | null }) {
|
||||
return resolveMarkdownTableMode({
|
||||
cfg: params.cfg,
|
||||
channel: "signal",
|
||||
accountId: params.accountId ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export const signalOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: (text, _limit) => text.split(/\n{2,}/).flatMap((chunk) => (chunk ? [chunk] : [])),
|
||||
chunkerMode: "text",
|
||||
textChunkLimit: 4000,
|
||||
sendFormattedText: async ({ cfg, to, text, accountId, deps, abortSignal }) => {
|
||||
const send = resolveSignalSender(deps);
|
||||
const maxBytes = resolveSignalMaxBytes({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
const limit = resolveTextChunkLimit(cfg, "signal", accountId ?? undefined, {
|
||||
fallbackLimit: 4000,
|
||||
});
|
||||
const tableMode = inferSignalTableMode({ cfg, accountId });
|
||||
let chunks =
|
||||
limit === undefined
|
||||
? markdownToSignalTextChunks(text, Number.POSITIVE_INFINITY, { tableMode })
|
||||
: markdownToSignalTextChunks(text, limit, { tableMode });
|
||||
if (chunks.length === 0 && text) {
|
||||
chunks = [{ text, styles: [] }];
|
||||
}
|
||||
const results = [];
|
||||
for (const chunk of chunks) {
|
||||
abortSignal?.throwIfAborted();
|
||||
const result = await send(to, chunk.text, {
|
||||
cfg,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
textMode: "plain",
|
||||
textStyles: chunk.styles,
|
||||
});
|
||||
results.push({ channel: "signal" as const, ...result });
|
||||
}
|
||||
return results;
|
||||
},
|
||||
sendFormattedMedia: async ({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
mediaLocalRoots,
|
||||
}),
|
||||
});
|
||||
accountId,
|
||||
deps,
|
||||
abortSignal,
|
||||
}) => {
|
||||
abortSignal?.throwIfAborted();
|
||||
const send = resolveSignalSender(deps);
|
||||
const maxBytes = resolveSignalMaxBytes({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
const tableMode = inferSignalTableMode({ cfg, accountId });
|
||||
const formatted = markdownToSignalTextChunks(text, Number.POSITIVE_INFINITY, {
|
||||
tableMode,
|
||||
})[0] ?? {
|
||||
text,
|
||||
styles: [],
|
||||
};
|
||||
const result = await send(to, formatted.text, {
|
||||
cfg,
|
||||
mediaUrl,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
textMode: "plain",
|
||||
textStyles: formatted.styles,
|
||||
mediaLocalRoots,
|
||||
});
|
||||
return { channel: "signal", ...result };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps }) => {
|
||||
const send = resolveSignalSender(deps);
|
||||
const maxBytes = resolveSignalMaxBytes({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
cfg,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return { channel: "signal", ...result };
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, deps }) => {
|
||||
const send = resolveSignalSender(deps);
|
||||
const maxBytes = resolveSignalMaxBytes({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
const result = await send(to, text, {
|
||||
cfg,
|
||||
mediaUrl,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
mediaLocalRoots,
|
||||
});
|
||||
return { channel: "signal", ...result };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { AgentAcpBinding } from "../../config/types.js";
|
||||
import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
|
||||
import type { ExecApprovalRequest, ExecApprovalResolved } from "../../infra/exec-approvals.js";
|
||||
import type { OutboundDeliveryResult, OutboundSendDeps } from "../../infra/outbound/deliver.js";
|
||||
import type { OutboundIdentity } from "../../infra/outbound/identity.js";
|
||||
import type { PluginRuntime } from "../../plugins/runtime/types.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { ConfigWriteTarget } from "./config-writes.js";
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelAccountState,
|
||||
@@ -137,12 +139,23 @@ export type ChannelOutboundPayloadContext = ChannelOutboundContext & {
|
||||
payload: ReplyPayload;
|
||||
};
|
||||
|
||||
export type ChannelOutboundFormattedContext = ChannelOutboundContext & {
|
||||
abortSignal?: AbortSignal;
|
||||
};
|
||||
|
||||
export type ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct" | "gateway" | "hybrid";
|
||||
chunker?: ((text: string, limit: number) => string[]) | null;
|
||||
chunkerMode?: "text" | "markdown";
|
||||
textChunkLimit?: number;
|
||||
pollMaxOptions?: number;
|
||||
normalizePayload?: (params: { payload: ReplyPayload }) => ReplyPayload | null;
|
||||
shouldSkipPlainTextSanitization?: (params: { payload: ReplyPayload }) => boolean;
|
||||
resolveEffectiveTextChunkLimit?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
fallbackLimit?: number;
|
||||
}) => number | undefined;
|
||||
resolveTarget?: (params: {
|
||||
cfg?: OpenClawConfig;
|
||||
to?: string;
|
||||
@@ -151,6 +164,10 @@ export type ChannelOutboundAdapter = {
|
||||
mode?: ChannelOutboundTargetMode;
|
||||
}) => { ok: true; to: string } | { ok: false; error: Error };
|
||||
sendPayload?: (ctx: ChannelOutboundPayloadContext) => Promise<OutboundDeliveryResult>;
|
||||
sendFormattedText?: (ctx: ChannelOutboundFormattedContext) => Promise<OutboundDeliveryResult[]>;
|
||||
sendFormattedMedia?: (
|
||||
ctx: ChannelOutboundFormattedContext & { mediaUrl: string },
|
||||
) => Promise<OutboundDeliveryResult>;
|
||||
sendText?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
|
||||
sendMedia?: (ctx: ChannelOutboundContext) => Promise<OutboundDeliveryResult>;
|
||||
sendPoll?: (ctx: ChannelPollContext) => Promise<ChannelPollResult>;
|
||||
@@ -464,9 +481,63 @@ export type ChannelExecApprovalAdapter = {
|
||||
};
|
||||
|
||||
export type ChannelAllowlistAdapter = {
|
||||
readConfig?: (params: { cfg: OpenClawConfig; accountId?: string | null }) =>
|
||||
| {
|
||||
dmAllowFrom?: Array<string | number>;
|
||||
groupAllowFrom?: Array<string | number>;
|
||||
dmPolicy?: string;
|
||||
groupPolicy?: string;
|
||||
groupOverrides?: Array<{ label: string; entries: Array<string | number> }>;
|
||||
}
|
||||
| Promise<{
|
||||
dmAllowFrom?: Array<string | number>;
|
||||
groupAllowFrom?: Array<string | number>;
|
||||
dmPolicy?: string;
|
||||
groupPolicy?: string;
|
||||
groupOverrides?: Array<{ label: string; entries: Array<string | number> }>;
|
||||
}>;
|
||||
resolveNames?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
scope: "dm" | "group";
|
||||
entries: string[];
|
||||
}) =>
|
||||
| Array<{ input: string; resolved: boolean; name?: string | null }>
|
||||
| Promise<Array<{ input: string; resolved: boolean; name?: string | null }>>;
|
||||
resolveConfigEdit?: (params: {
|
||||
scope: "dm" | "group";
|
||||
pathPrefix: string;
|
||||
writeTarget: ConfigWriteTarget;
|
||||
}) => {
|
||||
pathPrefix: string;
|
||||
writeTarget: ConfigWriteTarget;
|
||||
readPaths: string[][];
|
||||
writePath: string[];
|
||||
cleanupPaths?: string[][];
|
||||
} | null;
|
||||
supportsScope?: (params: { scope: "dm" | "group" | "all" }) => boolean;
|
||||
};
|
||||
|
||||
export type ChannelAcpBindingAdapter = {
|
||||
normalizeConfiguredBindingTarget?: (params: {
|
||||
binding: AgentAcpBinding;
|
||||
conversationId: string;
|
||||
}) => {
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
} | null;
|
||||
matchConfiguredBinding?: (params: {
|
||||
binding: AgentAcpBinding;
|
||||
bindingConversationId: string;
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
}) => {
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
matchPriority?: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type ChannelSecurityAdapter<ResolvedAccount = unknown> = {
|
||||
resolveDmPolicy?: (
|
||||
ctx: ChannelSecurityContext<ResolvedAccount>,
|
||||
|
||||
@@ -345,6 +345,12 @@ export type ChannelThreadingToolContext = {
|
||||
|
||||
export type ChannelMessagingAdapter = {
|
||||
normalizeTarget?: (raw: string) => string | undefined;
|
||||
parseExplicitTarget?: (params: { raw: string }) => {
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
chatType?: ChatType;
|
||||
} | null;
|
||||
inferTargetChatType?: (params: { to: string }) => ChatType | undefined;
|
||||
buildCrossContextComponents?: ChannelCrossContextComponentsFactory;
|
||||
enableInteractiveReplies?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ChannelSetupAdapter,
|
||||
ChannelStatusAdapter,
|
||||
ChannelAllowlistAdapter,
|
||||
ChannelAcpBindingAdapter,
|
||||
} from "./types.adapters.js";
|
||||
import type {
|
||||
ChannelAgentTool,
|
||||
@@ -77,6 +78,7 @@ export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknow
|
||||
lifecycle?: ChannelLifecycleAdapter;
|
||||
execApprovals?: ChannelExecApprovalAdapter;
|
||||
allowlist?: ChannelAllowlistAdapter;
|
||||
acpBindings?: ChannelAcpBindingAdapter;
|
||||
streaming?: ChannelStreamingAdapter;
|
||||
threading?: ChannelThreadingAdapter;
|
||||
messaging?: ChannelMessagingAdapter;
|
||||
|
||||
@@ -33,6 +33,7 @@ export type {
|
||||
ChannelOutboundAdapter,
|
||||
ChannelOutboundContext,
|
||||
ChannelAllowlistAdapter,
|
||||
ChannelAcpBindingAdapter,
|
||||
ChannelPairingAdapter,
|
||||
ChannelSecurityAdapter,
|
||||
ChannelSetupAdapter,
|
||||
|
||||
Reference in New Issue
Block a user