@tbox.cn/app-template-agent 0.1.0 → 0.7.4
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 +6 -0
- package/.tbox/app.json +12 -3
- package/AGENTS.md +17 -6
- package/ENV.md +38 -5
- package/README.md +131 -5
- package/apps/client/package.json +3 -2
- package/apps/client/src/app.tsx +28 -0
- package/apps/client/src/cards/CardRenderer.tsx +12 -2
- package/apps/client/src/components/ActionBar/DebugButton.tsx +5 -1
- package/apps/client/src/components/ActionBar/index.tsx +7 -1
- package/apps/client/src/components/ChatInput/index.less +54 -0
- package/apps/client/src/components/ChatInput/index.tsx +33 -9
- package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +20 -1
- package/apps/client/src/components/MessageBubble/index.less +12 -1
- package/apps/client/src/components/MessageBubble/index.tsx +16 -8
- package/apps/client/src/components/PullDownIndicator/index.less +1 -1
- package/apps/client/src/components/PullDownIndicator/index.tsx +3 -4
- package/apps/client/src/components/QuickCommands/index.tsx +9 -2
- package/apps/client/src/components/ToolCallBlock/index.less +40 -0
- package/apps/client/src/components/ToolCallBlock/index.tsx +84 -22
- package/apps/client/src/hooks/useMediaUpload.ts +7 -2
- package/apps/client/src/main.tsx +4 -1
- package/apps/client/src/pages/index/index.tsx +35 -13
- package/apps/client/src/platform/AgentChatApp.tsx +5 -1
- package/apps/client/src/store/chat-config.ts +12 -1
- package/apps/client/src/store/chat.ts +72 -71
- package/apps/client/vite.config.ts +46 -26
- package/apps/server/package.json +5 -10
- package/apps/server/src/app-tools.ts +13 -0
- package/apps/server/src/app.ts +452 -0
- package/apps/server/src/cli/collect-registry.ts +11 -9
- package/apps/server/src/config/deployment.ts +47 -0
- package/apps/server/src/config/integration.ts +81 -0
- package/apps/server/src/dev.ts +81 -0
- package/apps/server/src/index.ts +24 -219
- package/apps/server/src/intent-routes.ts +11 -4
- package/apps/server/src/modules.ts +12 -5
- package/apps/server/src/pre-tools.ts +25 -0
- package/apps/server/src/tools/clock.ts +24 -0
- package/apps/server/src/tools/translate.ts +10 -4
- package/apps/server/tests/cards-registry.test.ts +9 -17
- package/apps/server/tests/deployment-loader.test.ts +140 -0
- package/apps/server/tests/intent-dispatcher.test.ts +58 -0
- package/apps/server/tests/tool-runtime.test.ts +124 -22
- package/apps/server/tsup.config.ts +15 -2
- package/config/chat.json +4 -1
- package/config/llm_provider.json +3 -1
- package/package.json +19 -11
- package/packages/contracts-mall/package.json +30 -0
- package/packages/contracts-mall/src/index.ts +1 -0
- package/packages/contracts-mall/src/runtime.ts +1 -0
- package/packages/contracts-mall/src/server/index.ts +1 -0
- package/packages/contracts-mall/tsconfig.json +7 -0
- package/packages/module-weather/package.json +6 -5
- package/packages/module-weather/skills/weather-guide/SKILL.md +22 -0
- package/packages/module-weather/skills/weather-guide/references/dressing-tips.md +5 -0
- package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
- package/packages/module-weather/src/server/index.ts +27 -1
- package/packages/module-weather/src/server/pre-tools.ts +20 -0
- package/packages/module-weather/src/server/tool.ts +24 -24
- package/packages/provider-custom/AGENTS.md +33 -0
- package/packages/provider-custom/LICENSE +21 -0
- package/packages/provider-custom/README.md +8 -0
- package/packages/provider-custom/package.json +62 -0
- package/packages/provider-custom/src/index.ts +5 -0
- package/packages/provider-custom/src/server/index.ts +40 -0
- package/packages/provider-custom/tbox.module.json +29 -0
- package/packages/provider-custom/tests/provider-custom.test.ts +17 -0
- package/packages/provider-custom/tsconfig.json +7 -0
- package/packages/provider-custom/tsup.config.ts +18 -0
- package/pnpm-lock.template.yaml +6297 -0
- package/pnpm-workspace.template.yaml +45 -45
- package/scripts/build.sh +6 -12
- package/scripts/check-workspace.mjs +11 -10
- package/scripts/stop.sh +48 -14
- package/scripts/verify-generated.mjs +4 -4
- package/tsconfig.base.json +1 -0
- package/apps/client/babel-plugins/code-inspect.cjs +0 -110
- package/apps/client/src/hooks/usePullDownHistory.ts +0 -97
- package/scripts/check-inspector.sh +0 -91
- /package/packages/module-weather/{tbox.component.json → tbox.module.json} +0 -0
|
@@ -27,6 +27,12 @@ const OPTION_ICONS: Record<PickKind, { icon: string; label: string }> = {
|
|
|
27
27
|
|
|
28
28
|
const MENU_ANIMATION_MS = 250;
|
|
29
29
|
|
|
30
|
+
/** 连接终态阻断(登录失败/scope 拒绝):提示条 + 输入禁用 + 可选重试入口 */
|
|
31
|
+
export interface ChatBlocked {
|
|
32
|
+
message: string;
|
|
33
|
+
onRetry?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
/** 媒体上传默认约束(分类型数量 + 单文件大小)。如需放开/收紧按业务改这里。 */
|
|
31
37
|
const DEFAULT_MEDIA_CONFIG = {
|
|
32
38
|
image: { maxCount: 9, maxSize: 10 * 1024 * 1024 },
|
|
@@ -38,12 +44,14 @@ const MEDIA_MAX_TOTAL = 9;
|
|
|
38
44
|
/** 对话输入框(对齐原模板):媒体预览 + 新对话 + 药丸[输入框/按住说话 + 语音切换 + 发送/“+”] + 动画选取菜单 + 免责声明。 */
|
|
39
45
|
export function ChatInput({
|
|
40
46
|
generating,
|
|
47
|
+
blocked,
|
|
41
48
|
onSend,
|
|
42
49
|
onCancel,
|
|
43
50
|
onNewChat,
|
|
44
51
|
disclaimer = '以上内容由AI生成,请谨慎甄别。',
|
|
45
52
|
}: {
|
|
46
53
|
generating?: boolean;
|
|
54
|
+
blocked?: ChatBlocked;
|
|
47
55
|
onSend: (msg: ChatInputMessage) => void;
|
|
48
56
|
onCancel?: () => void;
|
|
49
57
|
onNewChat?: () => void;
|
|
@@ -64,14 +72,16 @@ export function ChatInput({
|
|
|
64
72
|
const enableVideo = useChatConfigStore((s) => s.enableVideoUpload);
|
|
65
73
|
const enableVoice = useChatConfigStore((s) => s.enableVoiceInput);
|
|
66
74
|
const defaultInputMode = useChatConfigStore((s) => s.defaultInputMode);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
75
|
+
// 派生模式(禁 useState 捕获初值——水合晚到时服务端 defaultInputMode 进不了已捕获 state):
|
|
76
|
+
// userMode=null 跟随默认(随水合重渲染更新);显式值 = 用户手动切换优先
|
|
77
|
+
const [userMode, setUserMode] = useState<'text' | 'voice' | null>(null);
|
|
78
|
+
const inputMode: 'text' | 'voice' =
|
|
79
|
+
!enableVoice ? 'text' : userMode ?? (defaultInputMode === 'VOICE' ? 'voice' : 'text');
|
|
70
80
|
const [menuVisible, setMenuVisible] = useState(false);
|
|
71
81
|
const [menuAnimating, setMenuAnimating] = useState(false);
|
|
72
82
|
|
|
73
83
|
const voice = useVoicePress({
|
|
74
|
-
disabled: generating,
|
|
84
|
+
disabled: generating || !!blocked,
|
|
75
85
|
asr,
|
|
76
86
|
onResult: (t) => onSend({ text: t, mediaItems: [], inputMethod: 'voice' }),
|
|
77
87
|
});
|
|
@@ -128,7 +138,7 @@ export function ChatInput({
|
|
|
128
138
|
|
|
129
139
|
useEffect(() => () => closeTimerRef.current && clearTimeout(closeTimerRef.current), []);
|
|
130
140
|
|
|
131
|
-
const canSend = !!(text.trim() || items.length > 0) && !uploading && inputMode === 'text';
|
|
141
|
+
const canSend = !!(text.trim() || items.length > 0) && !uploading && inputMode === 'text' && !blocked;
|
|
132
142
|
|
|
133
143
|
const submit = () => {
|
|
134
144
|
if (!canSend) return;
|
|
@@ -144,7 +154,10 @@ export function ChatInput({
|
|
|
144
154
|
e.target.value = '';
|
|
145
155
|
};
|
|
146
156
|
|
|
147
|
-
const toggleMode = () =>
|
|
157
|
+
const toggleMode = () => {
|
|
158
|
+
if (blocked) return;
|
|
159
|
+
setUserMode(inputMode === 'text' ? 'voice' : 'text');
|
|
160
|
+
};
|
|
148
161
|
|
|
149
162
|
return (
|
|
150
163
|
<div className="tbox-chat-input">
|
|
@@ -174,7 +187,7 @@ export function ChatInput({
|
|
|
174
187
|
|
|
175
188
|
{inputMode === 'voice' ? (
|
|
176
189
|
<VoicePressField
|
|
177
|
-
disabled={generating}
|
|
190
|
+
disabled={generating || !!blocked}
|
|
178
191
|
interactionState={voice.interactionState}
|
|
179
192
|
handlers={voice.handlers}
|
|
180
193
|
/>
|
|
@@ -182,8 +195,9 @@ export function ChatInput({
|
|
|
182
195
|
<textarea
|
|
183
196
|
className="tbox-chat-input__field"
|
|
184
197
|
value={text}
|
|
185
|
-
placeholder=
|
|
198
|
+
placeholder={blocked?.message ?? '有什么需要问我的吗'}
|
|
186
199
|
rows={1}
|
|
200
|
+
disabled={!!blocked}
|
|
187
201
|
onChange={(e) => setText(e.target.value)}
|
|
188
202
|
onKeyDown={(e) => {
|
|
189
203
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -204,7 +218,7 @@ export function ChatInput({
|
|
|
204
218
|
</div>
|
|
205
219
|
)}
|
|
206
220
|
|
|
207
|
-
{generating ? (
|
|
221
|
+
{blocked ? null : generating ? (
|
|
208
222
|
<div className="tbox-chat-input__send tbox-chat-input__send--active" onClick={onCancel}>
|
|
209
223
|
<img className="tbox-chat-input__send-img" src={stopIcon} alt="停止" />
|
|
210
224
|
</div>
|
|
@@ -223,6 +237,16 @@ export function ChatInput({
|
|
|
223
237
|
</div>
|
|
224
238
|
) : null}
|
|
225
239
|
</div>
|
|
240
|
+
{blocked && (
|
|
241
|
+
<div className="tbox-chat-input__blocked">
|
|
242
|
+
<span className="tbox-chat-input__blocked-msg">{blocked.message}</span>
|
|
243
|
+
{blocked.onRetry && (
|
|
244
|
+
<button type="button" className="tbox-chat-input__blocked-retry" onClick={blocked.onRetry}>
|
|
245
|
+
重试
|
|
246
|
+
</button>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
226
250
|
</div>
|
|
227
251
|
|
|
228
252
|
{menuVisible && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { IASRAdapter } from '@tbox.cn/app-sdk/client';
|
|
3
3
|
import { useRefreshSession } from '@tbox.cn/app-sdk/client';
|
|
4
4
|
import { isSessionExpiredError } from '@/utils/session-error';
|
|
@@ -81,6 +81,8 @@ export function useVoicePress({
|
|
|
81
81
|
const onTouchStart = useCallback(
|
|
82
82
|
async (e: React.TouchEvent) => {
|
|
83
83
|
if (disabled || !asr || recordingRef.current) return;
|
|
84
|
+
// 长按录音由应用接管,避免移动端浏览器启动原生选择/拖拽手势。
|
|
85
|
+
e.preventDefault();
|
|
84
86
|
startYRef.current = e.touches[0]?.clientY ?? 0;
|
|
85
87
|
setState('pressing');
|
|
86
88
|
|
|
@@ -133,6 +135,7 @@ export function useVoicePress({
|
|
|
133
135
|
const onTouchMove = useCallback(
|
|
134
136
|
(e: React.TouchEvent) => {
|
|
135
137
|
if (!recordingRef.current) return;
|
|
138
|
+
e.preventDefault();
|
|
136
139
|
const y = e.touches[0]?.clientY ?? 0;
|
|
137
140
|
setState(getMoveInteractionState(startYRef.current, y));
|
|
138
141
|
},
|
|
@@ -184,5 +187,21 @@ export function useVoicePress({
|
|
|
184
187
|
(interactionState === 'recording-active' || interactionState === 'recording-cancel' || interactionState === 'pressing');
|
|
185
188
|
const countdown = counting && duration >= MAX_DURATION - COUNTDOWN_START ? MAX_DURATION - duration : null;
|
|
186
189
|
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const locked = interactionState !== 'idle';
|
|
192
|
+
document.body.classList.toggle('tbox-voice-page-lock', locked);
|
|
193
|
+
if (!locked) return undefined;
|
|
194
|
+
const preventNativeGesture = (event: Event) => event.preventDefault();
|
|
195
|
+
document.addEventListener('selectstart', preventNativeGesture);
|
|
196
|
+
document.addEventListener('dragstart', preventNativeGesture);
|
|
197
|
+
document.addEventListener('contextmenu', preventNativeGesture);
|
|
198
|
+
return () => {
|
|
199
|
+
document.body.classList.remove('tbox-voice-page-lock');
|
|
200
|
+
document.removeEventListener('selectstart', preventNativeGesture);
|
|
201
|
+
document.removeEventListener('dragstart', preventNativeGesture);
|
|
202
|
+
document.removeEventListener('contextmenu', preventNativeGesture);
|
|
203
|
+
};
|
|
204
|
+
}, [interactionState]);
|
|
205
|
+
|
|
187
206
|
return { interactionState, overlayVisible: interactionState !== 'idle', countdown, handlers };
|
|
188
207
|
}
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* 意图空文本出卡(无气泡、有卡):卡片前间距由 meta 下边距提供——
|
|
47
|
+
meta 无 border → 折叠语义 max(12, 卡 mt),未来全局卡框间距调整自然跟随 */
|
|
48
|
+
.msg-meta--solo-card {
|
|
49
|
+
margin-bottom: 12px;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
.msg-bubble {
|
|
47
53
|
padding: 18px 22px;
|
|
48
54
|
border-radius: 20px;
|
|
@@ -74,6 +80,9 @@
|
|
|
74
80
|
box-shadow:
|
|
75
81
|
0 3px 8px 5px rgba(1, 112, 254, 0.0392),
|
|
76
82
|
0 2px 4px rgba(72, 78, 86, 0.0706);
|
|
83
|
+
/* 卡片在对话气泡之后显示:间距由气泡下边距提供(单一来源;
|
|
84
|
+
不写 .tbox-card 规则——避免特异性压过全局卡框、避免复制全局间距值) */
|
|
85
|
+
margin-bottom: 12px;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
&__text {
|
|
@@ -151,7 +160,9 @@
|
|
|
151
160
|
display: flex;
|
|
152
161
|
align-items: center;
|
|
153
162
|
gap: 12px;
|
|
154
|
-
margin-
|
|
163
|
+
/* 操作栏间距由前元素 margin-bottom 提供(气泡 mb 12 / 卡片 mb 12):
|
|
164
|
+
不能自设 margin-top——气泡有 border 阻断折叠,自设会与气泡 mb 叠加成 24px */
|
|
165
|
+
margin-top: 0;
|
|
155
166
|
flex-wrap: wrap;
|
|
156
167
|
}
|
|
157
168
|
|
|
@@ -9,6 +9,7 @@ import ToolCallBlock from '@/components/ToolCallBlock';
|
|
|
9
9
|
import { ActionBar, CopyButton, getMessageCopyText } from '@/components/ActionBar';
|
|
10
10
|
import { DebugButton } from '@/components/ActionBar/DebugButton';
|
|
11
11
|
import { CardRenderer } from '@/cards/CardRenderer';
|
|
12
|
+
import { CardCarousel } from '@tbox.cn/app-sdk/client';
|
|
12
13
|
import { useChatStore } from '@/store/chat';
|
|
13
14
|
import { copyText } from '@/utils/copyText';
|
|
14
15
|
import { toast } from '@/utils/toast';
|
|
@@ -31,9 +32,12 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
31
32
|
const isThinking = message.status === 'thinking';
|
|
32
33
|
const isStreaming = message.status === 'streaming';
|
|
33
34
|
const hasReasoning = !!message.reasoning;
|
|
35
|
+
// 018 tool-debug:服务端旗标单源(HELLO_ACK;无 URL 逃逸——数据面信任锚点在服务端)
|
|
36
|
+
const toolDebug = useChatStore((s) => s.helloAck?.toolDebug === true);
|
|
37
|
+
// 018:formSubmit 伪工具 prod 过滤不变;dev 放行(防御性——若事件到达则可见)
|
|
34
38
|
const visibleToolCalls = useMemo(
|
|
35
|
-
() => (message.toolCalls || []).filter((tc) => tc.name !== '__a2uiFormSubmit'),
|
|
36
|
-
[message.toolCalls],
|
|
39
|
+
() => (message.toolCalls || []).filter((tc) => toolDebug || tc.name !== '__a2uiFormSubmit'),
|
|
40
|
+
[message.toolCalls, toolDebug],
|
|
37
41
|
);
|
|
38
42
|
const hasToolCalls = visibleToolCalls.length > 0;
|
|
39
43
|
|
|
@@ -59,7 +63,9 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
59
63
|
return (
|
|
60
64
|
<div className={`msg-row ${isUser ? 'msg-row--user' : 'msg-row--bot'}`}>
|
|
61
65
|
<div className="msg-main">
|
|
62
|
-
<div
|
|
66
|
+
<div
|
|
67
|
+
className={`msg-meta ${isUser ? 'msg-meta--user' : 'msg-meta--bot'}${!showBubble && messageCards.length > 0 ? ' msg-meta--solo-card' : ''}`}
|
|
68
|
+
>
|
|
63
69
|
<span className="msg-meta__role">{roleName}</span>
|
|
64
70
|
<span className="msg-meta__time">{timeLabel}</span>
|
|
65
71
|
</div>
|
|
@@ -74,7 +80,7 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
74
80
|
|
|
75
81
|
{hasReasoning && <ReasoningBlock reasoning={message.reasoning!} isActive={isReasoningActive} />}
|
|
76
82
|
|
|
77
|
-
{hasToolCalls && <ToolCallBlock toolCalls={visibleToolCalls} />}
|
|
83
|
+
{hasToolCalls && <ToolCallBlock toolCalls={visibleToolCalls} debug={toolDebug} />}
|
|
78
84
|
|
|
79
85
|
{isThinking && hasToolCalls && !message.content && (
|
|
80
86
|
<div className="msg-thinking-inline">
|
|
@@ -84,10 +90,6 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
84
90
|
</div>
|
|
85
91
|
)}
|
|
86
92
|
|
|
87
|
-
{messageCards.map((card) => (
|
|
88
|
-
<CardRenderer key={card.id} card={card} />
|
|
89
|
-
))}
|
|
90
|
-
|
|
91
93
|
{showBubble && (
|
|
92
94
|
<div className={`msg-bubble ${isUser ? 'msg-bubble--user' : 'msg-bubble--bot'}`}>
|
|
93
95
|
{isUser ? (
|
|
@@ -113,6 +115,12 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
|
|
|
113
115
|
</div>
|
|
114
116
|
)}
|
|
115
117
|
|
|
118
|
+
{/* 卡片在对话气泡之后显示(决策出卡多张/意图卡/历史卡统一经 cardsByMessageId 挂载,多卡纵向铺开) */}
|
|
119
|
+
<CardCarousel
|
|
120
|
+
cards={messageCards}
|
|
121
|
+
renderItem={(card) => <CardRenderer key={card.id} card={card} />}
|
|
122
|
+
/>
|
|
123
|
+
|
|
116
124
|
{imageItems && imageItems.length > 0 && (
|
|
117
125
|
<div className="msg-media">
|
|
118
126
|
{imageItems.map((item, idx) => (
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type { PullStatus } from '
|
|
1
|
+
import type { PullStatus } from '@tbox.cn/app-sdk/client';
|
|
2
|
+
import { PULL_DOWN_THRESHOLD } from '@tbox.cn/app-sdk/client';
|
|
2
3
|
import './index.less';
|
|
3
4
|
|
|
4
|
-
const INDICATOR_HEIGHT = 60;
|
|
5
|
-
|
|
6
5
|
/** 下拉加载历史的顶部指示器。 */
|
|
7
6
|
export default function PullDownIndicator({ status, distance }: { status: PullStatus; distance: number }) {
|
|
8
7
|
if (status === 'idle') return null;
|
|
9
8
|
|
|
10
|
-
const marginTop = status === 'loading' ? 0 : -
|
|
9
|
+
const marginTop = status === 'loading' ? 0 : -PULL_DOWN_THRESHOLD + Math.min(distance, PULL_DOWN_THRESHOLD);
|
|
11
10
|
const showTransition = status === 'loading' || status === 'noMore';
|
|
12
11
|
|
|
13
12
|
const content =
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
// ⚠️ 平台标准组件:输入框上方的「快捷指令」。视觉还原时不改本组件、不另画。
|
|
2
2
|
// 改内容只改 client/config/chat.json 的 quickCommands 数组({ label, query, icon? })。
|
|
3
|
+
// 011 I2:服务端 HelloAck.chatConfig.quickCommands 优先(单一真源),本地 chat.json 为 fallback。
|
|
3
4
|
// label 只写纯文字;前面的星标由本组件渲染(icon 字段或默认 ✦),不要把 ✦/emoji 写进 label。
|
|
4
5
|
import { useChatConfigStore } from '@/store/chat-config';
|
|
6
|
+
import { useChatStore } from '@/store/chat';
|
|
5
7
|
import starIcon from '../../../assets/quick-command-star.svg';
|
|
6
8
|
import './index.less';
|
|
7
9
|
|
|
8
10
|
export function QuickCommands({ onCommand }: { onCommand: (query: string) => void }) {
|
|
9
|
-
const
|
|
11
|
+
const staticCommands = useChatConfigStore((s) => s.quickCommands);
|
|
12
|
+
const serverCommands = useChatStore((s) => s.helloAck?.chatConfig.quickCommands);
|
|
13
|
+
// 服务端下发优先;未收到 HELLO_ACK(非 mall 部署)回落本地静态
|
|
14
|
+
const commands = (serverCommands && serverCommands.length > 0
|
|
15
|
+
? serverCommands.map((c) => ({ label: c.label, query: c.prompt }))
|
|
16
|
+
: staticCommands) as { label: string; query: string }[];
|
|
10
17
|
if (!commands.length) return null;
|
|
11
18
|
|
|
12
19
|
return (
|
|
13
20
|
<div className="quick-commands">
|
|
14
21
|
{commands.map((cmd) => (
|
|
15
22
|
<button key={cmd.label} className="quick-commands__item" onClick={() => onCommand(cmd.query)}>
|
|
16
|
-
<img className="quick-commands__icon" src={
|
|
23
|
+
<img className="quick-commands__icon" src={starIcon} alt="" />
|
|
17
24
|
<span className="quick-commands__label">{cmd.label}</span>
|
|
18
25
|
</button>
|
|
19
26
|
))}
|
|
@@ -43,6 +43,17 @@
|
|
|
43
43
|
animation: tool-spin 0.8s linear infinite;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// 010:前置工具「自动」徽标(与模型主动调用区分)
|
|
47
|
+
&__badge {
|
|
48
|
+
font-size: 18px;
|
|
49
|
+
line-height: 1;
|
|
50
|
+
padding: 3px 8px;
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
background: rgba(93, 126, 198, 0.12);
|
|
53
|
+
color: #5f7ec6;
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
&__name {
|
|
47
58
|
font-size: 22px;
|
|
48
59
|
line-height: 1.4;
|
|
@@ -82,6 +93,35 @@
|
|
|
82
93
|
color: #dc2626;
|
|
83
94
|
}
|
|
84
95
|
}
|
|
96
|
+
|
|
97
|
+
// 018 dev 视图输入/输出双栏(紧凑单行 + 头尾保留)
|
|
98
|
+
&__io {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 4px;
|
|
102
|
+
|
|
103
|
+
& + & {
|
|
104
|
+
margin-top: 12px;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&__io-label {
|
|
109
|
+
font-size: 18px;
|
|
110
|
+
color: #8a97ad;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&__io-text {
|
|
114
|
+
font-size: 20px;
|
|
115
|
+
color: #6b7a99;
|
|
116
|
+
line-height: 1.6;
|
|
117
|
+
font-family: Menlo, Monaco, Consolas, monospace;
|
|
118
|
+
white-space: pre-wrap;
|
|
119
|
+
word-break: break-all;
|
|
120
|
+
|
|
121
|
+
&--error {
|
|
122
|
+
color: #dc2626;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
85
125
|
}
|
|
86
126
|
|
|
87
127
|
@keyframes tool-spin {
|
|
@@ -1,22 +1,85 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { Check, X } from '@phosphor-icons/react';
|
|
3
|
+
import { parseMemoryToolCallArgs, parsePreToolToolCallArgs } from '@tbox.cn/app-contracts';
|
|
3
4
|
import type { ToolCallInfo } from '@/types/chat';
|
|
5
|
+
import { describeToolCall, formatCompactToolIo } from '@tbox.cn/app-sdk/client';
|
|
4
6
|
import './index.less';
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* 工具调用状态卡片(调用中 spinner / 完成 / 失败)。
|
|
10
|
+
* 010:展示语义判定下沉 SDK describeToolCall(skill/preTool/normal/memory 四分支),
|
|
11
|
+
* 本组件退化为纯渲染(徽标/颜色/展开交互)。
|
|
12
|
+
* 018 tool-debug:debug 门控在渲染层——prod 恒无出入参展开入口(I4);
|
|
13
|
+
* dev 详情走 formatCompactToolIo(紧凑单行 + 头尾保留),输入输出双栏。
|
|
14
|
+
*/
|
|
15
|
+
export default function ToolCallBlock({
|
|
16
|
+
toolCalls,
|
|
17
|
+
debug = false,
|
|
18
|
+
}: {
|
|
19
|
+
toolCalls: ToolCallInfo[];
|
|
20
|
+
/** 018 tool-debug:dev 视图(出入参展开 + 紧凑格式);prod 缺省 false */
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
}) {
|
|
8
23
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
9
24
|
|
|
10
25
|
return (
|
|
11
26
|
<div className="tool-calls">
|
|
12
27
|
{toolCalls.map((tc) => {
|
|
28
|
+
const display = describeToolCall(tc);
|
|
13
29
|
const isCalling = tc.status === 'calling';
|
|
14
|
-
const isError =
|
|
15
|
-
|
|
30
|
+
const isError =
|
|
31
|
+
tc.status === 'error' ||
|
|
32
|
+
(display.kind === 'preTool' && display.status === 'error') ||
|
|
33
|
+
(display.kind === 'memory' && display.status === 'degraded');
|
|
34
|
+
const isDone = !isCalling && !isError;
|
|
16
35
|
const isExpanded = expandedId === tc.id;
|
|
17
|
-
|
|
36
|
+
// 018 I4:prod 恒无展开入口;dev 按 display.hasDetail
|
|
37
|
+
const hasDetail = debug && display.hasDetail;
|
|
18
38
|
const stateClass = isError ? 'tool-call--error' : isCalling ? 'tool-call--calling' : 'tool-call--done';
|
|
19
39
|
|
|
40
|
+
const displayName = (() => {
|
|
41
|
+
if (display.kind === 'skill') {
|
|
42
|
+
return isCalling ? `加载技能 ${display.label}` : `已加载技能 ${display.label}`;
|
|
43
|
+
}
|
|
44
|
+
if (display.kind === 'preTool') {
|
|
45
|
+
if (display.status === 'calling') return `自动查询 ${display.label}`;
|
|
46
|
+
if (display.status === 'error') return `自动查询失败 ${display.label}`;
|
|
47
|
+
return `已自动获取 ${display.label}`;
|
|
48
|
+
}
|
|
49
|
+
if (display.kind === 'memory') {
|
|
50
|
+
return display.label;
|
|
51
|
+
}
|
|
52
|
+
return isCalling
|
|
53
|
+
? `调用 ${display.label}`
|
|
54
|
+
: isDone
|
|
55
|
+
? `${display.label} 已完成`
|
|
56
|
+
: `${display.label} 调用失败`;
|
|
57
|
+
})();
|
|
58
|
+
|
|
59
|
+
// 详情文本(018:dev 态才计算——prod 无展开无渲染需求)
|
|
60
|
+
const detailText = (() => {
|
|
61
|
+
if (!hasDetail) return '';
|
|
62
|
+
if (display.kind === 'preTool') {
|
|
63
|
+
return (display.status === 'error' ? display.errorText : display.resultText) ?? '';
|
|
64
|
+
}
|
|
65
|
+
if (display.kind === 'memory') {
|
|
66
|
+
return display.detailText ?? '';
|
|
67
|
+
}
|
|
68
|
+
return isError ? (tc.error ?? '') : (tc.result ?? '');
|
|
69
|
+
})();
|
|
70
|
+
// dev 输入侧:normal 用 tc.args;preTool 解包 toolArgs(对齐 mall 视图模型);memory 取 query
|
|
71
|
+
const inputText = (() => {
|
|
72
|
+
if (!hasDetail) return '';
|
|
73
|
+
if (display.kind === 'normal') return tc.args ?? '';
|
|
74
|
+
if (display.kind === 'preTool') {
|
|
75
|
+
return JSON.stringify(parsePreToolToolCallArgs(tc.args)?.toolArgs ?? {});
|
|
76
|
+
}
|
|
77
|
+
if (display.kind === 'memory') {
|
|
78
|
+
return parseMemoryToolCallArgs(tc.args)?.query ?? '';
|
|
79
|
+
}
|
|
80
|
+
return '';
|
|
81
|
+
})();
|
|
82
|
+
|
|
20
83
|
return (
|
|
21
84
|
<div key={tc.id} className={`tool-call ${stateClass}`}>
|
|
22
85
|
<div
|
|
@@ -27,11 +90,10 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
27
90
|
{isCalling && <div className="tool-call__spinner" />}
|
|
28
91
|
{isDone && <Check size={16} color="#10a37f" strokeWidth={2.5} />}
|
|
29
92
|
{isError && <X size={16} color="#ef4444" strokeWidth={2.5} />}
|
|
30
|
-
<span className="tool-call__name">
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</span>
|
|
93
|
+
<span className="tool-call__name">{displayName}</span>
|
|
94
|
+
{display.kind === 'preTool' && (
|
|
95
|
+
<span className="tool-call__badge">自动</span>
|
|
96
|
+
)}
|
|
35
97
|
</div>
|
|
36
98
|
{hasDetail && (
|
|
37
99
|
<span className={`tool-call__toggle${isExpanded ? ' tool-call__toggle--up' : ''}`}>
|
|
@@ -41,9 +103,18 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
41
103
|
</div>
|
|
42
104
|
{isExpanded && hasDetail && (
|
|
43
105
|
<div className="tool-call__result">
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
106
|
+
{inputText && (
|
|
107
|
+
<div className="tool-call__io">
|
|
108
|
+
<span className="tool-call__io-label">输入</span>
|
|
109
|
+
<span className="tool-call__io-text">{formatCompactToolIo(inputText)}</span>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
<div className="tool-call__io">
|
|
113
|
+
<span className="tool-call__io-label">输出</span>
|
|
114
|
+
<span className={`tool-call__io-text${isError ? ' tool-call__io-text--error' : ''}`}>
|
|
115
|
+
{formatCompactToolIo(detailText) || '(无)'}
|
|
116
|
+
</span>
|
|
117
|
+
</div>
|
|
47
118
|
</div>
|
|
48
119
|
)}
|
|
49
120
|
</div>
|
|
@@ -52,12 +123,3 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
52
123
|
</div>
|
|
53
124
|
);
|
|
54
125
|
}
|
|
55
|
-
|
|
56
|
-
function formatResult(raw: string): string {
|
|
57
|
-
try {
|
|
58
|
-
const pretty = JSON.stringify(JSON.parse(raw), null, 2);
|
|
59
|
-
return pretty.length > 500 ? pretty.slice(0, 500) + '...' : pretty;
|
|
60
|
-
} catch {
|
|
61
|
-
return raw.length > 500 ? raw.slice(0, 500) + '...' : raw;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '@tbox.cn/app-sdk/client';
|
|
2
|
+
|
|
3
|
+
const mediaLogger = createLogger('media');
|
|
4
|
+
|
|
1
5
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
6
|
import { useUploadAdapter, useRefreshSession, chooseImage, chooseVideo } from '@tbox.cn/app-sdk/client';
|
|
3
7
|
|
|
@@ -109,7 +113,8 @@ export function useMediaUpload(maxTotal = 9, mediaConfig?: MediaConfig) {
|
|
|
109
113
|
done(await next.upload.upload(rec.source, { type: rec.type, name: rec.name }));
|
|
110
114
|
return;
|
|
111
115
|
}
|
|
112
|
-
} catch {
|
|
116
|
+
} catch (retryErr) {
|
|
117
|
+
mediaLogger.warn(`上传重试失败:err=${(retryErr as Error).message}`);
|
|
113
118
|
/* 重试失败 → 落到下方失败处理 */
|
|
114
119
|
}
|
|
115
120
|
}
|
|
@@ -211,7 +216,7 @@ export function useMediaUpload(maxTotal = 9, mediaConfig?: MediaConfig) {
|
|
|
211
216
|
typeCount += 1;
|
|
212
217
|
}
|
|
213
218
|
} catch (err) {
|
|
214
|
-
|
|
219
|
+
mediaLogger.warn('bridge pick failed:', err);
|
|
215
220
|
}
|
|
216
221
|
},
|
|
217
222
|
[adapter, enqueue, validate],
|
package/apps/client/src/main.tsx
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { configureRuntime } from '@tbox.cn/app-sdk/client';
|
|
3
|
+
import { configureRuntime, installGlobalErrorReporting } from '@tbox.cn/app-sdk/client';
|
|
4
4
|
import App from './app';
|
|
5
5
|
import './styles/global.less';
|
|
6
6
|
|
|
7
7
|
// H1:环境值注入点(SDK 零 Vite 依赖)——VITE_API_URL 空串 = 同源相对路径
|
|
8
8
|
configureRuntime({ apiBaseUrl: import.meta.env.VITE_API_URL as string | undefined });
|
|
9
9
|
|
|
10
|
+
// 全局错误兜底:未捕获异常/rejection → 统一日志行(本地落地;上报扩展点见 SDK client logger)
|
|
11
|
+
installGlobalErrorReporting();
|
|
12
|
+
|
|
10
13
|
const el = document.getElementById('root');
|
|
11
14
|
if (el) {
|
|
12
15
|
createRoot(el).render(
|