@tbox.cn/app-template-agent 0.7.5 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.gitignore +5 -0
  2. package/.tbox/app.json +9 -1
  3. package/AGENTS.md +12 -4
  4. package/ENV.md +105 -21
  5. package/README.md +40 -38
  6. package/apps/client/VISUAL_RESTORE.md +11 -11
  7. package/apps/client/index.html +3 -1
  8. package/apps/client/package.json +9 -8
  9. package/apps/client/src/business/App.tsx +4 -4
  10. package/apps/client/src/business/README.md +5 -11
  11. package/apps/client/src/cards/registry.ts +23 -8
  12. package/apps/client/src/main.tsx +36 -2
  13. package/apps/client/src/modules.ts +7 -4
  14. package/apps/client/src/pages/index/index.tsx +18 -162
  15. package/apps/client/src/platform/AgentChatApp.tsx +7 -6
  16. package/apps/client/src/platform/agent-runtime.ts +5 -0
  17. package/apps/client/src/store/chat.ts +47 -185
  18. package/apps/client/src/test/setup.ts +7 -0
  19. package/apps/client/vite.config.ts +16 -1
  20. package/apps/server/package.json +0 -3
  21. package/apps/server/src/app-tools.ts +2 -1
  22. package/apps/server/src/app.ts +248 -119
  23. package/apps/server/src/config/integration.ts +2 -6
  24. package/apps/server/src/dev.ts +0 -1
  25. package/apps/server/src/intent-routes.ts +3 -3
  26. package/apps/server/src/modules.ts +4 -4
  27. package/apps/server/src/pre-tools.ts +4 -3
  28. package/apps/server/tests/agent-prompt.test.ts +81 -0
  29. package/apps/server/tests/cards-registry.test.ts +1 -3
  30. package/apps/server/tests/deployment-loader.test.ts +35 -56
  31. package/apps/server/tests/tool-runtime.test.ts +25 -61
  32. package/package.json +5 -4
  33. package/packages/agent-ui-kit/LICENSE +21 -0
  34. package/packages/agent-ui-kit/README.md +17 -0
  35. package/packages/agent-ui-kit/package.json +58 -0
  36. package/packages/agent-ui-kit/src/ChatPage.less +62 -0
  37. package/packages/agent-ui-kit/src/ChatPage.tsx +305 -0
  38. package/packages/agent-ui-kit/src/chat-config.test.ts +82 -0
  39. package/packages/agent-ui-kit/src/chat-config.ts +62 -0
  40. package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/DebugButton.tsx +2 -2
  41. package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.less +19 -16
  42. package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.tsx +10 -15
  43. package/packages/agent-ui-kit/src/components/CardRenderer.tsx +36 -0
  44. package/packages/agent-ui-kit/src/components/ChatHeader/index.less +148 -0
  45. package/packages/agent-ui-kit/src/components/ChatHeader/index.test.tsx +74 -0
  46. package/{apps/client → packages/agent-ui-kit}/src/components/ChatHeader/index.tsx +27 -15
  47. package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.less +47 -39
  48. package/packages/agent-ui-kit/src/components/ChatInput/index.test.tsx +156 -0
  49. package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.tsx +13 -4
  50. package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/useVoicePress.ts +2 -2
  51. package/packages/agent-ui-kit/src/components/HistoryEntry/index.less +49 -0
  52. package/packages/agent-ui-kit/src/components/HistoryEntry/index.tsx +42 -0
  53. package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.less +3 -3
  54. package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.tsx +6 -6
  55. package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.less +25 -20
  56. package/packages/agent-ui-kit/src/components/MessageBubble/index.test.tsx +112 -0
  57. package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.tsx +22 -17
  58. package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.less +2 -2
  59. package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.tsx +2 -2
  60. package/packages/agent-ui-kit/src/components/QuickCommands/index.less +63 -0
  61. package/packages/agent-ui-kit/src/components/QuickCommands/index.test.tsx +99 -0
  62. package/packages/agent-ui-kit/src/components/QuickCommands/index.tsx +52 -0
  63. package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.less +1 -1
  64. package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.test.ts +346 -0
  65. package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.ts +204 -0
  66. package/packages/agent-ui-kit/src/components/ToolCallBlock/index.less +312 -0
  67. package/packages/agent-ui-kit/src/components/ToolCallBlock/index.test.tsx +261 -0
  68. package/packages/agent-ui-kit/src/components/ToolCallBlock/index.tsx +212 -0
  69. package/packages/agent-ui-kit/src/components/ToolCallBlock/titles.ts +102 -0
  70. package/packages/agent-ui-kit/src/hooks/use-drag-gesture.test.ts +66 -0
  71. package/packages/agent-ui-kit/src/hooks/use-drag-gesture.ts +129 -0
  72. package/packages/agent-ui-kit/src/hooks/use-pull-down-history.ts +241 -0
  73. package/packages/agent-ui-kit/src/hooks/use-pull-down-wheel-native.test.ts +64 -0
  74. package/{apps/client → packages/agent-ui-kit}/src/hooks/useMediaUpload.ts +3 -3
  75. package/{apps/client → packages/agent-ui-kit}/src/hooks/useTTSAutoPlay.ts +7 -5
  76. package/packages/agent-ui-kit/src/index.ts +69 -0
  77. package/packages/agent-ui-kit/src/store-context.tsx +92 -0
  78. package/packages/agent-ui-kit/src/test/setup.ts +7 -0
  79. package/packages/agent-ui-kit/src/theme/contract.json +72 -0
  80. package/packages/agent-ui-kit/src/theme/global.less +99 -0
  81. package/packages/agent-ui-kit/src/theme/motion.less +19 -0
  82. package/packages/agent-ui-kit/src/theme/theme-contract.test.ts +69 -0
  83. package/packages/agent-ui-kit/src/theme/theme.ts +17 -0
  84. package/packages/agent-ui-kit/src/theme/tokens.less +79 -0
  85. package/packages/agent-ui-kit/src/utils/haptics.test.ts +51 -0
  86. package/packages/agent-ui-kit/src/utils/haptics.ts +44 -0
  87. package/packages/agent-ui-kit/src/utils/pull-down-machine.test.ts +188 -0
  88. package/packages/agent-ui-kit/src/utils/pull-down-machine.ts +157 -0
  89. package/packages/agent-ui-kit/src/utils/tool-call-display.test.ts +240 -0
  90. package/packages/agent-ui-kit/src/utils/tool-call-display.ts +146 -0
  91. package/packages/agent-ui-kit/src/vite-env.d.ts +1 -0
  92. package/packages/agent-ui-kit/tsconfig.json +8 -0
  93. package/packages/agent-ui-kit/vitest.config.ts +7 -0
  94. package/pnpm-lock.template.yaml +177 -111
  95. package/pnpm-workspace.template.yaml +12 -4
  96. package/profiles/mall/config/agent.jsonc +33 -0
  97. package/profiles/mall/config/chat.json +21 -0
  98. package/profiles/mall/config/knowledge.json +5 -0
  99. package/profiles/mall/config/llm_provider.json +5 -0
  100. package/profiles/mall/config/plugins.json +3 -0
  101. package/profiles/mall.json +16 -0
  102. package/scripts/check-workspace.mjs +3 -2
  103. package/apps/client/assets/copy@2x.png +0 -0
  104. package/apps/client/assets/debug.png +0 -0
  105. package/apps/client/assets/dislike@2x.png +0 -0
  106. package/apps/client/assets/like@2x.png +0 -0
  107. package/apps/client/assets/speaker@2x.png +0 -0
  108. package/apps/client/src/cards/CardRenderer.tsx +0 -20
  109. package/apps/client/src/components/ChatHeader/index.less +0 -104
  110. package/apps/client/src/components/ErrorBoundary.tsx +0 -2
  111. package/apps/client/src/components/QuickCommands/index.less +0 -48
  112. package/apps/client/src/components/QuickCommands/index.tsx +0 -29
  113. package/apps/client/src/components/ToolCallBlock/index.less +0 -131
  114. package/apps/client/src/components/ToolCallBlock/index.tsx +0 -125
  115. package/apps/client/src/examples/tourism/TourismLanding.tsx +0 -96
  116. package/apps/client/src/examples/tourism/index.less +0 -192
  117. package/apps/client/src/pages/index/index.less +0 -43
  118. package/apps/client/src/store/chat-config.ts +0 -40
  119. package/apps/client/src/styles/global.less +0 -60
  120. package/apps/client/src/styles/tokens.less +0 -60
  121. package/apps/server/src/config/deployment.ts +0 -47
  122. package/packages/module-weather/package.json +0 -36
  123. package/packages/module-weather/skills/weather-guide/SKILL.md +0 -22
  124. package/packages/module-weather/skills/weather-guide/references/dressing-tips.md +0 -5
  125. package/packages/module-weather/src/client/cards/weather/index.less +0 -22
  126. package/packages/module-weather/src/client/cards/weather/index.tsx +0 -14
  127. package/packages/module-weather/src/client/index.ts +0 -19
  128. package/packages/module-weather/src/index.ts +0 -2
  129. package/packages/module-weather/src/server/cards/weather/meta.ts +0 -27
  130. package/packages/module-weather/src/server/index.ts +0 -45
  131. package/packages/module-weather/src/server/pre-tools.ts +0 -20
  132. package/packages/module-weather/src/server/tool.ts +0 -29
  133. package/packages/module-weather/tbox.module.json +0 -27
  134. package/packages/module-weather/tsconfig.json +0 -14
  135. /package/{apps/client → packages/agent-ui-kit}/assets/keyboard-circle.svg +0 -0
  136. /package/{apps/client → packages/agent-ui-kit}/assets/more-circle.svg +0 -0
  137. /package/{apps/client → packages/agent-ui-kit}/assets/new-chat.svg +0 -0
  138. /package/{apps/client → packages/agent-ui-kit}/assets/quick-command-star.svg +0 -0
  139. /package/{apps/client → packages/agent-ui-kit}/assets/send-gradient.svg +0 -0
  140. /package/{apps/client → packages/agent-ui-kit}/assets/stop@2x.png +0 -0
  141. /package/{apps/client → packages/agent-ui-kit}/assets/voice-circle.svg +0 -0
  142. /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoicePressField.tsx +0 -0
  143. /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +0 -0
  144. /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/machine.ts +0 -0
  145. /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.less +0 -0
  146. /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.tsx +0 -0
  147. /package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.tsx +0 -0
  148. /package/{apps/client/src/types/chat.ts → packages/agent-ui-kit/src/types.ts} +0 -0
  149. /package/{apps/client → packages/agent-ui-kit}/src/utils/copyText.ts +0 -0
  150. /package/{apps/client → packages/agent-ui-kit}/src/utils/imagePreview.ts +0 -0
  151. /package/{apps/client → packages/agent-ui-kit}/src/utils/session-error.ts +0 -0
  152. /package/{apps/client → packages/agent-ui-kit}/src/utils/toast.ts +0 -0
