@tbox.cn/app-template-agent 0.7.4 → 0.8.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.
- package/.gitignore +5 -0
- package/.tbox/app.json +9 -1
- package/AGENTS.md +12 -4
- package/ENV.md +102 -17
- package/README.md +36 -34
- package/apps/client/VISUAL_RESTORE.md +11 -11
- package/apps/client/index.html +3 -1
- package/apps/client/package.json +9 -8
- package/apps/client/src/business/App.tsx +4 -4
- package/apps/client/src/business/README.md +5 -11
- package/apps/client/src/cards/registry.ts +23 -8
- package/apps/client/src/main.tsx +36 -2
- package/apps/client/src/modules.ts +7 -4
- package/apps/client/src/pages/index/index.tsx +18 -162
- package/apps/client/src/platform/AgentChatApp.tsx +3 -3
- package/apps/client/src/platform/agent-runtime.ts +5 -0
- package/apps/client/src/store/chat.ts +47 -185
- package/apps/client/src/test/setup.ts +7 -0
- package/apps/client/vite.config.ts +16 -1
- package/apps/server/package.json +0 -1
- package/apps/server/src/app-tools.ts +2 -1
- package/apps/server/src/app.ts +178 -110
- package/apps/server/src/config/integration.ts +2 -6
- package/apps/server/src/dev.ts +0 -1
- package/apps/server/src/intent-routes.ts +3 -3
- package/apps/server/src/modules.ts +4 -4
- package/apps/server/src/pre-tools.ts +4 -3
- package/apps/server/tests/agent-prompt.test.ts +81 -0
- package/apps/server/tests/cards-registry.test.ts +1 -3
- package/apps/server/tests/deployment-loader.test.ts +35 -56
- package/apps/server/tests/tool-runtime.test.ts +25 -61
- package/package.json +5 -4
- package/packages/agent-ui-kit/LICENSE +21 -0
- package/packages/agent-ui-kit/README.md +17 -0
- package/packages/agent-ui-kit/package.json +58 -0
- package/packages/agent-ui-kit/src/ChatPage.less +62 -0
- package/packages/agent-ui-kit/src/ChatPage.tsx +305 -0
- package/packages/agent-ui-kit/src/chat-config.test.ts +82 -0
- package/packages/agent-ui-kit/src/chat-config.ts +62 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/DebugButton.tsx +2 -2
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.less +19 -16
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.tsx +10 -13
- package/packages/agent-ui-kit/src/components/CardRenderer.tsx +36 -0
- package/packages/agent-ui-kit/src/components/ChatHeader/index.less +148 -0
- package/packages/agent-ui-kit/src/components/ChatHeader/index.test.tsx +74 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatHeader/index.tsx +27 -15
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.less +47 -39
- package/packages/agent-ui-kit/src/components/ChatInput/index.test.tsx +156 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.tsx +13 -4
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/useVoicePress.ts +2 -2
- package/packages/agent-ui-kit/src/components/HistoryEntry/index.less +49 -0
- package/packages/agent-ui-kit/src/components/HistoryEntry/index.tsx +42 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.less +3 -3
- package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.tsx +6 -6
- package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.less +25 -20
- package/packages/agent-ui-kit/src/components/MessageBubble/index.test.tsx +112 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.tsx +22 -15
- package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.less +2 -2
- package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.tsx +2 -2
- package/packages/agent-ui-kit/src/components/QuickCommands/index.less +63 -0
- package/packages/agent-ui-kit/src/components/QuickCommands/index.test.tsx +99 -0
- package/packages/agent-ui-kit/src/components/QuickCommands/index.tsx +52 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.less +1 -1
- package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.test.ts +346 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.ts +204 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.less +312 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.test.tsx +261 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.tsx +212 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/titles.ts +102 -0
- package/packages/agent-ui-kit/src/hooks/use-drag-gesture.test.ts +66 -0
- package/packages/agent-ui-kit/src/hooks/use-drag-gesture.ts +129 -0
- package/packages/agent-ui-kit/src/hooks/use-pull-down-history.ts +241 -0
- package/packages/agent-ui-kit/src/hooks/use-pull-down-wheel-native.test.ts +64 -0
- package/{apps/client → packages/agent-ui-kit}/src/hooks/useMediaUpload.ts +3 -3
- package/{apps/client → packages/agent-ui-kit}/src/hooks/useTTSAutoPlay.ts +7 -5
- package/packages/agent-ui-kit/src/index.ts +69 -0
- package/packages/agent-ui-kit/src/store-context.tsx +92 -0
- package/packages/agent-ui-kit/src/test/setup.ts +7 -0
- package/packages/agent-ui-kit/src/theme/contract.json +72 -0
- package/packages/agent-ui-kit/src/theme/global.less +99 -0
- package/packages/agent-ui-kit/src/theme/motion.less +19 -0
- package/packages/agent-ui-kit/src/theme/theme-contract.test.ts +69 -0
- package/packages/agent-ui-kit/src/theme/theme.ts +17 -0
- package/packages/agent-ui-kit/src/theme/tokens.less +79 -0
- package/packages/agent-ui-kit/src/utils/haptics.test.ts +51 -0
- package/packages/agent-ui-kit/src/utils/haptics.ts +44 -0
- package/packages/agent-ui-kit/src/utils/pull-down-machine.test.ts +188 -0
- package/packages/agent-ui-kit/src/utils/pull-down-machine.ts +157 -0
- package/packages/agent-ui-kit/src/utils/tool-call-display.test.ts +240 -0
- package/packages/agent-ui-kit/src/utils/tool-call-display.ts +146 -0
- package/packages/agent-ui-kit/src/vite-env.d.ts +1 -0
- package/packages/agent-ui-kit/tsconfig.json +8 -0
- package/packages/agent-ui-kit/vitest.config.ts +7 -0
- package/pnpm-lock.template.yaml +177 -83
- package/pnpm-workspace.template.yaml +12 -4
- package/profiles/mall/config/agent.jsonc +33 -0
- package/profiles/mall/config/chat.json +21 -0
- package/profiles/mall/config/knowledge.json +5 -0
- package/profiles/mall/config/llm_provider.json +5 -0
- package/profiles/mall/config/plugins.json +3 -0
- package/profiles/mall.json +16 -0
- package/scripts/check-workspace.mjs +3 -2
- package/apps/client/assets/copy@2x.png +0 -0
- package/apps/client/assets/debug.png +0 -0
- package/apps/client/assets/dislike@2x.png +0 -0
- package/apps/client/assets/like@2x.png +0 -0
- package/apps/client/assets/speaker@2x.png +0 -0
- package/apps/client/src/cards/CardRenderer.tsx +0 -20
- package/apps/client/src/components/ChatHeader/index.less +0 -104
- package/apps/client/src/components/ErrorBoundary.tsx +0 -2
- package/apps/client/src/components/QuickCommands/index.less +0 -48
- package/apps/client/src/components/QuickCommands/index.tsx +0 -29
- package/apps/client/src/components/ToolCallBlock/index.less +0 -131
- package/apps/client/src/components/ToolCallBlock/index.tsx +0 -125
- package/apps/client/src/examples/tourism/TourismLanding.tsx +0 -96
- package/apps/client/src/examples/tourism/index.less +0 -192
- package/apps/client/src/pages/index/index.less +0 -43
- package/apps/client/src/store/chat-config.ts +0 -40
- package/apps/client/src/styles/global.less +0 -60
- package/apps/client/src/styles/tokens.less +0 -60
- package/apps/server/src/config/deployment.ts +0 -47
- package/packages/module-weather/package.json +0 -36
- package/packages/module-weather/skills/weather-guide/SKILL.md +0 -22
- package/packages/module-weather/skills/weather-guide/references/dressing-tips.md +0 -5
- package/packages/module-weather/src/client/cards/weather/index.less +0 -22
- package/packages/module-weather/src/client/cards/weather/index.tsx +0 -14
- package/packages/module-weather/src/client/index.ts +0 -19
- package/packages/module-weather/src/index.ts +0 -2
- package/packages/module-weather/src/server/cards/weather/meta.ts +0 -27
- package/packages/module-weather/src/server/index.ts +0 -45
- package/packages/module-weather/src/server/pre-tools.ts +0 -20
- package/packages/module-weather/src/server/tool.ts +0 -29
- package/packages/module-weather/tbox.module.json +0 -27
- package/packages/module-weather/tsconfig.json +0 -14
- /package/{apps/client → packages/agent-ui-kit}/assets/keyboard-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/more-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/new-chat.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/quick-command-star.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/send-gradient.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/stop@2x.png +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/voice-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoicePressField.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/machine.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.less +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.tsx +0 -0
- /package/{apps/client/src/types/chat.ts → packages/agent-ui-kit/src/types.ts} +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/copyText.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/imagePreview.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/session-error.ts +0 -0
- /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
|
+
}
|
package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.less
RENAMED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
z-index: 10;
|
|
14
14
|
opacity: 0;
|
|
15
15
|
transform: scale(0.8);
|
|
16
|
-
transition: opacity
|
|
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
|
+
});
|