Skip to content

Commit d163278

Browse files
committed
refactor: move channel delivery and ACP seams into plugins
1 parent d5b12f5 commit d163278

24 files changed

Lines changed: 1175 additions & 644 deletions

extensions/discord/src/channel.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js
3939
import { normalizeMessageChannel } from "../../../src/utils/message-channel.js";
4040
import { isDiscordExecApprovalClientEnabled } from "./exec-approvals.js";
4141
import type { DiscordProbe } from "./probe.js";
42+
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
4243
import { getDiscordRuntime } from "./runtime.js";
4344
import { fetchChannelPermissionsDiscord } from "./send.js";
4445
import { createDiscordSetupWizardProxy, discordSetupAdapter } from "./setup-core.js";
@@ -116,6 +117,84 @@ function hasDiscordExecApprovalDmRoute(cfg: OpenClawConfig): boolean {
116117
});
117118
}
118119

120+
function readDiscordAllowlistConfig(account: ResolvedDiscordAccount) {
121+
const groupOverrides: Array<{ label: string; entries: string[] }> = [];
122+
for (const [guildKey, guildCfg] of Object.entries(account.config.guilds ?? {})) {
123+
const entries = (guildCfg?.users ?? []).map(String).filter(Boolean);
124+
if (entries.length > 0) {
125+
groupOverrides.push({ label: `guild ${guildKey}`, entries });
126+
}
127+
for (const [channelKey, channelCfg] of Object.entries(guildCfg?.channels ?? {})) {
128+
const channelEntries = (channelCfg?.users ?? []).map(String).filter(Boolean);
129+
if (channelEntries.length > 0) {
130+
groupOverrides.push({
131+
label: `guild ${guildKey} / channel ${channelKey}`,
132+
entries: channelEntries,
133+
});
134+
}
135+
}
136+
}
137+
return {
138+
dmAllowFrom: (account.config.allowFrom ?? account.config.dm?.allowFrom ?? []).map(String),
139+
groupPolicy: account.config.groupPolicy,
140+
groupOverrides,
141+
};
142+
}
143+
144+
async function resolveDiscordAllowlistNames(params: {
145+
cfg: Parameters<typeof resolveDiscordAccount>[0]["cfg"];
146+
accountId?: string | null;
147+
entries: string[];
148+
}) {
149+
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
150+
const token = account.token?.trim();
151+
if (!token) {
152+
return [];
153+
}
154+
return await resolveDiscordUserAllowlist({ token, entries: params.entries });
155+
}
156+
157+
function normalizeDiscordAcpConversationId(conversationId: string) {
158+
const normalized = conversationId.trim();
159+
return normalized ? { conversationId: normalized } : null;
160+
}
161+
162+
function matchDiscordAcpConversation(params: {
163+
bindingConversationId: string;
164+
conversationId: string;
165+
parentConversationId?: string;
166+
}) {
167+
if (params.bindingConversationId === params.conversationId) {
168+
return { conversationId: params.conversationId, matchPriority: 2 };
169+
}
170+
if (
171+
params.parentConversationId &&
172+
params.parentConversationId !== params.conversationId &&
173+
params.bindingConversationId === params.parentConversationId
174+
) {
175+
return {
176+
conversationId: params.parentConversationId,
177+
matchPriority: 1,
178+
};
179+
}
180+
return null;
181+
}
182+
183+
function parseDiscordExplicitTarget(raw: string) {
184+
try {
185+
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
186+
if (!target) {
187+
return null;
188+
}
189+
return {
190+
to: target.id,
191+
chatType: target.kind === "user" ? ("direct" as const) : ("channel" as const),
192+
};
193+
} catch {
194+
return null;
195+
}
196+
}
197+
119198
const discordConfigAccessors = createScopedAccountConfigAccessors({
120199
resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
121200
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
@@ -177,6 +256,23 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
177256
}),
178257
...discordConfigAccessors,
179258
},
259+
allowlist: {
260+
supportsScope: ({ scope }) => scope === "dm",
261+
readConfig: ({ cfg, accountId }) =>
262+
readDiscordAllowlistConfig(resolveDiscordAccount({ cfg, accountId })),
263+
resolveNames: async ({ cfg, accountId, entries }) =>
264+
await resolveDiscordAllowlistNames({ cfg, accountId, entries }),
265+
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) =>
266+
scope === "dm"
267+
? {
268+
pathPrefix,
269+
writeTarget,
270+
readPaths: [["allowFrom"], ["dm", "allowFrom"]],
271+
writePath: ["allowFrom"],
272+
cleanupPaths: [["dm", "allowFrom"]],
273+
}
274+
: null,
275+
},
180276
security: {
181277
resolveDmPolicy: ({ cfg, accountId, account }) => {
182278
return buildAccountScopedDmSecurityPolicy({
@@ -238,6 +334,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
238334
},
239335
messaging: {
240336
normalizeTarget: normalizeDiscordMessagingTarget,
337+
parseExplicitTarget: ({ raw }) => parseDiscordExplicitTarget(raw),
338+
inferTargetChatType: ({ to }) => parseDiscordExplicitTarget(to)?.chatType,
241339
buildCrossContextComponents: buildDiscordCrossContextComponents,
242340
targetResolver: {
243341
looksLikeId: looksLikeDiscordTargetId,
@@ -356,6 +454,12 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
356454
silent: silent ?? undefined,
357455
}),
358456
},
457+
acpBindings: {
458+
normalizeConfiguredBindingTarget: ({ conversationId }) =>
459+
normalizeDiscordAcpConversationId(conversationId),
460+
matchConfiguredBinding: ({ bindingConversationId, conversationId, parentConversationId }) =>
461+
matchDiscordAcpConversation({ bindingConversationId, conversationId, parentConversationId }),
462+
},
359463
status: {
360464
defaultRuntime: {
361465
accountId: DEFAULT_ACCOUNT_ID,

extensions/feishu/src/channel.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
resolveDefaultFeishuAccountId,
2323
} from "./accounts.js";
2424
import { FeishuConfigSchema } from "./config-schema.js";
25+
import { parseFeishuConversationId } from "./conversation-id.js";
2526
import { listFeishuDirectoryPeers, listFeishuDirectoryGroups } from "./directory.static.js";
2627
import { resolveFeishuGroupToolPolicy } from "./policy.js";
2728
import { getFeishuRuntime } from "./runtime.js";
@@ -95,6 +96,77 @@ function areAnyFeishuReactionActionsEnabled(cfg: ClawdbotConfig): boolean {
9596
return false;
9697
}
9798

99+
function isSupportedFeishuDirectConversationId(conversationId: string): boolean {
100+
const trimmed = conversationId.trim();
101+
if (!trimmed || trimmed.includes(":")) {
102+
return false;
103+
}
104+
if (trimmed.startsWith("oc_") || trimmed.startsWith("on_")) {
105+
return false;
106+
}
107+
return true;
108+
}
109+
110+
function normalizeFeishuAcpConversationId(conversationId: string) {
111+
const parsed = parseFeishuConversationId({ conversationId });
112+
if (
113+
!parsed ||
114+
(parsed.scope !== "group_topic" &&
115+
parsed.scope !== "group_topic_sender" &&
116+
!isSupportedFeishuDirectConversationId(parsed.canonicalConversationId))
117+
) {
118+
return null;
119+
}
120+
return {
121+
conversationId: parsed.canonicalConversationId,
122+
parentConversationId:
123+
parsed.scope === "group_topic" || parsed.scope === "group_topic_sender"
124+
? parsed.chatId
125+
: undefined,
126+
};
127+
}
128+
129+
function matchFeishuAcpConversation(params: {
130+
bindingConversationId: string;
131+
conversationId: string;
132+
parentConversationId?: string;
133+
}) {
134+
const binding = normalizeFeishuAcpConversationId(params.bindingConversationId);
135+
if (!binding) {
136+
return null;
137+
}
138+
const incoming = parseFeishuConversationId({
139+
conversationId: params.conversationId,
140+
parentConversationId: params.parentConversationId,
141+
});
142+
if (
143+
!incoming ||
144+
(incoming.scope !== "group_topic" &&
145+
incoming.scope !== "group_topic_sender" &&
146+
!isSupportedFeishuDirectConversationId(incoming.canonicalConversationId))
147+
) {
148+
return null;
149+
}
150+
const matchesCanonicalConversation = binding.conversationId === incoming.canonicalConversationId;
151+
const matchesParentTopicForSenderScopedConversation =
152+
incoming.scope === "group_topic_sender" &&
153+
binding.parentConversationId === incoming.chatId &&
154+
binding.conversationId === `${incoming.chatId}:topic:${incoming.topicId}`;
155+
if (!matchesCanonicalConversation && !matchesParentTopicForSenderScopedConversation) {
156+
return null;
157+
}
158+
return {
159+
conversationId: matchesParentTopicForSenderScopedConversation
160+
? binding.conversationId
161+
: incoming.canonicalConversationId,
162+
parentConversationId:
163+
incoming.scope === "group_topic" || incoming.scope === "group_topic_sender"
164+
? incoming.chatId
165+
: undefined,
166+
matchPriority: matchesCanonicalConversation ? 2 : 1,
167+
};
168+
}
169+
98170
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
99171
id: "feishu",
100172
meta: {
@@ -393,6 +465,12 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
393465
});
394466
},
395467
},
468+
acpBindings: {
469+
normalizeConfiguredBindingTarget: ({ conversationId }) =>
470+
normalizeFeishuAcpConversationId(conversationId),
471+
matchConfiguredBinding: ({ bindingConversationId, conversationId, parentConversationId }) =>
472+
matchFeishuAcpConversation({ bindingConversationId, conversationId, parentConversationId }),
473+
},
396474
setup: feishuSetupAdapter,
397475
setupWizard: feishuSetupWizard,
398476
messaging: {

extensions/imessage/src/channel.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,24 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
124124
formatAllowFrom: ({ allowFrom }) => formatTrimmedAllowFromEntries(allowFrom),
125125
resolveDefaultTo: ({ cfg, accountId }) => resolveIMessageConfigDefaultTo({ cfg, accountId }),
126126
},
127+
allowlist: {
128+
supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all",
129+
readConfig: ({ cfg, accountId }) => {
130+
const account = resolveIMessageAccount({ cfg, accountId });
131+
return {
132+
dmAllowFrom: (account.config.allowFrom ?? []).map(String),
133+
groupAllowFrom: (account.config.groupAllowFrom ?? []).map(String),
134+
dmPolicy: account.config.dmPolicy,
135+
groupPolicy: account.config.groupPolicy,
136+
};
137+
},
138+
resolveConfigEdit: ({ scope, pathPrefix, writeTarget }) => ({
139+
pathPrefix,
140+
writeTarget,
141+
readPaths: [[scope === "dm" ? "allowFrom" : "groupAllowFrom"]],
142+
writePath: [scope === "dm" ? "allowFrom" : "groupAllowFrom"],
143+
}),
144+
},
127145
security: {
128146
resolveDmPolicy: ({ cfg, accountId, account }) => {
129147
return buildAccountScopedDmSecurityPolicy({

0 commit comments

Comments
 (0)