@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.
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 +38 -5
  5. package/README.md +131 -5
  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 +12 -2
  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/QuickCommands/index.tsx +9 -2
  19. package/apps/client/src/components/ToolCallBlock/index.less +40 -0
  20. package/apps/client/src/components/ToolCallBlock/index.tsx +84 -22
  21. package/apps/client/src/hooks/useMediaUpload.ts +7 -2
  22. package/apps/client/src/main.tsx +4 -1
  23. package/apps/client/src/pages/index/index.tsx +35 -13
  24. package/apps/client/src/platform/AgentChatApp.tsx +5 -1
  25. package/apps/client/src/store/chat-config.ts +12 -1
  26. package/apps/client/src/store/chat.ts +72 -71
  27. package/apps/client/vite.config.ts +46 -26
  28. package/apps/server/package.json +5 -10
  29. package/apps/server/src/app-tools.ts +13 -0
  30. package/apps/server/src/app.ts +452 -0
  31. package/apps/server/src/cli/collect-registry.ts +11 -9
  32. package/apps/server/src/config/deployment.ts +47 -0
  33. package/apps/server/src/config/integration.ts +81 -0
  34. package/apps/server/src/dev.ts +81 -0
  35. package/apps/server/src/index.ts +24 -219
  36. package/apps/server/src/intent-routes.ts +11 -4
  37. package/apps/server/src/modules.ts +12 -5
  38. package/apps/server/src/pre-tools.ts +25 -0
  39. package/apps/server/src/tools/clock.ts +24 -0
  40. package/apps/server/src/tools/translate.ts +10 -4
  41. package/apps/server/tests/cards-registry.test.ts +9 -17
  42. package/apps/server/tests/deployment-loader.test.ts +140 -0
  43. package/apps/server/tests/intent-dispatcher.test.ts +58 -0
  44. package/apps/server/tests/tool-runtime.test.ts +124 -22
  45. package/apps/server/tsup.config.ts +15 -2
  46. package/config/chat.json +4 -1
  47. package/config/llm_provider.json +3 -1
  48. package/package.json +19 -11
  49. package/packages/contracts-mall/package.json +30 -0
  50. package/packages/contracts-mall/src/index.ts +1 -0
  51. package/packages/contracts-mall/src/runtime.ts +1 -0
  52. package/packages/contracts-mall/src/server/index.ts +1 -0
  53. package/packages/contracts-mall/tsconfig.json +7 -0
  54. package/packages/module-weather/package.json +6 -5
  55. package/packages/module-weather/skills/weather-guide/SKILL.md +22 -0
  56. package/packages/module-weather/skills/weather-guide/references/dressing-tips.md +5 -0
  57. package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
  58. package/packages/module-weather/src/server/index.ts +27 -1
  59. package/packages/module-weather/src/server/pre-tools.ts +20 -0
  60. package/packages/module-weather/src/server/tool.ts +24 -24
  61. package/packages/provider-custom/AGENTS.md +33 -0
  62. package/packages/provider-custom/LICENSE +21 -0
  63. package/packages/provider-custom/README.md +8 -0
  64. package/packages/provider-custom/package.json +62 -0
  65. package/packages/provider-custom/src/index.ts +5 -0
  66. package/packages/provider-custom/src/server/index.ts +40 -0
  67. package/packages/provider-custom/tbox.module.json +29 -0
  68. package/packages/provider-custom/tests/provider-custom.test.ts +17 -0
  69. package/packages/provider-custom/tsconfig.json +7 -0
  70. package/packages/provider-custom/tsup.config.ts +18 -0
  71. package/pnpm-lock.template.yaml +6297 -0
  72. package/pnpm-workspace.template.yaml +45 -45
  73. package/scripts/build.sh +6 -12
  74. package/scripts/check-workspace.mjs +11 -10
  75. package/scripts/stop.sh +48 -14
  76. package/scripts/verify-generated.mjs +4 -4
  77. package/tsconfig.base.json +1 -0
  78. package/apps/client/babel-plugins/code-inspect.cjs +0 -110
  79. package/apps/client/src/hooks/usePullDownHistory.ts +0 -97
  80. package/scripts/check-inspector.sh +0 -91
  81. /package/packages/module-weather/{tbox.component.json → tbox.module.json} +0 -0
