@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.
Files changed (81) hide show
  1. package/.gitignore +6 -0
  2. package/.tbox/app.json +12 -3
  3. package/AGENTS.md +17 -6
  4. package/ENV.md +36 -5
  5. package/README.md +23 -7
  6. package/apps/client/package.json +3 -2
  7. package/apps/client/src/app.tsx +28 -0
  8. package/apps/client/src/cards/CardRenderer.tsx +4 -5
  9. package/apps/client/src/components/ActionBar/DebugButton.tsx +5 -1
  10. package/apps/client/src/components/ActionBar/index.tsx +7 -1
  11. package/apps/client/src/components/ChatInput/index.less +54 -0
  12. package/apps/client/src/components/ChatInput/index.tsx +33 -9
  13. package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +20 -1
  14. package/apps/client/src/components/MessageBubble/index.less +12 -1
  15. package/apps/client/src/components/MessageBubble/index.tsx +16 -8
  16. package/apps/client/src/components/PullDownIndicator/index.less +1 -1
  17. package/apps/client/src/components/PullDownIndicator/index.tsx +3 -4
  18. package/apps/client/src/components/ToolCallBlock/index.less +29 -0
  19. package/apps/client/src/components/ToolCallBlock/index.tsx +58 -24
  20. package/apps/client/src/hooks/useMediaUpload.ts +7 -2
  21. package/apps/client/src/main.tsx +4 -1
  22. package/apps/client/src/pages/index/index.tsx +35 -13
  23. package/apps/client/src/platform/AgentChatApp.tsx +5 -1
  24. package/apps/client/src/store/chat-config.ts +12 -1
  25. package/apps/client/src/store/chat.ts +62 -92
  26. package/apps/client/vite.config.ts +46 -26
  27. package/apps/server/package.json +5 -11
  28. package/apps/server/src/app.ts +452 -0
  29. package/apps/server/src/cli/collect-registry.ts +7 -6
  30. package/apps/server/src/config/deployment.ts +36 -70
  31. package/apps/server/src/config/integration.ts +81 -0
  32. package/apps/server/src/dev.ts +81 -0
  33. package/apps/server/src/index.ts +23 -376
  34. package/apps/server/src/intent-routes.ts +11 -4
  35. package/apps/server/src/modules.ts +3 -0
  36. package/apps/server/tests/cards-registry.test.ts +9 -17
  37. package/apps/server/tests/deployment-loader.test.ts +140 -0
  38. package/apps/server/tests/intent-dispatcher.test.ts +58 -0
  39. package/apps/server/tests/tool-runtime.test.ts +97 -23
  40. package/apps/server/tsup.config.ts +15 -2
  41. package/config/chat.json +4 -1
  42. package/config/llm_provider.json +3 -1
  43. package/package.json +19 -11
  44. package/packages/contracts-mall/package.json +30 -0
  45. package/packages/contracts-mall/src/index.ts +1 -0
  46. package/packages/contracts-mall/src/runtime.ts +1 -0
  47. package/packages/contracts-mall/src/server/index.ts +1 -0
  48. package/packages/contracts-mall/tsconfig.json +7 -0
  49. package/packages/module-weather/package.json +4 -4
  50. package/packages/module-weather/skills/weather-guide/SKILL.md +3 -0
  51. package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
  52. package/packages/module-weather/src/server/index.ts +1 -0
  53. package/packages/module-weather/src/server/tool.ts +24 -24
  54. package/packages/provider-custom/AGENTS.md +33 -0
  55. package/packages/provider-custom/LICENSE +21 -0
  56. package/packages/provider-custom/README.md +8 -0
  57. package/packages/provider-custom/package.json +62 -0
  58. package/packages/provider-custom/src/index.ts +5 -0
  59. package/packages/provider-custom/src/server/index.ts +40 -0
  60. package/packages/provider-custom/tbox.module.json +29 -0
  61. package/packages/provider-custom/tests/provider-custom.test.ts +17 -0
  62. package/packages/provider-custom/tsconfig.json +7 -0
  63. package/packages/provider-custom/tsup.config.ts +18 -0
  64. package/pnpm-lock.template.yaml +6297 -0
  65. package/pnpm-workspace.template.yaml +45 -46
  66. package/scripts/build.sh +6 -12
  67. package/scripts/check-workspace.mjs +11 -10
  68. package/scripts/stop.sh +48 -14
  69. package/scripts/verify-generated.mjs +4 -4
  70. package/tsconfig.base.json +1 -0
  71. package/apps/client/babel-plugins/code-inspect.cjs +0 -110
  72. package/apps/client/src/hooks/usePullDownHistory.ts +0 -97
  73. package/apps/server/skills/shopping-guide-demo/SKILL.md +0 -21
  74. package/apps/server/skills/shopping-guide-demo/references/guide/parking.md +0 -11
  75. package/apps/server/skills/shopping-guide-demo/references/mall-directory.md +0 -17
  76. package/apps/server/skills/shopping-guide-demo/references/promotion.md +0 -10
  77. package/apps/server/src/config/feature-gate.ts +0 -17
  78. package/apps/server/tests/feature-gate.test.ts +0 -30
  79. package/config/deployments/development.json +0 -27
  80. package/scripts/check-inspector.sh +0 -91
  81. /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-top: 12px;
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 className={`msg-meta ${isUser ? 'msg-meta--user' : 'msg-meta--bot'}`}>
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) => (
@@ -13,7 +13,7 @@
13
13
  width: 32px;
14
14
  height: 32px;
15
15
  border: 3px solid #d0d5dd;
16
- border-top-color: #1677ff;
16
+ border-top-color: var(--tbox-primary);
17
17
  border-radius: 50%;
18
18
  animation: pull-spin 0.6s linear infinite;
19
19
  }
@@ -1,13 +1,12 @@
1
- import type { PullStatus } from '@/hooks/usePullDownHistory';
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 : -INDICATOR_HEIGHT + Math.min(distance, INDICATOR_HEIGHT);
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({ toolCalls }: { toolCalls: ToolCallInfo[] }) {
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 = tc.status === 'error' || (display.kind === 'preTool' && display.status === 'error');
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
- const hasDetail = display.kind !== 'skill' && display.hasDetail;
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
- const detailText =
43
- display.kind === 'preTool'
44
- ? (display.status === 'error' ? display.errorText : display.resultText) ?? ''
45
- : isError
46
- ? tc.error ?? ''
47
- : tc.result ?? '';
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
- <span className={`tool-call__result-text${isError ? ' tool-call__result-text--error' : ''}`}>
73
- {formatResult(detailText)}
74
- </span>
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
- console.warn('[media] bridge pick failed:', err);
219
+ mediaLogger.warn('bridge pick failed:', err);
215
220
  }
216
221
  },
217
222
  [adapter, enqueue, validate],
@@ -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 '@/hooks/usePullDownHistory';
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 clearChat = useChatStore((s) => s.clearChat);
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
- const isAtTopRef = useRef(false);
53
+ // 初始 true:全新会话(未滚动)也可直接下拉加载历史(onScroll 才是真源更新)
54
+ const isAtTopRef = useRef(true);
43
55
  const isNearBottomRef = useRef(true);
44
56
 
45
- // TTS 自动播放:开启后边生成边流式朗读
46
- useTTSAutoPlay(ttsAutoPlay);
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
- initConversation();
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={clearChat}
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={clearChat} />
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) => console.error('[AgentChatApp] tbox session init failed:', 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
  }));