@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,89 @@
1
+ import {
2
+ diagnosePlugins,
3
+ type PluginDiagnostic,
4
+ } from '@tbox.cn/agent-plugin/doctor';
5
+ import { loadMastraPluginTools } from '@tbox.cn/agent-plugin/mastra';
6
+ import { createPluginIntegration } from '@tbox.cn/app-sdk/server';
7
+
8
+ import { collectRegistryState } from './collect-registry.js';
9
+
10
+ const args = new Set(process.argv.slice(2));
11
+ const live = args.has('--live');
12
+ const json = args.has('--json');
13
+ const pluginIntegration = createPluginIntegration();
14
+ const config = pluginIntegration.readConfig();
15
+ const { staticToolIds, bindings, cardTypes } = await collectRegistryState();
16
+ let loadedPluginTools;
17
+ let liveLoadError: string | undefined;
18
+ const runtimeMessages: string[] = [];
19
+
20
+ if (live && config.plugins.length > 0 && process.env.TBOX_API_KEY) {
21
+ const originalLog = console.log;
22
+ const originalWarn = console.warn;
23
+ if (json) {
24
+ const capture = (...values: unknown[]) => {
25
+ runtimeMessages.push(values.map(String).join(' '));
26
+ };
27
+ console.log = capture;
28
+ console.warn = capture;
29
+ }
30
+ try {
31
+ loadedPluginTools = (
32
+ await loadMastraPluginTools(pluginIntegration.integration, {
33
+ reservedToolIds: staticToolIds,
34
+ })
35
+ ).references;
36
+ } catch (error) {
37
+ liveLoadError = error instanceof Error ? error.message : String(error);
38
+ } finally {
39
+ console.log = originalLog;
40
+ console.warn = originalWarn;
41
+ }
42
+ }
43
+
44
+ let diagnostics: PluginDiagnostic[] = diagnosePlugins({
45
+ config,
46
+ bindings,
47
+ staticToolIds,
48
+ cardTypes,
49
+ apiKeyConfigured: Boolean(process.env.TBOX_API_KEY),
50
+ loadedPluginTools,
51
+ });
52
+
53
+ if (live && config.plugins.length > 0 && !process.env.TBOX_API_KEY) {
54
+ diagnostics = [
55
+ ...diagnostics,
56
+ {
57
+ level: 'error',
58
+ code: 'LIVE_CHECK_REQUIRES_API_KEY',
59
+ message: '--live 检查需要 TBOX_API_KEY',
60
+ },
61
+ ];
62
+ }
63
+
64
+ if (liveLoadError) {
65
+ diagnostics = [
66
+ ...diagnostics,
67
+ {
68
+ level: 'error',
69
+ code: 'LIVE_CHECK_FAILED',
70
+ message: liveLoadError,
71
+ },
72
+ ];
73
+ }
74
+
75
+ if (json) {
76
+ console.log(JSON.stringify({ live, diagnostics, runtimeMessages }, null, 2));
77
+ } else if (diagnostics.length === 0) {
78
+ console.log(`[plugin-doctor] ${live ? 'live' : 'offline'} 检查通过`);
79
+ } else {
80
+ diagnostics.forEach((diagnostic) => {
81
+ const output = `[plugin-doctor] ${diagnostic.level.toUpperCase()} ${diagnostic.code}: ${diagnostic.message}`;
82
+ if (diagnostic.level === 'error') console.error(output);
83
+ else console.warn(output);
84
+ });
85
+ }
86
+
87
+ if (diagnostics.some((diagnostic) => diagnostic.level === 'error')) {
88
+ process.exitCode = 1;
89
+ }
@@ -0,0 +1,49 @@
1
+ import { inspectPluginTool } from '@tbox.cn/agent-plugin/inspect';
2
+ // 豁免:CLI 诊断工具(plugin:inspect)直接调 @tbox-dev-js/sdk 的 TboxPluginClient 做运行时检查,
3
+ // 非应用运行时平台能力路径(007 铁律针对平台能力调用;诊断工具保持直连)
4
+ import { TboxPluginClient } from '@tbox-dev-js/sdk';
5
+ import { createPluginIntegration } from '@tbox.cn/app-sdk/server';
6
+
7
+ import { collectRegistryState } from './collect-registry.js';
8
+
9
+ const args = process.argv.slice(2).filter((arg) => arg !== '--json');
10
+ const [pluginId, toolId] = args;
11
+
12
+ if (!pluginId || !toolId) {
13
+ console.error('用法: pnpm plugin:inspect <pluginId> <toolId>');
14
+ process.exitCode = 1;
15
+ } else if (!process.env.TBOX_API_KEY) {
16
+ console.error('[plugin-inspect] 缺少 TBOX_API_KEY');
17
+ process.exitCode = 1;
18
+ } else {
19
+ const { readConfig } = createPluginIntegration();
20
+ const config = readConfig();
21
+ const client = new TboxPluginClient({ apiKey: process.env.TBOX_API_KEY });
22
+ try {
23
+ const { staticToolIds } = await collectRegistryState();
24
+ const inspection = await inspectPluginTool({
25
+ reference: { pluginId, toolId },
26
+ client,
27
+ config,
28
+ reservedToolIds: staticToolIds,
29
+ });
30
+ console.log(
31
+ JSON.stringify(
32
+ {
33
+ ...inspection,
34
+ note:
35
+ 'runtimeId 已考虑内置工具冲突;完整插件间冲突请运行 plugin:doctor --live',
36
+ },
37
+ null,
38
+ 2,
39
+ ),
40
+ );
41
+ } catch (error) {
42
+ console.error(
43
+ `[plugin-inspect] ${
44
+ error instanceof Error ? error.message : String(error)
45
+ }`,
46
+ );
47
+ process.exitCode = 1;
48
+ }
49
+ }
@@ -0,0 +1,230 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import { createServer } from 'http';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { registerAll } from './modules.js';
7
+ import {
8
+ createWsServer,
9
+ createServerContext,
10
+ createEngineFromContext,
11
+ tboxTraceExporter,
12
+ extractImageUrls,
13
+ TBOX_FILE_URL_PREFIX,
14
+ createAuthRuntime,
15
+ createPlatformApiRouter,
16
+ createTtsProxy,
17
+ createModelProvider,
18
+ createPluginIntegration,
19
+ ConversationService,
20
+ } from '@tbox.cn/app-sdk/server';
21
+ import { installInspector, injectScript, isReady } from '@tbox.cn/app-sdk/platform';
22
+ import { translateTool } from './tools/translate.js';
23
+ import { INTENT_ROUTES, createAppIntentDispatcher } from './intent-routes.js';
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+ const PORT = Number(process.env.PORT) || 8000;
27
+ const HOST = process.env.HOST || '0.0.0.0';
28
+ const isProduction = process.env.NODE_ENV === 'production';
29
+
30
+ const AI_MODEL_NAME = process.env.APP_AI_MODEL_NAME || '';
31
+
32
+ const AGENT_CONFIG = {
33
+ instructions: [
34
+ '你是一个通用 AI 助手。可以回答用户问题,也可以调用工具获取信息。用简洁、清晰的语言回复。',
35
+ '调用业务工具时正常按其 schema 传参,拿到结果后用自然语言回复用户;',
36
+ '是否向用户呈现卡片由系统自动决定,你不需要也不应该主动渲染 UI。',
37
+ ].join('\n'),
38
+ };
39
+
40
+ // 平台能力装配(003/006 平台轴,已下沉 @tbox.cn/app-sdk/server)
41
+ const auth = createAuthRuntime();
42
+ const { getModel } = createModelProvider();
43
+ const pluginTools = createPluginIntegration();
44
+
45
+ /** 懒初始化单例 ConversationService */
46
+ let _conversationService: ConversationService | null = null;
47
+ function getConversationService(): ConversationService | null {
48
+ if (!_conversationService) {
49
+ const apiKey = process.env.TBOX_API_KEY;
50
+ if (!apiKey) return null;
51
+ _conversationService = new ConversationService(apiKey);
52
+ }
53
+ return _conversationService;
54
+ }
55
+
56
+ // 进程级最后防线
57
+ process.on('unhandledRejection', (reason) => {
58
+ console.error('[server] unhandledRejection:', reason);
59
+ });
60
+ process.on('uncaughtException', (err) => {
61
+ console.error('[server] uncaughtException:', err);
62
+ });
63
+
64
+ const app = express();
65
+ app.use(cors());
66
+ app.use(express.json());
67
+
68
+ // 🔒 平台基础设施 — 禁止 AI 删除或修改(@infrastructure-locked)
69
+ installInspector(app);
70
+
71
+ // 非 prod 默认开启 dev-login:对暴露公网的实例给出可见警示(生产必须 NODE_ENV=production)
72
+ if (auth.config.devLoginEnabled) {
73
+ console.warn(
74
+ '[AUTH] dev-login 已开启(非 production 默认)。若本实例暴露公网,请设 NODE_ENV=production 或 AUTH_DEV_LOGIN_DISABLED=true。',
75
+ );
76
+ }
77
+
78
+ // 自注册闭环:模块路由挂载到 /api/m/<moduleId>/(在 apiRouter 之前,避免被 /api 兜底 404 截获)
79
+ // ctx 为模块级单例:registerAll 在 start() 内执行,ApiRouteRegistry 共享 root 保证延迟注册即时生效
80
+ const ctx = createServerContext();
81
+
82
+ // 全局 /api 鉴权(H2 挂载顺序契约):除 public(login/dev-login/health)外一律要求 Bearer token;
83
+ // optional 模式缺失 token → 匿名身份(写通道仍被 action gateway 拒绝)
84
+ // ⚠️ 必须先于 /api/m、/api/action、createPlatformApiRouter 挂载,保证全 API 面受鉴权约束
85
+ app.use('/api', auth.middleware);
86
+
87
+ app.use('/api/m', ctx.routes.mount());
88
+
89
+ // 业务动作网关(004 D3/C2):POST /api/action — SDK 黑盒唯一写入口(幂等/防重放/签名槽位;
90
+ // 006 D11:匿名写拒绝,不随 AUTH_MODE 变)
91
+ app.use('/api/action', ctx.actions.middleware());
92
+
93
+ // API 路由(平台路由面:auth/health/tbox/conversation + 404 兜底;卡片恢复内部化)
94
+ app.use('/api', createPlatformApiRouter({ ctx, auth }));
95
+
96
+ // 从 ServerContext 构造 ChatEngine(自注册闭环组装端)
97
+ async function createEngine() {
98
+ return createEngineFromContext(ctx, {
99
+ instructions: AGENT_CONFIG.instructions,
100
+ model: getModel(AI_MODEL_NAME),
101
+ traceExporter: tboxTraceExporter,
102
+ modelName: AI_MODEL_NAME,
103
+ fileUrlPrefix: TBOX_FILE_URL_PREFIX,
104
+ // B21:意图路由接线(业务意图表 + handlerDispatch)
105
+ intentRoutes: INTENT_ROUTES,
106
+ handlerDispatch: createAppIntentDispatcher(ctx),
107
+ loadPlugins: async () => {
108
+ const loaded = await pluginTools.loadTools({
109
+ reservedToolIds: Object.keys(ctx.tools.getAll()),
110
+ });
111
+ return { tools: loaded.tools, references: loaded.references };
112
+ },
113
+ saveMessage: async ({ conversationId, query, answer, mediaItems, outerBusinessId, cards }) => {
114
+ const agentId = process.env.TBOX_APP_ID;
115
+ const service = getConversationService();
116
+ if (!service || !agentId) return undefined;
117
+ const imageUrls = extractImageUrls(mediaItems, TBOX_FILE_URL_PREFIX);
118
+ return await service.saveMessage({
119
+ conversationId,
120
+ agentId,
121
+ query,
122
+ answer,
123
+ ...(outerBusinessId && { outerBusinessId }),
124
+ ...(imageUrls.length > 0 && {
125
+ multiModalInputs: { imageUrls },
126
+ }),
127
+ // P3 W1:卡片快照随消息持久化(MessageEmbedded,004 D1)。
128
+ // 若平台拒绝 inputs(V1 验证失败),此处切换为 extraParams 字符串或 FileCardPersistenceBackend 落盘。
129
+ ...(cards && cards.length > 0 && { inputs: { cards } }),
130
+ });
131
+ },
132
+ });
133
+ }
134
+
135
+ if (isProduction) {
136
+ const clientDistPath = path.join(__dirname, '../../client/dist');
137
+ app.use(express.static(clientDistPath));
138
+ app.get('*', (req, res) => {
139
+ if (!req.path.startsWith('/api')) {
140
+ res.sendFile(path.join(clientDistPath, 'index.html'));
141
+ } else {
142
+ res.status(404).json({ error: 'Not Found' });
143
+ }
144
+ });
145
+ } else {
146
+ // 开发环境:Vite middlewareMode 单进程
147
+ const { createServer: createViteServer } = await import('vite');
148
+ const { readFile } = await import('fs/promises');
149
+ const clientRoot = path.resolve(__dirname, '../../client');
150
+ const vite = await createViteServer({
151
+ root: clientRoot,
152
+ server: { middlewareMode: true },
153
+ // custom:不添加 Vite 自己的 SPA fallback / 404 处理器,
154
+ // 由下方 app.use('*') 统一处理 HTML 路由,避免 Vite 的 404 先返回。
155
+ appType: 'custom',
156
+ optimizeDeps: { entries: [] },
157
+ });
158
+
159
+ // Vite 中间件:处理静态资源、模块转换、HMR
160
+ app.use(vite.middlewares);
161
+
162
+ // HTML 路由兜底:非 /api、/ws 的请求一律返回 index.html(经 Vite 转换 + inspector 注入)
163
+ app.use('*', async (req, res, next) => {
164
+ if (req.originalUrl.startsWith('/api') || req.originalUrl.startsWith('/ws')) {
165
+ return next();
166
+ }
167
+ try {
168
+ const rawHtml = await readFile(path.join(clientRoot, 'index.html'), 'utf-8');
169
+ const template = await vite.transformIndexHtml(req.originalUrl, rawHtml);
170
+ const html = isReady() ? injectScript(template) : template;
171
+ res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
172
+ } catch (e) {
173
+ next(e);
174
+ }
175
+ });
176
+ }
177
+
178
+ const httpServer = createServer(app);
179
+
180
+ async function start() {
181
+ // 注册业务模块(自注册闭环)
182
+ await registerAll(ctx);
183
+
184
+ // 模板演示工具(apps 级工具,P3 建议迁入模块):不参与模块体系,直接注册
185
+ ctx.tools.register(translateTool);
186
+
187
+ const engine = await createEngine();
188
+
189
+ const chatWss = createWsServer(engine, { auth: auth.wsAuth });
190
+ const ttsWss = createTtsProxy();
191
+
192
+ httpServer.on('error', (err: NodeJS.ErrnoException) => {
193
+ if (err.code === 'EADDRINUSE') {
194
+ console.error(`[server] Port ${PORT} is already in use.`);
195
+ } else if (err.code === 'EACCES') {
196
+ console.error(`[server] Permission denied while binding ${HOST}:${PORT}.`);
197
+ } else {
198
+ console.error('[server] Failed to start HTTP server:', err);
199
+ }
200
+ process.exit(1);
201
+ });
202
+
203
+ httpServer.on('upgrade', (request, socket, head) => {
204
+ socket.on('error', (e) => console.warn('[upgrade] socket error:', e.message));
205
+ const { pathname } = new URL(request.url || '/', 'http://localhost');
206
+
207
+ if (pathname === '/ws') {
208
+ chatWss.handleUpgrade(request, socket, head, (ws) => {
209
+ chatWss.emit('connection', ws, request);
210
+ });
211
+ } else if (pathname === '/ws/tts') {
212
+ ttsWss.handleUpgrade(request, socket, head, (ws) => {
213
+ ttsWss.emit('connection', ws, request);
214
+ });
215
+ } else {
216
+ socket.destroy();
217
+ }
218
+ });
219
+
220
+ httpServer.listen(PORT, HOST, () => {
221
+ console.log(`🚀 Server running at http://${HOST}:${PORT}`);
222
+ console.log(`📡 WebSocket endpoint: ws://${HOST}:${PORT}/ws`);
223
+ console.log(`🔊 TTS endpoint: ws://${HOST}:${PORT}/ws/tts`);
224
+ });
225
+ }
226
+
227
+ start().catch((err) => {
228
+ console.error('[server] Failed to start:', err);
229
+ process.exit(1);
230
+ });
@@ -0,0 +1,23 @@
1
+ import type { IntentRoute, HandlerResult, ServerContext } from '@tbox.cn/app-sdk/server';
2
+
3
+ /**
4
+ * 业务意图表(B21:intent-framework hybrid 接线——SDK 框架 + 源码意图表,003 §3.3)。
5
+ * chat() 先 matchIntent(最长关键词命中);命中则经 handlerDispatch 出卡/文字,不走 LLM。
6
+ *
7
+ * 示例(商圈应用启用积分抵扣停车意图):
8
+ * { id: 'points-parking', keywords: ['积分抵扣停车', '停车抵扣'], handlerId: 'points-parking', action: 'run' }
9
+ * 本模板默认空表(保持 LLM + tool calling 主路径);业务方在此声明意图路由。
10
+ */
11
+ export const INTENT_ROUTES: IntentRoute[] = [];
12
+
13
+ /**
14
+ * 意图命中后的业务分发:经 ServerContext 的 HandlerRegistry.dispatch(组装端绑定 ctx)。
15
+ * 命名 createAppIntentDispatcher(区别于 SDK 导出的 createIntentDispatcher:后者先 matchIntent
16
+ * 再分发,本函数由 chat-engine 命中后直接分发)。
17
+ * P2-3:用户原始输入以 payload.content 传入,handler 侧可经 payload.content 取用。
18
+ */
19
+ export function createAppIntentDispatcher(ctx: ServerContext) {
20
+ return async (route: IntentRoute, content: string): Promise<HandlerResult | undefined> => {
21
+ return ctx.handlers.dispatch(route.handlerId, route.action, { content }, ctx);
22
+ };
23
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 服务端模块注册装配文件(与 apps/client/src/modules.ts 逐行同构)。
3
+ * tbox CLI 协助维护锚点区;你也可以直接编辑自由区。
4
+ *
5
+ * 锚点区:@tbox:imports-begin/end(模块 import 行)+ @tbox:register-begin/end(模块数组)
6
+ * CLI 行为:增量插入/删除模块行,保留非标行。
7
+ */
8
+ import type { ServerContext, InferModuleCards } from '@tbox.cn/app-sdk/server';
9
+
10
+ // @tbox:imports-begin
11
+ import { serverModule as moduleWeather } from '@app/module-weather/server';
12
+ // @tbox:imports-end
13
+
14
+ // @tbox:register-begin
15
+ const modules = [
16
+ moduleWeather,
17
+ ];
18
+ // @tbox:register-end
19
+
20
+ // 自由区:可做装配级定制——条件注册、顺序调整、包裹中间件
21
+ // const dynamicModule = process.env.ENABLE_X ? xModule : null;
22
+
23
+ /** 注册所有已安装模块(服务端装配——勿与 apps/client/src/modules.ts 混淆) */
24
+ export function registerAll(ctx: ServerContext): void {
25
+ for (const m of modules) {
26
+ m.register(ctx);
27
+ }
28
+ }
29
+
30
+ // 类型从运行时装配数组自动推导:增删模块后 CardType 自动收缩/扩展(002 §6.7)
31
+ export type CardType = InferModuleCards<(typeof modules)[number]>;
@@ -0,0 +1,41 @@
1
+ // 铁律:不直接 import @mastra/core,经 @tbox.cn/app-sdk/server 的 createTool 入口(单例铁律)
2
+ import { createTool } from '@tbox.cn/app-sdk/server';
3
+ import { z } from 'zod';
4
+
5
+ /**
6
+ * 翻译工具 — 中英文互译
7
+ * 由原 translator Skill 改造而来,现在作为 Tool 注册,LLM 自主调用
8
+ */
9
+ export const translateTool = createTool({
10
+ id: 'translate',
11
+ description:
12
+ '中英文互译工具。当用户需要翻译文本时调用此工具。自动检测源语言并翻译为目标语言:中文→英文,英文→中文。',
13
+ inputSchema: z.object({
14
+ text: z.string().describe('要翻译的文本'),
15
+ targetLang: z
16
+ .enum(['zh', 'en'])
17
+ .optional()
18
+ .describe('目标语言,不传则自动检测:中文输入翻译为英文,英文输入翻译为中文'),
19
+ }),
20
+ outputSchema: z.object({
21
+ original: z.string(),
22
+ translated: z.string(),
23
+ sourceLang: z.string(),
24
+ targetLang: z.string(),
25
+ }),
26
+ execute: async ({ text, targetLang }) => {
27
+ // 简单语言检测:包含中文字符则认为是中文
28
+ const isChinese = /[\u4e00-\u9fff]/.test(text);
29
+ const detectedTarget = targetLang || (isChinese ? 'en' : 'zh');
30
+ const sourceLang = isChinese ? 'zh' : 'en';
31
+
32
+ // 注意:这里返回指令让 LLM 来做实际翻译
33
+ // 实际生产环境可以调用翻译 API 或发起子 LLM 调用
34
+ return {
35
+ original: text,
36
+ translated: `[请将以下${sourceLang === 'zh' ? '中文' : '英文'}翻译为${detectedTarget === 'zh' ? '中文' : '英文'}]: ${text}`,
37
+ sourceLang,
38
+ targetLang: detectedTarget,
39
+ };
40
+ },
41
+ });
@@ -0,0 +1,55 @@
1
+ // 卡片 / 工具 registry 一致性守护测试(自注册闭环版)。
2
+ // 通过 registerAll(ctx) 收集真实注册状态,替代原手写 registry 静态断言。
3
+ // 注:C 方案下卡片渲染在 client(React 组件),server 无 template.html,故移除原 C12(模板占位符 ∈ schema)。
4
+ import { describe, it, expect } from 'vitest';
5
+ import { diagnosePlugins } from '@tbox.cn/agent-plugin/doctor';
6
+ import { createServerContext, buildToolCardBindings, createPluginIntegration } from '@tbox.cn/app-sdk/server';
7
+ import { registerAll } from '../src/modules.js';
8
+
9
+ describe('工具 / 卡片 registry 一致性(自注册闭环)', () => {
10
+ it('C2: ctx.tools 每个 key === tool.id', async () => {
11
+ const ctx = createServerContext();
12
+ await registerAll(ctx);
13
+ for (const [key, tool] of Object.entries(ctx.tools.getAll())) {
14
+ expect((tool as { id?: string }).id).toBe(key);
15
+ }
16
+ });
17
+
18
+ it('C3: 自动生成的 binding 的每个 cardType 都在 ctx.cards 注册', async () => {
19
+ const ctx = createServerContext();
20
+ await registerAll(ctx);
21
+ const cards = ctx.cards.getAll();
22
+ for (const binding of buildToolCardBindings(cards)) {
23
+ expect(Object.keys(cards)).toContain(binding.cardType);
24
+ }
25
+ });
26
+
27
+ it('C3: 工具卡片 binding 与静态工具、插件配置一致(不访问网络)', async () => {
28
+ const ctx = createServerContext();
29
+ await registerAll(ctx);
30
+ const cards = ctx.cards.getAll();
31
+ const diagnostics = diagnosePlugins({
32
+ config: createPluginIntegration().readConfig(),
33
+ bindings: buildToolCardBindings(cards),
34
+ staticToolIds: Object.keys(ctx.tools.getAll()),
35
+ cardTypes: Object.keys(cards),
36
+ apiKeyConfigured: true,
37
+ });
38
+ expect(diagnostics.filter((item) => item.level === 'error')).toEqual([]);
39
+ });
40
+
41
+ it('插件装配读取根目录 config/plugins.json', () => {
42
+ expect(createPluginIntegration().readConfig()).toMatchObject({
43
+ plugins: expect.any(Array),
44
+ warnings: [],
45
+ });
46
+ });
47
+
48
+ it('C3: 每张卡 meta.cardType === ctx.cards 的 key', async () => {
49
+ const ctx = createServerContext();
50
+ await registerAll(ctx);
51
+ for (const [key, meta] of Object.entries(ctx.cards.getAll())) {
52
+ expect((meta as { cardType?: string }).cardType).toBe(key);
53
+ }
54
+ });
55
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createServerContext, createSdkToolRuntime } from '@tbox.cn/app-sdk/server';
3
+ import { registerAll } from '../src/modules.js';
4
+
5
+ describe('tool runtime 宿主装配(自注册闭环)', () => {
6
+ it('合并模块工具并用同一次加载结果创建卡片 resolver', async () => {
7
+ const ctx = createServerContext();
8
+ await registerAll(ctx);
9
+
10
+ const runtime = await createSdkToolRuntime(ctx, {
11
+ loadPlugins: async () => ({ tools: {}, references: [] }),
12
+ });
13
+
14
+ expect(Object.keys(runtime.tools)).toEqual(
15
+ expect.arrayContaining(['getWeather']),
16
+ );
17
+ expect(runtime.cardResolver.issues).toEqual([]);
18
+ expect(
19
+ runtime.cardResolver.resolve('getWeather', {
20
+ city: '杭州',
21
+ temp: 28,
22
+ weather: '晴',
23
+ humidity: 60,
24
+ }),
25
+ ).toEqual({
26
+ status: 'ready',
27
+ cardType: 'weather',
28
+ data: {
29
+ city: '杭州',
30
+ temp: 28,
31
+ weather: '晴',
32
+ humidity: 60,
33
+ },
34
+ });
35
+ });
36
+
37
+ it('未注册的 tool 不出卡,仅作为模型上下文', async () => {
38
+ const ctx = createServerContext();
39
+ await registerAll(ctx);
40
+
41
+ const runtime = await createSdkToolRuntime(ctx, {
42
+ loadPlugins: async () => ({ tools: {}, references: [] }),
43
+ });
44
+
45
+ expect(runtime.cardResolver.resolve('notExistedTool', {})).toEqual({
46
+ status: 'unbound',
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {},
4
+ "include": ["src", "tests"]
5
+ }
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ target: 'node24',
7
+ platform: 'node',
8
+ outDir: 'dist',
9
+ clean: true,
10
+ sourcemap: true,
11
+ // 本地 workspace 源码包(exports 指向 src/*.ts,node 无法直接加载)必须内联:@app/*。
12
+ // @tbox.cn/{app-sdk,contracts,contracts-mall} 以发布态消费(dist 产物,node 可直载)→ external;
13
+ // 已发布 npm 的 @tbox.cn/agent-plugin(exports 指向 dist 产物)保持 external。
14
+ noExternal: [/^@app\//],
15
+ // 确保不打包 node 内置模块
16
+ external: [
17
+ 'express',
18
+ 'cors',
19
+ 'ws',
20
+ 'path',
21
+ 'url',
22
+ 'http',
23
+ 'fs',
24
+ ],
25
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ },
8
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "enableImageUpload": true,
3
+ "enableVideoUpload": true,
4
+ "enableTTS": true,
5
+ "enableVoiceInput": true,
6
+ "defaultInputMode": "TEXT",
7
+ "quickCommands": []
8
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "modelName": "glm-5.1"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "plugins": []
3
+ }
@@ -0,0 +1,41 @@
1
+ import tseslint from "typescript-eslint";
2
+ import reactHooks from "eslint-plugin-react-hooks";
3
+
4
+ export default [
5
+ {
6
+ files: ["apps/**/*.ts", "packages/**/*.ts", "apps/**/*.tsx", "packages/**/*.tsx"],
7
+ plugins: {
8
+ "@typescript-eslint": tseslint.plugin,
9
+ "react-hooks": reactHooks,
10
+ },
11
+ languageOptions: {
12
+ parser: tseslint.parser,
13
+ parserOptions: {
14
+ sourceType: "module",
15
+ },
16
+ },
17
+ linterOptions: {
18
+ reportUnusedDisableDirectives: "off",
19
+ },
20
+ rules: {
21
+ "quotes": ["error", "single", { "avoidEscape": true }],
22
+ "no-restricted-syntax": [
23
+ "error",
24
+ {
25
+ selector:
26
+ "Property[key.name='path'][parent.parent.callee.name='WebSocketServer'] > Literal:not([value='/ws'])",
27
+ message:
28
+ "WebSocket server path 必须为 '/ws',禁止修改。客户端环境变量依赖此路径。",
29
+ },
30
+ {
31
+ selector:
32
+ "CallExpression[callee.property.name='listen'] > Literal[raw=/^\\d{4,5}$/]",
33
+ message: "禁止硬编码端口号。必须使用 process.env.PORT || 8000。",
34
+ },
35
+ ],
36
+ },
37
+ },
38
+ {
39
+ ignores: ["**/dist/**", "**/node_modules/**", "**/.vite/**"],
40
+ },
41
+ ];