@@ -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
  }));
@@ -4,9 +4,15 @@
4
4
  * 共用同一份 set/get,避免双 store 断裂。
5
5
  */
6
6
  import { create } from 'zustand';
7
- import { createCoreChatState, getWsUrl } from '@tbox.cn/app-sdk/client';
7
+ import { createLogger } from '@tbox.cn/app-sdk/client';
8
+
9
+ const conversationLogger = createLogger('CONVERSATION');
10
+ const historyLogger = createLogger('HISTORY');
11
+ const feedbackLogger = createLogger('FEEDBACK');
12
+ import { createCoreChatState, getWsUrl, configureHelloContext } from '@tbox.cn/app-sdk/client';
8
13
  import type { CoreChatState } from '@tbox.cn/app-sdk/client';
9
- import type { CardSnapshot, TboxCardPayload } from '@tbox.cn/app-contracts';
14
+ import { convertHistoryMessages } from '@tbox.cn/app-sdk/client';
15
+ import type { PlatformHistoryMessage } from '@tbox.cn/app-sdk/client';
10
16
  import type { ChatMessage } from '@/types/chat';
11
17
  import type { IFeedbackAdapter } from '@tbox.cn/app-sdk/client';
12
18
  import type { AdapterContextValue } from '@tbox.cn/app-sdk/client';
@@ -14,62 +20,23 @@ import { isSessionExpiredError } from '@/utils/session-error';
14
20
  import { toast } from '@/utils/toast';
15
21
  import { authClient, platformApi } from '@/platform/agent-runtime';
16
22
 
17
- interface HistoryMessageInfo {
18
- messageId?: string;
19
- query?: string;
20
- answer?: string;
21
- content?: string;
22
- outerBusinessId?: string;
23
- createTime?: number;
24
- multiModalInputs?: Record<string, unknown>;
25
- /** P3 W1:服务端已恢复(migrate + isHistory=true)的卡片快照 */
26
- cards?: CardSnapshot[];
27
- }
28
-
29
- function convertHistoryMessages(messages: HistoryMessageInfo[]): ChatMessage[] {
30
- let idSeq = 0;
31
- const nextId = (prefix: string) => `${prefix}_${Date.now()}_${idSeq++}`;
32
- const result: ChatMessage[] = [];
33
- for (const info of messages) {
34
- if (info.query) {
35
- const userMsg: ChatMessage = {
36
- id: `history-user-${info.messageId}`,
37
- role: 'user',
38
- content: info.query,
39
- status: 'done',
40
- createdAt: info.createTime || Date.now(),
41
- ...(info.outerBusinessId && { requestId: info.outerBusinessId }),
42
- };
43
- const imageUrls = info.multiModalInputs?.imageUrls as string[] | undefined;
44
- if (imageUrls && imageUrls.length > 0) {
45
- userMsg.mediaItems = imageUrls.map((url) => ({
46
- id: nextId('history-img'),
47
- type: 'image' as const,
48
- src: url,
49
- }));
50
- }
51
- result.push(userMsg);
52
- }
53
- if (info.answer || info.content) {
54
- result.push({
55
- id: `history-asst-${info.messageId}`,
56
- role: 'assistant',
57
- content: info.answer || info.content || '',
58
- status: 'done',
59
- createdAt: info.createTime || Date.now(),
60
- ...(info.outerBusinessId && { requestId: info.outerBusinessId }),
61
- });
62
- }
63
- }
64
- return result;
65
- }
23
+ // ── 历史加载代际标志(模块级):newConversation 递增作废在途 loadMoreHistory(stale 响应整段丢弃)──
24
+ let historyEpoch = 0;
25
+ // ── 新建会话 in-flight 标志(模块级):双击「新对话」防双建会话 ──
26
+ let newConversationInFlight = false;
27
+ // ── 终态阻断重试 in-flight 标志(模块级):防并发重试 ──
28
+ let retryBootstrapInFlight: Promise<void> | null = null;
66
29
 