@@ -0,0 +1,99 @@
1
+ // @vitest-environment happy-dom
2
+ /**
3
+ * QuickCommands 快捷胶囊(W2-1 自 mall 随迁):断言原样——harness 改 kit context 注入形式
4
+ * (原 mock 应用 store → 现最小测试 store + KitChatProvider;本地图文合并经 config store 快照注入)。
5
+ */
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { createRoot } from 'react-dom/client';
8
+ import { flushSync } from 'react-dom';
9
+ import type { ReactNode } from 'react';
10
+ import { create } from 'zustand';
11
+ import type { CoreChatStore } from '@tbox.cn/app-sdk/client';
12
+ import { configureChatConfigDefaults, useChatConfigStore } from '../../chat-config';
13
+ import { KitChatProvider } from '../../store-context';
14
+ import { QuickCommands } from './index';
15
+
16
+ /** 最小测试 store(仅 helloAck 消费面);注入形式经 KitChatProvider。 */
17
+ const testStore = create<{ helloAck: unknown }>(() => ({ helloAck: null })) as unknown as CoreChatStore;
18
+
19
+ /** 构建期快照(对齐 mall config/chat.json 三条——本地图标合并真源) */
20
+ const STATIC_COMMANDS = [
21
+ {
22
+ label: '今日优惠',
23
+ query: '现在有什么优惠?',
24
+ icon: 'https://mdn.alipayobjects.com/agt_content/afts/img/yirAQ7obEFcAAAAAAAAAAAAAehe3AQBr/original',
25
+ },
26
+ {
27
+ label: '品牌导览',
28
+ query: '品牌导览',
29
+ icon: 'https://mdn.alipayobjects.com/agt_content/afts/img/6-OCR5kWFaAAAAAAAAAAAAAAehe3AQBr/original',
30
+ },
31
+ {
32
+ label: '活动中心',
33
+ query: '活动中心',
34
+ icon: 'https://mdn.alipayobjects.com/agt_content/afts/img/yirAQ7obEFcAAAAAAAAAAAAAehe3AQBr/original',
35
+ },
36
+ ];
37
+
38
+ function renderWithKit(container: HTMLElement, node: ReactNode): void {
39
+ flushSync(() =>
40
+ createRoot(container).render(
41
+ <KitChatProvider store={testStore} cards={{}} rawCards={{}}>
42
+ {node}
43
+ </KitChatProvider>,
44
+ ),
45
+ );
46
+ }
47
+
48
+ beforeEach(() => {
49
+ configureChatConfigDefaults({ quickCommands: STATIC_COMMANDS });
50
+ });
51
+
52
+ afterEach(() => {
53
+ testStore.setState({ helloAck: null } as never);
54
+ useChatConfigStore.setState({ quickCommands: [] });
55
+ document.body.innerHTML = '';
56
+ });
57
+
58
+ describe('QuickCommands 快捷胶囊', () => {
59
+ it('按服务端顺序渲染三项、合并本地图标并发送服务端 prompt', () => {
60
+ testStore.setState({
61
+ helloAck: {
62
+ chatConfig: {
63
+ quickCommands: [
64
+ { label: '今日优惠', prompt: '服务端优惠查询' },
65
+ { label: '品牌导览', prompt: '服务端品牌导览' },
66
+ { label: '活动中心', prompt: '服务端活动查询' },
67
+ ],
68
+ },
69
+ },
70
+ } as never);
71
+ const onCommand = vi.fn();
72
+ const container = document.createElement('div');
73
+ document.body.appendChild(container);
74
+ renderWithKit(container, <QuickCommands onCommand={onCommand} />);
75
+
76
+ const buttons = [...container.querySelectorAll<HTMLButtonElement>('.quick-commands__item')];
77
+ expect(buttons.map((button) => button.textContent)).toEqual(['今日优惠', '品牌导览', '活动中心']);
78
+ expect(container.textContent).not.toContain('找门店');
79
+ expect(buttons[1]?.querySelector('img')?.src).toContain('6-OCR5kWFa');
80
+ flushSync(() => buttons[1]!.click());
81
+ expect(onCommand).toHaveBeenCalledWith('服务端品牌导览');
82
+ });
83
+
84
+ it('隐藏时保留胶囊容器,但禁止交互', () => {
85
+ const onCommand = vi.fn();
86
+ const container = document.createElement('div');
87
+ document.body.appendChild(container);
88
+ renderWithKit(container, <QuickCommands hidden onCommand={onCommand} />);
89
+
90
+ const commands = container.querySelector('.quick-commands');
91
+ const button = container.querySelector<HTMLButtonElement>('.quick-commands__item');
92
+ expect(commands).not.toBeNull();
93
+ expect(commands?.classList.contains('quick-commands--hidden')).toBe(true);
94
+ expect(commands?.getAttribute('aria-hidden')).toBe('true');
95
+ expect(button?.disabled).toBe(true);
96
+ flushSync(() => button?.click());
97
+ expect(onCommand).not.toHaveBeenCalled();
98
+ });
99
+ });
@@ -0,0 +1,52 @@
1
+ // 输入框上方的快捷指令;文案由 chatConfig 驱动,服务端下发不含 icon 时按 label 合并本地配置。
2
+ // 011 I2:服务端 HelloAck.chatConfig.quickCommands 优先(单一真源),本地 chat.json 为 fallback。
3
+ // label 只写纯文字;前面的星标由本组件渲染(icon 字段或默认 ✦),不要把 ✦/emoji 写进 label。
4
+ import { useChatConfigStore } from '../../chat-config';
5
+ import { useChatStore } from '../../store-context';
6
+ import starIcon from '../../../assets/quick-command-star.svg';
7
+ import './index.less';
8
+
9
+ export function QuickCommands({
10
+ onCommand,
11
+ hidden = false,
12
+ }: {
13
+ onCommand: (query: string) => void;
14
+ hidden?: boolean;
15
+ }) {
16
+ const staticCommands = useChatConfigStore((s) => s.quickCommands);
17
+ const serverCommands = useChatStore((s) => s.helloAck?.chatConfig.quickCommands);
18
+ // 服务端下发优先;未收到 HELLO_ACK(非 mall 部署)回落本地静态
19
+ const staticByLabel = new Map(staticCommands.map((command) => [command.label, command]));
20
+ const commands = serverCommands && serverCommands.length > 0
21
+ ? serverCommands.map((command) => ({
22
+ label: command.label,
23
+ query: command.prompt,
24
+ icon: staticByLabel.get(command.label)?.icon,
25
+ }))
26
+ : staticCommands;
27
+ if (!commands.length) return null;
28
+
29
+ return (
30
+ <div
31
+ className={`quick-commands${hidden ? ' quick-commands--hidden' : ''}`}
32
+ aria-hidden={hidden || undefined}
33
+ >
34
+ {commands.map((cmd) => (
35
+ <button
36
+ key={cmd.label}
37
+ className="quick-commands__item"
38
+ disabled={hidden}
39
+ onClick={() => onCommand(cmd.query)}
40
+ >
41
+ <img
42
+ className="quick-commands__icon"
43
+ src={cmd.icon ?? starIcon}
44
+ alt=""
45
+ onError={(event) => { event.currentTarget.src = starIcon; }}
46
+ />
47
+ <span className="quick-commands__label">{cmd.label}</span>
48
+ </button>
49
+ ))}
50
+ </div>
51
+ );
52
+ }
@@ -13,7 +13,7 @@
13
13
  z-index: 10;
