mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-25 08:02:04 +00:00
fix: move bootstrap session grammar into plugins
This commit is contained in:
@@ -40,6 +40,11 @@ in the plugin with `messaging.resolveSessionConversation(...)`. That is the
|
||||
canonical hook for mapping `rawId` to the base conversation id, optional thread
|
||||
id, and any `parentConversationCandidates`.
|
||||
|
||||
Bundled plugins that need the same parsing before the channel registry boots
|
||||
can also expose a top-level `session-key-api.ts` file with a matching
|
||||
`resolveSessionConversation(...)` export. Core uses that bootstrap-safe surface
|
||||
only when the runtime plugin registry is not available yet.
|
||||
|
||||
`messaging.resolveParentConversationCandidates(...)` remains available as a
|
||||
legacy compatibility fallback when a plugin only needs parent fallbacks on top
|
||||
of the generic/raw id. If both hooks exist, core uses
|
||||
|
||||
1
extensions/feishu/session-key-api.ts
Normal file
1
extensions/feishu/session-key-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { resolveFeishuSessionConversation as resolveSessionConversation } from "./src/session-conversation.js";
|
||||
@@ -55,6 +55,10 @@ import {
|
||||
import { listFeishuDirectoryPeers, listFeishuDirectoryGroups } from "./directory.static.js";
|
||||
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import {
|
||||
resolveFeishuParentConversationCandidates,
|
||||
resolveFeishuSessionConversation,
|
||||
} from "./session-conversation.js";
|
||||
import { resolveFeishuOutboundSessionRoute } from "./session-route.js";
|
||||
import { feishuSetupAdapter } from "./setup-core.js";
|
||||
import { feishuSetupWizard } from "./setup-surface.js";
|
||||
@@ -275,43 +279,6 @@ function normalizeFeishuAcpConversationId(conversationId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveFeishuParentConversationCandidates(rawId: string): string[] {
|
||||
const parsed = parseFeishuConversationId({ conversationId: rawId });
|
||||
if (!parsed) {
|
||||
return [];
|
||||
}
|
||||
switch (parsed.scope) {
|
||||
case "group_topic_sender":
|
||||
return [
|
||||
buildFeishuConversationId({
|
||||
chatId: parsed.chatId,
|
||||
scope: "group_topic",
|
||||
topicId: parsed.topicId,
|
||||
}),
|
||||
parsed.chatId,
|
||||
];
|
||||
case "group_topic":
|
||||
case "group_sender":
|
||||
return [parsed.chatId];
|
||||
case "group":
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFeishuSessionConversation(rawId: string) {
|
||||
const parsed = parseFeishuConversationId({ conversationId: rawId });
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: parsed.canonicalConversationId,
|
||||
parentConversationCandidates: resolveFeishuParentConversationCandidates(
|
||||
parsed.canonicalConversationId,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function matchFeishuAcpConversation(params: {
|
||||
bindingConversationId: string;
|
||||
conversationId: string;
|
||||
@@ -1105,7 +1072,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
|
||||
setupWizard: feishuSetupWizard,
|
||||
messaging: {
|
||||
normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined,
|
||||
resolveSessionConversation: ({ rawId }) => resolveFeishuSessionConversation(rawId),
|
||||
resolveSessionConversation: ({ kind, rawId }) =>
|
||||
resolveFeishuSessionConversation({ kind, rawId }),
|
||||
resolveOutboundSessionRoute: (params) => resolveFeishuOutboundSessionRoute(params),
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeFeishuId,
|
||||
|
||||
41
extensions/feishu/src/session-conversation.ts
Normal file
41
extensions/feishu/src/session-conversation.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { buildFeishuConversationId, parseFeishuConversationId } from "./conversation-id.js";
|
||||
|
||||
export function resolveFeishuParentConversationCandidates(rawId: string): string[] {
|
||||
const parsed = parseFeishuConversationId({ conversationId: rawId });
|
||||
if (!parsed) {
|
||||
return [];
|
||||
}
|
||||
switch (parsed.scope) {
|
||||
case "group_topic_sender":
|
||||
return [
|
||||
buildFeishuConversationId({
|
||||
chatId: parsed.chatId,
|
||||
scope: "group_topic",
|
||||
topicId: parsed.topicId,
|
||||
}),
|
||||
parsed.chatId,
|
||||
];
|
||||
case "group_topic":
|
||||
case "group_sender":
|
||||
return [parsed.chatId];
|
||||
case "group":
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveFeishuSessionConversation(params: {
|
||||
kind: "group" | "channel";
|
||||
rawId: string;
|
||||
}) {
|
||||
const parsed = parseFeishuConversationId({ conversationId: params.rawId });
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: parsed.canonicalConversationId,
|
||||
parentConversationCandidates: resolveFeishuParentConversationCandidates(
|
||||
parsed.canonicalConversationId,
|
||||
),
|
||||
};
|
||||
}
|
||||
1
extensions/telegram/session-key-api.ts
Normal file
1
extensions/telegram/session-key-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { resolveTelegramSessionConversation as resolveSessionConversation } from "./src/session-conversation.js";
|
||||
@@ -72,6 +72,7 @@ import type { TelegramProbe } from "./probe.js";
|
||||
import { resolveTelegramReactionLevel } from "./reaction-level.js";
|
||||
import { getTelegramRuntime } from "./runtime.js";
|
||||
import { sendMessageTelegram, sendPollTelegram, sendTypingTelegram } from "./send.js";
|
||||
import { resolveTelegramSessionConversation } from "./session-conversation.js";
|
||||
import { telegramSetupAdapter } from "./setup-core.js";
|
||||
import { telegramSetupWizard } from "./setup-surface.js";
|
||||
import {
|
||||
@@ -232,18 +233,6 @@ function normalizeTelegramAcpConversationId(conversationId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTelegramSessionConversation(rawId: string) {
|
||||
const parsed = parseTelegramTopicConversation({ conversationId: rawId });
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: parsed.chatId,
|
||||
threadId: parsed.topicId,
|
||||
parentConversationCandidates: [parsed.chatId],
|
||||
};
|
||||
}
|
||||
|
||||
function matchTelegramAcpConversation(params: {
|
||||
bindingConversationId: string;
|
||||
conversationId: string;
|
||||
@@ -542,7 +531,8 @@ export const telegramPlugin = createChatChannelPlugin({
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeTelegramMessagingTarget,
|
||||
resolveSessionConversation: ({ rawId }) => resolveTelegramSessionConversation(rawId),
|
||||
resolveSessionConversation: ({ kind, rawId }) =>
|
||||
resolveTelegramSessionConversation({ kind, rawId }),
|
||||
parseExplicitTarget: ({ raw }) => parseTelegramExplicitTarget(raw),
|
||||
inferTargetChatType: ({ to }) => parseTelegramExplicitTarget(to).chatType,
|
||||
formatTargetDisplay: ({ target, display, kind }) => {
|
||||
|
||||
16
extensions/telegram/src/session-conversation.ts
Normal file
16
extensions/telegram/src/session-conversation.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { parseTelegramTopicConversation } from "openclaw/plugin-sdk/telegram-core";
|
||||
|
||||
export function resolveTelegramSessionConversation(params: {
|
||||
kind: "group" | "channel";
|
||||
rawId: string;
|
||||
}) {
|
||||
const parsed = parseTelegramTopicConversation({ conversationId: params.rawId });
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: parsed.chatId,
|
||||
threadId: parsed.topicId,
|
||||
parentConversationCandidates: [parsed.chatId],
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { resetPluginRuntimeStateForTest, 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";
|
||||
@@ -167,4 +167,27 @@ describe("resolveChannelModelOverride", () => {
|
||||
expect(resolved?.model).toBe("demo-provider/demo-channel-model");
|
||||
expect(resolved?.matchKey).toBe("thread-parent");
|
||||
});
|
||||
|
||||
it("keeps bundled Feishu parent fallback matching before registry bootstrap", () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +61,25 @@ describe("session conversation routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bundled Feishu parent fallbacks available before registry bootstrap", () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
|
||||
expect(
|
||||
resolveSessionConversationRef(
|
||||
"agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
|
||||
),
|
||||
).toEqual({
|
||||
channel: "feishu",
|
||||
kind: "group",
|
||||
rawId: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
|
||||
id: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
|
||||
threadId: undefined,
|
||||
baseSessionKey:
|
||||
"agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
|
||||
parentConversationCandidates: ["oc_group_chat:topic:om_topic_root", "oc_group_chat"],
|
||||
});
|
||||
});
|
||||
|
||||
it("lets Feishu own parent fallback candidates", () => {
|
||||
expect(
|
||||
resolveSessionConversationRef(
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { parseTelegramTopicConversation } from "../../acp/conversation-id.js";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadBundledPluginPublicSurfaceModuleSync } from "../../plugin-sdk/facade-runtime.js";
|
||||
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
|
||||
import { resolveBundledPluginPublicSurfacePath } from "../../plugins/bundled-plugin-metadata.js";
|
||||
import {
|
||||
parseRawSessionConversationRef,
|
||||
parseThreadSessionSuffix,
|
||||
@@ -30,6 +33,20 @@ type SessionConversationHookResult = {
|
||||
parentConversationCandidates?: string[];
|
||||
};
|
||||
|
||||
type SessionConversationResolverParams = {
|
||||
kind: "group" | "channel";
|
||||
rawId: string;
|
||||
};
|
||||
|
||||
type BundledSessionKeyModule = {
|
||||
resolveSessionConversation?: (
|
||||
params: SessionConversationResolverParams,
|
||||
) => SessionConversationHookResult | null;
|
||||
};
|
||||
|
||||
const OPENCLAW_PACKAGE_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
|
||||
const SESSION_KEY_API_ARTIFACT_BASENAME = "session-key-api.js";
|
||||
|
||||
type NormalizedSessionConversationResolution = ResolvedSessionConversation & {
|
||||
hasExplicitParentConversationCandidates: boolean;
|
||||
};
|
||||
@@ -111,23 +128,36 @@ function normalizeSessionConversationResolution(
|
||||
|
||||
function resolveBundledSessionConversationFallback(params: {
|
||||
channel: string;
|
||||
kind: "group" | "channel";
|
||||
rawId: string;
|
||||
}): NormalizedSessionConversationResolution | null {
|
||||
if (normalizeResolvedChannel(params.channel) !== "telegram") {
|
||||
const dirName = normalizeResolvedChannel(params.channel);
|
||||
if (
|
||||
!resolveBundledPluginPublicSurfacePath({
|
||||
rootDir: OPENCLAW_PACKAGE_ROOT,
|
||||
bundledPluginsDir: resolveBundledPluginsDir(),
|
||||
dirName,
|
||||
artifactBasename: SESSION_KEY_API_ARTIFACT_BASENAME,
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = parseTelegramTopicConversation({ conversationId: params.rawId });
|
||||
if (!parsed) {
|
||||
const resolveSessionConversation =
|
||||
loadBundledPluginPublicSurfaceModuleSync<BundledSessionKeyModule>({
|
||||
dirName,
|
||||
artifactBasename: SESSION_KEY_API_ARTIFACT_BASENAME,
|
||||
}).resolveSessionConversation;
|
||||
if (typeof resolveSessionConversation !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: parsed.chatId,
|
||||
threadId: parsed.topicId,
|
||||
parentConversationCandidates: [parsed.chatId],
|
||||
hasExplicitParentConversationCandidates: true,
|
||||
};
|
||||
return normalizeSessionConversationResolution(
|
||||
resolveSessionConversation({
|
||||
kind: params.kind,
|
||||
rawId: params.rawId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSessionConversationResolution(params: {
|
||||
@@ -151,6 +181,7 @@ function resolveSessionConversationResolution(params: {
|
||||
pluginResolved ??
|
||||
resolveBundledSessionConversationFallback({
|
||||
channel: params.channel,
|
||||
kind: params.kind,
|
||||
rawId,
|
||||
}) ??
|
||||
buildGenericConversationResolution(rawId);
|
||||
|
||||
@@ -403,6 +403,8 @@ export type ChannelMessagingAdapter = {
|
||||
* inside `rawId` (for example Telegram topics or Feishu sender scopes).
|
||||
* Return `parentConversationCandidates` here when you can so parsing and
|
||||
* inheritance stay in one place.
|
||||
* Bundled plugins that need the same grammar before runtime bootstrap can
|
||||
* mirror this contract through a top-level `session-key-api.ts` surface.
|
||||
*/
|
||||
resolveSessionConversation?: (params: { kind: "group" | "channel"; rawId: string }) => {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user