























@@ -222,4 +222,248 @@ describe("resolveChannelModelOverride", () => {
222222expect(resolved?.model).toBe("demo-provider/demo-parent-model");
223223expect(resolved?.matchKey).toBe("-100123");
224224});
225+226+it("matches direct-user-specific model override via directUserId", () => {
227+const resolved = resolveChannelModelOverride({
228+cfg: {
229+channels: {
230+modelByChannel: {
231+telegram: {
232+user123: "demo-provider/demo-direct-user-model",
233+"*": "demo-provider/demo-wildcard-model",
234+},
235+},
236+},
237+} as unknown as OpenClawConfig,
238+channel: "telegram",
239+groupChatType: "direct",
240+directUserIds: ["user123"],
241+});
242+243+expect(resolved?.model).toBe("demo-provider/demo-direct-user-model");
244+expect(resolved?.matchKey).toBe("user123");
245+});
246+247+it("falls back to wildcard when no directUserId match exists", () => {
248+const resolved = resolveChannelModelOverride({
249+cfg: {
250+channels: {
251+modelByChannel: {
252+telegram: {
253+user999: "demo-provider/demo-other-user-model",
254+"*": "demo-provider/demo-wildcard-model",
255+},
256+},
257+},
258+} as unknown as OpenClawConfig,
259+channel: "telegram",
260+groupChatType: "direct",
261+directUserIds: ["user123"],
262+});
263+264+expect(resolved?.model).toBe("demo-provider/demo-wildcard-model");
265+expect(resolved?.matchKey).toBe("*");
266+expect(resolved?.matchSource).toBe("wildcard");
267+});
268+269+it("matches direct-user-specific model override via directUserId from origin.from", () => {
270+const resolved = resolveChannelModelOverride({
271+cfg: {
272+channels: {
273+modelByChannel: {
274+slack: {
275+"user:U12345": "demo-provider/demo-slack-dm-model",
276+},
277+},
278+},
279+} as unknown as OpenClawConfig,
280+channel: "slack",
281+groupChatType: "direct",
282+directUserIds: ["user:U12345"],
283+});
284+285+expect(resolved?.model).toBe("demo-provider/demo-slack-dm-model");
286+expect(resolved?.matchKey).toBe("user:U12345");
287+});
288+289+it("ignores directUserId when a groupId is present (group takes precedence)", () => {
290+const resolved = resolveChannelModelOverride({
291+cfg: {
292+channels: {
293+modelByChannel: {
294+telegram: {
295+"-100123": "demo-provider/demo-group-model",
296+user456: "demo-provider/demo-direct-user-model",
297+},
298+},
299+},
300+} as unknown as OpenClawConfig,
301+channel: "telegram",
302+groupId: "-100123",
303+directUserIds: ["user456"],
304+});
305+306+expect(resolved?.model).toBe("demo-provider/demo-group-model");
307+expect(resolved?.matchKey).toBe("-100123");
308+});
309+310+it("matches slack DM when origin.from is slack:U... but config has user:U... (multi-candidate)", () => {
311+const resolved = resolveChannelModelOverride({
312+cfg: {
313+channels: {
314+modelByChannel: {
315+slack: {
316+"user:U12345": "demo-provider/demo-slack-dm-model",
317+},
318+},
319+},
320+} as unknown as OpenClawConfig,
321+channel: "slack",
322+groupChatType: "direct",
323+directUserIds: ["slack:U12345", "user:U12345"],
324+});
325+326+expect(resolved?.model).toBe("demo-provider/demo-slack-dm-model");
327+expect(resolved?.matchKey).toBe("user:U12345");
328+});
329+330+it("matches discord DM when multiple candidate forms are present", () => {
331+const resolved = resolveChannelModelOverride({
332+cfg: {
333+channels: {
334+modelByChannel: {
335+discord: {
336+"12345": "demo-provider/demo-discord-dm-model",
337+},
338+},
339+},
340+} as unknown as OpenClawConfig,
341+channel: "discord",
342+groupChatType: "direct",
343+directUserIds: ["discord:12345", "user:12345", "12345"],
344+});
345+346+expect(resolved?.model).toBe("demo-provider/demo-discord-dm-model");
347+expect(resolved?.matchKey).toBe("12345");
348+});
349+350+it("matches telegram DM when raw SenderId is in candidates alongside prefixed forms", () => {
351+const resolved = resolveChannelModelOverride({
352+cfg: {
353+channels: {
354+modelByChannel: {
355+telegram: {
356+"67890": "demo-provider/demo-telegram-dm-model",
357+},
358+},
359+},
360+} as unknown as OpenClawConfig,
361+channel: "telegram",
362+groupChatType: "direct",
363+directUserIds: ["telegram:67890", "user:67890", "67890"],
364+});
365+366+expect(resolved?.model).toBe("demo-provider/demo-telegram-dm-model");
367+expect(resolved?.matchKey).toBe("67890");
368+});
369+370+it("prefers first matching candidate over later candidates", () => {
371+const resolved = resolveChannelModelOverride({
372+cfg: {
373+channels: {
374+modelByChannel: {
375+slack: {
376+"slack:U12345": "demo-provider/demo-prefixed-model",
377+"user:U12345": "demo-provider/demo-user-model",
378+},
379+},
380+},
381+} as unknown as OpenClawConfig,
382+channel: "slack",
383+groupChatType: "direct",
384+directUserIds: ["slack:U12345", "user:U12345"],
385+});
386+387+expect(resolved?.model).toBe("demo-provider/demo-prefixed-model");
388+expect(resolved?.matchKey).toBe("slack:U12345");
389+});
390+391+it("derives raw peer ID from channel-prefixed origin.from for telegram DM", () => {
392+const resolved = resolveChannelModelOverride({
393+cfg: {
394+channels: {
395+modelByChannel: {
396+telegram: {
397+"12345": "demo-provider/demo-telegram-dm-model",
398+},
399+},
400+},
401+} as unknown as OpenClawConfig,
402+channel: "telegram",
403+groupChatType: "direct",
404+directUserIds: ["telegram:12345"],
405+});
406+407+expect(resolved?.model).toBe("demo-provider/demo-telegram-dm-model");
408+expect(resolved?.matchKey).toBe("12345");
409+});
410+411+it("derives raw peer ID from channel-prefixed origin.from for discord DM", () => {
412+const resolved = resolveChannelModelOverride({
413+cfg: {
414+channels: {
415+modelByChannel: {
416+discord: {
417+"67890": "demo-provider/demo-discord-dm-model",
418+},
419+},
420+},
421+} as unknown as OpenClawConfig,
422+channel: "discord",
423+groupChatType: "direct",
424+directUserIds: ["discord:67890"],
425+});
426+427+expect(resolved?.model).toBe("demo-provider/demo-discord-dm-model");
428+expect(resolved?.matchKey).toBe("67890");
429+});
430+431+it("does not strip prefix for a different channel", () => {
432+const resolved = resolveChannelModelOverride({
433+cfg: {
434+channels: {
435+modelByChannel: {
436+telegram: {
437+"12345": "demo-provider/demo-telegram-dm-model",
438+},
439+},
440+},
441+} as unknown as OpenClawConfig,
442+channel: "telegram",
443+groupChatType: "direct",
444+directUserIds: ["discord:12345"],
445+});
446+447+expect(resolved).toBeNull();
448+});
449+450+it("does not leak directUserId match into non-direct conversations", () => {
451+const resolved = resolveChannelModelOverride({
452+cfg: {
453+channels: {
454+modelByChannel: {
455+telegram: {
456+user123: "demo-provider/demo-dm-model",
457+},
458+},
459+},
460+} as unknown as OpenClawConfig,
461+channel: "telegram",
462+groupChatType: "group",
463+groupId: "some-group",
464+directUserIds: ["user123"],
465+});
466+467+expect(resolved).toBeNull();
468+});
225469});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。