67
30
  interface ChatState extends CoreChatState {
68
31
  historyMessages: ChatMessage[];
69
32
  historyHasMore: boolean;
70
33
  isLoadingHistory: boolean;
71
34
  initConversation: () => Promise<void>;
35
+ /** 新建会话:清会话态(连接复用)+ 重置历史态 + create 新会话 */
36
+ newConversation: () => Promise<void>;
72
37
  loadMoreHistory: () => Promise<void>;
38
+ /** 终态阻断后重试:清匿名降级缓存 → 拆除连接 → 重走 initConversation(重复点击防抖) */
39
+ retryBootstrap: () => Promise<void>;
73
40
  submitFeedback: (
74
41
  messageId: string,
75
42
  type: 'like' | 'dislike' | 'cancel',
@@ -84,8 +51,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
84
51
  { getWsUrl: () => getWsUrl(), toast },
85
52
  set as (partial: any) => void,
86
53
  get as () => any,
87
- ),
88
- // 业务扩展状态
54
+ ), // 业务扩展状态
89
55
  historyMessages: [],
90
56
  historyHasMore: true,
91
57
  isLoadingHistory: false,
@@ -97,28 +63,58 @@ export const useChatStore = create<ChatState>((set, get) => ({
97
63
  let conversationId: string | null = null;
98
64
  // 006:create 的 userId 只来自 token 身份(body 废弃);匿名(无 token)不可建会话 → 直接走 no-conv 分支
99
65
  if (!authClient.getAuthState()?.token) {
100
- console.warn('[CONVERSATION] anonymous: skip create conversation (no-conv)');
66
+ conversationLogger.warn('anonymous: skip create conversation (no-conv)');
101
67
  } else {
102
68
  for (let i = 0; i < 2 && !conversationId; i++) {
103
69
  try {
104
70
  const { status, data } = await platformApi.createConversation();
105
71
  if (status === 200 && data?.conversationId) conversationId = data.conversationId;
106
- } catch {
107
- // retry
72
+ } catch (e) {
73
+ conversationLogger.warn(`createConversation 失败(重试 ${i + 1}/2):err=${(e as Error).message}`);
108
74
  }
109
75
  }
110
76
  }
111
77
  if (conversationId) set({ conversationId });
112
78
  get().init();
113
79
  } catch (err) {
114
- console.error('[CONVERSATION] init error:', err);
80
+ conversationLogger.error('init error:', err);
115
81
  get().init();
116
82
  }
117
83
  },
118
84
 
85
+ // 终态阻断(登录失败/scope 拒绝)后重试:清匿名降级缓存(不清则必复败)→ 拆连接 → 重走完整引导
86
+ retryBootstrap: () => {
87
+ if (retryBootstrapInFlight) return retryBootstrapInFlight; // 并发重试防抖
88
+ const task = (async () => {
89
+ authClient.clearAuth(); // 匿名降级结果被缓存,不清则重试必然复败
90
+ get().destroy();
91
+ await get().initConversation();
92
+ })();
93
+ const tracked = task.finally(() => {
94
+ if (retryBootstrapInFlight === tracked) retryBootstrapInFlight = null;
95
+ });
96
+ retryBootstrapInFlight = tracked;
97
+ return tracked;
98
+ },
99
+
100
+ // 新建会话:连接复用(clearChat 不断连——连接/身份是身份域),create 拿新会话键
101
+ newConversation: async () => {
102
+ if (newConversationInFlight) return; // 双击防双建会话
103
+ newConversationInFlight = true;
104
+ try {
105
+ historyEpoch++; // 作废在途历史加载(stale 响应整段丢弃,含 isLoadingHistory 复位)
106
+ get().clearChat(); // SDK:内聚取消运行中 run + 清会话态
107
+ set({ historyMessages: [], historyHasMore: true, isLoadingHistory: false });
108
+ await get().initConversation(); // create 新会话;init 对 OPEN 连接幂等早返
109
+ } finally {
110
+ newConversationInFlight = false;
111
+ }
112
+ },
113
+
119
114
  loadMoreHistory: async () => {
120
115
  const { isLoadingHistory, historyHasMore, historyMessages } = get();
121
116
  if (isLoadingHistory || !historyHasMore) return;
117
+ const epoch = historyEpoch; // 快照:newConversation 递增后本响应作废
122
118
  set({ isLoadingHistory: true });
123
119
 
124
120
  const pageSize = 5;
@@ -136,22 +132,16 @@ export const useChatStore = create<ChatState>((set, get) => ({
136
132
  : { userId: get().userId }),
137
133
  ...(earliestRequestId ? { beforeId: earliestRequestId } : {}),
138
134
  });
135
+ // stale 响应(newConversation 已重置态):整段丢弃——含 isLoadingHistory 复位也不落
136
+ if (epoch !== historyEpoch) return;
139
137
  if (status === 200 && data) {
140
- const rawList: HistoryMessageInfo[] = (data.data as HistoryMessageInfo[]) || [];
138
+ const rawList: PlatformHistoryMessage[] = (data.data as PlatformHistoryMessage[]) || [];
141
139
  const currentRequestIds = new Set(get().messages.map((m) => m.requestId).filter(Boolean));
142
140
  const filtered = rawList.filter(
143
141
  (item) => !item.outerBusinessId || !currentRequestIds.has(item.outerBusinessId),
144
142
  );
145
- const newMessages = convertHistoryMessages(filtered.reverse());
146
- // P3 W1:历史卡片按 messageId 挂载到 assistant 气泡(isHistory 已由服务端标记)
147
- const cardsByMessageId: Record<string, TboxCardPayload[]> = {};
148
- for (const info of filtered) {
149
- if (!info.messageId || !info.cards || info.cards.length === 0) continue;
150
- cardsByMessageId[`history-asst-${info.messageId}`] = info.cards.map((s) => ({
151
- ...s,
152
- isHistory: true,
153
- }));
154
- }
143
+ // SDK 单真源:消息对 + 历史卡片桶同 key 派生(平台 id 字段归一;isHistory 已由服务端标记)
144
+ const { messages: newMessages, cardsByMessageId } = convertHistoryMessages(filtered.reverse());
155
145
  set((s) => ({
156
146
  historyMessages: [...newMessages, ...s.historyMessages],
157
147
  historyHasMore: rawList.length >= pageSize,
@@ -162,7 +152,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
162
152
  set({ isLoadingHistory: false });
163
153
  }
164
154
  } catch (err) {
165
- console.error('[HISTORY] load more error:', err);
155
+ historyLogger.error('load more error:', err);
156
+ if (epoch !== historyEpoch) return; // stale:newConversation 已重置 isLoadingHistory
166
157
  set({ isLoadingHistory: false });
167
158
  }
168
159
  },
@@ -172,7 +163,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
172
163
  const oldFeedbackState = oldMsg?.feedbackState ?? 'none';
173
164
  const feedbackRequestId = oldMsg?.requestId;
174
165
  if (!feedbackRequestId) {
175
- console.warn('[FEEDBACK] no requestId for message', messageId);
166
+ feedbackLogger.warn('no requestId for message', messageId);
176
167
  return;
177
168
  }
178
169
  const newFeedbackState = type === 'cancel' ? 'none' : type;
@@ -192,10 +183,20 @@ export const useChatStore = create<ChatState>((set, get) => ({
192
183
  return;
193
184
  }
194
185
  } catch (retryErr) {
195
- console.error('[FEEDBACK] retry failed:', retryErr);
186
+ feedbackLogger.error('retry failed:', retryErr);
196
187
  }
197
188
  }
198
189
  get().updateMessage(messageId, { feedbackState: oldFeedbackState });
199
190
  }
200
191
  },
201
192
  }));
