




















@@ -150,6 +150,107 @@ const replyMediaPathMocks = vi.hoisted(() => ({
150150const runtimePluginMocks = vi.hoisted(() => ({
151151ensureRuntimePluginsLoaded: vi.fn(),
152152}));
153+const conversationBindingMocks = vi.hoisted(() => {
154+type BindingMsgContext = {
155+OriginatingChannel?: string | null;
156+Surface?: string | null;
157+Provider?: string | null;
158+AccountId?: string | null;
159+MessageThreadId?: string | number | null;
160+ThreadParentId?: string | null;
161+SenderId?: string | null;
162+SessionKey?: string | null;
163+ParentSessionKey?: string | null;
164+OriginatingTo?: string | null;
165+To?: string | null;
166+From?: string | null;
167+NativeChannelId?: string | null;
168+};
169+type BindingConfig = {
170+channels?: Record<string, { defaultAccount?: string | null } | undefined>;
171+};
172+173+const normalizeText = (value: string | number | null | undefined) =>
174+typeof value === "number" ? `${value}` : (value ?? "").trim();
175+const normalizeChannel = (value: string | null | undefined) => normalizeText(value).toLowerCase();
176+const resolveChannel = (ctx: BindingMsgContext, commandChannel?: string | null) =>
177+normalizeChannel(ctx.OriginatingChannel ?? commandChannel ?? ctx.Surface ?? ctx.Provider);
178+const resolveAccountId = (ctx: BindingMsgContext, cfg: BindingConfig, channel: string) =>
179+normalizeText(ctx.AccountId) ||
180+normalizeText(cfg.channels?.[channel]?.defaultAccount) ||
181+"default";
182+const resolveTarget = (channel: string, value: string | null | undefined) => {
183+const target = normalizeText(value);
184+if (!target) {
185+return undefined;
186+}
187+const channelPrefix = `${channel}:`;
188+return target.toLowerCase().startsWith(channelPrefix)
189+ ? target.slice(channelPrefix.length)
190+ : target;
191+};
192+const resolveThreadId = (ctx: BindingMsgContext) =>
193+normalizeText(ctx.MessageThreadId) || undefined;
194+195+const resolveConversationBindingContextFromMessage = vi.fn(
196+(params: { cfg: BindingConfig; ctx: BindingMsgContext }) => {
197+const channel = resolveChannel(params.ctx);
198+if (!channel) {
199+return null;
200+}
201+const threadId = resolveThreadId(params.ctx);
202+const baseConversationId =
203+resolveTarget(channel, params.ctx.OriginatingTo) ?? resolveTarget(channel, params.ctx.To);
204+const conversationId = threadId ?? baseConversationId;
205+if (!conversationId) {
206+return null;
207+}
208+const parentConversationId =
209+threadId && baseConversationId && baseConversationId !== threadId
210+ ? baseConversationId
211+ : resolveTarget(channel, params.ctx.ThreadParentId);
212+return {
213+ channel,
214+accountId: resolveAccountId(params.ctx, params.cfg, channel),
215+ conversationId,
216+ ...(parentConversationId ? { parentConversationId } : {}),
217+ ...(threadId ? { threadId } : {}),
218+};
219+},
220+);
221+222+return {
223+resolveConversationBindingAccountIdFromMessage: (params: {
224+ctx: BindingMsgContext;
225+cfg: BindingConfig;
226+commandChannel?: string | null;
227+}) =>
228+resolveAccountId(params.ctx, params.cfg, resolveChannel(params.ctx, params.commandChannel)),
229+resolveConversationBindingChannelFromMessage: (
230+ctx: BindingMsgContext,
231+commandChannel?: string | null,
232+) => resolveChannel(ctx, commandChannel),
233+resolveConversationBindingContextFromAcpCommand: (params: {
234+cfg: BindingConfig;
235+ctx: BindingMsgContext;
236+command?: { to?: string | null; senderId?: string | null };
237+sessionKey?: string | null;
238+parentSessionKey?: string | null;
239+}) =>
240+resolveConversationBindingContextFromMessage({
241+cfg: params.cfg,
242+ctx: {
243+ ...params.ctx,
244+SenderId: params.command?.senderId ?? params.ctx.SenderId,
245+SessionKey: params.sessionKey ?? params.ctx.SessionKey,
246+ParentSessionKey: params.parentSessionKey ?? params.ctx.ParentSessionKey,
247+To: params.command?.to ?? params.ctx.To,
248+},
249+}),
250+ resolveConversationBindingContextFromMessage,
251+resolveConversationBindingThreadIdFromMessage: (ctx: BindingMsgContext) => resolveThreadId(ctx),
252+};
253+});
153254const threadInfoMocks = vi.hoisted(() => ({
154255parseSessionThreadInfo: vi.fn<
155256(sessionKey: string | undefined) => {
@@ -345,6 +446,18 @@ vi.mock("./reply-media-paths.runtime.js", () => ({
345446vi.mock("../../agents/runtime-plugins.js", () => ({
346447ensureRuntimePluginsLoaded: runtimePluginMocks.ensureRuntimePluginsLoaded,
347448}));
449+vi.mock("./conversation-binding-input.js", () => ({
450+resolveConversationBindingAccountIdFromMessage:
451+conversationBindingMocks.resolveConversationBindingAccountIdFromMessage,
452+resolveConversationBindingChannelFromMessage:
453+conversationBindingMocks.resolveConversationBindingChannelFromMessage,
454+resolveConversationBindingContextFromAcpCommand:
455+conversationBindingMocks.resolveConversationBindingContextFromAcpCommand,
456+resolveConversationBindingContextFromMessage:
457+conversationBindingMocks.resolveConversationBindingContextFromMessage,
458+resolveConversationBindingThreadIdFromMessage:
459+conversationBindingMocks.resolveConversationBindingThreadIdFromMessage,
460+}));
348461vi.mock("../../tts/status-config.js", () => ({
349462resolveStatusTtsSnapshot: () => ({
350463autoMode: "always",
@@ -771,7 +884,8 @@ describe("dispatchReplyFromConfig", () => {
771884OriginatingTo: undefined,
772885});
773886774-const replyResolver = async () => ({ text: "hi" }) satisfies ReplyPayload;
887+const replyResolver = async () =>
888+({ text: "hi", mediaUrl: "https://example.test/reply.png" }) satisfies ReplyPayload;
775889await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver });
776890777891expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。