mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 21:40:53 +00:00
refactor: move remaining channel seams into plugins
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Separator, TextDisplay } from "@buape/carbon";
|
||||
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectOpenProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyConfiguredRouteWarnings,
|
||||
@@ -262,16 +263,19 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
||||
readDiscordAllowlistConfig(resolveDiscordAccount({ cfg, accountId })),
|
||||
resolveNames: async ({ cfg, accountId, entries }) =>
|
||||
await resolveDiscordAllowlistNames({ cfg, accountId, entries }),
|
||||
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) =>
|
||||
scope === "dm"
|
||||
? {
|
||||
pathPrefix,
|
||||
writeTarget,
|
||||
readPaths: [["allowFrom"], ["dm", "allowFrom"]],
|
||||
writePath: ["allowFrom"],
|
||||
cleanupPaths: [["dm", "allowFrom"]],
|
||||
}
|
||||
: null,
|
||||
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
|
||||
channelId: "discord",
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
discordConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolvePaths: (scope) =>
|
||||
scope === "dm"
|
||||
? {
|
||||
readPaths: [["allowFrom"], ["dm", "allowFrom"]],
|
||||
writePath: ["allowFrom"],
|
||||
cleanupPaths: [["dm", "allowFrom"]],
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
} from "openclaw/plugin-sdk/compat";
|
||||
@@ -135,11 +136,13 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
||||
groupPolicy: account.config.groupPolicy,
|
||||
};
|
||||
},
|
||||
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({
|
||||
pathPrefix,
|
||||
writeTarget,
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
|
||||
channelId: "imessage",
|
||||
normalize: ({ values }) => formatTrimmedAllowFromEntries(values),
|
||||
resolvePaths: (scope) => ({
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
createScopedAccountConfigAccessors,
|
||||
collectAllowlistProviderRestrictSendersWarnings,
|
||||
@@ -283,11 +284,14 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
|
||||
groupPolicy: account.config.groupPolicy,
|
||||
};
|
||||
},
|
||||
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({
|
||||
pathPrefix,
|
||||
writeTarget,
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
|
||||
channelId: "signal",
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
signalConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolvePaths: (scope) => ({
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
collectOpenProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyConfiguredRouteWarnings,
|
||||
@@ -279,16 +280,19 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
||||
readSlackAllowlistConfig(resolveSlackAccount({ cfg, accountId })),
|
||||
resolveNames: async ({ cfg, accountId, entries }) =>
|
||||
await resolveSlackAllowlistNames({ cfg, accountId, entries }),
|
||||
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) =>
|
||||
scope === "dm"
|
||||
? {
|
||||
pathPrefix,
|
||||
writeTarget,
|
||||
readPaths: [["allowFrom"], ["dm", "allowFrom"]],
|
||||
writePath: ["allowFrom"],
|
||||
cleanupPaths: [["dm", "allowFrom"]],
|
||||
}
|
||||
: null,
|
||||
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
|
||||
channelId: "slack",
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
slackConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolvePaths: (scope) =>
|
||||
scope === "dm"
|
||||
? {
|
||||
readPaths: [["allowFrom"], ["dm", "allowFrom"]],
|
||||
writePath: ["allowFrom"],
|
||||
cleanupPaths: [["dm", "allowFrom"]],
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildAccountScopedAllowlistConfigEditor,
|
||||
collectAllowlistProviderGroupPolicyWarnings,
|
||||
collectOpenGroupPolicyRouteAllowlistWarnings,
|
||||
createScopedAccountConfigAccessors,
|
||||
@@ -358,11 +359,14 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all",
|
||||
readConfig: ({ cfg, accountId }) =>
|
||||
readTelegramAllowlistConfig(resolveTelegramAccount({ cfg, accountId })),
|
||||
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({
|
||||
pathPrefix,
|
||||
writeTarget,
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
|
||||
channelId: "telegram",
|
||||
normalize: ({ cfg, accountId, values }) =>
|
||||
telegramConfigAccessors.formatAllowFrom!({ cfg, accountId, allowFrom: values }),
|
||||
resolvePaths: (scope) => ({
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
acpBindings: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/compat";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
buildAccountScopedDmSecurityPolicy,
|
||||
@@ -195,11 +196,13 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
||||
groupPolicy: account.groupPolicy,
|
||||
};
|
||||
},
|
||||
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({
|
||||
pathPrefix,
|
||||
writeTarget,
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
applyConfigEdit: buildAccountScopedAllowlistConfigEditor({
|
||||
channelId: "whatsapp",
|
||||
normalize: ({ values }) => formatWhatsAppConfigAllowFromEntries(values),
|
||||
resolvePaths: (scope) => ({
|
||||
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
|
||||
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getChannelDock } from "../channels/dock.js";
|
||||
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type {
|
||||
ChannelAgentTool,
|
||||
@@ -73,8 +72,7 @@ export function resolveChannelMessageToolHints(params: {
|
||||
if (!channelId) {
|
||||
return [];
|
||||
}
|
||||
const dock = getChannelDock(channelId);
|
||||
const resolve = dock?.agentPrompt?.messageToolHints;
|
||||
const resolve = getChannelPlugin(channelId)?.agentPrompt?.messageToolHints;
|
||||
if (!resolve) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getChannelDock } from "../channels/dock.js";
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../config/agent-limits.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
|
||||
@@ -315,14 +315,14 @@ export function resolveGroupToolPolicy(params: {
|
||||
if (!channel) {
|
||||
return undefined;
|
||||
}
|
||||
let dock;
|
||||
let plugin;
|
||||
try {
|
||||
dock = getChannelDock(channel);
|
||||
plugin = getChannelPlugin(channel);
|
||||
} catch {
|
||||
dock = undefined;
|
||||
plugin = undefined;
|
||||
}
|
||||
const toolsConfig =
|
||||
dock?.groups?.resolveToolPolicy?.({
|
||||
plugin?.groups?.resolveToolPolicy?.({
|
||||
cfg: params.config,
|
||||
groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ChannelDock } from "../channels/dock.js";
|
||||
import { getChannelDock, listChannelDocks } from "../channels/dock.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import { normalizeAnyChannelId } from "../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
@@ -52,19 +51,19 @@ function resolveProviderFromContext(ctx: MsgContext, cfg: OpenClawConfig): Chann
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
const configured = listChannelDocks()
|
||||
.map((dock) => {
|
||||
if (!dock.config?.resolveAllowFrom) {
|
||||
const configured = listChannelPlugins()
|
||||
.map((plugin) => {
|
||||
if (!plugin.config?.resolveAllowFrom) {
|
||||
return null;
|
||||
}
|
||||
const allowFrom = dock.config.resolveAllowFrom({
|
||||
const allowFrom = plugin.config.resolveAllowFrom({
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
});
|
||||
if (!Array.isArray(allowFrom) || allowFrom.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return dock.id;
|
||||
return plugin.id;
|
||||
})
|
||||
.filter((value): value is ChannelId => Boolean(value));
|
||||
if (configured.length === 1) {
|
||||
@@ -74,29 +73,29 @@ function resolveProviderFromContext(ctx: MsgContext, cfg: OpenClawConfig): Chann
|
||||
}
|
||||
|
||||
function formatAllowFromList(params: {
|
||||
dock?: ChannelDock;
|
||||
plugin?: ChannelPlugin;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
allowFrom: Array<string | number>;
|
||||
}): string[] {
|
||||
const { dock, cfg, accountId, allowFrom } = params;
|
||||
const { plugin, cfg, accountId, allowFrom } = params;
|
||||
if (!allowFrom || allowFrom.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (dock?.config?.formatAllowFrom) {
|
||||
return dock.config.formatAllowFrom({ cfg, accountId, allowFrom });
|
||||
if (plugin?.config?.formatAllowFrom) {
|
||||
return plugin.config.formatAllowFrom({ cfg, accountId, allowFrom });
|
||||
}
|
||||
return normalizeStringEntries(allowFrom);
|
||||
}
|
||||
|
||||
function normalizeAllowFromEntry(params: {
|
||||
dock?: ChannelDock;
|
||||
plugin?: ChannelPlugin;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
value: string;
|
||||
}): string[] {
|
||||
const normalized = formatAllowFromList({
|
||||
dock: params.dock,
|
||||
plugin: params.plugin,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
allowFrom: [params.value],
|
||||
@@ -105,7 +104,7 @@ function normalizeAllowFromEntry(params: {
|
||||
}
|
||||
|
||||
function resolveOwnerAllowFromList(params: {
|
||||
dock?: ChannelDock;
|
||||
plugin?: ChannelPlugin;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
providerId?: ChannelId;
|
||||
@@ -139,7 +138,7 @@ function resolveOwnerAllowFromList(params: {
|
||||
filtered.push(trimmed);
|
||||
}
|
||||
return formatAllowFromList({
|
||||
dock: params.dock,
|
||||
plugin: params.plugin,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
allowFrom: filtered,
|
||||
@@ -152,12 +151,12 @@ function resolveOwnerAllowFromList(params: {
|
||||
* Returns null if commands.allowFrom is not configured at all (fall back to channel allowFrom).
|
||||
*/
|
||||
function resolveCommandsAllowFromList(params: {
|
||||
dock?: ChannelDock;
|
||||
plugin?: ChannelPlugin;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
providerId?: ChannelId;
|
||||
}): string[] | null {
|
||||
const { dock, cfg, accountId, providerId } = params;
|
||||
const { plugin, cfg, accountId, providerId } = params;
|
||||
const commandsAllowFrom = cfg.commands?.allowFrom;
|
||||
if (!commandsAllowFrom || typeof commandsAllowFrom !== "object") {
|
||||
return null; // Not configured, fall back to channel allowFrom
|
||||
@@ -174,7 +173,7 @@ function resolveCommandsAllowFromList(params: {
|
||||
}
|
||||
|
||||
return formatAllowFromList({
|
||||
dock,
|
||||
plugin,
|
||||
cfg,
|
||||
accountId,
|
||||
allowFrom: rawList,
|
||||
@@ -211,7 +210,7 @@ function shouldUseFromAsSenderFallback(params: {
|
||||
}
|
||||
|
||||
function resolveSenderCandidates(params: {
|
||||
dock?: ChannelDock;
|
||||
plugin?: ChannelPlugin;
|
||||
providerId?: ChannelId;
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -220,7 +219,7 @@ function resolveSenderCandidates(params: {
|
||||
from?: string | null;
|
||||
chatType?: string | null;
|
||||
}): string[] {
|
||||
const { dock, cfg, accountId } = params;
|
||||
const { plugin, cfg, accountId } = params;
|
||||
const candidates: string[] = [];
|
||||
const pushCandidate = (value?: string | null) => {
|
||||
const trimmed = (value ?? "").trim();
|
||||
@@ -245,7 +244,7 @@ function resolveSenderCandidates(params: {
|
||||
|
||||
const normalized: string[] = [];
|
||||
for (const sender of candidates) {
|
||||
const entries = normalizeAllowFromEntry({ dock, cfg, accountId, value: sender });
|
||||
const entries = normalizeAllowFromEntry({ plugin, cfg, accountId, value: sender });
|
||||
for (const entry of entries) {
|
||||
if (!normalized.includes(entry)) {
|
||||
normalized.push(entry);
|
||||
@@ -262,36 +261,36 @@ export function resolveCommandAuthorization(params: {
|
||||
}): CommandAuthorization {
|
||||
const { ctx, cfg, commandAuthorized } = params;
|
||||
const providerId = resolveProviderFromContext(ctx, cfg);
|
||||
const dock = providerId ? getChannelDock(providerId) : undefined;
|
||||
const plugin = providerId ? getChannelPlugin(providerId) : undefined;
|
||||
const from = (ctx.From ?? "").trim();
|
||||
const to = (ctx.To ?? "").trim();
|
||||
|
||||
// Check if commands.allowFrom is configured (separate command authorization)
|
||||
const commandsAllowFromList = resolveCommandsAllowFromList({
|
||||
dock,
|
||||
plugin,
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
providerId,
|
||||
});
|
||||
|
||||
const allowFromRaw = dock?.config?.resolveAllowFrom
|
||||
? dock.config.resolveAllowFrom({ cfg, accountId: ctx.AccountId })
|
||||
const allowFromRaw = plugin?.config?.resolveAllowFrom
|
||||
? plugin.config.resolveAllowFrom({ cfg, accountId: ctx.AccountId })
|
||||
: [];
|
||||
const allowFromList = formatAllowFromList({
|
||||
dock,
|
||||
plugin,
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [],
|
||||
});
|
||||
const configOwnerAllowFromList = resolveOwnerAllowFromList({
|
||||
dock,
|
||||
plugin,
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
providerId,
|
||||
allowFrom: cfg.commands?.ownerAllowFrom,
|
||||
});
|
||||
const contextOwnerAllowFromList = resolveOwnerAllowFromList({
|
||||
dock,
|
||||
plugin,
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
providerId,
|
||||
@@ -303,7 +302,7 @@ export function resolveCommandAuthorization(params: {
|
||||
const ownerCandidatesForCommands = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
|
||||
if (!allowAll && ownerCandidatesForCommands.length === 0 && to) {
|
||||
const normalizedTo = normalizeAllowFromEntry({
|
||||
dock,
|
||||
plugin,
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
value: to,
|
||||
@@ -328,7 +327,7 @@ export function resolveCommandAuthorization(params: {
|
||||
);
|
||||
|
||||
const senderCandidates = resolveSenderCandidates({
|
||||
dock,
|
||||
plugin,
|
||||
providerId,
|
||||
cfg,
|
||||
accountId: ctx.AccountId,
|
||||
@@ -345,7 +344,7 @@ export function resolveCommandAuthorization(params: {
|
||||
: undefined;
|
||||
const senderId = matchedSender ?? senderCandidates[0];
|
||||
|
||||
const enforceOwner = Boolean(dock?.commands?.enforceOwnerForCommands);
|
||||
const enforceOwner = Boolean(plugin?.commands?.enforceOwnerForCommands);
|
||||
const senderIsOwnerByIdentity = Boolean(matchedSender);
|
||||
const senderIsOwnerByScope =
|
||||
isInternalMessageChannel(ctx.Provider) &&
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listChannelDocks } from "../channels/dock.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { COMMAND_ARG_FORMATTERS } from "./commands-args.js";
|
||||
import type {
|
||||
@@ -46,14 +46,14 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti
|
||||
};
|
||||
}
|
||||
|
||||
type ChannelDock = ReturnType<typeof listChannelDocks>[number];
|
||||
type ChannelPlugin = ReturnType<typeof listChannelPlugins>[number];
|
||||
|
||||
function defineDockCommand(dock: ChannelDock): ChatCommandDefinition {
|
||||
function defineDockCommand(plugin: ChannelPlugin): ChatCommandDefinition {
|
||||
return defineChatCommand({
|
||||
key: `dock:${dock.id}`,
|
||||
nativeName: `dock_${dock.id}`,
|
||||
description: `Switch to ${dock.id} for replies.`,
|
||||
textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`],
|
||||
key: `dock:${plugin.id}`,
|
||||
nativeName: `dock_${plugin.id}`,
|
||||
description: `Switch to ${plugin.id} for replies.`,
|
||||
textAliases: [`/dock-${plugin.id}`, `/dock_${plugin.id}`],
|
||||
category: "docks",
|
||||
});
|
||||
}
|
||||
@@ -758,9 +758,9 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||
},
|
||||
],
|
||||
}),
|
||||
...listChannelDocks()
|
||||
.filter((dock) => dock.capabilities.nativeCommands)
|
||||
.map((dock) => defineDockCommand(dock)),
|
||||
...listChannelPlugins()
|
||||
.filter((plugin) => plugin.capabilities.nativeCommands)
|
||||
.map((plugin) => defineDockCommand(plugin)),
|
||||
];
|
||||
|
||||
registerAlias(commands, "whoami", "/id");
|
||||
@@ -792,9 +792,9 @@ export function getNativeCommandSurfaces(): Set<string> {
|
||||
return cachedNativeCommandSurfaces;
|
||||
}
|
||||
cachedNativeCommandSurfaces = new Set(
|
||||
listChannelDocks()
|
||||
.filter((dock) => dock.capabilities.nativeCommands)
|
||||
.map((dock) => dock.id),
|
||||
listChannelPlugins()
|
||||
.filter((plugin) => plugin.capabilities.nativeCommands)
|
||||
.map((plugin) => plugin.id),
|
||||
);
|
||||
cachedNativeRegistry = registry;
|
||||
return cachedNativeCommandSurfaces;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
|
||||
import type { NormalizedUsage } from "../../agents/usage.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js";
|
||||
import { normalizeAnyChannelId, normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -44,8 +44,8 @@ export function buildThreadingToolContext(params: {
|
||||
}
|
||||
const provider = normalizeChannelId(rawProvider) ?? normalizeAnyChannelId(rawProvider);
|
||||
// Fallback for unrecognized/plugin channels (e.g., BlueBubbles before plugin registry init)
|
||||
const dock = provider ? getChannelDock(provider) : undefined;
|
||||
if (!dock?.threading?.buildToolContext) {
|
||||
const threading = provider ? getChannelPlugin(provider)?.threading : undefined;
|
||||
if (!threading?.buildToolContext) {
|
||||
return {
|
||||
currentChannelId: originTo?.trim() || undefined,
|
||||
currentChannelProvider: provider ?? (rawProvider as ChannelId),
|
||||
@@ -54,7 +54,7 @@ export function buildThreadingToolContext(params: {
|
||||
};
|
||||
}
|
||||
const context =
|
||||
dock.threading.buildToolContext({
|
||||
threading.buildToolContext({
|
||||
cfg: config,
|
||||
accountId: sessionCtx.AccountId,
|
||||
context: {
|
||||
@@ -72,7 +72,7 @@ export function buildThreadingToolContext(params: {
|
||||
}) ?? {};
|
||||
return {
|
||||
...context,
|
||||
currentChannelProvider: provider!, // guaranteed non-null since dock exists
|
||||
currentChannelProvider: provider!, // guaranteed non-null since threading exists
|
||||
currentMessageId: context.currentMessageId ?? currentMessageId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
|
||||
import { resolveAccountEntry } from "../../routing/account-lookup.js";
|
||||
@@ -34,7 +33,7 @@ function resolveProviderChunkContext(
|
||||
const providerKey = normalizeChunkProvider(provider);
|
||||
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
|
||||
const providerChunkLimit = providerId
|
||||
? getChannelDock(providerId)?.outbound?.textChunkLimit
|
||||
? getChannelPlugin(providerId)?.outbound?.textChunkLimit
|
||||
: undefined;
|
||||
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, {
|
||||
fallbackLimit: providerChunkLimit,
|
||||
@@ -209,7 +208,7 @@ export function resolveBlockStreamingCoalescing(
|
||||
// when chunkMode="newline", matching the delivery-time splitting behavior.
|
||||
const chunkMode = opts?.chunkMode ?? resolveChunkMode(cfg, providerKey, accountId);
|
||||
const providerDefaults = providerId
|
||||
? getChannelDock(providerId)?.streaming?.blockStreamingCoalesceDefaults
|
||||
? getChannelPlugin(providerId)?.streaming?.blockStreamingCoalesceDefaults
|
||||
: undefined;
|
||||
const providerCfg = resolveProviderBlockStreamingCoalesce({
|
||||
cfg,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import { listPairingChannels } from "../../channels/plugins/pairing.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
validateConfigObjectWithPlugins,
|
||||
writeConfigFile,
|
||||
} from "../../config/config.js";
|
||||
import { isBlockedObjectKey } from "../../infra/prototype-keys.js";
|
||||
import {
|
||||
addChannelAllowFromStoreEntry,
|
||||
readChannelAllowFromStore,
|
||||
@@ -198,104 +196,6 @@ async function updatePairingStoreAllowlist(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAccountTarget(
|
||||
parsed: Record<string, unknown>,
|
||||
channelId: ChannelId,
|
||||
accountId?: string | null,
|
||||
) {
|
||||
const channels = (parsed.channels ??= {}) as Record<string, unknown>;
|
||||
const channel = (channels[channelId] ??= {}) as Record<string, unknown>;
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
if (isBlockedObjectKey(normalizedAccountId)) {
|
||||
return {
|
||||
target: channel,
|
||||
pathPrefix: `channels.${channelId}`,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
writeTarget: { kind: "channel", scope: { channelId } } as const,
|
||||
};
|
||||
}
|
||||
const hasAccounts = Boolean(channel.accounts && typeof channel.accounts === "object");
|
||||
const useAccount = normalizedAccountId !== DEFAULT_ACCOUNT_ID || hasAccounts;
|
||||
if (!useAccount) {
|
||||
return {
|
||||
target: channel,
|
||||
pathPrefix: `channels.${channelId}`,
|
||||
accountId: normalizedAccountId,
|
||||
writeTarget: { kind: "channel", scope: { channelId } } as const,
|
||||
};
|
||||
}
|
||||
const accounts = (channel.accounts ??= {}) as Record<string, unknown>;
|
||||
const existingAccount = Object.hasOwn(accounts, normalizedAccountId)
|
||||
? accounts[normalizedAccountId]
|
||||
: undefined;
|
||||
if (!existingAccount || typeof existingAccount !== "object") {
|
||||
accounts[normalizedAccountId] = {};
|
||||
}
|
||||
const account = accounts[normalizedAccountId] as Record<string, unknown>;
|
||||
return {
|
||||
target: account,
|
||||
pathPrefix: `channels.${channelId}.accounts.${normalizedAccountId}`,
|
||||
accountId: normalizedAccountId,
|
||||
writeTarget: {
|
||||
kind: "account",
|
||||
scope: { channelId, accountId: normalizedAccountId },
|
||||
} as const,
|
||||
};
|
||||
}
|
||||
|
||||
function getNestedValue(root: Record<string, unknown>, path: string[]): unknown {
|
||||
let current: unknown = root;
|
||||
for (const key of path) {
|
||||
if (!current || typeof current !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function ensureNestedObject(
|
||||
root: Record<string, unknown>,
|
||||
path: string[],
|
||||
): Record<string, unknown> {
|
||||
let current = root;
|
||||
for (const key of path) {
|
||||
const existing = current[key];
|
||||
if (!existing || typeof existing !== "object") {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key] as Record<string, unknown>;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function setNestedValue(root: Record<string, unknown>, path: string[], value: unknown) {
|
||||
if (path.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (path.length === 1) {
|
||||
root[path[0]] = value;
|
||||
return;
|
||||
}
|
||||
const parent = ensureNestedObject(root, path.slice(0, -1));
|
||||
parent[path[path.length - 1]] = value;
|
||||
}
|
||||
|
||||
function deleteNestedValue(root: Record<string, unknown>, path: string[]) {
|
||||
if (path.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (path.length === 1) {
|
||||
delete root[path[0]];
|
||||
return;
|
||||
}
|
||||
const parent = getNestedValue(root, path.slice(0, -1));
|
||||
if (!parent || typeof parent !== "object") {
|
||||
return;
|
||||
}
|
||||
delete (parent as Record<string, unknown>)[path[path.length - 1]];
|
||||
}
|
||||
|
||||
function mapResolvedAllowlistNames(entries: ResolvedAllowlistName[]): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
for (const entry of entries) {
|
||||
@@ -375,7 +275,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
||||
const plugin = getChannelPlugin(channelId);
|
||||
|
||||
if (parsed.action === "list") {
|
||||
const supportsStore = listPairingChannels().includes(channelId);
|
||||
const supportsStore = Boolean(plugin?.pairing);
|
||||
if (!plugin?.allowlist?.readConfig && !supportsStore) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
@@ -493,7 +393,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
||||
}
|
||||
|
||||
const shouldUpdateConfig = parsed.target !== "store";
|
||||
const shouldTouchStore = parsed.target !== "config" && listPairingChannels().includes(channelId);
|
||||
const shouldTouchStore = parsed.target !== "config" && Boolean(plugin?.pairing);
|
||||
|
||||
if (shouldUpdateConfig) {
|
||||
if (parsed.scope === "all") {
|
||||
@@ -502,19 +402,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
||||
reply: { text: "⚠️ /allowlist add|remove requires scope dm or group." },
|
||||
};
|
||||
}
|
||||
const {
|
||||
target,
|
||||
pathPrefix,
|
||||
accountId: normalizedAccountId,
|
||||
writeTarget,
|
||||
} = resolveAccountTarget(structuredClone({ channels: {} }), channelId, accountId);
|
||||
void target;
|
||||
const editSpec = plugin?.allowlist?.resolveConfigEdit?.({
|
||||
scope: parsed.scope,
|
||||
pathPrefix,
|
||||
writeTarget,
|
||||
});
|
||||
if (!editSpec) {
|
||||
if (!plugin?.allowlist?.applyConfigEdit) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
@@ -531,14 +419,35 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
||||
};
|
||||
}
|
||||
const parsedConfig = structuredClone(snapshot.parsed as Record<string, unknown>);
|
||||
const resolvedTarget = resolveAccountTarget(parsedConfig, channelId, accountId);
|
||||
const editResult = await plugin.allowlist.applyConfigEdit({
|
||||
cfg: params.cfg,
|
||||
parsedConfig,
|
||||
accountId,
|
||||
scope: parsed.scope,
|
||||
action: parsed.action,
|
||||
entry: parsed.entry,
|
||||
});
|
||||
if (!editResult) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: `⚠️ ${channelId} does not support ${parsed.scope} allowlist edits via /allowlist.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (editResult.kind === "invalid-entry") {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "⚠️ Invalid allowlist entry." },
|
||||
};
|
||||
}
|
||||
const deniedText = resolveConfigWriteDeniedText({
|
||||
cfg: params.cfg,
|
||||
channel: params.command.channel,
|
||||
channelId,
|
||||
accountId: params.ctx.AccountId,
|
||||
gatewayClientScopes: params.ctx.GatewayClientScopes,
|
||||
target: editSpec.writeTarget,
|
||||
target: editResult.writeTarget,
|
||||
});
|
||||
if (deniedText) {
|
||||
return {
|
||||
@@ -548,82 +457,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const existing: string[] = [];
|
||||
for (const path of editSpec.readPaths) {
|
||||
const existingRaw = getNestedValue(resolvedTarget.target, path);
|
||||
if (!Array.isArray(existingRaw)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of existingRaw) {
|
||||
const value = String(entry).trim();
|
||||
if (!value || existing.includes(value)) {
|
||||
continue;
|
||||
}
|
||||
existing.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedEntry = normalizeAllowFrom({
|
||||
cfg: params.cfg,
|
||||
channelId,
|
||||
accountId: normalizedAccountId,
|
||||
values: [parsed.entry],
|
||||
});
|
||||
if (normalizedEntry.length === 0) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "⚠️ Invalid allowlist entry." },
|
||||
};
|
||||
}
|
||||
|
||||
const existingNormalized = normalizeAllowFrom({
|
||||
cfg: params.cfg,
|
||||
channelId,
|
||||
accountId: normalizedAccountId,
|
||||
values: existing,
|
||||
});
|
||||
|
||||
const shouldMatch = (value: string) => normalizedEntry.includes(value);
|
||||
|
||||
let configChanged = false;
|
||||
let next = existing;
|
||||
const configHasEntry = existingNormalized.some((value) => shouldMatch(value));
|
||||
if (parsed.action === "add") {
|
||||
if (!configHasEntry) {
|
||||
next = [...existing, parsed.entry.trim()];
|
||||
configChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.action === "remove") {
|
||||
const keep: string[] = [];
|
||||
for (const entry of existing) {
|
||||
const normalized = normalizeAllowFrom({
|
||||
cfg: params.cfg,
|
||||
channelId,
|
||||
accountId: normalizedAccountId,
|
||||
values: [entry],
|
||||
});
|
||||
if (normalized.some((value) => shouldMatch(value))) {
|
||||
configChanged = true;
|
||||
continue;
|
||||
}
|
||||
keep.push(entry);
|
||||
}
|
||||
next = keep;
|
||||
}
|
||||
|
||||
if (configChanged) {
|
||||
if (next.length === 0) {
|
||||
deleteNestedValue(resolvedTarget.target, editSpec.writePath);
|
||||
} else {
|
||||
setNestedValue(resolvedTarget.target, editSpec.writePath, next);
|
||||
}
|
||||
for (const path of editSpec.cleanupPaths ?? []) {
|
||||
deleteNestedValue(resolvedTarget.target, path);
|
||||
}
|
||||
}
|
||||
const configChanged = editResult.changed;
|
||||
|
||||
if (configChanged) {
|
||||
const validated = validateConfigObjectWithPlugins(parsedConfig);
|
||||
@@ -655,7 +489,7 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
||||
const scopeLabel = parsed.scope === "dm" ? "DM" : "group";
|
||||
const locations: string[] = [];
|
||||
if (configChanged) {
|
||||
locations.push(`${resolvedTarget.pathPrefix}.${editSpec.writePath.join(".")}`);
|
||||
locations.push(editResult.pathLabel);
|
||||
}
|
||||
if (shouldTouchStore) {
|
||||
locations.push("pairing store");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createOpenClawTools } from "../../agents/openclaw-tools.js";
|
||||
import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js";
|
||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||
import { applyOwnerOnlyToolPolicy } from "../../agents/tool-policy.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
@@ -402,7 +402,7 @@ export async function handleInlineActions(params: {
|
||||
|
||||
const isEmptyConfig = Object.keys(cfg).length === 0;
|
||||
const skipWhenConfigEmpty = command.channelId
|
||||
? Boolean(getChannelDock(command.channelId)?.commands?.skipWhenConfigEmpty)
|
||||
? Boolean(getChannelPlugin(command.channelId)?.commands?.skipWhenConfigEmpty)
|
||||
: false;
|
||||
if (
|
||||
skipWhenConfigEmpty &&
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import {
|
||||
getChannelPlugin,
|
||||
normalizeChannelId as normalizePluginChannelId,
|
||||
@@ -39,7 +38,7 @@ function resolveDockChannelId(raw?: string | null): ChannelId | null {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (getChannelDock(normalized as ChannelId)) {
|
||||
if (getChannelPlugin(normalized as ChannelId)) {
|
||||
return normalized as ChannelId;
|
||||
}
|
||||
} catch {
|
||||
@@ -68,7 +67,7 @@ export function resolveGroupRequireMention(params: {
|
||||
const groupSpace = ctx.GroupSpace?.trim();
|
||||
let requireMention: boolean | undefined;
|
||||
try {
|
||||
requireMention = getChannelDock(channel)?.groups?.resolveRequireMention?.({
|
||||
requireMention = getChannelPlugin(channel)?.groups?.resolveRequireMention?.({
|
||||
cfg,
|
||||
groupId,
|
||||
groupChannel,
|
||||
@@ -158,7 +157,7 @@ export function buildGroupIntro(params: {
|
||||
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim();
|
||||
const groupSpace = params.sessionCtx.GroupSpace?.trim();
|
||||
const providerIdsLine = providerId
|
||||
? getChannelDock(providerId)?.groups?.resolveGroupIntroHint?.({
|
||||
? getChannelPlugin(providerId)?.groups?.resolveGroupIntroHint?.({
|
||||
cfg: params.cfg,
|
||||
groupId,
|
||||
groupChannel,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { compileConfigRegexes, type ConfigRegexRejectReason } from "../../security/config-regex.js";
|
||||
@@ -199,7 +198,7 @@ export function stripMentions(
|
||||
): string {
|
||||
let result = text;
|
||||
const providerId = ctx.Provider ? normalizeChannelId(ctx.Provider) : null;
|
||||
const providerMentions = providerId ? getChannelDock(providerId)?.mentions : undefined;
|
||||
const providerMentions = providerId ? getChannelPlugin(providerId)?.mentions : undefined;
|
||||
const configRegexes = compileMentionPatternsCached({
|
||||
patterns: normalizeMentionPatterns(resolveMentionPatterns(cfg, agentId)),
|
||||
flags: "gi",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import type { AgentElevatedAllowFromConfig, OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeStringEntries } from "../../shared/string-normalization.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
@@ -34,8 +33,9 @@ function resolveAllowFromFormatter(params: {
|
||||
accountId?: string;
|
||||
}): AllowFromFormatter {
|
||||
const normalizedProvider = normalizeChannelId(params.provider);
|
||||
const dock = normalizedProvider ? getChannelDock(normalizedProvider) : undefined;
|
||||
const formatAllowFrom = dock?.config?.formatAllowFrom;
|
||||
const formatAllowFrom = normalizedProvider
|
||||
? getChannelPlugin(normalizedProvider)?.config?.formatAllowFrom
|
||||
: undefined;
|
||||
if (!formatAllowFrom) {
|
||||
return (values) => normalizeStringEntries(values);
|
||||
}
|
||||
@@ -192,11 +192,12 @@ export function resolveElevatedPermissions(params: {
|
||||
}
|
||||
|
||||
const normalizedProvider = normalizeChannelId(params.provider);
|
||||
const dock = normalizedProvider ? getChannelDock(normalizedProvider) : undefined;
|
||||
const fallbackAllowFrom = dock?.elevated?.allowFromFallback?.({
|
||||
cfg: params.cfg,
|
||||
accountId: params.ctx.AccountId,
|
||||
});
|
||||
const fallbackAllowFrom = normalizedProvider
|
||||
? getChannelPlugin(normalizedProvider)?.elevated?.allowFromFallback?.({
|
||||
cfg: params.cfg,
|
||||
accountId: params.ctx.AccountId,
|
||||
})
|
||||
: undefined;
|
||||
const formatAllowFrom = resolveAllowFromFormatter({
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { ReplyToMode } from "../../config/types.js";
|
||||
import type { OriginatingChannelType } from "../templating.js";
|
||||
@@ -15,7 +14,7 @@ export function resolveReplyToMode(
|
||||
if (!provider) {
|
||||
return "all";
|
||||
}
|
||||
const resolved = getChannelDock(provider)?.threading?.resolveReplyToMode?.({
|
||||
const resolved = getChannelPlugin(provider)?.threading?.resolveReplyToMode?.({
|
||||
cfg,
|
||||
accountId,
|
||||
chatType,
|
||||
@@ -59,9 +58,9 @@ export function createReplyToModeFilterForChannel(
|
||||
const isWebchat = normalized === "webchat";
|
||||
// Default: allow explicit reply tags/directives even when replyToMode is "off".
|
||||
// Unknown channels fail closed; internal webchat stays allowed.
|
||||
const dock = provider ? getChannelDock(provider) : undefined;
|
||||
const threading = provider ? getChannelPlugin(provider)?.threading : undefined;
|
||||
const allowExplicitReplyTagsWhenOff = provider
|
||||
? (dock?.threading?.allowExplicitReplyTagsWhenOff ?? dock?.threading?.allowTagsWhenOff ?? true)
|
||||
? (threading?.allowExplicitReplyTagsWhenOff ?? threading?.allowTagsWhenOff ?? true)
|
||||
: isWebchat;
|
||||
return createReplyToModeFilter(mode, {
|
||||
allowExplicitReplyTagsWhenOff,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js";
|
||||
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
|
||||
import { discordOutbound } from "../../channels/plugins/outbound/discord.js";
|
||||
import { imessageOutbound } from "../../channels/plugins/outbound/imessage.js";
|
||||
import { signalOutbound } from "../../channels/plugins/outbound/signal.js";
|
||||
import { slackOutbound } from "../../channels/plugins/outbound/slack.js";
|
||||
import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js";
|
||||
import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js";
|
||||
import {
|
||||
discordOutbound,
|
||||
imessageOutbound,
|
||||
signalOutbound,
|
||||
slackOutbound,
|
||||
telegramOutbound,
|
||||
whatsappOutbound,
|
||||
} from "../../../test/channel-outbounds.js";
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Shim: re-exports from extension
|
||||
export * from "../../../../extensions/discord/src/outbound-adapter.js";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { imessageOutbound } from "../../../../test/channel-outbounds.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { imessageOutbound } from "./imessage.js";
|
||||
|
||||
describe("imessageOutbound", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { sendMessageIMessage } from "../../../../extensions/imessage/src/send.js";
|
||||
import {
|
||||
resolveOutboundSendDep,
|
||||
type OutboundSendDeps,
|
||||
} from "../../../infra/outbound/send-deps.js";
|
||||
import {
|
||||
createScopedChannelMediaMaxBytesResolver,
|
||||
createDirectTextMediaOutbound,
|
||||
} from "./direct-text-media.js";
|
||||
|
||||
function resolveIMessageSender(deps: OutboundSendDeps | undefined) {
|
||||
return (
|
||||
resolveOutboundSendDep<typeof sendMessageIMessage>(deps, "imessage") ?? sendMessageIMessage
|
||||
);
|
||||
}
|
||||
|
||||
export const imessageOutbound = createDirectTextMediaOutbound({
|
||||
channel: "imessage",
|
||||
resolveSender: resolveIMessageSender,
|
||||
resolveMaxBytes: createScopedChannelMediaMaxBytesResolver("imessage"),
|
||||
buildTextOptions: ({ cfg, maxBytes, accountId, replyToId }) => ({
|
||||
config: cfg,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? undefined,
|
||||
}),
|
||||
buildMediaOptions: ({ cfg, mediaUrl, maxBytes, accountId, replyToId, mediaLocalRoots }) => ({
|
||||
config: cfg,
|
||||
mediaUrl,
|
||||
maxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? undefined,
|
||||
mediaLocalRoots,
|
||||
}),
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { signalOutbound } from "../../../../test/channel-outbounds.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import { signalOutbound } from "./signal.js";
|
||||
|
||||
describe("signalOutbound", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
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 type { ChannelOutboundAdapter } from "../types.js";
|
||||
import { createScopedChannelMediaMaxBytesResolver } from "./direct-text-media.js";
|
||||
|
||||
function resolveSignalSender(deps: OutboundSendDeps | undefined) {
|
||||
return resolveOutboundSendDep<typeof sendMessageSignal>(deps, "signal") ?? sendMessageSignal;
|
||||
}
|
||||
|
||||
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,
|
||||
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,10 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { slackOutbound } from "../../../../test/channel-outbounds.js";
|
||||
import type { ReplyPayload } from "../../../auto-reply/types.js";
|
||||
import {
|
||||
installSendPayloadContractSuite,
|
||||
primeSendMock,
|
||||
} from "../../../test-utils/send-payload-contract.js";
|
||||
import { slackOutbound } from "./slack.js";
|
||||
|
||||
function createHarness(params: {
|
||||
payload: ReplyPayload;
|
||||
|
||||
@@ -10,8 +10,8 @@ vi.mock("../../../plugins/hook-runner-global.js", () => ({
|
||||
}));
|
||||
|
||||
import { sendMessageSlack } from "../../../../extensions/slack/src/send.js";
|
||||
import { slackOutbound } from "../../../../test/channel-outbounds.js";
|
||||
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
|
||||
import { slackOutbound } from "./slack.js";
|
||||
|
||||
type SlackSendTextCtx = {
|
||||
to: string;
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
import { parseSlackBlocksInput } from "../../../../extensions/slack/src/blocks-input.js";
|
||||
import {
|
||||
buildSlackInteractiveBlocks,
|
||||
type SlackBlock,
|
||||
} from "../../../../extensions/slack/src/blocks-render.js";
|
||||
import { sendMessageSlack, type SlackSendIdentity } from "../../../../extensions/slack/src/send.js";
|
||||
import type { OutboundIdentity } from "../../../infra/outbound/identity.js";
|
||||
import { resolveOutboundSendDep } from "../../../infra/outbound/send-deps.js";
|
||||
import {
|
||||
resolveInteractiveTextFallback,
|
||||
type InteractiveReply,
|
||||
} from "../../../interactive/payload.js";
|
||||
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
import {
|
||||
resolvePayloadMediaUrls,
|
||||
sendPayloadMediaSequence,
|
||||
sendTextMediaPayload,
|
||||
} from "./direct-text-media.js";
|
||||
|
||||
const SLACK_MAX_BLOCKS = 50;
|
||||
|
||||
function resolveRenderedInteractiveBlocks(
|
||||
interactive?: InteractiveReply,
|
||||
): SlackBlock[] | undefined {
|
||||
if (!interactive) {
|
||||
return undefined;
|
||||
}
|
||||
const blocks = buildSlackInteractiveBlocks(interactive);
|
||||
return blocks.length > 0 ? blocks : undefined;
|
||||
}
|
||||
|
||||
function resolveSlackSendIdentity(identity?: OutboundIdentity): SlackSendIdentity | undefined {
|
||||
if (!identity) {
|
||||
return undefined;
|
||||
}
|
||||
const username = identity.name?.trim() || undefined;
|
||||
const iconUrl = identity.avatarUrl?.trim() || undefined;
|
||||
const rawEmoji = identity.emoji?.trim();
|
||||
const iconEmoji = !iconUrl && rawEmoji && /^:[^:\s]+:$/.test(rawEmoji) ? rawEmoji : undefined;
|
||||
if (!username && !iconUrl && !iconEmoji) {
|
||||
return undefined;
|
||||
}
|
||||
return { username, iconUrl, iconEmoji };
|
||||
}
|
||||
|
||||
async function applySlackMessageSendingHooks(params: {
|
||||
to: string;
|
||||
text: string;
|
||||
threadTs?: string;
|
||||
accountId?: string;
|
||||
mediaUrl?: string;
|
||||
}): Promise<{ cancelled: boolean; text: string }> {
|
||||
const hookRunner = getGlobalHookRunner();
|
||||
if (!hookRunner?.hasHooks("message_sending")) {
|
||||
return { cancelled: false, text: params.text };
|
||||
}
|
||||
const hookResult = await hookRunner.runMessageSending(
|
||||
{
|
||||
to: params.to,
|
||||
content: params.text,
|
||||
metadata: {
|
||||
threadTs: params.threadTs,
|
||||
channelId: params.to,
|
||||
...(params.mediaUrl ? { mediaUrl: params.mediaUrl } : {}),
|
||||
},
|
||||
},
|
||||
{ channelId: "slack", accountId: params.accountId ?? undefined },
|
||||
);
|
||||
if (hookResult?.cancel) {
|
||||
return { cancelled: true, text: params.text };
|
||||
}
|
||||
return { cancelled: false, text: hookResult?.content ?? params.text };
|
||||
}
|
||||
|
||||
async function sendSlackOutboundMessage(params: {
|
||||
cfg: NonNullable<Parameters<typeof sendMessageSlack>[2]>["cfg"];
|
||||
to: string;
|
||||
text: string;
|
||||
mediaUrl?: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
blocks?: NonNullable<Parameters<typeof sendMessageSlack>[2]>["blocks"];
|
||||
accountId?: string | null;
|
||||
deps?: { [channelId: string]: unknown } | null;
|
||||
replyToId?: string | null;
|
||||
threadId?: string | number | null;
|
||||
identity?: OutboundIdentity;
|
||||
}) {
|
||||
const send =
|
||||
resolveOutboundSendDep<typeof sendMessageSlack>(params.deps, "slack") ?? sendMessageSlack;
|
||||
// Use threadId fallback so routed tool notifications stay in the Slack thread.
|
||||
const threadTs =
|
||||
params.replyToId ?? (params.threadId != null ? String(params.threadId) : undefined);
|
||||
const hookResult = await applySlackMessageSendingHooks({
|
||||
to: params.to,
|
||||
text: params.text,
|
||||
threadTs,
|
||||
mediaUrl: params.mediaUrl,
|
||||
accountId: params.accountId ?? undefined,
|
||||
});
|
||||
if (hookResult.cancelled) {
|
||||
return {
|
||||
channel: "slack" as const,
|
||||
messageId: "cancelled-by-hook",
|
||||
channelId: params.to,
|
||||
meta: { cancelled: true },
|
||||
};
|
||||
}
|
||||
|
||||
const slackIdentity = resolveSlackSendIdentity(params.identity);
|
||||
const result = await send(params.to, hookResult.text, {
|
||||
cfg: params.cfg,
|
||||
threadTs,
|
||||
accountId: params.accountId ?? undefined,
|
||||
...(params.mediaUrl
|
||||
? { mediaUrl: params.mediaUrl, mediaLocalRoots: params.mediaLocalRoots }
|
||||
: {}),
|
||||
...(params.blocks ? { blocks: params.blocks } : {}),
|
||||
...(slackIdentity ? { identity: slackIdentity } : {}),
|
||||
});
|
||||
return { channel: "slack" as const, ...result };
|
||||
}
|
||||
|
||||
function resolveSlackBlocks(payload: {
|
||||
channelData?: Record<string, unknown>;
|
||||
interactive?: InteractiveReply;
|
||||
}) {
|
||||
const slackData = payload.channelData?.slack;
|
||||
const renderedInteractive = resolveRenderedInteractiveBlocks(payload.interactive);
|
||||
if (!slackData || typeof slackData !== "object" || Array.isArray(slackData)) {
|
||||
return renderedInteractive;
|
||||
}
|
||||
let existingBlocks: SlackBlock[] | undefined;
|
||||
existingBlocks = parseSlackBlocksInput((slackData as { blocks?: unknown }).blocks) as
|
||||
| SlackBlock[]
|
||||
| undefined;
|
||||
const mergedBlocks = [...(existingBlocks ?? []), ...(renderedInteractive ?? [])];
|
||||
if (mergedBlocks.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (mergedBlocks.length > SLACK_MAX_BLOCKS) {
|
||||
throw new Error(
|
||||
`Slack blocks cannot exceed ${SLACK_MAX_BLOCKS} items after interactive render`,
|
||||
);
|
||||
}
|
||||
return mergedBlocks;
|
||||
}
|
||||
|
||||
export const slackOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: null,
|
||||
textChunkLimit: 4000,
|
||||
sendPayload: async (ctx) => {
|
||||
const payload = {
|
||||
...ctx.payload,
|
||||
text:
|
||||
resolveInteractiveTextFallback({
|
||||
text: ctx.payload.text,
|
||||
interactive: ctx.payload.interactive,
|
||||
}) ?? "",
|
||||
};
|
||||
const blocks = resolveSlackBlocks(payload);
|
||||
if (!blocks) {
|
||||
return await sendTextMediaPayload({
|
||||
channel: "slack",
|
||||
ctx: {
|
||||
...ctx,
|
||||
payload,
|
||||
},
|
||||
adapter: slackOutbound,
|
||||
});
|
||||
}
|
||||
const mediaUrls = resolvePayloadMediaUrls(payload);
|
||||
if (mediaUrls.length === 0) {
|
||||
return await sendSlackOutboundMessage({
|
||||
cfg: ctx.cfg,
|
||||
to: ctx.to,
|
||||
text: payload.text ?? "",
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
blocks,
|
||||
accountId: ctx.accountId,
|
||||
deps: ctx.deps,
|
||||
replyToId: ctx.replyToId,
|
||||
threadId: ctx.threadId,
|
||||
identity: ctx.identity,
|
||||
});
|
||||
}
|
||||
await sendPayloadMediaSequence({
|
||||
text: "",
|
||||
mediaUrls,
|
||||
send: async ({ text, mediaUrl }) =>
|
||||
await sendSlackOutboundMessage({
|
||||
cfg: ctx.cfg,
|
||||
to: ctx.to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
accountId: ctx.accountId,
|
||||
deps: ctx.deps,
|
||||
replyToId: ctx.replyToId,
|
||||
threadId: ctx.threadId,
|
||||
identity: ctx.identity,
|
||||
}),
|
||||
});
|
||||
return await sendSlackOutboundMessage({
|
||||
cfg: ctx.cfg,
|
||||
to: ctx.to,
|
||||
text: payload.text ?? "",
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
blocks,
|
||||
accountId: ctx.accountId,
|
||||
deps: ctx.deps,
|
||||
replyToId: ctx.replyToId,
|
||||
threadId: ctx.threadId,
|
||||
identity: ctx.identity,
|
||||
});
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity }) => {
|
||||
return await sendSlackOutboundMessage({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId,
|
||||
deps,
|
||||
replyToId,
|
||||
threadId,
|
||||
identity,
|
||||
});
|
||||
},
|
||||
sendMedia: async ({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaLocalRoots,
|
||||
accountId,
|
||||
deps,
|
||||
replyToId,
|
||||
threadId,
|
||||
identity,
|
||||
}) => {
|
||||
return await sendSlackOutboundMessage({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
mediaUrl,
|
||||
mediaLocalRoots,
|
||||
accountId,
|
||||
deps,
|
||||
replyToId,
|
||||
threadId,
|
||||
identity,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "../../../../extensions/telegram/src/outbound-adapter.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Shim: re-exports from extensions/whatsapp/src/outbound-adapter.ts
|
||||
export * from "../../../../extensions/whatsapp/src/outbound-adapter.js";
|
||||
@@ -1,10 +1,9 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { normalizeSignalAccountInput } from "../../../extensions/signal/src/setup-surface.js";
|
||||
import { telegramOutbound, whatsappOutbound } from "../../../test/channel-outbounds.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeIMessageMessagingTarget } from "./normalize/imessage.js";
|
||||
import { looksLikeSignalTargetId, normalizeSignalMessagingTarget } from "./normalize/signal.js";
|
||||
import { telegramOutbound } from "./outbound/telegram.js";
|
||||
import { whatsappOutbound } from "./outbound/whatsapp.js";
|
||||
|
||||
function expectWhatsAppTargetResolutionError(result: unknown) {
|
||||
expect(result).toEqual({
|
||||
|
||||
@@ -481,6 +481,35 @@ export type ChannelExecApprovalAdapter = {
|
||||
};
|
||||
|
||||
export type ChannelAllowlistAdapter = {
|
||||
applyConfigEdit?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
parsedConfig: Record<string, unknown>;
|
||||
accountId?: string | null;
|
||||
scope: "dm" | "group";
|
||||
action: "add" | "remove";
|
||||
entry: string;
|
||||
}) =>
|
||||
| {
|
||||
kind: "ok";
|
||||
changed: boolean;
|
||||
pathLabel: string;
|
||||
writeTarget: ConfigWriteTarget;
|
||||
}
|
||||
| {
|
||||
kind: "invalid-entry";
|
||||
}
|
||||
| Promise<
|
||||
| {
|
||||
kind: "ok";
|
||||
changed: boolean;
|
||||
pathLabel: string;
|
||||
writeTarget: ConfigWriteTarget;
|
||||
}
|
||||
| {
|
||||
kind: "invalid-entry";
|
||||
}
|
||||
>
|
||||
| null;
|
||||
readConfig?: (params: { cfg: OpenClawConfig; accountId?: string | null }) =>
|
||||
| {
|
||||
dmAllowFrom?: Array<string | number>;
|
||||
@@ -504,17 +533,6 @@ export type ChannelAllowlistAdapter = {
|
||||
}) =>
|
||||
| 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import { resolveConversationLabel } from "../../channels/conversation-label.js";
|
||||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import { normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js";
|
||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import { buildGroupDisplayName, resolveGroupSessionKey } from "./group.js";
|
||||
import type { GroupKeyResolution, SessionEntry, SessionOrigin } from "./types.js";
|
||||
@@ -111,7 +110,7 @@ export function deriveGroupSessionPatch(params: {
|
||||
const normalizedChannel = normalizeChannelId(channel);
|
||||
const isChannelProvider = Boolean(
|
||||
normalizedChannel &&
|
||||
getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"),
|
||||
getChannelPlugin(normalizedChannel)?.capabilities.chatTypes.includes("channel"),
|
||||
);
|
||||
const nextGroupChannel =
|
||||
explicitChannel ??
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import "./isolated-agent.mocks.js";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
discordOutbound,
|
||||
imessageOutbound,
|
||||
signalOutbound,
|
||||
slackOutbound,
|
||||
telegramOutbound,
|
||||
whatsappOutbound,
|
||||
} from "../../test/channel-outbounds.js";
|
||||
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
|
||||
import { discordOutbound } from "../channels/plugins/outbound/discord.js";
|
||||
import { imessageOutbound } from "../channels/plugins/outbound/imessage.js";
|
||||
import { signalOutbound } from "../channels/plugins/outbound/signal.js";
|
||||
import { slackOutbound } from "../channels/plugins/outbound/slack.js";
|
||||
import { telegramOutbound } from "../channels/plugins/outbound/telegram.js";
|
||||
import { whatsappOutbound } from "../channels/plugins/outbound/whatsapp.js";
|
||||
import type { CliDeps } from "../cli/deps.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { vi } from "vitest";
|
||||
import { signalOutbound, telegramOutbound } from "../../test/channel-outbounds.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||
import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js";
|
||||
import { signalOutbound } from "../channels/plugins/outbound/signal.js";
|
||||
import { telegramOutbound } from "../channels/plugins/outbound/telegram.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { telegramOutbound } from "../channels/plugins/outbound/telegram.js";
|
||||
import { discordPlugin } from "../../extensions/discord/src/channel.js";
|
||||
import { telegramPlugin } from "../../extensions/telegram/src/channel.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import { createExecApprovalForwarder } from "./exec-approval-forwarder.js";
|
||||
|
||||
const baseRequest = {
|
||||
@@ -25,7 +26,12 @@ const emptyRegistry = createTestRegistry([]);
|
||||
const defaultRegistry = createTestRegistry([
|
||||
{
|
||||
pluginId: "telegram",
|
||||
plugin: createOutboundTestPlugin({ id: "telegram", outbound: telegramOutbound }),
|
||||
plugin: telegramPlugin,
|
||||
source: "test",
|
||||
},
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: discordPlugin,
|
||||
source: "test",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -3,9 +3,9 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { parseTelegramTarget } from "../../extensions/telegram/src/targets.js";
|
||||
import { whatsappOutbound } from "../../test/channel-outbounds.js";
|
||||
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
|
||||
import * as replyModule from "../auto-reply/reply.js";
|
||||
import { whatsappOutbound } from "../channels/plugins/outbound/whatsapp.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveAgentIdFromSessionKey,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { vi } from "vitest";
|
||||
import { signalOutbound } from "../../channels/plugins/outbound/signal.js";
|
||||
import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js";
|
||||
import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js";
|
||||
import {
|
||||
signalOutbound,
|
||||
telegramOutbound,
|
||||
whatsappOutbound,
|
||||
} from "../../../test/channel-outbounds.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { markdownToSignalTextChunks } from "../../../extensions/signal/src/format.js";
|
||||
import { signalOutbound } from "../../channels/plugins/outbound/signal.js";
|
||||
import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js";
|
||||
import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js";
|
||||
import {
|
||||
signalOutbound,
|
||||
telegramOutbound,
|
||||
whatsappOutbound,
|
||||
} from "../../../test/channel-outbounds.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { STATE_DIR } from "../../config/paths.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js";
|
||||
import { telegramOutbound } from "../../channels/plugins/outbound/telegram.js";
|
||||
import { whatsappOutbound } from "../../channels/plugins/outbound/whatsapp.js";
|
||||
import { telegramOutbound, whatsappOutbound } from "../../../test/channel-outbounds.js";
|
||||
import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
|
||||
@@ -241,6 +241,7 @@ export { buildChannelSendResult } from "./channel-send-result.js";
|
||||
export type { ChannelSendRawResult } from "./channel-send-result.js";
|
||||
export { createPluginRuntimeStore } from "./runtime-store.js";
|
||||
export { createScopedChannelConfigBase } from "./channel-config-helpers.js";
|
||||
export { buildAccountScopedAllowlistConfigEditor } from "./allowlist-config-edit.js";
|
||||
export {
|
||||
AllowFromEntrySchema,
|
||||
AllowFromListSchema,
|
||||
|
||||
@@ -54,7 +54,7 @@ export {
|
||||
parseTelegramThreadId,
|
||||
} from "../../extensions/telegram/src/outbound-params.js";
|
||||
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
||||
export { sendTelegramPayloadMessages } from "../channels/plugins/outbound/telegram.js";
|
||||
export { sendTelegramPayloadMessages } from "../../extensions/telegram/src/outbound-adapter.js";
|
||||
|
||||
export {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { normalizeIMessageHandle } from "../../extensions/imessage/src/targets.js";
|
||||
import { imessageOutbound } from "../channels/plugins/outbound/imessage.js";
|
||||
import { imessageOutbound } from "../../test/channel-outbounds.js";
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import { collectStatusIssuesFromLastError } from "../plugin-sdk/status-helpers.js";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user