@tbox.cn/app-template-agent 0.2.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 +36 -5
- package/README.md +23 -7
- package/apps/client/package.json +3 -2
- package/apps/client/src/app.tsx +28 -0
- package/apps/client/src/cards/CardRenderer.tsx +4 -5
- 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/ToolCallBlock/index.less +29 -0
- package/apps/client/src/components/ToolCallBlock/index.tsx +58 -24
- 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 +62 -92
- package/apps/client/vite.config.ts +46 -26
- package/apps/server/package.json +5 -11
- package/apps/server/src/app.ts +452 -0
- package/apps/server/src/cli/collect-registry.ts +7 -6
- package/apps/server/src/config/deployment.ts +36 -70
- package/apps/server/src/config/integration.ts +81 -0
- package/apps/server/src/dev.ts +81 -0
- package/apps/server/src/index.ts +23 -376
- package/apps/server/src/intent-routes.ts +11 -4
- package/apps/server/src/modules.ts +3 -0
- 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 +97 -23
- 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 +4 -4
- package/packages/module-weather/skills/weather-guide/SKILL.md +3 -0
- package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
- package/packages/module-weather/src/server/index.ts +1 -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 -46
- 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/apps/server/skills/shopping-guide-demo/SKILL.md +0 -21
- package/apps/server/skills/shopping-guide-demo/references/guide/parking.md +0 -11
- package/apps/server/skills/shopping-guide-demo/references/mall-directory.md +0 -17
- package/apps/server/skills/shopping-guide-demo/references/promotion.md +0 -10
- package/apps/server/src/config/feature-gate.ts +0 -17
- package/apps/server/tests/feature-gate.test.ts +0 -30
- package/config/deployments/development.json +0 -27
- package/scripts/check-inspector.sh +0 -91
- /package/packages/module-weather/{tbox.component.json → tbox.module.json} +0 -0
|
@@ -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 =
|
|
@@ -93,6 +93,35 @@
|
|
|
93
93
|
color: #dc2626;
|
|
94
94
|
}
|
|
95
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
|
+
}
|
|
96
125
|
}
|
|
97
126
|
|
|
98
127
|
@keyframes tool-spin {
|
|
@@ -1,15 +1,25 @@
|
|
|
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';
|
|
4
|
-
import { describeToolCall } from '@tbox.cn/app-sdk/client';
|
|
5
|
+
import { describeToolCall, formatCompactToolIo } from '@tbox.cn/app-sdk/client';
|
|
5
6
|
import './index.less';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* 工具调用状态卡片(调用中 spinner / 完成 /
|
|
9
|
-
* 010:展示语义判定下沉 SDK describeToolCall(skill/preTool/normal
|
|
9
|
+
* 工具调用状态卡片(调用中 spinner / 完成 / 失败)。
|
|
10
|
+
* 010:展示语义判定下沉 SDK describeToolCall(skill/preTool/normal/memory 四分支),
|
|
10
11
|
* 本组件退化为纯渲染(徽标/颜色/展开交互)。
|
|
12
|
+
* 018 tool-debug:debug 门控在渲染层——prod 恒无出入参展开入口(I4);
|
|
13
|
+
* dev 详情走 formatCompactToolIo(紧凑单行 + 头尾保留),输入输出双栏。
|
|
11
14
|
*/
|
|
12
|
-
export default function ToolCallBlock({
|
|
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
|
+
}) {
|
|
13
23
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
14
24
|
|
|
15
25
|
return (
|
|
@@ -17,10 +27,14 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
17
27
|
{toolCalls.map((tc) => {
|
|
18
28
|
const display = describeToolCall(tc);
|
|
19
29
|
const isCalling = tc.status === 'calling';
|
|
20
|
-
const isError =
|
|
30
|
+
const isError =
|
|
31
|
+
tc.status === 'error' ||
|
|
32
|
+
(display.kind === 'preTool' && display.status === 'error') ||
|
|
33
|
+
(display.kind === 'memory' && display.status === 'degraded');
|
|
21
34
|
const isDone = !isCalling && !isError;
|
|
22
35
|
const isExpanded = expandedId === tc.id;
|
|
23
|
-
|
|
36
|
+
// 018 I4:prod 恒无展开入口;dev 按 display.hasDetail
|
|
37
|
+
const hasDetail = debug && display.hasDetail;
|
|
24
38
|
const stateClass = isError ? 'tool-call--error' : isCalling ? 'tool-call--calling' : 'tool-call--done';
|
|
25
39
|
|
|
26
40
|
const displayName = (() => {
|
|
@@ -32,6 +46,9 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
32
46
|
if (display.status === 'error') return `自动查询失败 ${display.label}`;
|
|
33
47
|
return `已自动获取 ${display.label}`;
|
|
34
48
|
}
|
|
49
|
+
if (display.kind === 'memory') {
|
|
50
|
+
return display.label;
|
|
51
|
+
}
|
|
35
52
|
return isCalling
|
|
36
53
|
? `调用 ${display.label}`
|
|
37
54
|
: isDone
|
|
@@ -39,12 +56,29 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
39
56
|
: `${display.label} 调用失败`;
|
|
40
57
|
})();
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
})();
|
|
48
82
|
|
|
49
83
|
return (
|
|
50
84
|
<div key={tc.id} className={`tool-call ${stateClass}`}>
|
|
@@ -69,9 +103,18 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
69
103
|
</div>
|
|
70
104
|
{isExpanded && hasDetail && (
|
|
71
105
|
<div className="tool-call__result">
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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>
|
|
75
118
|
</div>
|
|
76
119
|
)}
|
|
77
120
|
</div>
|
|
@@ -80,12 +123,3 @@ export default function ToolCallBlock({ toolCalls }: { toolCalls: ToolCallInfo[]
|
|
|
80
123
|
</div>
|
|
81
124
|
);
|
|
82
125
|
}
|
|
83
|
-
|
|
84
|
-
function formatResult(raw: string): string {
|
|
85
|
-
try {
|
|
86
|
-
const pretty = JSON.stringify(JSON.parse(raw), null, 2);
|
|
87
|
-
return pretty.length > 500 ? pretty.slice(0, 500) + '...' : pretty;
|
|
88
|
-
} catch {
|
|
89
|
-
return raw.length > 500 ? raw.slice(0, 500) + '...' : raw;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -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(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, type ComponentType } from 'react';
|
|
2
2
|
import { useChatStore } from '@/store/chat';
|
|
3
|
+
import { useChatConfigStore } from '@/store/chat-config';
|
|
3
4
|
import { useTTSAutoPlay } from '@/hooks/useTTSAutoPlay';
|
|
4
5
|
import { MessageBubble } from '@/components/MessageBubble';
|
|
5
6
|
import { ChatInput } from '@/components/ChatInput';
|
|
@@ -7,7 +8,7 @@ import { QuickCommands } from '@/components/QuickCommands';
|
|
|
7
8
|
import ChatHeader from '@/components/ChatHeader';
|
|
8
9
|
import ScrollToBottomButton from '@/components/ScrollToBottomButton';
|
|
9
10
|
import PullDownIndicator from '@/components/PullDownIndicator';
|
|
10
|
-
import { usePullDownHistory } from '
|
|
11
|
+
import { usePullDownHistory, consumeAutoSendQuery, runAutoSend } from '@tbox.cn/app-sdk/client';
|
|
11
12
|
import { CardRenderer } from '@/cards/CardRenderer';
|
|
12
13
|
import type { ChatInputMessage } from '@/types/chat';
|
|
13
14
|
import './index.less';
|
|
@@ -30,23 +31,40 @@ export default function IndexPage({ landing: Landing }: IndexPageProps) {
|
|
|
30
31
|
const loadMoreHistory = useChatStore((s) => s.loadMoreHistory);
|
|
31
32
|
const isGenerating = useChatStore((s) => s.isGenerating);
|
|
32
33
|
const orphanCards = useChatStore((s) => s.orphanCards);
|
|
34
|
+
const cardsByMessageId = useChatStore((s) => s.cardsByMessageId);
|
|
33
35
|
const initConversation = useChatStore((s) => s.initConversation);
|
|
34
36
|
const destroy = useChatStore((s) => s.destroy);
|
|
35
37
|
const sendMessage = useChatStore((s) => s.sendMessage);
|
|
36
38
|
const cancelGeneration = useChatStore((s) => s.cancelGeneration);
|
|
37
|
-
const
|
|
39
|
+
const newConversation = useChatStore((s) => s.newConversation);
|
|
40
|
+
const authBlocked = useChatStore((s) => s.authBlocked);
|
|
41
|
+
const retryBootstrap = useChatStore((s) => s.retryBootstrap);
|
|
42
|
+
// chat settings 水合:HELLO_ACK 服务端真源 → 静态 store(ack 未达/键缺席保留构建期快照;
|
|
43
|
+
// 重连/新会话产生新 helloAck 再水合——applyServerSettings 幂等)
|
|
44
|
+
const serverSettings = useChatStore((s) => s.helloAck?.chatConfig?.settings);
|
|
45
|
+
const applyServerSettings = useChatConfigStore((s) => s.applyServerSettings);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
applyServerSettings(serverSettings);
|
|
48
|
+
}, [serverSettings, applyServerSettings]);
|
|
38
49
|
|
|
39
50
|
const [ttsAutoPlay, setTtsAutoPlay] = useState(false);
|
|
40
51
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
|
41
52
|
const bodyRef = useRef<HTMLDivElement>(null);
|
|
42
|
-
|
|
53
|
+
// 初始 true:全新会话(未滚动)也可直接下拉加载历史(onScroll 才是真源更新)
|
|
54
|
+
const isAtTopRef = useRef(true);
|
|
43
55
|
const isNearBottomRef = useRef(true);
|
|
44
56
|
|
|
45
|
-
// TTS
|
|
46
|
-
|
|
57
|
+
// TTS 自动播放:开启后边生成边流式朗读(服务端关态经水合强制停——防按钮隐藏仍朗读的僵尸路径)
|
|
58
|
+
const enableTTS = useChatConfigStore((s) => s.enableTTS);
|
|
59
|
+
useTTSAutoPlay(ttsAutoPlay && enableTTS);
|
|
47
60
|
|
|
48
61
|
const send = useCallback((input: string | ChatInputMessage) => sendMessage(input), [sendMessage]);
|
|
49
62
|
|
|
63
|
+
// 连接终态阻断(登录失败/scope 拒绝):禁用输入 + 提示条 + 重试(authBlocked 非空 ⇒ socket 已关 ⇒ 发送必失败)
|
|
64
|
+
const blocked = authBlocked
|
|
65
|
+
? { message: authBlocked, onRetry: () => void retryBootstrap() }
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
50
68
|
const scrollToBottom = useCallback(() => {
|
|
51
69
|
const el = bodyRef.current;
|
|
52
70
|
if (el) el.scrollTop = el.scrollHeight;
|
|
@@ -80,21 +98,28 @@ export default function IndexPage({ landing: Landing }: IndexPageProps) {
|
|
|
80
98
|
isLoadingHistory,
|
|
81
99
|
hasMore: historyHasMore,
|
|
82
100
|
onLoad: onLoadHistory,
|
|
101
|
+
// touchmove 走容器上的非 passive 原生监听(web-view 内合成事件 passive,阻止不了原生滚动)
|
|
102
|
+
scrollRef: bodyRef,
|
|
83
103
|
});
|
|
84
104
|
|
|
85
105
|
useEffect(() => {
|
|
86
|
-
|
|
106
|
+
// 深链自动发送:同步消费(StrictMode 双挂载/刷新安全),引导完成后等连接就绪发送
|
|
107
|
+
const autoQuery = consumeAutoSendQuery();
|
|
108
|
+
void initConversation().then(async () => {
|
|
109
|
+
if (autoQuery) await runAutoSend(autoQuery, sendMessage);
|
|
110
|
+
});
|
|
87
111
|
return () => destroy();
|
|
88
112
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
89
113
|
}, []);
|
|
90
114
|
|
|
91
115
|
// 仅在贴近底部时自动滚动,避免打断用户上翻查看历史
|
|
116
|
+
// 依赖含 cardsByMessageId:012 主卡在文字完成后挂载、消息行增高,需补滚避免卡片截出视口
|
|
92
117
|
useEffect(() => {
|
|
93
118
|
if (isNearBottomRef.current) {
|
|
94
119
|
const el = bodyRef.current;
|
|
95
120
|
if (el) el.scrollTop = el.scrollHeight;
|
|
96
121
|
}
|
|
97
|
-
}, [messages, orphanCards]);
|
|
122
|
+
}, [messages, orphanCards, cardsByMessageId]);
|
|
98
123
|
|
|
99
124
|
return (
|
|
100
125
|
<div className="chat-page">
|
|
@@ -102,7 +127,7 @@ export default function IndexPage({ landing: Landing }: IndexPageProps) {
|
|
|
102
127
|
<ChatHeader
|
|
103
128
|
ttsAutoPlay={ttsAutoPlay}
|
|
104
129
|
onToggleTTS={() => setTtsAutoPlay((v) => !v)}
|
|
105
|
-
onClearChat={
|
|
130
|
+
onClearChat={newConversation}
|
|
106
131
|
/>
|
|
107
132
|
|
|
108
133
|
<div className="chat-page__body-wrapper">
|
|
@@ -110,9 +135,6 @@ export default function IndexPage({ landing: Landing }: IndexPageProps) {
|
|
|
110
135
|
className="chat-page__body"
|
|
111
136
|
ref={bodyRef}
|
|
112
137
|
onScroll={onScroll}
|
|
113
|
-
onTouchStart={pull.handleTouchStart}
|
|
114
|
-
onTouchMove={pull.handleTouchMove}
|
|
115
|
-
onTouchEnd={pull.handleTouchEnd}
|
|
116
138
|
>
|
|
117
139
|
{/* 下拉加载历史指示器 */}
|
|
118
140
|
<PullDownIndicator status={pull.pullStatus} distance={pull.pullDistance} />
|
|
@@ -138,8 +160,8 @@ export default function IndexPage({ landing: Landing }: IndexPageProps) {
|
|
|
138
160
|
<ScrollToBottomButton visible={showScrollToBottom && isGenerating} onClick={scrollToBottom} />
|
|
139
161
|
</div>
|
|
140
162
|
|
|
141
|
-
<QuickCommands onCommand={send} />
|
|
142
|
-
<ChatInput generating={isGenerating} onSend={send} onCancel={cancelGeneration} onNewChat={
|
|
163
|
+
{!blocked && <QuickCommands onCommand={send} />}
|
|
164
|
+
<ChatInput generating={isGenerating} blocked={blocked} onSend={send} onCancel={cancelGeneration} onNewChat={newConversation} />
|
|
143
165
|
</div>
|
|
144
166
|
);
|
|
145
167
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '@tbox.cn/app-sdk/client';
|
|
2
|
+
|
|
3
|
+
const appLogger = createLogger('AgentChatApp');
|
|
4
|
+
|
|
1
5
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
6
|
import { AdapterProvider, createPlatformAdapters, type AdapterContextValue } from '@tbox.cn/app-sdk/client';
|
|
3
7
|
import type { TboxSessionData } from '@tbox.cn/app-contracts';
|
|
@@ -15,7 +19,7 @@ export default function AgentChatApp({ landing }: { landing?: ChatLandingCompone
|
|
|
15
19
|
.ensureAuth()
|
|
16
20
|
.then((auth) => platformApi.getTboxSession(auth.identity.userId))
|
|
17
21
|
.then(setSession)
|
|
18
|
-
.catch((err) =>
|
|
22
|
+
.catch((err) => appLogger.error('tbox session init failed:', err));
|
|
19
23
|
}, []);
|
|
20
24
|
|
|
21
25
|
const refreshSession = useCallback(async (): Promise<AdapterContextValue> => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
+
import type { ChatUiSettings } from '@tbox.cn/app-sdk/client';
|
|
2
3
|
import chatConfigJson from '../../../../config/chat.json';
|
|
3
4
|
|
|
4
5
|
export interface QuickCommand {
|
|
@@ -15,15 +16,25 @@ interface ChatConfigStore {
|
|
|
15
16
|
enableVoiceInput: boolean;
|
|
16
17
|
defaultInputMode: 'TEXT' | 'VOICE';
|
|
17
18
|
quickCommands: QuickCommand[];
|
|
19
|
+
/** HELLO_ACK settings 水合:服务端单一真源;undefined 键保留构建期快照(五键整组语义,禁部分水合) */
|
|
20
|
+
applyServerSettings: (settings: ChatUiSettings | undefined) => void;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const cfg = chatConfigJson as Record<string, unknown>;
|
|
21
24
|
|
|
22
|
-
export const useChatConfigStore = create<ChatConfigStore>()(() => ({
|
|
25
|
+
export const useChatConfigStore = create<ChatConfigStore>()((set) => ({
|
|
23
26
|
enableImageUpload: (cfg.enableImageUpload as boolean) ?? true,
|
|
24
27
|
enableVideoUpload: (cfg.enableVideoUpload as boolean) ?? true,
|
|
25
28
|
enableTTS: (cfg.enableTTS as boolean) ?? true,
|
|
26
29
|
enableVoiceInput: (cfg.enableVoiceInput as boolean) ?? true,
|
|
27
30
|
defaultInputMode: (cfg.defaultInputMode as 'TEXT' | 'VOICE') ?? 'TEXT',
|
|
28
31
|
quickCommands: (cfg.quickCommands as QuickCommand[]) ?? [],
|
|
32
|
+
applyServerSettings: (settings) =>
|
|
33
|
+
set((cur) => ({
|
|
34
|
+
enableImageUpload: settings?.enableImageUpload ?? cur.enableImageUpload,
|
|
35
|
+
enableVideoUpload: settings?.enableVideoUpload ?? cur.enableVideoUpload,
|
|
36
|
+
enableTTS: settings?.enableTTS ?? cur.enableTTS,
|
|
37
|
+
enableVoiceInput: settings?.enableVoiceInput ?? cur.enableVoiceInput,
|
|
38
|
+
defaultInputMode: settings?.defaultInputMode ?? cur.defaultInputMode,
|
|
39
|
+
})),
|
|
29
40
|
}));
|