@tbox.cn/app-template-agent 0.1.0

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 (119) hide show
  1. package/.gitignore +13 -0
  2. package/.nvmrc +1 -0
  3. package/.tbox/app.json +4 -0
  4. package/AGENTS.md +101 -0
  5. package/ENV.md +77 -0
  6. package/LEGAL.md +7 -0
  7. package/LICENSE +21 -0
  8. package/README.md +62 -0
  9. package/apps/client/STYLE_GUIDE.md +81 -0
  10. package/apps/client/VISUAL_RESTORE.md +90 -0
  11. package/apps/client/assets/copy@2x.png +0 -0
  12. package/apps/client/assets/debug.png +0 -0
  13. package/apps/client/assets/dislike@2x.png +0 -0
  14. package/apps/client/assets/keyboard-circle.svg +12 -0
  15. package/apps/client/assets/like@2x.png +0 -0
  16. package/apps/client/assets/more-circle.svg +3 -0
  17. package/apps/client/assets/new-chat.svg +3 -0
  18. package/apps/client/assets/quick-command-star.svg +3 -0
  19. package/apps/client/assets/send-gradient.svg +14 -0
  20. package/apps/client/assets/speaker@2x.png +0 -0
  21. package/apps/client/assets/stop@2x.png +0 -0
  22. package/apps/client/assets/voice-circle.svg +3 -0
  23. package/apps/client/babel-plugins/code-inspect.cjs +110 -0
  24. package/apps/client/index.html +36 -0
  25. package/apps/client/package.json +34 -0
  26. package/apps/client/src/app.tsx +5 -0
  27. package/apps/client/src/business/App.tsx +11 -0
  28. package/apps/client/src/business/README.md +82 -0
  29. package/apps/client/src/cards/CardRenderer.tsx +10 -0
  30. package/apps/client/src/cards/registry.ts +22 -0
  31. package/apps/client/src/components/ActionBar/DebugButton.tsx +49 -0
  32. package/apps/client/src/components/ActionBar/index.less +120 -0
  33. package/apps/client/src/components/ActionBar/index.tsx +157 -0
  34. package/apps/client/src/components/ChatHeader/index.less +104 -0
  35. package/apps/client/src/components/ChatHeader/index.tsx +63 -0
  36. package/apps/client/src/components/ChatInput/index.less +332 -0
  37. package/apps/client/src/components/ChatInput/index.tsx +251 -0
  38. package/apps/client/src/components/ChatInput/voice/VoicePressField.tsx +34 -0
  39. package/apps/client/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +51 -0
  40. package/apps/client/src/components/ChatInput/voice/machine.ts +27 -0
  41. package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +188 -0
  42. package/apps/client/src/components/ErrorBoundary.tsx +2 -0
  43. package/apps/client/src/components/MediaList/index.less +241 -0
  44. package/apps/client/src/components/MediaList/index.tsx +256 -0
  45. package/apps/client/src/components/MessageBubble/index.less +309 -0
  46. package/apps/client/src/components/MessageBubble/index.tsx +163 -0
  47. package/apps/client/src/components/PullDownIndicator/index.less +32 -0
  48. package/apps/client/src/components/PullDownIndicator/index.tsx +33 -0
  49. package/apps/client/src/components/QuickCommands/index.less +48 -0
  50. package/apps/client/src/components/QuickCommands/index.tsx +22 -0
  51. package/apps/client/src/components/ReasoningBlock/index.less +90 -0
  52. package/apps/client/src/components/ReasoningBlock/index.tsx +56 -0
  53. package/apps/client/src/components/ScrollToBottomButton/index.less +32 -0
  54. package/apps/client/src/components/ScrollToBottomButton/index.tsx +10 -0
  55. package/apps/client/src/components/ToolCallBlock/index.less +91 -0
  56. package/apps/client/src/components/ToolCallBlock/index.tsx +63 -0
  57. package/apps/client/src/examples/tourism/TourismLanding.tsx +96 -0
  58. package/apps/client/src/examples/tourism/index.less +192 -0
  59. package/apps/client/src/hooks/useMediaUpload.ts +237 -0
  60. package/apps/client/src/hooks/usePullDownHistory.ts +97 -0
  61. package/apps/client/src/hooks/useTTSAutoPlay.ts +80 -0
  62. package/apps/client/src/main.tsx +17 -0
  63. package/apps/client/src/modules.ts +30 -0
  64. package/apps/client/src/pages/index/index.less +43 -0
  65. package/apps/client/src/pages/index/index.tsx +145 -0
  66. package/apps/client/src/platform/AgentChatApp.tsx +39 -0
  67. package/apps/client/src/platform/agent-runtime.ts +15 -0
  68. package/apps/client/src/platform/state/createPersistedStore.ts +19 -0
  69. package/apps/client/src/shims/node-stub.ts +14 -0
  70. package/apps/client/src/store/chat-config.ts +29 -0
  71. package/apps/client/src/store/chat.ts +201 -0
  72. package/apps/client/src/styles/global.less +60 -0
  73. package/apps/client/src/styles/tokens.less +60 -0
  74. package/apps/client/src/types/chat.ts +29 -0
  75. package/apps/client/src/utils/copyText.ts +11 -0
  76. package/apps/client/src/utils/imagePreview.ts +10 -0
  77. package/apps/client/src/utils/session-error.ts +5 -0
  78. package/apps/client/src/utils/toast.ts +15 -0
  79. package/apps/client/src/vite-env.d.ts +3 -0
  80. package/apps/client/tsconfig.json +11 -0
  81. package/apps/client/vite.config.ts +40 -0
  82. package/apps/server/package.json +40 -0
  83. package/apps/server/src/cli/collect-registry.ts +26 -0
  84. package/apps/server/src/cli/plugin-doctor.ts +89 -0
  85. package/apps/server/src/cli/plugin-inspect.ts +49 -0
  86. package/apps/server/src/index.ts +230 -0
  87. package/apps/server/src/intent-routes.ts +23 -0
  88. package/apps/server/src/modules.ts +31 -0
  89. package/apps/server/src/tools/translate.ts +41 -0
  90. package/apps/server/tests/cards-registry.test.ts +55 -0
  91. package/apps/server/tests/tool-runtime.test.ts +49 -0
  92. package/apps/server/tsconfig.json +5 -0
  93. package/apps/server/tsup.config.ts +25 -0
  94. package/apps/server/vitest.config.ts +8 -0
  95. package/config/chat.json +8 -0
  96. package/config/llm_provider.json +3 -0
  97. package/config/plugins.json +3 -0
  98. package/eslint.config.mjs +41 -0
  99. package/package.json +51 -0
  100. package/packages/module-weather/package.json +35 -0
  101. package/packages/module-weather/src/client/cards/weather/index.less +22 -0
  102. package/packages/module-weather/src/client/cards/weather/index.tsx +14 -0
  103. package/packages/module-weather/src/client/index.ts +19 -0
  104. package/packages/module-weather/src/index.ts +2 -0
  105. package/packages/module-weather/src/server/cards/weather/meta.ts +28 -0
  106. package/packages/module-weather/src/server/index.ts +19 -0
  107. package/packages/module-weather/src/server/tool.ts +29 -0
  108. package/packages/module-weather/tbox.component.json +27 -0
  109. package/packages/module-weather/tsconfig.json +14 -0
  110. package/pnpm-workspace.template.yaml +60 -0
  111. package/scripts/build.sh +39 -0
  112. package/scripts/check-inspector.sh +91 -0
  113. package/scripts/check-workspace.mjs +164 -0
  114. package/scripts/dev.sh +15 -0
  115. package/scripts/prepare.sh +11 -0
  116. package/scripts/start.sh +11 -0
  117. package/scripts/stop.sh +26 -0
  118. package/scripts/verify-generated.mjs +142 -0
  119. package/tsconfig.base.json +20 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 平台运行时装配(client 平台轴,已下沉 @tbox.cn/app-sdk/client)。
