@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
|
@@ -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
|
}));
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* 共用同一份 set/get,避免双 store 断裂。
|
|
5
5
|
*/
|
|
6
6
|
import { create } from 'zustand';
|
|
7
|
-
import {
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
import codeInspectPlugin from '
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
});
|
package/apps/server/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "tsx watch --exclude ../../apps/client src/
|
|
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
|
-
"
|
|
24
|
-
"
|
|
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
|
+
}
|