mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
refactor(acp): generalize message-channel binds
This commit is contained in:
@@ -127,7 +127,7 @@
|
||||
"exportName": "ChannelConfiguredBindingProvider",
|
||||
"kind": "type",
|
||||
"source": {
|
||||
"line": 578,
|
||||
"line": 590,
|
||||
"path": "src/channels/plugins/types.adapters.ts"
|
||||
}
|
||||
},
|
||||
@@ -1201,6 +1201,15 @@
|
||||
"path": "src/channels/plugins/types.core.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"declaration": "export type ChannelCommandConversationContext = ChannelCommandConversationContext;",
|
||||
"exportName": "ChannelCommandConversationContext",
|
||||
"kind": "type",
|
||||
"source": {
|
||||
"line": 578,
|
||||
"path": "src/channels/plugins/types.adapters.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"declaration": "export type ChannelGroupContext = ChannelGroupContext;",
|
||||
"exportName": "ChannelGroupContext",
|
||||
@@ -1783,6 +1792,15 @@
|
||||
"path": "src/channels/plugins/types.adapters.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"declaration": "export type ChannelCommandConversationContext = ChannelCommandConversationContext;",
|
||||
"exportName": "ChannelCommandConversationContext",
|
||||
"kind": "type",
|
||||
"source": {
|
||||
"line": 578,
|
||||
"path": "src/channels/plugins/types.adapters.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"declaration": "export type ChannelConfigAdapter = ChannelConfigAdapter<ResolvedAccount>;",
|
||||
"exportName": "ChannelConfigAdapter",
|
||||
@@ -1815,7 +1833,7 @@
|
||||
"exportName": "ChannelConfiguredBindingProvider",
|
||||
"kind": "type",
|
||||
"source": {
|
||||
"line": 578,
|
||||
"line": 590,
|
||||
"path": "src/channels/plugins/types.adapters.ts"
|
||||
}
|
||||
},
|
||||
@@ -2184,7 +2202,7 @@
|
||||
"exportName": "ChannelSecurityAdapter",
|
||||
"kind": "type",
|
||||
"source": {
|
||||
"line": 591,
|
||||
"line": 606,
|
||||
"path": "src/channels/plugins/types.adapters.ts"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{"declaration":"export type ChannelConfigUiHint = ChannelConfigUiHint;","entrypoint":"index","exportName":"ChannelConfigUiHint","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":37,"sourcePath":"src/channels/plugins/types.plugin.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"index","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":569,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"index","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":574,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"index","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":578,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"index","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":590,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelGatewayContext = ChannelGatewayContext<ResolvedAccount>;","entrypoint":"index","exportName":"ChannelGatewayContext","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":243,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelId = ChannelId;","entrypoint":"index","exportName":"ChannelId","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":13,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"index","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
@@ -131,6 +131,7 @@
|
||||
{"declaration":"export type BaseTokenResolution = BaseTokenResolution;","entrypoint":"channel-contract","exportName":"BaseTokenResolution","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":565,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelAccountSnapshot = ChannelAccountSnapshot;","entrypoint":"channel-contract","exportName":"ChannelAccountSnapshot","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":144,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelAgentTool = ChannelAgentTool;","entrypoint":"channel-contract","exportName":"ChannelAgentTool","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":18,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelCommandConversationContext = ChannelCommandConversationContext;","entrypoint":"channel-contract","exportName":"ChannelCommandConversationContext","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":578,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelGroupContext = ChannelGroupContext;","entrypoint":"channel-contract","exportName":"ChannelGroupContext","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":216,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelMessageActionAdapter = ChannelMessageActionAdapter;","entrypoint":"channel-contract","exportName":"ChannelMessageActionAdapter","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":516,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelMessageActionContext = ChannelMessageActionContext;","entrypoint":"channel-contract","exportName":"ChannelMessageActionContext","importSpecifier":"openclaw/plugin-sdk/channel-contract","kind":"type","recordType":"export","sourceLine":482,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
@@ -195,10 +196,11 @@
|
||||
{"declaration":"export type ChannelCapabilitiesDisplayLine = ChannelCapabilitiesDisplayLine;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDisplayLine","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":46,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelCapabilitiesDisplayTone = ChannelCapabilitiesDisplayTone;","entrypoint":"channel-runtime","exportName":"ChannelCapabilitiesDisplayTone","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":44,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelCommandAdapter = ChannelCommandAdapter;","entrypoint":"channel-runtime","exportName":"ChannelCommandAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":449,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelCommandConversationContext = ChannelCommandConversationContext;","entrypoint":"channel-runtime","exportName":"ChannelCommandConversationContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":578,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfigAdapter = ChannelConfigAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelConfigAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":95,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingConversationRef = ChannelConfiguredBindingConversationRef;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingConversationRef","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":569,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingMatch;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingMatch","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":574,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":578,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelConfiguredBindingProvider = ChannelConfiguredBindingProvider;","entrypoint":"channel-runtime","exportName":"ChannelConfiguredBindingProvider","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":590,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelDirectoryAdapter = ChannelDirectoryAdapter;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":411,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelDirectoryEntry = ChannelDirectoryEntry;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryEntry","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":469,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelDirectoryEntryKind = ChannelDirectoryEntryKind;","entrypoint":"channel-runtime","exportName":"ChannelDirectoryEntryKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":467,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
@@ -239,7 +241,7 @@
|
||||
{"declaration":"export type ChannelResolveKind = ChannelResolveKind;","entrypoint":"channel-runtime","exportName":"ChannelResolveKind","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":422,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelResolverAdapter = ChannelResolverAdapter;","entrypoint":"channel-runtime","exportName":"ChannelResolverAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":432,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelResolveResult = ChannelResolveResult;","entrypoint":"channel-runtime","exportName":"ChannelResolveResult","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":424,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelSecurityAdapter = ChannelSecurityAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":591,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelSecurityAdapter = ChannelSecurityAdapter<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":606,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
{"declaration":"export type ChannelSecurityContext = ChannelSecurityContext<ResolvedAccount>;","entrypoint":"channel-runtime","exportName":"ChannelSecurityContext","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":254,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelSecurityDmPolicy = ChannelSecurityDmPolicy;","entrypoint":"channel-runtime","exportName":"ChannelSecurityDmPolicy","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":245,"sourcePath":"src/channels/plugins/types.core.ts"}
|
||||
{"declaration":"export type ChannelSetupAdapter = ChannelSetupAdapter;","entrypoint":"channel-runtime","exportName":"ChannelSetupAdapter","importSpecifier":"openclaw/plugin-sdk/channel-runtime","kind":"type","recordType":"export","sourceLine":60,"sourcePath":"src/channels/plugins/types.adapters.ts"}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { createBlueBubblesConversationBindingManager } from "./conversation-bind
|
||||
import {
|
||||
matchBlueBubblesAcpConversation,
|
||||
normalizeBlueBubblesAcpConversationId,
|
||||
resolveBlueBubblesConversationIdFromTarget,
|
||||
} from "./conversation-id.js";
|
||||
import {
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
@@ -108,6 +109,13 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount, BlueBu
|
||||
bindingConversationId: compiledBinding.conversationId,
|
||||
conversationId,
|
||||
}),
|
||||
resolveCommandConversation: ({ originatingTo, commandTo, fallbackTo }) => {
|
||||
const conversationId =
|
||||
resolveBlueBubblesConversationIdFromTarget(originatingTo ?? "") ??
|
||||
resolveBlueBubblesConversationIdFromTarget(commandTo ?? "") ??
|
||||
resolveBlueBubblesConversationIdFromTarget(fallbackTo ?? "");
|
||||
return conversationId ? { conversationId } : null;
|
||||
},
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeBlueBubblesMessagingTarget,
|
||||
|
||||
@@ -334,6 +334,44 @@ describe("discordPlugin outbound", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("discordPlugin bindings", () => {
|
||||
it("preserves user-prefixed current conversation ids for DM binds", () => {
|
||||
const result = discordPlugin.bindings?.resolveCommandConversation?.({
|
||||
accountId: "default",
|
||||
originatingTo: "user:123456789012345678",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
conversationId: "user:123456789012345678",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves channel-prefixed current conversation ids for channel binds", () => {
|
||||
const result = discordPlugin.bindings?.resolveCommandConversation?.({
|
||||
accountId: "default",
|
||||
originatingTo: "channel:987654321098765432",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
conversationId: "channel:987654321098765432",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves channel-prefixed parent ids for thread binds", () => {
|
||||
const result = discordPlugin.bindings?.resolveCommandConversation?.({
|
||||
accountId: "default",
|
||||
originatingTo: "channel:thread-42",
|
||||
threadId: "thread-42",
|
||||
threadParentId: "parent-9",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
conversationId: "thread-42",
|
||||
parentConversationId: "channel:parent-9",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("discordPlugin security", () => {
|
||||
it("normalizes dm allowlist entries with trimmed prefixes and mentions", () => {
|
||||
const resolveDmPolicy = discordPlugin.security?.resolveDmPolicy;
|
||||
|
||||
@@ -367,6 +367,66 @@ function matchDiscordAcpConversation(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveDiscordConversationIdFromTargets(
|
||||
targets: Array<string | undefined>,
|
||||
): string | undefined {
|
||||
for (const raw of targets) {
|
||||
const trimmed = raw?.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const target = parseDiscordTarget(trimmed, { defaultKind: "channel" });
|
||||
if (target?.normalized) {
|
||||
return target.normalized;
|
||||
}
|
||||
} catch {
|
||||
const mentionMatch = trimmed.match(/^<#(\d+)>$/);
|
||||
if (mentionMatch?.[1]) {
|
||||
return `channel:${mentionMatch[1]}`;
|
||||
}
|
||||
if (/^\d{6,}$/.test(trimmed)) {
|
||||
return normalizeDiscordMessagingTarget(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseDiscordParentChannelFromSessionKey(raw: unknown): string | undefined {
|
||||
const sessionKey = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||
if (!sessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
const match = sessionKey.match(/(?:^|:)channel:([^:]+)$/);
|
||||
return match?.[1] ? `channel:${match[1]}` : undefined;
|
||||
}
|
||||
|
||||
function resolveDiscordCommandConversation(params: {
|
||||
threadId?: string;
|
||||
threadParentId?: string;
|
||||
parentSessionKey?: string;
|
||||
originatingTo?: string;
|
||||
commandTo?: string;
|
||||
fallbackTo?: string;
|
||||
}) {
|
||||
const targets = [params.originatingTo, params.commandTo, params.fallbackTo];
|
||||
if (params.threadId) {
|
||||
const parentConversationId =
|
||||
normalizeDiscordMessagingTarget(params.threadParentId?.trim() ?? "") ||
|
||||
parseDiscordParentChannelFromSessionKey(params.parentSessionKey) ||
|
||||
resolveDiscordConversationIdFromTargets(targets);
|
||||
return {
|
||||
conversationId: params.threadId,
|
||||
...(parentConversationId && parentConversationId !== params.threadId
|
||||
? { parentConversationId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
const conversationId = resolveDiscordConversationIdFromTargets(targets);
|
||||
return conversationId ? { conversationId } : null;
|
||||
}
|
||||
|
||||
function parseDiscordExplicitTarget(raw: string) {
|
||||
try {
|
||||
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
||||
@@ -553,6 +613,22 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
}),
|
||||
resolveCommandConversation: ({
|
||||
threadId,
|
||||
threadParentId,
|
||||
parentSessionKey,
|
||||
originatingTo,
|
||||
commandTo,
|
||||
fallbackTo,
|
||||
}) =>
|
||||
resolveDiscordCommandConversation({
|
||||
threadId,
|
||||
threadParentId,
|
||||
parentSessionKey,
|
||||
originatingTo,
|
||||
commandTo,
|
||||
fallbackTo,
|
||||
}),
|
||||
},
|
||||
status: createComputedAccountStatusAdapter<ResolvedDiscordAccount, DiscordProbe, unknown>({
|
||||
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createAllowlistProviderGroupPolicyWarningCollector,
|
||||
projectConfigAccountIdWarningCollector,
|
||||
} from "openclaw/plugin-sdk/channel-policy";
|
||||
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
createChannelDirectoryAdapter,
|
||||
@@ -44,7 +45,12 @@ import {
|
||||
import { FEISHU_CARD_INTERACTION_VERSION } from "./card-interaction.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
import { parseFeishuConversationId } from "./conversation-id.js";
|
||||
import {
|
||||
buildFeishuConversationId,
|
||||
parseFeishuConversationId,
|
||||
parseFeishuDirectConversationId,
|
||||
parseFeishuTargetId,
|
||||
} from "./conversation-id.js";
|
||||
import { listFeishuDirectoryPeers, listFeishuDirectoryGroups } from "./directory.static.js";
|
||||
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
@@ -309,6 +315,99 @@ function matchFeishuAcpConversation(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveFeishuSenderScopedCommandConversation(params: {
|
||||
accountId: string;
|
||||
parentConversationId?: string;
|
||||
threadId?: string;
|
||||
senderId?: string;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
}): string | undefined {
|
||||
const parentConversationId = params.parentConversationId?.trim();
|
||||
const threadId = params.threadId?.trim();
|
||||
const senderId = params.senderId?.trim();
|
||||
if (!parentConversationId || !threadId || !senderId) {
|
||||
return undefined;
|
||||
}
|
||||
const expectedScopePrefix = `feishu:group:${parentConversationId.toLowerCase()}:topic:${threadId.toLowerCase()}:sender:`;
|
||||
const isSenderScopedSession = [params.sessionKey, params.parentSessionKey].some((candidate) => {
|
||||
const normalized = typeof candidate === "string" ? candidate.trim().toLowerCase() : "";
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const scopedRest = normalized.replace(/^agent:[^:]+:/, "");
|
||||
return scopedRest.startsWith(expectedScopePrefix);
|
||||
});
|
||||
const senderScopedConversationId = buildFeishuConversationId({
|
||||
chatId: parentConversationId,
|
||||
scope: "group_topic_sender",
|
||||
topicId: threadId,
|
||||
senderOpenId: senderId,
|
||||
});
|
||||
if (isSenderScopedSession) {
|
||||
return senderScopedConversationId;
|
||||
}
|
||||
if (!params.sessionKey?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
const boundConversation = getSessionBindingService()
|
||||
.listBySession(params.sessionKey)
|
||||
.find((binding) => {
|
||||
if (
|
||||
binding.conversation.channel !== "feishu" ||
|
||||
binding.conversation.accountId !== params.accountId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return binding.conversation.conversationId === senderScopedConversationId;
|
||||
});
|
||||
return boundConversation?.conversation.conversationId;
|
||||
}
|
||||
|
||||
function resolveFeishuCommandConversation(params: {
|
||||
accountId: string;
|
||||
threadId?: string;
|
||||
senderId?: string;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
originatingTo?: string;
|
||||
commandTo?: string;
|
||||
fallbackTo?: string;
|
||||
}) {
|
||||
if (params.threadId) {
|
||||
const parentConversationId =
|
||||
parseFeishuTargetId(params.originatingTo) ??
|
||||
parseFeishuTargetId(params.commandTo) ??
|
||||
parseFeishuTargetId(params.fallbackTo);
|
||||
if (!parentConversationId) {
|
||||
return null;
|
||||
}
|
||||
const senderScopedConversationId = resolveFeishuSenderScopedCommandConversation({
|
||||
accountId: params.accountId,
|
||||
parentConversationId,
|
||||
threadId: params.threadId,
|
||||
senderId: params.senderId,
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.parentSessionKey,
|
||||
});
|
||||
return {
|
||||
conversationId:
|
||||
senderScopedConversationId ??
|
||||
buildFeishuConversationId({
|
||||
chatId: parentConversationId,
|
||||
scope: "group_topic",
|
||||
topicId: params.threadId,
|
||||
}),
|
||||
parentConversationId,
|
||||
};
|
||||
}
|
||||
const conversationId =
|
||||
parseFeishuDirectConversationId(params.originatingTo) ??
|
||||
parseFeishuDirectConversationId(params.commandTo) ??
|
||||
parseFeishuDirectConversationId(params.fallbackTo);
|
||||
return conversationId ? { conversationId } : null;
|
||||
}
|
||||
|
||||
function jsonActionResult(details: Record<string, unknown>) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(details) }],
|
||||
@@ -942,6 +1041,26 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
}),
|
||||
resolveCommandConversation: ({
|
||||
accountId,
|
||||
threadId,
|
||||
senderId,
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
originatingTo,
|
||||
commandTo,
|
||||
fallbackTo,
|
||||
}) =>
|
||||
resolveFeishuCommandConversation({
|
||||
accountId,
|
||||
threadId,
|
||||
senderId,
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
originatingTo,
|
||||
commandTo,
|
||||
fallbackTo,
|
||||
}),
|
||||
},
|
||||
setup: feishuSetupAdapter,
|
||||
setupWizard: feishuSetupWizard,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createIMessageConversationBindingManager } from "./conversation-binding
|
||||
import {
|
||||
matchIMessageAcpConversation,
|
||||
normalizeIMessageAcpConversationId,
|
||||
resolveIMessageConversationIdFromTarget,
|
||||
} from "./conversation-id.js";
|
||||
import {
|
||||
resolveIMessageGroupRequireMention,
|
||||
@@ -141,6 +142,13 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount, IMessageProb
|
||||
bindingConversationId: compiledBinding.conversationId,
|
||||
conversationId,
|
||||
}),
|
||||
resolveCommandConversation: ({ originatingTo, commandTo, fallbackTo }) => {
|
||||
const conversationId =
|
||||
resolveIMessageConversationIdFromTarget(originatingTo ?? "") ??
|
||||
resolveIMessageConversationIdFromTarget(commandTo ?? "") ??
|
||||
resolveIMessageConversationIdFromTarget(fallbackTo ?? "");
|
||||
return conversationId ? { conversationId } : null;
|
||||
},
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeIMessageMessagingTarget,
|
||||
|
||||
@@ -238,6 +238,31 @@ function matchMatrixAcpConversation(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveMatrixCommandConversation(params: {
|
||||
threadId?: string;
|
||||
originatingTo?: string;
|
||||
commandTo?: string;
|
||||
fallbackTo?: string;
|
||||
}) {
|
||||
const parentConversationId = [params.originatingTo, params.commandTo, params.fallbackTo]
|
||||
.map((candidate) => {
|
||||
const trimmed = candidate?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const target = resolveMatrixTargetIdentity(trimmed);
|
||||
return target?.kind === "room" ? target.id : undefined;
|
||||
})
|
||||
.find((candidate): candidate is string => Boolean(candidate));
|
||||
if (params.threadId) {
|
||||
return {
|
||||
conversationId: params.threadId,
|
||||
...(parentConversationId ? { parentConversationId } : {}),
|
||||
};
|
||||
}
|
||||
return parentConversationId ? { conversationId: parentConversationId } : null;
|
||||
}
|
||||
|
||||
export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
|
||||
createChatChannelPlugin<ResolvedMatrixAccount, MatrixProbe>({
|
||||
base: {
|
||||
@@ -322,6 +347,13 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
}),
|
||||
resolveCommandConversation: ({ threadId, originatingTo, commandTo, fallbackTo }) =>
|
||||
resolveMatrixCommandConversation({
|
||||
threadId,
|
||||
originatingTo,
|
||||
commandTo,
|
||||
fallbackTo,
|
||||
}),
|
||||
},
|
||||
status: createComputedAccountStatusAdapter<ResolvedMatrixAccount, MatrixProbe>({
|
||||
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
||||
|
||||
@@ -223,6 +223,36 @@ function matchTelegramAcpConversation(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTelegramCommandConversation(params: {
|
||||
threadId?: string;
|
||||
originatingTo?: string;
|
||||
commandTo?: string;
|
||||
fallbackTo?: string;
|
||||
}) {
|
||||
const chatId = [params.originatingTo, params.commandTo, params.fallbackTo]
|
||||
.map((candidate) => {
|
||||
const trimmed = candidate?.trim();
|
||||
return trimmed ? parseTelegramTarget(trimmed).chatId.trim() : "";
|
||||
})
|
||||
.find((candidate) => candidate.length > 0);
|
||||
if (!chatId) {
|
||||
return null;
|
||||
}
|
||||
if (params.threadId) {
|
||||
return {
|
||||
conversationId: `${chatId}:topic:${params.threadId}`,
|
||||
parentConversationId: chatId,
|
||||
};
|
||||
}
|
||||
if (chatId.startsWith("-")) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
conversationId: chatId,
|
||||
parentConversationId: chatId,
|
||||
};
|
||||
}
|
||||
|
||||
function parseTelegramExplicitTarget(raw: string) {
|
||||
const target = parseTelegramTarget(raw);
|
||||
return {
|
||||
@@ -372,6 +402,13 @@ export const telegramPlugin = createChatChannelPlugin({
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
}),
|
||||
resolveCommandConversation: ({ threadId, originatingTo, commandTo, fallbackTo }) =>
|
||||
resolveTelegramCommandConversation({
|
||||
threadId,
|
||||
originatingTo,
|
||||
commandTo,
|
||||
fallbackTo,
|
||||
}),
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||
|
||||
@@ -116,7 +116,7 @@ type FakeBinding = {
|
||||
targetSessionKey: string;
|
||||
targetKind: "subagent" | "session";
|
||||
conversation: {
|
||||
channel: "discord" | "matrix" | "telegram" | "feishu" | "bluebubbles" | "imessage";
|
||||
channel: string;
|
||||
accountId: string;
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
@@ -241,7 +241,7 @@ function createSessionBindingCapabilities() {
|
||||
type AcpBindInput = {
|
||||
targetSessionKey: string;
|
||||
conversation: {
|
||||
channel?: "discord" | "matrix" | "telegram" | "feishu" | "bluebubbles" | "imessage";
|
||||
channel?: string;
|
||||
accountId: string;
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
@@ -255,47 +255,16 @@ function createAcpThreadBinding(input: AcpBindInput): FakeBinding {
|
||||
input.placement === "child" ? "thread-created" : input.conversation.conversationId;
|
||||
const boundBy = typeof input.metadata?.boundBy === "string" ? input.metadata.boundBy : "user-1";
|
||||
const channel = input.conversation.channel ?? "discord";
|
||||
const conversation =
|
||||
channel === "discord"
|
||||
? {
|
||||
channel: "discord" as const,
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
parentConversationId: "parent-1",
|
||||
}
|
||||
: channel === "matrix"
|
||||
? {
|
||||
channel: "matrix" as const,
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
parentConversationId:
|
||||
input.placement === "child"
|
||||
? input.conversation.conversationId
|
||||
: input.conversation.parentConversationId,
|
||||
}
|
||||
: channel === "feishu"
|
||||
? {
|
||||
channel: "feishu" as const,
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
}
|
||||
: channel === "bluebubbles"
|
||||
? {
|
||||
channel: "bluebubbles" as const,
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
}
|
||||
: channel === "imessage"
|
||||
? {
|
||||
channel: "imessage" as const,
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
}
|
||||
: {
|
||||
channel: "telegram" as const,
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
};
|
||||
const nextParentConversationId =
|
||||
input.placement === "child"
|
||||
? input.conversation.conversationId
|
||||
: input.conversation.parentConversationId;
|
||||
const conversation = {
|
||||
channel,
|
||||
accountId: input.conversation.accountId,
|
||||
conversationId: nextConversationId,
|
||||
...(nextParentConversationId ? { parentConversationId: nextParentConversationId } : {}),
|
||||
};
|
||||
return createSessionBinding({
|
||||
targetSessionKey: input.targetSessionKey,
|
||||
conversation,
|
||||
@@ -337,28 +306,33 @@ function createThreadParams(commandBody: string, cfg: OpenClawConfig = baseCfg)
|
||||
return params;
|
||||
}
|
||||
|
||||
function createTelegramTopicParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
OriginatingChannel: "telegram",
|
||||
OriginatingTo: "telegram:-1003841603622",
|
||||
AccountId: "default",
|
||||
MessageThreadId: "498",
|
||||
});
|
||||
params.command.senderId = "user-1";
|
||||
return params;
|
||||
}
|
||||
type ConversationCommandFixture = {
|
||||
accountId?: string;
|
||||
channel: string;
|
||||
originatingTo: string;
|
||||
senderId?: string;
|
||||
sessionKey?: string;
|
||||
threadId?: string;
|
||||
threadParentId?: string;
|
||||
};
|
||||
|
||||
function createTelegramDmParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
function createConversationParams(
|
||||
commandBody: string,
|
||||
fixture: ConversationCommandFixture,
|
||||
cfg: OpenClawConfig = baseCfg,
|
||||
) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
OriginatingChannel: "telegram",
|
||||
OriginatingTo: "telegram:123456789",
|
||||
AccountId: "default",
|
||||
Provider: fixture.channel,
|
||||
Surface: fixture.channel,
|
||||
OriginatingChannel: fixture.channel,
|
||||
OriginatingTo: fixture.originatingTo,
|
||||
AccountId: fixture.accountId ?? "default",
|
||||
...(fixture.senderId ? { SenderId: fixture.senderId } : {}),
|
||||
...(fixture.sessionKey ? { SessionKey: fixture.sessionKey } : {}),
|
||||
...(fixture.threadId ? { MessageThreadId: fixture.threadId } : {}),
|
||||
...(fixture.threadParentId ? { ThreadParentId: fixture.threadParentId } : {}),
|
||||
});
|
||||
params.command.senderId = "user-1";
|
||||
params.command.senderId = fixture.senderId ?? "user-1";
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -371,86 +345,106 @@ async function runThreadAcpCommand(commandBody: string, cfg: OpenClawConfig = ba
|
||||
}
|
||||
|
||||
async function runTelegramAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createTelegramTopicParams(commandBody, cfg), true);
|
||||
return handleAcpCommand(
|
||||
createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "telegram",
|
||||
originatingTo: "telegram:-1003841603622",
|
||||
threadId: "498",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async function runTelegramDmAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createTelegramDmParams(commandBody, cfg), true);
|
||||
}
|
||||
|
||||
function createMatrixRoomParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "matrix",
|
||||
Surface: "matrix",
|
||||
OriginatingChannel: "matrix",
|
||||
OriginatingTo: "room:!room:example.org",
|
||||
AccountId: "default",
|
||||
});
|
||||
params.command.senderId = "user-1";
|
||||
return params;
|
||||
return handleAcpCommand(
|
||||
createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "telegram",
|
||||
originatingTo: "telegram:123456789",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function createMatrixThreadParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = createMatrixRoomParams(commandBody, cfg);
|
||||
const params = createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "matrix",
|
||||
originatingTo: "room:!room:example.org",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
params.ctx.MessageThreadId = "$thread-root";
|
||||
return params;
|
||||
}
|
||||
|
||||
async function runMatrixAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createMatrixRoomParams(commandBody, cfg), true);
|
||||
return handleAcpCommand(
|
||||
createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "matrix",
|
||||
originatingTo: "room:!room:example.org",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async function runMatrixThreadAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createMatrixThreadParams(commandBody, cfg), true);
|
||||
}
|
||||
|
||||
function createFeishuDmParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "feishu",
|
||||
Surface: "feishu",
|
||||
OriginatingChannel: "feishu",
|
||||
OriginatingTo: "user:ou_sender_1",
|
||||
AccountId: "default",
|
||||
SenderId: "ou_sender_1",
|
||||
});
|
||||
params.command.senderId = "user-1";
|
||||
return params;
|
||||
}
|
||||
|
||||
async function runFeishuDmAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createFeishuDmParams(commandBody, cfg), true);
|
||||
}
|
||||
|
||||
function createBlueBubblesDmParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "bluebubbles",
|
||||
Surface: "bluebubbles",
|
||||
OriginatingChannel: "bluebubbles",
|
||||
OriginatingTo: "bluebubbles:+15555550123",
|
||||
AccountId: "default",
|
||||
});
|
||||
params.command.senderId = "user-1";
|
||||
return params;
|
||||
return handleAcpCommand(
|
||||
createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "feishu",
|
||||
originatingTo: "user:ou_sender_1",
|
||||
senderId: "ou_sender_1",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async function runBlueBubblesDmAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createBlueBubblesDmParams(commandBody, cfg), true);
|
||||
}
|
||||
|
||||
function createIMessageDmParams(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
const params = buildCommandTestParams(commandBody, cfg, {
|
||||
Provider: "imessage",
|
||||
Surface: "imessage",
|
||||
OriginatingChannel: "imessage",
|
||||
OriginatingTo: "imessage:+15555550123",
|
||||
AccountId: "default",
|
||||
});
|
||||
params.command.senderId = "user-1";
|
||||
return params;
|
||||
return handleAcpCommand(
|
||||
createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "bluebubbles",
|
||||
originatingTo: "bluebubbles:+15555550123",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async function runIMessageDmAcpCommand(commandBody: string, cfg: OpenClawConfig = baseCfg) {
|
||||
return handleAcpCommand(createIMessageDmParams(commandBody, cfg), true);
|
||||
return handleAcpCommand(
|
||||
createConversationParams(
|
||||
commandBody,
|
||||
{
|
||||
channel: "imessage",
|
||||
originatingTo: "imessage:+15555550123",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
async function runInternalAcpCommand(params: {
|
||||
@@ -801,7 +795,7 @@ describe("/acp command", () => {
|
||||
|
||||
const result = await runDiscordAcpCommand("/acp spawn codex --bind here", cfg);
|
||||
|
||||
expect(result?.reply?.text).toContain("Bound this channel to");
|
||||
expect(result?.reply?.text).toContain("Bound this conversation to");
|
||||
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
placement: "current",
|
||||
@@ -943,7 +937,7 @@ describe("/acp command", () => {
|
||||
const result = await runFeishuDmAcpCommand("/acp spawn codex --thread here");
|
||||
|
||||
expect(result?.reply?.text).toContain("Spawned ACP session agent:codex:acp:");
|
||||
expect(result?.reply?.text).toContain("Bound this thread to");
|
||||
expect(result?.reply?.text).toContain("Bound this conversation to");
|
||||
expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
placement: "current",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { setDefaultChannelPluginRegistryForTests } from "../../../commands/channel-test-helpers.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
__testing as sessionBindingTesting,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
} from "../../../infra/outbound/session-binding-service.js";
|
||||
import { buildCommandTestParams } from "../commands-spawn.test-harness.js";
|
||||
import {
|
||||
isAcpCommandDiscordChannel,
|
||||
resolveAcpCommandBindingContext,
|
||||
resolveAcpCommandConversationId,
|
||||
resolveAcpCommandParentConversationId,
|
||||
@@ -51,6 +51,7 @@ function registerFeishuBindingAdapterForTest(accountId: string) {
|
||||
|
||||
describe("commands-acp context", () => {
|
||||
beforeEach(() => {
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
});
|
||||
|
||||
@@ -69,9 +70,8 @@ describe("commands-acp context", () => {
|
||||
accountId: "work",
|
||||
threadId: "thread-42",
|
||||
conversationId: "thread-42",
|
||||
parentConversationId: "parent-1",
|
||||
parentConversationId: "channel:parent-1",
|
||||
});
|
||||
expect(isAcpCommandDiscordChannel(params)).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves discord thread parent from ParentSessionKey when targets point at the thread", () => {
|
||||
@@ -90,7 +90,7 @@ describe("commands-acp context", () => {
|
||||
accountId: "work",
|
||||
threadId: "thread-42",
|
||||
conversationId: "thread-42",
|
||||
parentConversationId: "parent-9",
|
||||
parentConversationId: "channel:parent-9",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ describe("commands-acp context", () => {
|
||||
accountId: "work",
|
||||
threadId: "thread-42",
|
||||
conversationId: "thread-42",
|
||||
parentConversationId: "parent-11",
|
||||
parentConversationId: "channel:parent-11",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +129,6 @@ describe("commands-acp context", () => {
|
||||
conversationId: "123456789",
|
||||
});
|
||||
expect(resolveAcpCommandConversationId(params)).toBe("123456789");
|
||||
expect(isAcpCommandDiscordChannel(params)).toBe(false);
|
||||
});
|
||||
|
||||
it("builds canonical telegram topic conversation ids from originating chat + thread", () => {
|
||||
|
||||
@@ -1,78 +1,7 @@
|
||||
import {
|
||||
buildTelegramTopicConversationId,
|
||||
normalizeConversationText,
|
||||
parseTelegramChatIdFromTarget,
|
||||
} from "../../../acp/conversation-id.js";
|
||||
import { DISCORD_THREAD_BINDING_CHANNEL } from "../../../channels/thread-bindings-policy.js";
|
||||
import { normalizeConversationText } from "../../../acp/conversation-id.js";
|
||||
import { resolveChannelConfiguredBindingProviderByChannel } from "../../../channels/plugins/binding-provider.js";
|
||||
import { resolveConversationIdFromTargets } from "../../../infra/outbound/conversation-id.js";
|
||||
import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
|
||||
import { resolveBlueBubblesConversationIdFromTarget } from "../../../plugin-sdk/bluebubbles.js";
|
||||
import {
|
||||
buildFeishuConversationId,
|
||||
parseFeishuDirectConversationId,
|
||||
parseFeishuTargetId,
|
||||
} from "../../../plugin-sdk/feishu.js";
|
||||
import { resolveIMessageConversationIdFromTarget } from "../../../plugin-sdk/imessage-core.js";
|
||||
import { parseAgentSessionKey } from "../../../routing/session-key.js";
|
||||
import type { HandleCommandsParams } from "../commands-types.js";
|
||||
import { parseDiscordParentChannelFromSessionKey } from "../discord-parent-channel.js";
|
||||
import {
|
||||
resolveMatrixConversationId,
|
||||
resolveMatrixParentConversationId,
|
||||
} from "../matrix-context.js";
|
||||
import { resolveTelegramConversationId } from "../telegram-context.js";
|
||||
|
||||
function resolveFeishuSenderScopedConversationId(params: {
|
||||
accountId: string;
|
||||
parentConversationId?: string;
|
||||
threadId?: string;
|
||||
senderId?: string;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
}): string | undefined {
|
||||
const parentConversationId = normalizeConversationText(params.parentConversationId);
|
||||
const threadId = normalizeConversationText(params.threadId);
|
||||
const senderId = normalizeConversationText(params.senderId);
|
||||
const expectedScopePrefix = `feishu:group:${parentConversationId?.toLowerCase()}:topic:${threadId?.toLowerCase()}:sender:`;
|
||||
const isSenderScopedSession = [params.sessionKey, params.parentSessionKey].some((candidate) => {
|
||||
const scopedRest = parseAgentSessionKey(candidate)?.rest?.trim().toLowerCase() ?? "";
|
||||
return Boolean(scopedRest && expectedScopePrefix && scopedRest.startsWith(expectedScopePrefix));
|
||||
});
|
||||
if (!parentConversationId || !threadId || !senderId) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isSenderScopedSession && params.sessionKey?.trim()) {
|
||||
const boundConversation = getSessionBindingService()
|
||||
.listBySession(params.sessionKey)
|
||||
.find((binding) => {
|
||||
if (
|
||||
binding.conversation.channel !== "feishu" ||
|
||||
binding.conversation.accountId !== params.accountId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
binding.conversation.conversationId ===
|
||||
buildFeishuConversationId({
|
||||
chatId: parentConversationId,
|
||||
scope: "group_topic_sender",
|
||||
topicId: threadId,
|
||||
senderOpenId: senderId,
|
||||
})
|
||||
);
|
||||
});
|
||||
if (boundConversation) {
|
||||
return boundConversation.conversation.conversationId;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return buildFeishuConversationId({
|
||||
chatId: parentConversationId,
|
||||
scope: "group_topic_sender",
|
||||
topicId: threadId,
|
||||
senderOpenId: senderId,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveAcpCommandChannel(params: HandleCommandsParams): string {
|
||||
const raw =
|
||||
@@ -96,159 +25,56 @@ export function resolveAcpCommandThreadId(params: HandleCommandsParams): string
|
||||
return threadId || undefined;
|
||||
}
|
||||
|
||||
export function resolveAcpCommandConversationId(params: HandleCommandsParams): string | undefined {
|
||||
function resolveAcpCommandConversationRef(params: HandleCommandsParams): {
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
} | null {
|
||||
const channel = resolveAcpCommandChannel(params);
|
||||
if (channel === "matrix") {
|
||||
return resolveMatrixConversationId({
|
||||
ctx: {
|
||||
MessageThreadId: params.ctx.MessageThreadId,
|
||||
OriginatingTo: params.ctx.OriginatingTo,
|
||||
To: params.ctx.To,
|
||||
},
|
||||
command: {
|
||||
to: params.command.to,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (channel === "telegram") {
|
||||
const telegramConversationId = resolveTelegramConversationId({
|
||||
ctx: {
|
||||
MessageThreadId: params.ctx.MessageThreadId,
|
||||
OriginatingTo: params.ctx.OriginatingTo,
|
||||
To: params.ctx.To,
|
||||
},
|
||||
command: {
|
||||
to: params.command.to,
|
||||
},
|
||||
});
|
||||
if (telegramConversationId) {
|
||||
return telegramConversationId;
|
||||
}
|
||||
const threadId = resolveAcpCommandThreadId(params);
|
||||
const parentConversationId = resolveAcpCommandParentConversationId(params);
|
||||
if (threadId && parentConversationId) {
|
||||
return (
|
||||
buildTelegramTopicConversationId({
|
||||
chatId: parentConversationId,
|
||||
topicId: threadId,
|
||||
}) ?? threadId
|
||||
);
|
||||
}
|
||||
}
|
||||
if (channel === "feishu") {
|
||||
const threadId = resolveAcpCommandThreadId(params);
|
||||
const parentConversationId = resolveAcpCommandParentConversationId(params);
|
||||
if (threadId && parentConversationId) {
|
||||
const senderScopedConversationId = resolveFeishuSenderScopedConversationId({
|
||||
accountId: resolveAcpCommandAccountId(params),
|
||||
parentConversationId,
|
||||
threadId,
|
||||
senderId: params.command.senderId ?? params.ctx.SenderId,
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: params.ctx.ParentSessionKey,
|
||||
});
|
||||
return (
|
||||
senderScopedConversationId ??
|
||||
buildFeishuConversationId({
|
||||
chatId: parentConversationId,
|
||||
scope: "group_topic",
|
||||
topicId: threadId,
|
||||
})
|
||||
);
|
||||
}
|
||||
return (
|
||||
parseFeishuDirectConversationId(params.ctx.OriginatingTo) ??
|
||||
parseFeishuDirectConversationId(params.command.to) ??
|
||||
parseFeishuDirectConversationId(params.ctx.To)
|
||||
);
|
||||
}
|
||||
if (channel === "bluebubbles") {
|
||||
return (
|
||||
resolveBlueBubblesConversationIdFromTarget(params.ctx.OriginatingTo ?? "") ??
|
||||
resolveBlueBubblesConversationIdFromTarget(params.command.to ?? "") ??
|
||||
resolveBlueBubblesConversationIdFromTarget(params.ctx.To ?? "")
|
||||
);
|
||||
}
|
||||
if (channel === "imessage") {
|
||||
return (
|
||||
resolveIMessageConversationIdFromTarget(params.ctx.OriginatingTo ?? "") ??
|
||||
resolveIMessageConversationIdFromTarget(params.command.to ?? "") ??
|
||||
resolveIMessageConversationIdFromTarget(params.ctx.To ?? "")
|
||||
);
|
||||
}
|
||||
return resolveConversationIdFromTargets({
|
||||
threadId: params.ctx.MessageThreadId,
|
||||
targets: [params.ctx.OriginatingTo, params.command.to, params.ctx.To],
|
||||
const threadId = resolveAcpCommandThreadId(params);
|
||||
const provider = resolveChannelConfiguredBindingProviderByChannel(channel);
|
||||
const resolvedByProvider = provider?.resolveCommandConversation?.({
|
||||
accountId: resolveAcpCommandAccountId(params),
|
||||
threadId,
|
||||
threadParentId: normalizeConversationText(params.ctx.ThreadParentId),
|
||||
senderId: normalizeConversationText(params.command.senderId ?? params.ctx.SenderId),
|
||||
sessionKey: params.sessionKey,
|
||||
parentSessionKey: normalizeConversationText(params.ctx.ParentSessionKey),
|
||||
originatingTo: params.ctx.OriginatingTo,
|
||||
commandTo: params.command.to,
|
||||
fallbackTo: params.ctx.To,
|
||||
});
|
||||
if (resolvedByProvider?.conversationId) {
|
||||
return resolvedByProvider;
|
||||
}
|
||||
const targets = [params.ctx.OriginatingTo, params.command.to, params.ctx.To];
|
||||
const conversationId = resolveConversationIdFromTargets({
|
||||
threadId,
|
||||
targets,
|
||||
});
|
||||
if (!conversationId) {
|
||||
return null;
|
||||
}
|
||||
const parentConversationId = threadId
|
||||
? resolveConversationIdFromTargets({
|
||||
targets,
|
||||
})
|
||||
: undefined;
|
||||
return {
|
||||
conversationId,
|
||||
...(parentConversationId && parentConversationId !== conversationId
|
||||
? { parentConversationId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDiscordParentChannelFromContext(raw: unknown): string | undefined {
|
||||
const parentId = normalizeConversationText(raw);
|
||||
if (!parentId) {
|
||||
return undefined;
|
||||
}
|
||||
return parentId;
|
||||
export function resolveAcpCommandConversationId(params: HandleCommandsParams): string | undefined {
|
||||
return resolveAcpCommandConversationRef(params)?.conversationId;
|
||||
}
|
||||
|
||||
export function resolveAcpCommandParentConversationId(
|
||||
params: HandleCommandsParams,
|
||||
): string | undefined {
|
||||
const channel = resolveAcpCommandChannel(params);
|
||||
if (channel === "matrix") {
|
||||
return resolveMatrixParentConversationId({
|
||||
ctx: {
|
||||
MessageThreadId: params.ctx.MessageThreadId,
|
||||
OriginatingTo: params.ctx.OriginatingTo,
|
||||
To: params.ctx.To,
|
||||
},
|
||||
command: {
|
||||
to: params.command.to,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (channel === "telegram") {
|
||||
return (
|
||||
parseTelegramChatIdFromTarget(params.ctx.OriginatingTo) ??
|
||||
parseTelegramChatIdFromTarget(params.command.to) ??
|
||||
parseTelegramChatIdFromTarget(params.ctx.To)
|
||||
);
|
||||
}
|
||||
if (channel === "feishu") {
|
||||
const threadId = resolveAcpCommandThreadId(params);
|
||||
if (!threadId) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
parseFeishuTargetId(params.ctx.OriginatingTo) ??
|
||||
parseFeishuTargetId(params.command.to) ??
|
||||
parseFeishuTargetId(params.ctx.To)
|
||||
);
|
||||
}
|
||||
if (channel === DISCORD_THREAD_BINDING_CHANNEL) {
|
||||
const threadId = resolveAcpCommandThreadId(params);
|
||||
if (!threadId) {
|
||||
return undefined;
|
||||
}
|
||||
const fromContext = parseDiscordParentChannelFromContext(params.ctx.ThreadParentId);
|
||||
if (fromContext && fromContext !== threadId) {
|
||||
return fromContext;
|
||||
}
|
||||
const fromParentSession = parseDiscordParentChannelFromSessionKey(params.ctx.ParentSessionKey);
|
||||
if (fromParentSession && fromParentSession !== threadId) {
|
||||
return fromParentSession;
|
||||
}
|
||||
const fromTargets = resolveConversationIdFromTargets({
|
||||
targets: [params.ctx.OriginatingTo, params.command.to, params.ctx.To],
|
||||
});
|
||||
if (fromTargets && fromTargets !== threadId) {
|
||||
return fromTargets;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isAcpCommandDiscordChannel(params: HandleCommandsParams): boolean {
|
||||
return resolveAcpCommandChannel(params) === DISCORD_THREAD_BINDING_CHANNEL;
|
||||
return resolveAcpCommandConversationRef(params)?.parentConversationId;
|
||||
}
|
||||
|
||||
export function resolveAcpCommandBindingContext(params: HandleCommandsParams): {
|
||||
@@ -258,12 +84,21 @@ export function resolveAcpCommandBindingContext(params: HandleCommandsParams): {
|
||||
conversationId?: string;
|
||||
parentConversationId?: string;
|
||||
} {
|
||||
const parentConversationId = resolveAcpCommandParentConversationId(params);
|
||||
const conversationRef = resolveAcpCommandConversationRef(params);
|
||||
if (!conversationRef) {
|
||||
return {
|
||||
channel: resolveAcpCommandChannel(params),
|
||||
accountId: resolveAcpCommandAccountId(params),
|
||||
threadId: resolveAcpCommandThreadId(params),
|
||||
};
|
||||
}
|
||||
return {
|
||||
channel: resolveAcpCommandChannel(params),
|
||||
accountId: resolveAcpCommandAccountId(params),
|
||||
threadId: resolveAcpCommandThreadId(params),
|
||||
conversationId: resolveAcpCommandConversationId(params),
|
||||
...(parentConversationId ? { parentConversationId } : {}),
|
||||
conversationId: conversationRef.conversationId,
|
||||
...(conversationRef.parentConversationId
|
||||
? { parentConversationId: conversationRef.parentConversationId }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ import {
|
||||
import {
|
||||
formatThreadBindingDisabledError,
|
||||
formatThreadBindingSpawnDisabledError,
|
||||
requiresNativeThreadContextForThreadHere,
|
||||
resolveThreadBindingIdleTimeoutMsForChannel,
|
||||
resolveThreadBindingMaxAgeMsForChannel,
|
||||
resolveThreadBindingPlacementForCurrentContext,
|
||||
resolveThreadBindingSpawnPolicy,
|
||||
} from "../../../channels/thread-bindings-policy.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
@@ -55,23 +57,17 @@ import {
|
||||
import { resolveAcpTargetSessionKey } from "./targets.js";
|
||||
|
||||
function resolveAcpBindingLabelNoun(params: {
|
||||
channel: string;
|
||||
conversationId?: string;
|
||||
placement: "current" | "child";
|
||||
threadId?: string;
|
||||
bindMode: "thread" | "current";
|
||||
}): string {
|
||||
if (params.bindMode === "current") {
|
||||
if (params.channel === "discord" && !params.threadId) {
|
||||
return "channel";
|
||||
}
|
||||
if (
|
||||
params.channel === "telegram" ||
|
||||
params.channel === "bluebubbles" ||
|
||||
params.channel === "imessage"
|
||||
) {
|
||||
return "conversation";
|
||||
}
|
||||
if (params.placement === "child") {
|
||||
return "thread";
|
||||
}
|
||||
return params.channel === "telegram" ? "conversation" : "thread";
|
||||
if (!params.threadId) {
|
||||
return "conversation";
|
||||
}
|
||||
return params.conversationId === params.threadId ? "thread" : "conversation";
|
||||
}
|
||||
|
||||
async function bindSpawnedAcpSessionToCurrentConversation(params: {
|
||||
@@ -158,9 +154,14 @@ async function bindSpawnedAcpSessionToCurrentConversation(params: {
|
||||
? existingBinding.metadata.boundBy.trim()
|
||||
: "";
|
||||
if (existingBinding && boundBy && boundBy !== "system" && senderId && senderId !== boundBy) {
|
||||
const currentLabel = resolveAcpBindingLabelNoun({
|
||||
placement: "current",
|
||||
threadId: bindingContext.threadId,
|
||||
conversationId: currentConversationId,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: `Only ${boundBy} can rebind this ${channel === "discord" && !bindingContext.threadId ? "channel" : "conversation"}.`,
|
||||
error: `Only ${boundBy} can rebind this ${currentLabel}.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,7 +288,7 @@ async function bindSpawnedAcpSessionToThread(params: {
|
||||
|
||||
const currentThreadId = bindingContext.threadId ?? "";
|
||||
const currentConversationId = bindingContext.conversationId?.trim() || "";
|
||||
const requiresThreadIdForHere = channel !== "telegram" && channel !== "feishu";
|
||||
const requiresThreadIdForHere = requiresNativeThreadContextForThreadHere(channel);
|
||||
if (
|
||||
threadMode === "here" &&
|
||||
((requiresThreadIdForHere && !currentThreadId) ||
|
||||
@@ -299,12 +300,10 @@ async function bindSpawnedAcpSessionToThread(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const placement =
|
||||
channel === "telegram" || channel === "feishu"
|
||||
? "current"
|
||||
: currentThreadId
|
||||
? "current"
|
||||
: "child";
|
||||
const placement = resolveThreadBindingPlacementForCurrentContext({
|
||||
channel,
|
||||
threadId: currentThreadId || undefined,
|
||||
});
|
||||
if (!capabilities.placements.includes(placement)) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -335,9 +334,14 @@ async function bindSpawnedAcpSessionToThread(params: {
|
||||
? existingBinding.metadata.boundBy.trim()
|
||||
: "";
|
||||
if (existingBinding && boundBy && boundBy !== "system" && senderId && senderId !== boundBy) {
|
||||
const currentLabel = resolveAcpBindingLabelNoun({
|
||||
placement,
|
||||
threadId: currentThreadId || undefined,
|
||||
conversationId: currentConversationId,
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
error: `Only ${boundBy} can rebind this ${channel === "telegram" ? "conversation" : "thread"}.`,
|
||||
error: `Only ${boundBy} can rebind this ${currentLabel}.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -537,9 +541,12 @@ export async function handleAcpSpawnAction(
|
||||
const currentConversationId = resolveAcpCommandConversationId(params)?.trim() || "";
|
||||
const boundConversationId = binding.conversation.conversationId.trim();
|
||||
const placementLabel = resolveAcpBindingLabelNoun({
|
||||
channel: binding.conversation.channel,
|
||||
conversationId: currentConversationId,
|
||||
placement:
|
||||
currentConversationId && boundConversationId === currentConversationId
|
||||
? "current"
|
||||
: "child",
|
||||
threadId: resolveAcpCommandThreadId(params),
|
||||
bindMode: spawn.bind !== "off" ? "current" : "thread",
|
||||
});
|
||||
if (currentConversationId && boundConversationId === currentConversationId) {
|
||||
parts.push(`Bound this ${placementLabel} to ${sessionKey}.`);
|
||||
|
||||
@@ -2,10 +2,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { toAcpRuntimeErrorText } from "../../../acp/runtime/error-text.js";
|
||||
import type { AcpRuntimeError } from "../../../acp/runtime/errors.js";
|
||||
import type { AcpRuntimeSessionMode } from "../../../acp/runtime/types.js";
|
||||
import {
|
||||
DISCORD_THREAD_BINDING_CHANNEL,
|
||||
MATRIX_THREAD_BINDING_CHANNEL,
|
||||
} from "../../../channels/thread-bindings-policy.js";
|
||||
import { supportsAutomaticThreadBindingSpawn } from "../../../channels/thread-bindings-policy.js";
|
||||
import type { AcpSessionRuntimeOptions } from "../../../config/sessions/types.js";
|
||||
import { normalizeAgentId } from "../../../routing/session-key.js";
|
||||
import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js";
|
||||
@@ -174,7 +171,7 @@ function normalizeAcpOptionToken(raw: string): string {
|
||||
|
||||
function resolveDefaultSpawnThreadMode(params: HandleCommandsParams): AcpSpawnThreadMode {
|
||||
const channel = resolveAcpCommandChannel(params);
|
||||
if (channel !== DISCORD_THREAD_BINDING_CHANNEL && channel !== MATRIX_THREAD_BINDING_CHANNEL) {
|
||||
if (!supportsAutomaticThreadBindingSpawn(channel)) {
|
||||
return "off";
|
||||
}
|
||||
const currentThreadId = resolveAcpCommandThreadId(params);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getChannelPlugin, normalizeChannelId } from "./registry.js";
|
||||
import type { ChannelConfiguredBindingProvider } from "./types.adapters.js";
|
||||
import type { ChannelPlugin } from "./types.plugin.js";
|
||||
|
||||
@@ -12,3 +13,13 @@ export function resolveChannelConfiguredBindingProvider(
|
||||
): ChannelConfiguredBindingProvider | undefined {
|
||||
return plugin?.bindings;
|
||||
}
|
||||
|
||||
export function resolveChannelConfiguredBindingProviderByChannel(
|
||||
channel: string,
|
||||
): ChannelConfiguredBindingProvider | undefined {
|
||||
const normalizedChannel = normalizeChannelId(channel);
|
||||
if (!normalizedChannel) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveChannelConfiguredBindingProvider(getChannelPlugin(normalizedChannel));
|
||||
}
|
||||
|
||||
@@ -575,6 +575,18 @@ export type ChannelConfiguredBindingMatch = ChannelConfiguredBindingConversation
|
||||
matchPriority?: number;
|
||||
};
|
||||
|
||||
export type ChannelCommandConversationContext = {
|
||||
accountId: string;
|
||||
threadId?: string;
|
||||
threadParentId?: string;
|
||||
senderId?: string;
|
||||
sessionKey?: string;
|
||||
parentSessionKey?: string;
|
||||
originatingTo?: string;
|
||||
commandTo?: string;
|
||||
fallbackTo?: string;
|
||||
};
|
||||
|
||||
export type ChannelConfiguredBindingProvider = {
|
||||
compileConfiguredBinding: (params: {
|
||||
binding: ConfiguredBindingRule;
|
||||
@@ -586,6 +598,9 @@ export type ChannelConfiguredBindingProvider = {
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
}) => ChannelConfiguredBindingMatch | null;
|
||||
resolveCommandConversation?: (
|
||||
params: ChannelCommandConversationContext,
|
||||
) => ChannelConfiguredBindingConversationRef | null;
|
||||
};
|
||||
|
||||
export type ChannelSecurityAdapter<ResolvedAccount = unknown> = {
|
||||
|
||||
@@ -33,6 +33,7 @@ export type {
|
||||
ChannelOutboundAdapter,
|
||||
ChannelOutboundContext,
|
||||
ChannelAllowlistAdapter,
|
||||
ChannelCommandConversationContext,
|
||||
ChannelConfiguredBindingConversationRef,
|
||||
ChannelConfiguredBindingMatch,
|
||||
ChannelConfiguredBindingProvider,
|
||||
|
||||
39
src/channels/thread-bindings-policy.test.ts
Normal file
39
src/channels/thread-bindings-policy.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
requiresNativeThreadContextForThreadHere,
|
||||
resolveThreadBindingPlacementForCurrentContext,
|
||||
supportsAutomaticThreadBindingSpawn,
|
||||
} from "./thread-bindings-policy.js";
|
||||
|
||||
describe("thread binding spawn policy helpers", () => {
|
||||
it("treats Discord and Matrix as automatic child-thread spawn channels", () => {
|
||||
expect(supportsAutomaticThreadBindingSpawn("discord")).toBe(true);
|
||||
expect(supportsAutomaticThreadBindingSpawn("matrix")).toBe(true);
|
||||
expect(supportsAutomaticThreadBindingSpawn("telegram")).toBe(false);
|
||||
});
|
||||
|
||||
it("allows thread-here on threadless conversation channels without a native thread id", () => {
|
||||
expect(requiresNativeThreadContextForThreadHere("telegram")).toBe(false);
|
||||
expect(requiresNativeThreadContextForThreadHere("feishu")).toBe(false);
|
||||
expect(requiresNativeThreadContextForThreadHere("discord")).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves current vs child placement from the current channel context", () => {
|
||||
expect(
|
||||
resolveThreadBindingPlacementForCurrentContext({
|
||||
channel: "discord",
|
||||
}),
|
||||
).toBe("child");
|
||||
expect(
|
||||
resolveThreadBindingPlacementForCurrentContext({
|
||||
channel: "discord",
|
||||
threadId: "thread-1",
|
||||
}),
|
||||
).toBe("current");
|
||||
expect(
|
||||
resolveThreadBindingPlacementForCurrentContext({
|
||||
channel: "telegram",
|
||||
}),
|
||||
).toBe("current");
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,28 @@ function normalizeChannelId(value: string | undefined | null): string {
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export function supportsAutomaticThreadBindingSpawn(channel: string): boolean {
|
||||
const normalized = normalizeChannelId(channel);
|
||||
return (
|
||||
normalized === DISCORD_THREAD_BINDING_CHANNEL || normalized === MATRIX_THREAD_BINDING_CHANNEL
|
||||
);
|
||||
}
|
||||
|
||||
export function requiresNativeThreadContextForThreadHere(channel: string): boolean {
|
||||
const normalized = normalizeChannelId(channel);
|
||||
return normalized !== "telegram" && normalized !== "feishu";
|
||||
}
|
||||
|
||||
export function resolveThreadBindingPlacementForCurrentContext(params: {
|
||||
channel: string;
|
||||
threadId?: string;
|
||||
}): "current" | "child" {
|
||||
if (!requiresNativeThreadContextForThreadHere(params.channel)) {
|
||||
return "current";
|
||||
}
|
||||
return params.threadId ? "current" : "child";
|
||||
}
|
||||
|
||||
function normalizeBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value !== "boolean") {
|
||||
return undefined;
|
||||
@@ -180,9 +202,7 @@ export function resolveThreadBindingSpawnPolicy(params: {
|
||||
const spawnFlagKey = resolveSpawnFlagKey(params.kind);
|
||||
const spawnEnabledRaw =
|
||||
normalizeBoolean(account?.[spawnFlagKey]) ?? normalizeBoolean(root?.[spawnFlagKey]);
|
||||
const spawnEnabled =
|
||||
spawnEnabledRaw ??
|
||||
(channel !== DISCORD_THREAD_BINDING_CHANNEL && channel !== MATRIX_THREAD_BINDING_CHANNEL);
|
||||
const spawnEnabled = spawnEnabledRaw ?? !supportsAutomaticThreadBindingSpawn(channel);
|
||||
return {
|
||||
channel,
|
||||
accountId,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "../../agents/test-helpers/fast-coding-tools.js";
|
||||
import { createOpenClawCodingTools } from "../../agents/pi-tools.js";
|
||||
import {
|
||||
loadRunCronIsolatedAgentTurn,
|
||||
resetRunCronIsolatedAgentTurnHarness,
|
||||
@@ -67,12 +66,6 @@ describe("runCronIsolatedAgentTurn owner auth", () => {
|
||||
expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1);
|
||||
const senderIsOwner = runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.senderIsOwner;
|
||||
expect(senderIsOwner).toBe(true);
|
||||
|
||||
const toolNames = createOpenClawCodingTools({ senderIsOwner }).map(
|
||||
(tool: { name: string }) => tool.name,
|
||||
);
|
||||
expect(toolNames).toContain("cron");
|
||||
expect(toolNames).toContain("gateway");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ export type {
|
||||
BaseTokenResolution,
|
||||
ChannelAgentTool,
|
||||
ChannelAccountSnapshot,
|
||||
ChannelCommandConversationContext,
|
||||
ChannelGroupContext,
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionContext,
|
||||
|
||||
@@ -28,7 +28,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export * from "./src/send.js";',
|
||||
],
|
||||
"extensions/imessage/runtime-api.ts": [
|
||||
'export { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, buildComputedAccountStatusSnapshot, buildChannelConfigSchema, collectStatusIssuesFromLastError, formatTrimmedAllowFromEntries, getChatChannelMeta, looksLikeIMessageTargetId, normalizeIMessageMessagingTarget, resolveChannelMediaMaxBytes, resolveIMessageConfigAllowFrom, resolveIMessageConfigDefaultTo, IMessageConfigSchema, type ChannelPlugin, type IMessageAccountConfig } from "openclaw/plugin-sdk/imessage";',
|
||||
'export { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, buildComputedAccountStatusSnapshot, buildChannelConfigSchema, chunkTextForOutbound, collectStatusIssuesFromLastError, formatTrimmedAllowFromEntries, getChatChannelMeta, looksLikeIMessageTargetId, normalizeIMessageMessagingTarget, resolveChannelMediaMaxBytes, resolveIMessageConfigAllowFrom, resolveIMessageConfigDefaultTo, IMessageConfigSchema, type ChannelPlugin, type IMessageAccountConfig } from "openclaw/plugin-sdk/imessage";',
|
||||
'export { resolveIMessageGroupRequireMention, resolveIMessageGroupToolPolicy } from "./src/group-policy.js";',
|
||||
'export { monitorIMessageProvider } from "./src/monitor.js";',
|
||||
'export type { MonitorIMessageOpts } from "./src/monitor.js";',
|
||||
|
||||
Reference in New Issue
Block a user