3
+ * 模板唯一的 SDK 单例持有点:authClient(鉴权)+ platformApi(能力凭证/会话/历史)。
4
+ * 环境值(VITE_API_URL / VITE_AUTH_PROVIDER)由 main.tsx 经 configureRuntime / 选项注入(H1)。
5
+ */
6
+ import { createAuthClient, createPlatformApi } from '@tbox.cn/app-sdk/client';
7
+
8
+ /** VITE_AUTH_PROVIDER=external 时壳内换码登录转发外部(服务端需同步配置 AUTH_EXTERNAL_*) */
9
+ const AUTH_PROVIDER = import.meta.env.VITE_AUTH_PROVIDER as string | undefined;
10
+
11
+ export const authClient = createAuthClient({
12
+ ...(AUTH_PROVIDER === 'external' ? { loginPlatform: 'external' } : {}),
13
+ });
14
+
15
+ export const platformApi = createPlatformApi();
@@ -0,0 +1,19 @@
1
+ import { create, type StateCreator } from 'zustand';
2
+ import { createJSONStorage, persist, type PersistOptions } from 'zustand/middleware';
3
+
4
+ interface PersistedStoreOptions<T> extends Omit<PersistOptions<T>, 'name' | 'storage'> {
5
+ name: string;
6
+ }
7
+
8
+ /** 创建使用 localStorage 的业务 store,并统一保留 Zustand 的版本迁移能力。 */
9
+ export function createPersistedStore<T>(
10
+ stateCreator: StateCreator<T, [], []>,
11
+ options: PersistedStoreOptions<T>,
12
+ ) {
13
+ return create<T>()(
14
+ persist(stateCreator, {
15
+ ...options,
16
+ storage: createJSONStorage(() => localStorage),
17
+ }),
18
+ );
19
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 浏览器端 fs / path 桩。
3
+ *
4
+ * @tbox-dev-js/sdk 的 ESM 顶层静态 import 了 Node 的 fs/path(用于 Node 环境的代码路径,
5
+ * 浏览器不会执行到),但静态 import 会让 vite 生产构建因"命名导出不存在"而失败。
6
+ * 用本桩 alias 掉 fs/path,提供同名空实现即可(浏览器永不调用这些路径)。
7
+ */
8
+ export const existsSync = (): boolean => false;
9
+ export const readFileSync = (): string => '';
10
+ export const resolve = (...p: string[]): string => p.join('/');
11
+ export const join = (...p: string[]): string => p.join('/');
12
+ export const dirname = (p = ''): string => p.split('/').slice(0, -1).join('/');
13
+
14
+ export default { existsSync, readFileSync, resolve, join, dirname };
@@ -0,0 +1,29 @@
1
+ import { create } from 'zustand';
2
+ import chatConfigJson from '../../../../config/chat.json';
3
+
4
+ export interface QuickCommand {
5
+ /** 纯文案;前导星标/图标由 QuickCommands 组件渲染,不要写进 label */
6
+ label: string;
7
+ query: string;
8
+ icon?: string;
9
+ }
10
+
11
+ interface ChatConfigStore {
12
+ enableImageUpload: boolean;
13
+ enableVideoUpload: boolean;
14
+ enableTTS: boolean;
15
+ enableVoiceInput: boolean;
16
+ defaultInputMode: 'TEXT' | 'VOICE';
17
+ quickCommands: QuickCommand[];
18
+ }
19
+
20
+ const cfg = chatConfigJson as Record<string, unknown>;
21
+
22
+ export const useChatConfigStore = create<ChatConfigStore>()(() => ({
23
+ enableImageUpload: (cfg.enableImageUpload as boolean) ?? true,
24
+ enableVideoUpload: (cfg.enableVideoUpload as boolean) ?? true,
25
+ enableTTS: (cfg.enableTTS as boolean) ?? true,
26
+ enableVoiceInput: (cfg.enableVoiceInput as boolean) ?? true,
27
+ defaultInputMode: (cfg.defaultInputMode as 'TEXT' | 'VOICE') ?? 'TEXT',
28
+ quickCommands: (cfg.quickCommands as QuickCommand[]) ?? [],
29
+ }));
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 对话状态(Zustand)。
3
+ * 使用 createCoreChatState 在同一个 create() 中合并核心状态与业务方法,
4
+ * 共用同一份 set/get,避免双 store 断裂。
5
+ */
6
+ import { create } from 'zustand';
7
+ import { createCoreChatState, getWsUrl } from '@tbox.cn/app-sdk/client';
8
+ import type { CoreChatState } from '@tbox.cn/app-sdk/client';
9
+ import type { CardSnapshot, TboxCardPayload } from '@tbox.cn/app-contracts';
10
+ import type { ChatMessage } from '@/types/chat';
11
+ import type { IFeedbackAdapter } from '@tbox.cn/app-sdk/client';
12
+ import type { AdapterContextValue } from '@tbox.cn/app-sdk/client';
13
+ import { isSessionExpiredError } from '@/utils/session-error';
14
+ import { toast } from '@/utils/toast';
15
+ import { authClient, platformApi } from '@/platform/agent-runtime';
16
+
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
+ }
66
+
67
+ interface ChatState extends CoreChatState {
68
+ historyMessages: ChatMessage[];
69
+ historyHasMore: boolean;
70
+ isLoadingHistory: boolean;
71
+ initConversation: () => Promise<void>;
72
+ loadMoreHistory: () => Promise<void>;
73
+ submitFeedback: (
74
+ messageId: string,
75
+ type: 'like' | 'dislike' | 'cancel',
76
+ adapter: IFeedbackAdapter,
77
+ refreshSession?: () => Promise<AdapterContextValue>,
78
+ ) => Promise<void>;
79
+ }
80
+
81
+ export const useChatStore = create<ChatState>((set, get) => ({
82
+ // 核心状态:所有 set/get 闭包绑定到此 store(006:不再创建客户端 sessionId)
83
+ ...createCoreChatState(
84
+ { getWsUrl: () => getWsUrl(), toast },
85
+ set as (partial: any) => void,
86
+ get as () => any,
87
+ ),
88
+ // 业务扩展状态
89
+ historyMessages: [],
90
+ historyHasMore: true,
91
+ isLoadingHistory: false,
92
+
93
+ initConversation: async () => {
94
+ const userId = await authClient.resolveUserId();
95
+ set({ userId });
96
+ try {
97
+ let conversationId: string | null = null;
98
+ // 006:create 的 userId 只来自 token 身份(body 废弃);匿名(无 token)不可建会话 → 直接走 no-conv 分支
99
+ if (!authClient.getAuthState()?.token) {
100
+ console.warn('[CONVERSATION] anonymous: skip create conversation (no-conv)');
101
+ } else {
102
+ for (let i = 0; i < 2 && !conversationId; i++) {
103
+ try {
104
+ const { status, data } = await platformApi.createConversation();
105
+ if (status === 200 && data?.conversationId) conversationId = data.conversationId;
106
+ } catch {
107
+ // retry
108
+ }
109
+ }
110
+ }
111
+ if (conversationId) set({ conversationId });
112
+ get().init();
113
+ } catch (err) {
114
+ console.error('[CONVERSATION] init error:', err);
115
+ get().init();
116
+ }
117
+ },
118
+
119
+ loadMoreHistory: async () => {
120
+ const { isLoadingHistory, historyHasMore, historyMessages } = get();
121
+ if (isLoadingHistory || !historyHasMore) return;
122
+ set({ isLoadingHistory: true });
123
+
124
+ const pageSize = 5;
125
+ const earliestRequestId =
126
+ historyMessages.length > 0
127
+ ? historyMessages[0].requestId
128
+ : get().messages.find((m) => m.requestId)?.requestId;
129
+
130
+ // 006:有 token 身份时服务端按 token 取归属;无 token(匿名/兼容)保留 query userId
131
+ try {
132
+ const { status, data } = await platformApi.listMessages({
133
+ pageSize,
134
+ ...(authClient.getAuthState()?.token
135
+ ? {}
136
+ : { userId: get().userId }),
137
+ ...(earliestRequestId ? { beforeId: earliestRequestId } : {}),
138
+ });
139
+ if (status === 200 && data) {
140
+ const rawList: HistoryMessageInfo[] = (data.data as HistoryMessageInfo[]) || [];
141
+ const currentRequestIds = new Set(get().messages.map((m) => m.requestId).filter(Boolean));
142
+ const filtered = rawList.filter(
143
+ (item) => !item.outerBusinessId || !currentRequestIds.has(item.outerBusinessId),
144
+ );
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
+ }
155
+ set((s) => ({
156
+ historyMessages: [...newMessages, ...s.historyMessages],
157
+ historyHasMore: rawList.length >= pageSize,
158
+ isLoadingHistory: false,
159
+ cardsByMessageId: { ...s.cardsByMessageId, ...cardsByMessageId },
160
+ }));
161
+ } else {
162
+ set({ isLoadingHistory: false });
163
+ }
164
+ } catch (err) {
165
+ console.error('[HISTORY] load more error:', err);
166
+ set({ isLoadingHistory: false });
167
+ }
168
+ },
169
+
170
+ submitFeedback: async (messageId, type, adapter, refreshSession) => {
171
+ const oldMsg = get().messages.find((m) => m.id === messageId);
172
+ const oldFeedbackState = oldMsg?.feedbackState ?? 'none';
173
+ const feedbackRequestId = oldMsg?.requestId;
174
+ if (!feedbackRequestId) {
175
+ console.warn('[FEEDBACK] no requestId for message', messageId);
176
+ return;
177
+ }
178
+ const newFeedbackState = type === 'cancel' ? 'none' : type;
179
+ get().updateMessage(messageId, { feedbackState: newFeedbackState as 'none' | 'like' | 'dislike' });
180
+
181
+ const feedbackMap = { like: 'LIKE' as const, dislike: 'DISLIKE' as const, cancel: 'CANCEL' as const };
182
+ try {
183
+ const result = await adapter.addTag(feedbackRequestId, feedbackMap[type]);
184
+ if (!result.success) get().updateMessage(messageId, { feedbackState: oldFeedbackState });
185
+ } catch (err) {
186
+ if (isSessionExpiredError(err) && refreshSession) {
187
+ try {
188
+ const newAdapters = await refreshSession();
189
+ if (newAdapters.feedback) {
190
+ const retry = await newAdapters.feedback.addTag(feedbackRequestId, feedbackMap[type]);
191
+ if (!retry.success) get().updateMessage(messageId, { feedbackState: oldFeedbackState });
192
+ return;
193
+ }
194
+ } catch (retryErr) {
195
+ console.error('[FEEDBACK] retry failed:', retryErr);
196
+ }
197
+ }
198
+ get().updateMessage(messageId, { feedbackState: oldFeedbackState });
199
+ }
200
+ },
201
+ }));
@@ -0,0 +1,60 @@
1
+ @import './tokens.less';
2
+
3
+ * {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ html,
8
+ body,
9
+ #root {
10
+ margin: 0;
11
+ padding: 0;
12
+ height: 100%;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
17
+ font-size: var(--tbox-fs-body);
18
+ color: var(--tbox-text-primary);
19
+ background: var(--tbox-bg-subtle);
20
+ -webkit-font-smoothing: antialiased;
21
+ }
22
+
23
+ /* 顶层容器全宽(渐变底铺满),内容区由各处 max-width:800px 居中(对齐原模板 #app)。 */
24
+ #root {
25
+ width: 100%;
26
+ position: relative;
27
+ }
28
+
29
+ /* 去掉移动端点击高亮 */
30
+ * {
31
+ -webkit-tap-highlight-color: transparent;
32
+ }
33
+
34
+ /* 卡片统一外层 = 卡片「卡框」。所有卡片共有的框在这里,单卡组件别重复写:
35
+ - 间距:卡片与上下元素(工具调用 / 气泡)的间距,与 .tool-calls 一致;
36
+ - 外观:默认背景(白) / 圆角 / 阴影;`overflow:hidden` 把内容裁进圆角。
37
+ 值为源码 px(2x 基线、折半渲染):圆角 40≈20、阴影 0 4px 16px≈0 2px 8px。
38
+ ⚠️ **内边距不在卡框里**——由卡片自己写。卡框给 padding 会让「自带背景色的卡」在外面露一圈白边(卡里套卡)。
39
+ 宽度也不在卡框里:默认占满整列(≈移动端 350~400px 设计宽),紧凑卡自己写 max-width。
40
+ 单卡组件(.tbox-<type>)只画内容;**要整卡换底色 / 出血图,直接在自己 root 上 `background:<色>` 或铺满图**——
41
+ 会铺满卡框、裁进圆角、不出白边(别再自己设圆角,卡框已裁)。不写背景则露卡框默认白底,不透明。 */
42
+ .tbox-card {
43
+ margin-bottom: 12px;
44
+ background: var(--tbox-bg-card);
45
+ border-radius: 40px;
46
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
47
+ overflow: hidden;
48
+ }
49
+
50
+ /* 兜底态(未知 cardType / 渲染出错,由 CardRenderer 给外层加此修饰类):
51
+ 复用卡框,弱化成无阴影的虚线提示框(内边距自带,因为卡框不再给 padding) */
52
+ .tbox-card--fallback {
53
+ max-width: 80%;
54
+ padding: 32px 24px;
55
+ background: var(--tbox-bg-subtle);
56
+ border: 1px dashed var(--tbox-border);
57
+ box-shadow: none;
58
+ color: var(--tbox-text-muted);
59
+ font-size: var(--tbox-fs-caption);
60
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 设计 token —— 从原 server `cards/_shared/tokens.css` 搬家到 client。
3
+ *
4
+ * C 方案下卡片是 client React 组件、不再有 iframe 下发 tokens,
5
+ * 所以这些设计变量必须落在 client。卡片组件与页面统一用 var(--tbox-*) 引用,
6
+ * 禁止硬编码十六进制色值 / px 字号。
7
+ *
8
+ * 注:这里的 px 会被 postcss-px-to-viewport(750 基准)转成 vw。
9
+ */
10
+ :root {
11
+ /* 主色 */
12
+ --tbox-primary: #1677ff;
13
+ --tbox-primary-dark: #0958d9;
14
+ --tbox-primary-light: #e6f4ff;
15
+
16
+ /* 文本 */
17
+ --tbox-text-primary: #1f1f1f;
18
+ --tbox-text-secondary: #595959;
19
+ --tbox-text-muted: #8c8c8c;
20
+
21
+ /* 背景 */
22
+ --tbox-bg-card: #ffffff;
23
+ --tbox-bg-subtle: #f5f7fa;
24
+
25
+ /* 边框 */
26
+ --tbox-border: #e8ebed;
27
+
28
+ /* 状态色 */
29
+ --tbox-success: #52c41a;
30
+ --tbox-warning: #faad14;
31
+ --tbox-error: #ff4d4f;
32
+
33
+ /* 字号——按 750/2x 设计稿基线,对标消息气泡(MessageBubble):
34
+ 正文 .msg-markdown / __text = 25px,次要信息(时间/角色) = 18~19px,标题 h1~h4 = 36/32/30/28px。
35
+ 注意:rootValue 100 + 根字号脚本会把 px 折半渲染(750 稿 → ≈375 手机列),
36
+ 所以这里写的是 2x 值,别按普通 web 的 12/14/16 写(那样渲染出来只有一半、发虚)。
37
+ 价格等强调大号字单独硬编(~32,≈气泡 h2),不挤进这 3 档。 */
38
+ --tbox-fs-heading: 30px; /* ≈气泡 h3:商品名 / 店名 */
39
+ --tbox-fs-body: 25px; /* =气泡正文 */
40
+ --tbox-fs-caption: 20px; /* ≈气泡次要信息(时间/角色) */
41
+
42
+ /* 圆角 */
43
+ --tbox-radius-sm: 4px;
44
+ --tbox-radius-md: 8px;
45
+
46
+ /* 内容最大宽度(对齐原模板):全宽渐变底 + 内容区 800px 居中。
47
+ 大写 PX 不被 postcss-pxtorem 转换(正则大小写敏感),保持固定像素。 */
48
+ --tbox-content-max-width: 800PX;
49
+
50
+ /* 媒体缩略图 */
51
+ --tbox-media-item-size: 174px;
52
+ --tbox-media-item-radius: 12px;
53
+
54
+ /* 输入框(对齐原模板 AdvancedChatInput) */
55
+ --tbox-input-bg: rgba(255, 255, 255, 0.7);
56
+ --tbox-input-radius: 50px;
57
+ --tbox-input-padding: 20px 24px;
58
+ --tbox-send-btn-size: 44px;
59
+ --tbox-icon-color: #4b6282;
60
+ }
@@ -0,0 +1,29 @@
1
+ /** 媒体 / 消息类型(协议类型已下沉 @tbox.cn/app-sdk/client,此处 re-export 保持单一来源) */
2
+ export type {
3
+ MediaType,
4
+ InputMethod,
5
+ MediaItem,
6
+ ChatInputMessage,
7
+ MessageRole,
8
+ MessageStatus,
9
+ ToolCallInfo,
10
+ ChatMessage,
11
+ } from '@tbox.cn/app-sdk/client';
12
+
13
+ /** 单种媒体的上传约束(对齐原模板 FileTypeConfig,UI 层约定,留在模板) */
14
+ export interface FileTypeConfig {
15
+ /** 该类型最多数量 */
16
+ maxCount: number;
17
+ /** 单文件最大字节数 */
18
+ maxSize: number;
19
+ /** 允许的扩展名/类型(仅约定,校验以 maxCount/maxSize 为主) */
20
+ accept?: string[];
21
+ }
22
+
23
+ /** 媒体上传分类型配置(不配的类型只受总数 maxTotal 约束) */
24
+ export interface MediaConfig {
25
+ image?: FileTypeConfig;
26
+ video?: FileTypeConfig;
27
+ audio?: FileTypeConfig;
28
+ file?: FileTypeConfig;
29
+ }
@@ -0,0 +1,11 @@
1
+ /** 复制消息文本;平台预览 iframe 需开放 clipboard-write 权限。 */
2
+ export async function copyText(text: string): Promise<boolean> {
3
+ if (!text || !navigator.clipboard?.writeText) return false;
4
+
5
+ try {
6
+ await navigator.clipboard.writeText(text);
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 图片预览(懒加载 antd-mobile 的 ImageViewer)。
3
+ * ImageViewer 带捏合/缩放/手势依赖较重——用动态 import 切成单独 chunk,
4
+ * 仅在用户首次点开图片时加载,不占首屏体积。
5
+ */
6
+ export async function previewImages(images: string[], defaultIndex = 0): Promise<void> {
7
+ if (!images.length) return;
8
+ const { ImageViewer } = await import('antd-mobile');
9
+ ImageViewer.Multi.show({ images, defaultIndex: Math.max(0, defaultIndex) });
10
+ }
@@ -0,0 +1,5 @@
1
+ /** 判断错误是否为 session 过期(保守策略:宁可多重试一次)。 */
2
+ export function isSessionExpiredError(err: unknown): boolean {
3
+ const msg = String((err as Error)?.message || err || '');
4
+ return /过期|expired|PARAM_INVALID|unauthorized|token/i.test(msg);
5
+ }
@@ -0,0 +1,15 @@
1
+ /** 提示语气(对齐原模板 TboxCard.toast 的 tone):info 无图标、success/error 带图标 */
2
+ export type ToastTone = 'info' | 'success' | 'error';
3
+
4
+ /**
5
+ * 轻提示(懒加载 antd-mobile 的 Toast)。
6
+ * 与 imagePreview 共用同一个 antd-mobile 动态 chunk——首次弹提示/看大图时才加载,
7
+ * 不占首屏体积。错误反馈等延迟一帧无感。
8
+ *
9
+ * @param tone 语气:success → 成功图标,error → 失败图标,info/省略 → 无图标(对齐旧模板映射)
10
+ */
11
+ export async function toast(content: string, tone: ToastTone = 'info'): Promise<void> {
12
+ const { Toast } = await import('antd-mobile');
13
+ const icon = tone === 'success' ? 'success' : tone === 'error' ? 'fail' : undefined;
14
+ Toast.show(icon ? { content, icon } : { content });
15
+ }
@@ -0,0 +1,3 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module 'postcss-pxtorem';
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "jsx": "react-jsx",
6
+ "paths": {
7
+ "@/*": ["src/*"]
8
+ }
9
+ },
10
+ "include": ["src", "vite.config.ts"]
11
+ }
@@ -0,0 +1,40 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import pxToRem from 'postcss-pxtorem';
4
+ 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';
9
+
10
+ const pxToRemPlugin = pxToRem({
11
+ rootValue: 100,
12
+ unitPrecision: 5,
13
+ propList: ['*'],
14
+ selectorBlackList: ['.no-rem'],
15
+ minPixelValue: 1,
16
+ mediaQuery: false,
17
+ exclude: /node_modules/,
18
+ });
19
+
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'),
31
+ },
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
+ }));
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@app/server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch --exclude ../../apps/client src/index.ts",
8
+ "build": "tsup",
9
+ "start": "node dist/index.js",
10
+ "test": "vitest run",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@app/module-weather": "workspace:*",
15
+ "@ag-ui/core": "catalog:",
16
+ "@tbox.cn/agent-plugin": "catalog:",
17
+ "@tbox.cn/app-sdk": "catalog:",
18
+ "@tbox.cn/app-contracts": "catalog:",
19
+ "@tbox-dev-js/sdk": "catalog:",
20
+ "ai": "catalog:",
21
+ "cors": "catalog:",
22
+ "express": "catalog:",
23
+ "ws": "catalog:",
24
+ "zod": "catalog:"
25
+ },
26
+ "peerDependencies": {
27
+ "vite": "catalog:"
28
+ },
29
+ "devDependencies": {
30
+ "@types/cors": "catalog:",
31
+ "@types/express": "catalog:",
32
+ "@types/node": "catalog:",
33
+ "@types/ws": "catalog:",
34
+ "tsx": "catalog:",
35
+ "tsup": "catalog:",
36
+ "typescript": "catalog:",
37
+ "vite": "catalog:",
38
+ "vitest": "catalog:"
39
+ }
40
+ }
@@ -0,0 +1,26 @@
1
+ import { createServerContext, buildToolCardBindings } from '@tbox.cn/app-sdk/server';
2
+ import { registerAll } from '../modules.js';
3
+ import { translateTool } from '../tools/translate.js';
4
+
5
+ export interface RegistryState {
6
+ staticToolIds: string[];
7
+ cardTypes: string[];
8
+ bindings: ReturnType<typeof buildToolCardBindings>;
9
+ }
10
+
11
+ /**
12
+ * 从自注册闭环收集当前注册状态(静态工具 id / cardType / tool→card 绑定)。
13
+ * 供 plugin-doctor / plugin-inspect 复用——替代已删除的手写 tools/registry.ts + cards/registry.ts。
14
+ * 与运行时(apps/server/src/index.ts start())保持一致的注册面:registerAll + 模板演示工具 translate。
15
+ */
16
+ export async function collectRegistryState(): Promise<RegistryState> {
17
+ const ctx = createServerContext();
18
+ await registerAll(ctx);
19
+ ctx.tools.register(translateTool); // 与运行时一致(index.ts 中 translate 为 apps 级演示工具)
20
+ const cards = ctx.cards.getAll();
21
+ return {
22
+ staticToolIds: Object.keys(ctx.tools.getAll()),
23
+ cardTypes: Object.keys(cards),
24
+ bindings: buildToolCardBindings(cards),
25
+ };
26
+ }