14
14
  opacity: 0;
15
15
  transform: scale(0.8);
16
- transition: opacity 0.2s ease, transform 0.2s ease;
16
+ transition: opacity var(--tbox-motion-duration-fast) ease, transform var(--tbox-motion-duration-fast) ease;
17
17
  pointer-events: none;
18
18
 
19
19
  &--visible {
@@ -0,0 +1,346 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ buildMemoryToolCallPayload,
4
+ buildPreToolArgsPayload,
5
+ buildPreToolResultPayload,
6
+ buildPreToolToolCallPayload,
7
+ buildSkillToolCallPayload,
8
+ MEMORY_TOOL_CALL_NAME,
9
+ PRE_TOOL_CALL_NAME,
10
+ SKILL_TOOL_CALL_NAME,
11
+ } from '@tbox.cn/app-contracts';
12
+ import type { ToolCallInfo } from '@tbox.cn/app-sdk/client';
13
+ import {
14
+ buildSummaryText,
15
+ deriveSummary,
16
+ deriveToolCallItem,
17
+ prettyDetailText,
18
+ } from './detail';
19
+ import type { ToolCallViewItem } from './detail';
20
+
21
+ /** 载荷一律经契约 builder 构造——与线上实时/历史路径字节级同构 */
22
+ const mkTc = (over: Partial<ToolCallInfo>): ToolCallInfo => ({
23
+ id: 't0',
24
+ name: 'plain_tool',
25
+ status: 'done',
26
+ ...over,
27
+ });
28
+
29
+ describe('deriveToolCallItem(三分支视图模型)', () => {
30
+ it('skill 带 description:前置技能徽标 + detail(input=描述 / output=已加载)', () => {
31
+ const payload = buildSkillToolCallPayload({ name: 'member-guide', description: '会员导购技能' });
32
+ const item = deriveToolCallItem(
33
+ mkTc({ id: 's1', name: SKILL_TOOL_CALL_NAME, args: payload.args, result: payload.result }),
34
+ );
35
+ expect(item.kind).toBe('skill');
36
+ expect(item.label).toBe('会员导购技能');
37
+ expect(item.badge).toBe('前置技能');
38
+ expect(item.status).toBe('done');
39
+ expect(item.detail).toEqual({ inputText: '会员导购技能', outputText: '已加载', isError: false });
40
+ });
41
+
42
+ it('skill 无中文 description → 中文通用标题;args 非法 JSON 不泄漏技术名;calling 态无详情入口', () => {
43
+ const payload = buildSkillToolCallPayload({ name: 'minimal-skill' });
44
+ const noDesc = deriveToolCallItem(
45
+ mkTc({ id: 's2', name: SKILL_TOOL_CALL_NAME, args: payload.args, result: payload.result }),
46
+ );
47
+ expect(noDesc.label).toBe('加载技能');
48
+ expect(noDesc.detail).toBeUndefined();
49
+ const badArgs = deriveToolCallItem(
50
+ mkTc({ id: 's3', name: SKILL_TOOL_CALL_NAME, args: 'not-json' }),
51
+ );
52
+ expect(badArgs.label).toBe('加载技能');
53
+ expect(badArgs.detail).toBeUndefined();
54
+ // 实时路径 START→ARGS→RESULT 逐帧,RESULT 前存在 calling 瞬态——详情入口必须屏蔽
55
+ const withDesc = buildSkillToolCallPayload({ name: 's', description: 'd' });
56
+ const calling = deriveToolCallItem(
57
+ mkTc({ id: 's4', name: SKILL_TOOL_CALL_NAME, status: 'calling', args: withDesc.args }),
58
+ );
59
+ expect(calling.status).toBe('calling');
60
+ expect(calling.label).toBe('加载技能');
61
+ expect(calling.detail).toBeUndefined();
62
+ });
63
+
64
+ it('preTool done:前置工具徽标 + input=toolArgs 紧凑串 + output=pretty(result)', () => {
65
+ const { args, result } = buildPreToolToolCallPayload({
66
+ toolName: 'member-balance',
67
+ description: '会员卡余额查询',
68
+ args: { cardNo: 'C001' },
69
+ result: { balance: 1200 },
70
+ });
71
+ const item = deriveToolCallItem(
72
+ mkTc({ id: 'p1', name: PRE_TOOL_CALL_NAME, args, result }),
73
+ );
74
+ expect(item.kind).toBe('preTool');
75
+ expect(item.label).toBe('会员卡余额查询');
76
+ expect(item.badge).toBe('前置工具');
77
+ expect(item.status).toBe('done');
78
+ expect(item.detail?.inputText).toBe('{"cardNo":"C001"}');
79
+ expect(item.detail?.outputText).toBe(JSON.stringify({ balance: 1200 }, null, 2));
80
+ expect(item.detail?.isError).toBe(false);
81
+ });
82
+
83
+ it('preTool error(payload status,tc.status 恒 done):error 态 + output=错误文案', () => {
84
+ const item = deriveToolCallItem(
85
+ mkTc({
86
+ id: 'p2',
87
+ name: PRE_TOOL_CALL_NAME,
88
+ args: buildPreToolArgsPayload({ toolName: 'parking-fee', description: '停车费查询', toolArgs: { plate: '沪A12345' } }),
89
+ result: buildPreToolResultPayload({ toolName: 'parking-fee', status: 'error', error: '接口超时' }),
90
+ }),
91
+ );
92
+ expect(item.status).toBe('error');
93
+ expect(item.detail?.isError).toBe(true);
94
+ expect(item.detail?.outputText).toBe('接口超时');
95
+ });
96
+
97
+ it('preTool calling(只有 args 无 result):无详情入口', () => {
98
+ const item = deriveToolCallItem(
99
+ mkTc({
100
+ id: 'p3',
101
+ name: PRE_TOOL_CALL_NAME,
102
+ status: 'calling',
103
+ args: buildPreToolArgsPayload({ toolName: 'member-balance', toolArgs: {} }),
104
+ }),
105
+ );
106
+ expect(item.status).toBe('calling');
107
+ expect(item.label).toBe('查询会员余额');
108
+ expect(item.detail).toBeUndefined();
109
+ });
110
+
111
+ it('normal done:中文标题 + 无徽标 + input=args / output=result 原文', () => {
112
+ const item = deriveToolCallItem(
113
+ mkTc({ id: 'n1', name: 'search_stores', args: '{"keyword":"咖啡"}', result: '{"total":3}' }),
114
+ );
115
+ expect(item.label).toBe('搜索商场门店');
116
+ expect(item.badge).toBeUndefined();
117
+ expect(item.detail).toEqual({ inputText: '{"keyword":"咖啡"}', outputText: '{"total":3}', isError: false });
118
+ });
119
+
120
+ it('normal error 与中断(error=请求中断):isError + output=错误文本', () => {
121
+ const failed = deriveToolCallItem(
122
+ mkTc({ id: 'n2', name: 'pay_order', status: 'error', args: '{}', error: '支付网关拒绝' }),
123
+ );
124
+ expect(failed.status).toBe('error');
125
+ expect(failed.detail?.isError).toBe(true);
126
+ expect(failed.detail?.outputText).toBe('支付网关拒绝');
127
+ const interrupted = deriveToolCallItem(
128
+ mkTc({ id: 'n3', name: 'search_stores', args: '{"k":"v"}', status: 'error', error: '请求中断' }),
129
+ );
130
+ expect(interrupted.detail?.outputText).toBe('请求中断');
131
+ expect(interrupted.detail?.isError).toBe(true);
132
+ });
133
+
134
+ it('normal 全空(CANCEL_RUN 后无 result)→ 无详情入口', () => {
135
+ const item = deriveToolCallItem(mkTc({ id: 'n4', name: 'search_stores', status: 'done' }));
136
+ expect(item.detail).toBeUndefined();
137
+ });
138
+
139
+ it('未知英文 normal 工具使用中文通用标题,不直接暴露技术名', () => {
140
+ const item = deriveToolCallItem(mkTc({ id: 'n5', name: 'future_vendor_lookup_v2', status: 'calling' }));
141
+ expect(item.label).toBe('处理工具任务');
142
+ expect(item.label).not.toContain('future_vendor_lookup_v2');
143
+ });
144
+ });
145
+
146
+ describe('prettyDetailText(归一 + 幂等 + 不截断)', () => {
147
+ it('合法紧凑 JSON → pretty;pretty 再入不变;纯文本原文;长文本不截断', () => {
148
+ const compact = '{"a":1,"b":[2,3]}';
149
+ const pretty = prettyDetailText(compact);
150
+ expect(pretty).toBe(JSON.stringify({ a: 1, b: [2, 3] }, null, 2));
151
+ expect(prettyDetailText(pretty)).toBe(pretty);
152
+ expect(prettyDetailText('plain text')).toBe('plain text');
153
+ const long = 'x'.repeat(600);
154
+ expect(prettyDetailText(long)).toHaveLength(600);
155
+ expect(prettyDetailText('')).toBe('');
156
+ });
157
+ });
158
+
159
+ describe('deriveToolCallItem memory 分支(018 tool-debug)', () => {
160
+ const memoryTc = (status: 'hit' | 'miss' | 'degraded' | 'skipped', over?: { error?: string; reason?: string }) => {
161
+ const payload = buildMemoryToolCallPayload({
162
+ query: '推荐适合父母的餐厅',
163
+ status,
164
+ ...(status === 'hit'
165
+ ? {
166
+ records: [
167
+ { id: 'm1', kind: 'preference', content: '用户偏好安静的家庭聚餐环境' },
168
+ { id: 'm2', kind: 'fact', content: '用户是黄金会员' },
169
+ ],
170
+ }
171
+ : {}),
172
+ ...(over?.error ? { error: over.error } : {}),
173
+ ...(over?.reason ? { reason: over.reason } : {}),
174
+ });
175
+ return mkTc({ id: 'mem-1', name: MEMORY_TOOL_CALL_NAME, args: payload.args, result: payload.result });
176
+ };
177
+
178
+ it('hit:记忆召回徽标 + label 命中数 + 详情(input=query / output=records 预览)', () => {
179
+ const item = deriveToolCallItem(memoryTc('hit'));
180
+ expect(item.kind).toBe('memory');
181
+ expect(item.badge).toBe('记忆召回');
182
+ expect(item.label).toBe('记忆召回 · 2 条命中');
183
+ expect(item.status).toBe('done');
184
+ expect(item.techName).toBe(MEMORY_TOOL_CALL_NAME);
185
+ expect(item.detail?.inputText).toBe('推荐适合父母的餐厅');
186
+ expect(item.detail?.outputText).toContain('[preference] 用户偏好安静的家庭聚餐环境');
187
+ expect(item.detail?.isError).toBe(false);
188
+ });
189
+
190
+ it('degraded:error 视觉态 + 详情=错误信息', () => {
191
+ const item = deriveToolCallItem(memoryTc('degraded', { error: '记忆召回超时' }));
192
+ expect(item.status).toBe('error');
193
+ expect(item.label).toBe('记忆召回降级');
194
+ expect(item.detail?.isError).toBe(true);
195
+ expect(item.detail?.outputText).toBe('记忆召回超时');
196
+ });
197
+
198
+ it('skipped(intent):done 态 + label 跳过原因 + 无详情', () => {
199
+ const item = deriveToolCallItem(memoryTc('skipped', { reason: 'intent' }));
200
+ expect(item.status).toBe('done');
201
+ expect(item.label).toBe('记忆召回 · 跳过(意图命中)');
202
+ expect(item.detail).toBeUndefined();
203
+ });
204
+
205
+ it('miss:done 态 + 无详情', () => {
206
+ const item = deriveToolCallItem(memoryTc('miss'));
207
+ expect(item.status).toBe('done');
208
+ expect(item.label).toBe('记忆召回');
209
+ expect(item.detail).toBeUndefined();
210
+ });
211
+ });
212
+
213
+ describe('techName(018 dev 视图技术标识)', () => {
214
+ it('normal=协议名 / preTool=被预执行工具 id / skill=skillName / memory=伪工具名', () => {
215
+ const normal = deriveToolCallItem(mkTc({ id: 'n1', name: 'search_stores', args: '{}', result: '{}' }));
216
+ expect(normal.techName).toBe('search_stores');
217
+ const { args: preArgs, result: preResult } = buildPreToolToolCallPayload({
218
+ toolName: 'member-balance',
219
+ args: { cardNo: 'C001' },
220
+ result: { balance: 1 },
221
+ });
222
+ const pre = deriveToolCallItem(mkTc({ id: 'p1', name: PRE_TOOL_CALL_NAME, args: preArgs, result: preResult }));
223
+ expect(pre.techName).toBe('member-balance');
224
+ const skill = buildSkillToolCallPayload({ name: 'member-guide' });
225
+ const sk = deriveToolCallItem(mkTc({ id: 's1', name: SKILL_TOOL_CALL_NAME, args: skill.args, result: skill.result }));
226
+ expect(sk.techName).toBe('member-guide');
227
+ const mem = deriveToolCallItem(mkTc({ id: 'm1', name: MEMORY_TOOL_CALL_NAME, args: '{}', result: '{}' }));
228
+ expect(mem.techName).toBe(MEMORY_TOOL_CALL_NAME);
229
+ });
230
+ });
231
+
232
+ describe('deriveSummary + buildSummaryText(L1 聚合)', () => {
233
+ const mkItem = (over: Partial<ToolCallViewItem>): ToolCallViewItem => ({
234
+ id: 'i0',
235
+ kind: 'normal',
236
+ label: 'x',
237
+ status: 'done',
238
+ ...over,
239
+ });
240
+
241
+ it('混合条目:技能/工具计数 + 最近执行标题 + calling + 失败数', () => {
242
+ const s = deriveSummary([
243
+ mkItem({ id: '1', kind: 'skill' }),
244
+ mkItem({ id: '2', kind: 'skill', status: 'error' }),
245
+ mkItem({ id: '3', kind: 'preTool' }),
246
+ mkItem({ id: '4', kind: 'preTool', label: '查询停车费用', status: 'calling' }),
247
+ mkItem({ id: '5', status: 'error' }),
248
+ ]);
249
+ expect(s).toEqual({
250
+ skillCount: 2,
251
+ toolCount: 3,
252
+ totalCount: 5,
253
+ callingCount: 1,
254
+ activeTitle: '查询停车费用',
255
+ calling: true,
256
+ failedCount: 2,
257
+ });
258
+ });
259
+
260
+ it('多个 calling 条目使用最后开始的任务作为收起态主标题', () => {
261
+ const s = deriveSummary([
262
+ mkItem({ id: '1', label: '查询会员信息', status: 'calling' }),
263
+ mkItem({ id: '2', label: '搜索商场门店', status: 'calling' }),
264
+ ]);
265
+ expect(s.callingCount).toBe(2);
266
+ expect(s.activeTitle).toBe('搜索商场门店');
267
+ expect(buildSummaryText(s)).toEqual(['正在搜索商场门店 · 另有 1 项']);
268
+ });
269
+
270
+ it('buildSummaryText:执行中标题优先、数量降级;稳定态按技能/工具有序汇总', () => {
271
+ expect(buildSummaryText({
272
+ skillCount: 0,
273
+ toolCount: 3,
274
+ totalCount: 3,
275
+ callingCount: 0,
276
+ calling: false,
277
+ failedCount: 0,
278
+ })).toEqual([
279
+ '已调用工具 3 个',
280
+ ]);
281
+ expect(buildSummaryText({
282
+ skillCount: 2,
283
+ toolCount: 0,
284
+ totalCount: 2,
285
+ callingCount: 0,
286
+ calling: false,
287
+ failedCount: 0,
288
+ })).toEqual([
289
+ '已加载技能 2 个',
290
+ ]);
291
+ expect(buildSummaryText({
292
+ skillCount: 2,
293
+ toolCount: 3,
294
+ totalCount: 5,
295
+ callingCount: 0,
296
+ calling: false,
297
+ failedCount: 0,
298
+ })).toEqual([
299
+ '已加载技能 2 个',
300
+ '已调用工具 3 个',
301
+ ]);
302
+ expect(buildSummaryText({
303
+ skillCount: 1,
304
+ toolCount: 2,
305
+ totalCount: 3,
306
+ callingCount: 2,
307
+ activeTitle: '搜索商场门店',
308
+ calling: true,
309
+ failedCount: 0,
310
+ })).toEqual([
311
+ '正在搜索商场门店 · 另有 1 项',
312
+ ]);
313
+ expect(buildSummaryText({
314
+ skillCount: 0,
315
+ toolCount: 1,
316
+ totalCount: 1,
317
+ callingCount: 1,
318
+ activeTitle: '查询停车费用',
319
+ calling: true,
320
+ failedCount: 0,
321
+ })).toEqual([
322
+ '正在查询停车费用',
323
+ ]);
324
+ expect(buildSummaryText({
325
+ skillCount: 0,
326
+ toolCount: 1,
327
+ totalCount: 1,
328
+ callingCount: 1,
329
+ activeTitle: '正在同步停车信息',
330
+ calling: true,
331
+ failedCount: 0,
332
+ })).toEqual([
333
+ '正在同步停车信息',
334
+ ]);
335
+ expect(buildSummaryText({
336
+ skillCount: 0,
337
+ toolCount: 0,
338
+ totalCount: 0,
339
+ callingCount: 0,
340
+ calling: false,
341
+ failedCount: 0,
342
+ })).toEqual([
343
+ '暂无工具活动',
344
+ ]);
345
+ });
346
+ });