193
+
194
+ // 011 L4:HELLO context 采集——mallId 经 URL query 采集(mall 部署;无 mallId 时不携带 context)
195
+ configureHelloContext(() => {
196
+ const mallId = new URLSearchParams(window.location.search).get('mallId');
197
+ const miniAppId = new URLSearchParams(window.location.search).get('miniAppId');
198
+ if (!mallId) return undefined;
199
+ return { mall: { mallId, ...(miniAppId ? { miniAppId } : {}) } };
200
+ });
201
+
202
+ // 4002 scope 拒绝 / 4001 鉴权耗尽的终态提示与禁用由 SDK 统一出站(chat-store configureWsFatalCloseHandler)
@@ -1,11 +1,12 @@
1
- import { defineConfig } from 'vite';
1
+ import { defineConfig, type Plugin } from 'vite';
2
2
  import react from '@vitejs/plugin-react';
3
3
  import pxToRem from 'postcss-pxtorem';
4
+ import { existsSync } from 'node:fs';
4
5
  import { resolve } from 'node:path';
5
- // 平台微调配套 babel plugin:给每个 JSX 注入 data-code-inspect-* 源码位置属性,
6
- // 供 inspector runtime(/__vs_inspector__.js) 点选元素→定位源码。仅 dev 启用(见下)。
7
- // @ts-expect-error 平台 babel plugin(CJS、无类型声明),运行时由 esbuild 解析。
8
- import codeInspectPlugin from './babel-plugins/code-inspect.cjs';
6
+ import { pathToFileURL } from 'node:url';
7
+ // 平台微调配套 babel plugin(随 @tbox.cn/app-sdk 分发):给每个 JSX 注入 data-code-inspect-*
8
+ // 源码位置属性,供 inspector runtime(/__vs_inspector__.js) 点选元素→定位源码。仅 dev 启用(见下)。
9
+ import codeInspectPlugin from '@tbox.cn/app-sdk/platform/code-inspect';
9
10
 
