diff --git a/extensions/feishu/src/conversation-id.test.ts b/extensions/feishu/src/conversation-id.test.ts new file mode 100644 index 00000000000..2411d884610 --- /dev/null +++ b/extensions/feishu/src/conversation-id.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { buildFeishuModelOverrideParentCandidates } from "./conversation-id.js"; + +describe("buildFeishuModelOverrideParentCandidates", () => { + it("returns topic and chat fallback ids for sender-scoped topics", () => { + expect( + buildFeishuModelOverrideParentCandidates( + "oc_group_chat:Topic:om_topic_root:Sender:ou_topic_user", + ), + ).toEqual(["oc_group_chat:topic:om_topic_root", "oc_group_chat"]); + }); + + it("returns chat fallback ids for sender-scoped chats", () => { + expect(buildFeishuModelOverrideParentCandidates("oc_group_chat:sender:ou_topic_user")).toEqual([ + "oc_group_chat", + ]); + }); +}); diff --git a/src/channels/model-overrides.test.ts b/src/channels/model-overrides.test.ts index ef00f76a7c0..bc3d6e6e8ae 100644 --- a/src/channels/model-overrides.test.ts +++ b/src/channels/model-overrides.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { createSessionConversationTestRegistry } from "../test-utils/session-conversation-registry.js"; import { resolveChannelModelOverride } from "./model-overrides.js"; @@ -64,48 +64,6 @@ describe("resolveChannelModelOverride", () => { }, expected: { model: "demo-provider/demo-parent-model", matchKey: "123" }, }, - { - name: "preserves feishu topic ids for direct matches", - input: { - cfg: { - channels: { - modelByChannel: { - feishu: { - "oc_group_chat:topic:om_topic_root": "demo-provider/demo-feishu-topic-model", - }, - }, - }, - } as unknown as OpenClawConfig, - channel: "feishu", - groupId: "oc_group_chat:topic:om_topic_root", - }, - expected: { - model: "demo-provider/demo-feishu-topic-model", - matchKey: "oc_group_chat:topic:om_topic_root", - }, - }, - { - name: "preserves feishu topic ids when falling back from parent session key", - input: { - cfg: { - channels: { - modelByChannel: { - feishu: { - "oc_group_chat:topic:om_topic_root": "demo-provider/demo-feishu-topic-model", - }, - }, - }, - } as unknown as OpenClawConfig, - channel: "feishu", - groupId: "unrelated", - parentSessionKey: - "agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user", - }, - expected: { - model: "demo-provider/demo-feishu-topic-model", - matchKey: "oc_group_chat:topic:om_topic_root", - }, - }, ] as const)("$name", ({ input, expected }) => { const resolved = resolveChannelModelOverride(input); expect(resolved?.model).toBe(expected.model); @@ -168,48 +126,58 @@ describe("resolveChannelModelOverride", () => { expect(resolved?.matchKey).toBe("thread-parent"); }); - it("keeps bundled Feishu parent fallback matching before registry bootstrap", () => { - resetPluginRuntimeStateForTest(); + it("uses plugin-owned parent fallback candidates", () => { + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "scoped-chat", + source: "test", + plugin: { + id: "scoped-chat", + meta: { + id: "scoped-chat", + label: "Scoped Chat", + selectionLabel: "Scoped Chat", + docsPath: "/channels/scoped-chat", + blurb: "test stub.", + }, + capabilities: { chatTypes: ["group"] }, + conversationBindings: { + buildModelOverrideParentCandidates: ({ + parentConversationId, + }: { + parentConversationId?: string | null; + }) => + parentConversationId === "room:topic:thread:sender:user" + ? ["room:topic:thread", "room"] + : [], + }, + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + }, + }, + }, + ]), + ); const resolved = resolveChannelModelOverride({ cfg: { channels: { modelByChannel: { - feishu: { - "oc_group_chat:topic:om_topic_root": "demo-provider/demo-feishu-topic-model", + "scoped-chat": { + "room:topic:thread": "demo-provider/demo-scoped-model", }, }, }, } as unknown as OpenClawConfig, - channel: "feishu", + channel: "scoped-chat", groupId: "unrelated", - parentSessionKey: - "agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user", + parentSessionKey: "agent:main:scoped-chat:group:room:topic:thread:sender:user", }); - expect(resolved?.model).toBe("demo-provider/demo-feishu-topic-model"); - expect(resolved?.matchKey).toBe("oc_group_chat:topic:om_topic_root"); - }); - - it("keeps mixed-case Feishu scoped markers when matching parent session fallbacks", () => { - const resolved = resolveChannelModelOverride({ - cfg: { - channels: { - modelByChannel: { - feishu: { - "oc_group_chat:topic:om_topic_root": "demo-provider/demo-feishu-topic-model", - }, - }, - }, - } as unknown as OpenClawConfig, - channel: "feishu", - groupId: "unrelated", - parentSessionKey: - "agent:main:feishu:group:oc_group_chat:Topic:om_topic_root:Sender:ou_topic_user", - }); - - expect(resolved?.model).toBe("demo-provider/demo-feishu-topic-model"); - expect(resolved?.matchKey).toBe("oc_group_chat:topic:om_topic_root"); + expect(resolved?.model).toBe("demo-provider/demo-scoped-model"); + expect(resolved?.matchKey).toBe("room:topic:thread"); }); it("prefers parent conversation ids over channel-name fallbacks", () => { diff --git a/src/channels/model-overrides.ts b/src/channels/model-overrides.ts index 010235943f3..7425bf25d95 100644 --- a/src/channels/model-overrides.ts +++ b/src/channels/model-overrides.ts @@ -69,10 +69,6 @@ function buildChannelCandidates( normalizeOptionalLowercaseString(params.channel); const groupId = normalizeOptionalString(params.groupId); const sessionConversation = resolveSessionConversationRef(params.parentSessionKey); - const feishuParentOverrideFallbacks = - normalizedChannel === "feishu" - ? buildFeishuParentOverrideCandidates(sessionConversation?.rawId) - : []; const parentOverrideFallbacks = (normalizedChannel ? getChannelPlugin( @@ -105,7 +101,6 @@ function buildChannelCandidates( sessionConversation?.rawId, ...(groupConversation?.parentConversationCandidates ?? []), ...(sessionConversation?.parentConversationCandidates ?? []), - ...feishuParentOverrideFallbacks, ...parentOverrideFallbacks, ), parentKeys: buildChannelKeyCandidates( @@ -128,48 +123,15 @@ function buildGenericParentOverrideCandidates(sessionKey: string | null | undefi return buildChannelKeyCandidates(threadId ? baseSessionKey : raw.rawId); } -function buildFeishuParentOverrideCandidates(rawId: string | undefined): string[] { - const value = normalizeOptionalString(rawId); - if (!value) { - return []; - } - const topicSenderMatch = value.match(/^(.+):topic:([^:]+):sender:([^:]+)$/i); - if (topicSenderMatch) { - const chatId = normalizeOptionalLowercaseString(topicSenderMatch[1]); - const topicId = normalizeOptionalLowercaseString(topicSenderMatch[2]); - return [`${chatId}:topic:${topicId}`, chatId].filter((entry): entry is string => - Boolean(entry), - ); - } - const topicMatch = value.match(/^(.+):topic:([^:]+)$/i); - if (topicMatch) { - const chatId = normalizeOptionalLowercaseString(topicMatch[1]); - const topicId = normalizeOptionalLowercaseString(topicMatch[2]); - return [`${chatId}:topic:${topicId}`, chatId].filter((entry): entry is string => - Boolean(entry), - ); - } - const senderMatch = value.match(/^(.+):sender:([^:]+)$/i); - if (senderMatch) { - const chatId = normalizeOptionalLowercaseString(senderMatch[1]); - return chatId ? [chatId] : []; - } - return []; -} - function resolveDirectChannelModelMatch(params: { channel: string; providerEntries: Record; groupId?: string | null; parentSessionKey?: string | null; }): { model: string; matchKey?: string; matchSource?: ChannelMatchSource } | null { - const rawParent = parseRawSessionConversationRef(params.parentSessionKey); const directKeys = buildChannelKeyCandidates( params.groupId, ...buildGenericParentOverrideCandidates(params.parentSessionKey), - ...(normalizeOptionalLowercaseString(params.channel) === "feishu" - ? buildFeishuParentOverrideCandidates(rawParent?.rawId) - : []), ); if (directKeys.length === 0) { return null; diff --git a/src/secrets/channel-contract-surface-guardrails.test.ts b/src/secrets/channel-contract-surface-guardrails.test.ts index 876e8ab83d2..ce2ee815623 100644 --- a/src/secrets/channel-contract-surface-guardrails.test.ts +++ b/src/secrets/channel-contract-surface-guardrails.test.ts @@ -54,6 +54,10 @@ const CORE_SECRET_SURFACE_GUARDS = [ path: "src/gateway/channel-health-policy.ts", forbiddenPatterns: [/\btelegram\b/], }, + { + path: "src/channels/model-overrides.ts", + forbiddenPatterns: [/\bfeishu\b/], + }, { path: "src/media-understanding/defaults.ts", forbiddenPatterns: [