10
11
  const pxToRemPlugin = pxToRem({
11
12
  rootValue: 100,
@@ -14,27 +15,46 @@ const pxToRemPlugin = pxToRem({
14
15
  selectorBlackList: ['.no-rem'],
15
16
  minPixelValue: 1,
16
17
  mediaQuery: false,
17
- exclude: /node_modules/,
18
+ // 0.3.2/0.2.4 D3:排除 node_modules 内三方样式,但放行 pnpm 布局下的 @tbox.cn 模块 CSS。
19
+ // pxtorem 6.1.0 regex 语义为全路径任意 match(源码 index.js:131-140);五形态推演:
20
+ // 1) pnpm 装 @tbox.cn 模块(.pnpm/@tbox.cn+x/node_modules/@tbox.cn/x/...)→ 转换 ✓
21
+ // 2) pnpm 装三方(antd-mobile 等)→ 排除 ✓
22
+ // 3) npm flat 装 @tbox.cn(node_modules/@tbox.cn/x)→ 转换 ✓
23
+ // 4) npm flat 装三方 → 排除 ✓
24
+ // 5) dev 软链(Vite 解析真实路径,无 node_modules 前缀)→ 转换 ✓
25
+ exclude: /node_modules\/(?!\.pnpm|@tbox\.cn)/,
18
26
  });
19
27
 
20
- export default defineConfig(({ command }) => ({
21
- plugins: [
22
- react({
23
- babel: { plugins: command === 'serve' ? [codeInspectPlugin] : [] },
24
- }),
25
- ],
26
- resolve: {
27
- alias: {
28
- '@': resolve(__dirname, 'src'),
29
- fs: resolve(__dirname, 'src/shims/node-stub.ts'),
30
- path: resolve(__dirname, 'src/shims/node-stub.ts'),
28
+ export default defineConfig(async ({ command }) => {
29
+ // tbox-app dev 探测行(dev 探针常量;构件区在位时启用原地装配改指,缺席零行为变化。
30
+ // guard 断言本标记在场——重定位需同步 cli 探针与 guard 两处)
31
+ // __dirname 基准 = apps/client:构件区在模板根(上一级×2)
32
+ const devOverridePath = resolve(__dirname, '../../.tbox-dev.local/vite-override.mjs');
33
+ const devPlugins: Plugin[] = [];
34
+ if (existsSync(devOverridePath)) {
35
+ const { default: tboxDevOverride } = await import(pathToFileURL(devOverridePath).href);
36
+ devPlugins.push(tboxDevOverride());
37
+ }
38
+ return {
39
+ plugins: [
40
+ react({
41
+ babel: { plugins: command === 'serve' ? [codeInspectPlugin] : [] },
42
+ }),
43
+ ...devPlugins,
44
+ ],
45
+ resolve: {
46
+ alias: {
47
+ '@': resolve(__dirname, 'src'),
48
+ fs: resolve(__dirname, 'src/shims/node-stub.ts'),
49
+ path: resolve(__dirname, 'src/shims/node-stub.ts'),
50
+ },
31
51
  },
32
- },
33
- css: {
34
- postcss: { plugins: [pxToRemPlugin] },
35
- },
36
- build: {
37
- rollupOptions: { input: { index: resolve(__dirname, 'index.html') } },
38
- },
39
- // middlewareMode 下不需要 server 配置(由 server 端 vite middleware 托管)
40
- }));
52
+ css: {
53
+ postcss: { plugins: [pxToRemPlugin] },
54
+ },
55
+ build: {
56
+ rollupOptions: { input: { index: resolve(__dirname, 'index.html') } },
57
+ },
58
+ // middlewareMode 下不需要 server 配置(由 server 端 vite middleware 托管)
59
+ };
60
+ });
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "tsx watch --exclude ../../apps/client src/index.ts",
7
+ "dev": "tsx watch --exclude ../../apps/client src/dev.ts",
8
8
  "build": "tsup",
9
9
  "start": "node dist/index.js",
10
10
  "test": "vitest run",
@@ -12,29 +12,24 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@app/module-weather": "workspace:*",
15
- "@ag-ui/core": "catalog:",
16
15
  "@tbox.cn/agent-plugin": "catalog:",
17
16
  "@tbox.cn/app-sdk": "catalog:",
18
17
  "@tbox.cn/app-contracts": "catalog:",
18
+ "@app/contracts-mall": "workspace:*",
19
19
  "@tbox-dev-js/sdk": "catalog:",
20
- "ai": "catalog:",
21
20
  "cors": "catalog:",
22
21
  "express": "catalog:",
23
- "ws": "catalog:",
24
- "zod": "catalog:"
25
- },
26
- "peerDependencies": {
27
- "vite": "catalog:"
22
+ "zod": "catalog:",
23
+ "@app/provider-custom": "workspace:*"
28
24
  },
29
25
  "devDependencies": {
30
26
  "@types/cors": "catalog:",
31
27
  "@types/express": "catalog:",
32
28
  "@types/node": "catalog:",
33
- "@types/ws": "catalog:",
34
29
  "tsx": "catalog:",
35
30
  "tsup": "catalog:",
36
31
  "typescript": "catalog:",
37
32
  "vite": "catalog:",
38
33
  "vitest": "catalog:"
39
34
  }
40
- }
35
+ }
@@ -0,0 +1,13 @@
1
+ import type { ServerContext } from '@tbox.cn/app-sdk/server';
2
+ import { translateTool } from './tools/translate.js';
3
+ import { clockTool } from './tools/clock.js';
4
+
5
+ /**
6
+ * apps 级演示工具统一注册面(010:双注册面同步):
7
+ * start()(运行时)与 cli/collect-registry.ts(doctor/inspect 视图)两处同调本函数,
8
+ * 未来新增 apps 级工具只改此处,避免两处注册面失同步(R 评审修正)。
9
+ */
10
+ export function registerAppTools(ctx: ServerContext): void {
11
+ ctx.tools.register(translateTool);
12
+ ctx.tools.register(clockTool);
13
+ }