@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vite dev 入口(仅 tsx 运行:pnpm dev;物理不进 tsup bundle——forbid-vite 插件拦截)。
|
|
3
|
+
* middlewareMode + 单端口:HTTP 与 HMR WS 全走应用端口(沙箱仅暴露一个端口亦可开发)。
|
|
4
|
+
* 部署形态一律走 dist/index.js(静态产物,零 HMR WS)。
|
|
5
|
+
*/
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { createServer as createViteServer } from 'vite';
|
|
11
|
+
import { injectScript, isReady } from '@tbox.cn/app-sdk/platform';
|
|
12
|
+
import { createLogger } from '@tbox.cn/app-sdk/server';
|
|
13
|
+
import { app, registerUpgradeDelegate, start } from './app.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const serverLogger = createLogger('server');
|
|
17
|
+
const clientRoot = path.resolve(__dirname, '../../client');
|
|
18
|
+
|
|
19
|
+
// HMR WS 独立路径:dev 基础设施命名空间(双下划线呼应 /@vite/*),与业务命名空间(/ws*)
|
|
20
|
+
// 解耦——delegate 只认领此路径,根路径归还业务路由。
|
|
21
|
+
const HMR_WS_PATH = '/__vite_hmr';
|
|
22
|
+
|
|
23
|
+
// HMR WS hub:永不 listen 的纯事件载体。hmr.server 在场 → vite 不自建 24678 独立端口,
|
|
24
|
+
// 客户端 __HMR_PORT__=null → 按页面同源端口连接(公网端口映射无关)。vite 对 hub 仅
|
|
25
|
+
// on/off('upgrade')(6.4.3 源码实证);协议常量(vite-hmr/vite-ping)与认领路径
|
|
26
|
+
// posix.join(base, hmr.path) 是 vite 契约面,由 scripts/smoke-dev-hmr.sh 守护。
|
|
27
|
+
const hmrHub = createServer();
|
|
28
|
+
|
|
29
|
+
const vite = await createViteServer({
|
|
30
|
+
root: clientRoot,
|
|
31
|
+
server: {
|
|
32
|
+
middlewareMode: true,
|
|
33
|
+
// 沙箱域名 Host 透传放行(dev-only;dist 物理不含 vite)。安全边界四条
|
|
34
|
+
// (cors() 既存全开 / 代理收敛公网入口 / token 轴不变 / /@fs 另有 deny)见 Agent Note 2026-09-09。
|
|
35
|
+
allowedHosts: true,
|
|
36
|
+
// /@fs 加固:默认 deny 不含服务端凭证目录(client 永不需要经 /@fs 读它)
|
|
37
|
+
fs: { deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**', '**/config/credentials/**'] },
|
|
38
|
+
hmr: { server: hmrHub, path: HMR_WS_PATH },
|
|
39
|
+
},
|
|
40
|
+
// custom:不加 Vite 自己的 SPA fallback,由下方 app.use('*') 统一处理 HTML 路由
|
|
41
|
+
appType: 'custom',
|
|
42
|
+
optimizeDeps: { entries: [] },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 升级委托:与 vite hmrServerWsListener 同条件(协议整串匹配 + pathname === 认领路径)。
|
|
46
|
+
// viteHmrPath 与 vite 同一 resolved config、同式 posix.join 派生——恒等无漂移(字面比较
|
|
47
|
+
// 在未来设 base 时有悬挂方向:delegate 认领转发而 vite 不接,socket 无人销毁)。协议轴
|
|
48
|
+
// 必须保留:hub 是纯事件载体无 destroy,仅匹配 path 的非 vite 协议升级会转发后无人认领。
|
|
49
|
+
// 未认领的升级回落 app 路由(业务 /ws、根路径归还业务、fail-fast 语义不变)。
|
|
50
|
+
const viteHmrPath = path.posix.join(vite.config.base, HMR_WS_PATH);
|
|
51
|
+
registerUpgradeDelegate((request, socket, head) => {
|
|
52
|
+
const protocol = request.headers['sec-websocket-protocol'];
|
|
53
|
+
if (protocol !== 'vite-hmr' && protocol !== 'vite-ping') return false;
|
|
54
|
+
const { pathname } = new URL(request.url || '/', 'http://localhost');
|
|
55
|
+
if (pathname !== viteHmrPath) return false;
|
|
56
|
+
hmrHub.emit('upgrade', request, socket, head);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Vite 中间件:静态资源、模块转换、HMR(装配已在 ./app.js import 期完成)
|
|
61
|
+
app.use(vite.middlewares);
|
|
62
|
+
|
|
63
|
+
// HTML 路由兜底:非 /api、/ws 一律返回 index.html(经 Vite 转换 + inspector 注入)
|
|
64
|
+
app.use('*', async (req, res, next) => {
|
|
65
|
+
if (req.originalUrl.startsWith('/api') || req.originalUrl.startsWith('/ws')) {
|
|
66
|
+
return next();
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const rawHtml = await readFile(path.join(clientRoot, 'index.html'), 'utf-8');
|
|
70
|
+
const template = await vite.transformIndexHtml(req.originalUrl, rawHtml);
|
|
71
|
+
const html = isReady() ? injectScript(template) : template;
|
|
72
|
+
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
next(e);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
start().catch((err) => {
|
|
79
|
+
serverLogger.error('Failed to start:', err);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
});
|
package/apps/server/src/index.ts
CHANGED
|
@@ -1,230 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 静态入口(唯一 tsup entry → dist/index.js):express.static 供给 client/dist。
|
|
3
|
+
* dist 形态恒静态——vite dev 仅存在于 src/dev.ts(tsx 运行,禁入 tsup entry,
|
|
4
|
+
* tsup.config.ts forbid-vite 插件拦截)。NODE_ENV 只影响 SDK 安全姿态(dev-login/密钥链)。
|
|
5
|
+
*/
|
|
4
6
|
import path from 'path';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
5
8
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
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';
|
|
9
|
+
import express from 'express';
|
|
10
|
+
import { createLogger } from '@tbox.cn/app-sdk/server';
|
|
11
|
+
import { app, start } from './app.js';
|
|
24
12
|
|
|
25
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const
|
|
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());
|
|
14
|
+
const serverLogger = createLogger('server');
|
|
67
15
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (auth.config.devLoginEnabled) {
|
|
73
|
-
console.warn(
|
|
74
|
-
'[AUTH] dev-login 已开启(非 production 默认)。若本实例暴露公网,请设 NODE_ENV=production 或 AUTH_DEV_LOGIN_DISABLED=true。',
|
|
16
|
+
const clientDistPath = path.join(__dirname, '../../client/dist');
|
|
17
|
+
if (!existsSync(path.join(clientDistPath, 'index.html'))) {
|
|
18
|
+
serverLogger.warn(
|
|
19
|
+
`client/dist 缺失(${clientDistPath})——页面将不可用(API/WS 不受影响);请先执行 pnpm build:client`,
|
|
75
20
|
);
|
|
76
21
|
}
|
|
77
22
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
23
|
+
app.use(express.static(clientDistPath));
|
|
24
|
+
app.get('*', (req, res) => {
|
|
25
|
+
if (!req.path.startsWith('/api')) {
|
|
26
|
+
res.sendFile(path.join(clientDistPath, 'index.html'));
|
|
27
|
+
} else {
|
|
28
|
+
res.status(404).json({ error: 'Not Found' });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
226
31
|
|
|
227
32
|
start().catch((err) => {
|
|
228
|
-
|
|
33
|
+
serverLogger.error('Failed to start:', err);
|
|
229
34
|
process.exit(1);
|
|
230
|
-
});
|
|
35
|
+
});
|
|
@@ -2,9 +2,10 @@ import type { IntentRoute, HandlerResult, ServerContext } from '@tbox.cn/app-sdk
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 业务意图表(B21:intent-framework hybrid 接线——SDK 框架 + 源码意图表,003 §3.3)。
|
|
5
|
-
* chat() 先 matchIntent
|
|
5
|
+
* chat() 先 matchIntent(统一检索层 BM25 竞争:长词 OR ≥2 token 规则,score 降序 + id 字典序);
|
|
6
|
+
* handler 缺省直接出卡/文字,也可用 continueWith 续接完整 Agent。
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* 示例(商圈应用启用积分抵扣停车意图,handlerId 对应已装模块的 handler id):
|
|
8
9
|
* { id: 'points-parking', keywords: ['积分抵扣停车', '停车抵扣'], handlerId: 'points-parking', action: 'run' }
|
|
9
10
|
* 本模板默认空表(保持 LLM + tool calling 主路径);业务方在此声明意图路由。
|
|
10
11
|
*/
|
|
@@ -17,7 +18,13 @@ export const INTENT_ROUTES: IntentRoute[] = [];
|
|
|
17
18
|
* P2-3:用户原始输入以 payload.content 传入,handler 侧可经 payload.content 取用。
|
|
18
19
|
*/
|
|
19
20
|
export function createAppIntentDispatcher(ctx: ServerContext) {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// 上下文统一化:第三参 reqCtx(引擎意图分发注入的运行上下文)透传 handlers.dispatch
|
|
22
|
+
// 第五参——意图 handler 经 handle 第四参消费;断链 = 意图路径恒「缺少运行上下文」。
|
|
23
|
+
return async (
|
|
24
|
+
route: IntentRoute,
|
|
25
|
+
content: string,
|
|
26
|
+
reqCtx?: import('@tbox.cn/app-contracts').RequestContext,
|
|
27
|
+
): Promise<HandlerResult | undefined> => {
|
|
28
|
+
return ctx.handlers.dispatch(route.handlerId, route.action, { content }, ctx, reqCtx);
|
|
22
29
|
};
|
|
23
30
|
}
|
|
@@ -8,23 +8,30 @@
|
|
|
8
8
|
import type { ServerContext, InferModuleCards } from '@tbox.cn/app-sdk/server';
|
|
9
9
|
|
|
10
10
|
// @tbox:imports-begin
|
|
11
|
+
// SDK 装配框架(非模块行,CLI 增量维护保留非标行)
|
|
12
|
+
import { registerServerModules } from '@tbox.cn/app-sdk/server';
|
|
11
13
|
import { serverModule as moduleWeather } from '@app/module-weather/server';
|
|
14
|
+
import { serverModule as providerCustom } from '@app/provider-custom/server';
|
|
12
15
|
// @tbox:imports-end
|
|
13
16
|
|
|
14
17
|
// @tbox:register-begin
|
|
18
|
+
// base 模板出厂预装 provider-custom(自定义 API 落点)+ weather 演示模块
|
|
15
19
|
const modules = [
|
|
16
20
|
moduleWeather,
|
|
21
|
+
providerCustom,
|
|
17
22
|
];
|
|
18
23
|
// @tbox:register-end
|
|
19
24
|
|
|
20
25
|
// 自由区:可做装配级定制——条件注册、顺序调整、包裹中间件
|
|
21
26
|
// const dynamicModule = process.env.ENABLE_X ? xModule : null;
|
|
22
27
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 注册所有已安装模块(服务端装配——勿与 apps/client/src/modules.ts 混淆)。
|
|
30
|
+
* 010 下沉:声明式字段自动注册(skills/preTools)与 async register 等待由
|
|
31
|
+
* SDK registerServerModules 承载——未来新增声明式字段只改 SDK,本文件零改动。
|
|
32
|
+
*/
|
|
33
|
+
export function registerAll(ctx: ServerContext): Promise<void> {
|
|
34
|
+
return registerServerModules(ctx, modules);
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
// 类型从运行时装配数组自动推导:增删模块后 CardType 自动收缩/扩展(002 §6.7)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 铁律:不直接 import @mastra/core,经 @tbox.cn/app-sdk/server 的 definePreTool 入口(单例铁律)
|
|
2
|
+
import { definePreTool } from '@tbox.cn/app-sdk/server';
|
|
3
|
+
import { clockTool } from './tools/clock.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 应用级前置工具声明(010):BuildEngineOptions.preTools 通道。
|
|
7
|
+
*
|
|
8
|
+
* 声明形态(详见 README「前置 Tool 加速执行」节):
|
|
9
|
+
* ① tool —— 工具实例引用(单一真源,id/description 派生)
|
|
10
|
+
* ② routing —— 结构化路由:{ mode: 'keywords', terms } 宽召回词表(与 skill 同源打分)
|
|
11
|
+
* 或 { mode: 'always' } 必选候选(decide 门控;慎用防每轮噪音)
|
|
12
|
+
* ③ decide —— 决策即构造:{ decision: 'run', args } = 预执行 / { decision: 'skip' } = 不生效
|
|
13
|
+
*
|
|
14
|
+
* 演示形态:零参工程化——词表命中即生效(decide 恒 run 空参)。
|
|
15
|
+
* 词表刻意排除「今天/时间」等泛词,防止与天气类 preTool 叠加误触发(词表精度示范)。
|
|
16
|
+
*/
|
|
17
|
+
export const APP_PRE_TOOLS = [
|
|
18
|
+
definePreTool(clockTool, {
|
|
19
|
+
routing: {
|
|
20
|
+
mode: 'keywords',
|
|
21
|
+
terms: ['几点', '几点了', '现在时间', '日期', '星期几'],
|
|
22
|
+
},
|
|
23
|
+
decide: () => ({ decision: 'run', args: {} }),
|
|
24
|
+
}),
|
|
25
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
* 当前时间工具 — 前置工具(pre-tool)演示:零参工具,参数无需模型抽取,
|
|
7
|
+
* 词表命中即可工程化预执行(见 apps/server/src/pre-tools.ts)。
|
|
8
|
+
*/
|
|
9
|
+
export const clockTool = createTool({
|
|
10
|
+
id: 'getCurrentTime',
|
|
11
|
+
description: '查询当前日期和时间',
|
|
12
|
+
inputSchema: z.object({}),
|
|
13
|
+
outputSchema: z.object({
|
|
14
|
+
iso: z.string(),
|
|
15
|
+
zh: z.string(),
|
|
16
|
+
}),
|
|
17
|
+
execute: async () => {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
return {
|
|
20
|
+
iso: now.toISOString(),
|
|
21
|
+
zh: now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -12,10 +12,11 @@ export const translateTool = createTool({
|
|
|
12
12
|
'中英文互译工具。当用户需要翻译文本时调用此工具。自动检测源语言并翻译为目标语言:中文→英文,英文→中文。',
|
|
13
13
|
inputSchema: z.object({
|
|
14
14
|
text: z.string().describe('要翻译的文本'),
|
|
15
|
+
// D11 参数规范:仅 string 形态(LLM 生成字符串最可靠);非法值工具内回落自动检测
|
|
15
16
|
targetLang: z
|
|
16
|
-
.
|
|
17
|
+
.string()
|
|
17
18
|
.optional()
|
|
18
|
-
.describe('
|
|
19
|
+
.describe('目标语言(zh 或 en),不传则自动检测:中文输入翻译为英文,英文输入翻译为中文'),
|
|
19
20
|
}),
|
|
20
21
|
outputSchema: z.object({
|
|
21
22
|
original: z.string(),
|
|
@@ -24,9 +25,14 @@ export const translateTool = createTool({
|
|
|
24
25
|
targetLang: z.string(),
|
|
25
26
|
}),
|
|
26
27
|
execute: async ({ text, targetLang }) => {
|
|
27
|
-
//
|
|
28
|
+
// 简单语言检测:包含中文字符则认为是中文;非法 targetLang 回落自动检测
|
|
28
29
|
const isChinese = /[\u4e00-\u9fff]/.test(text);
|
|
29
|
-
const detectedTarget =
|
|
30
|
+
const detectedTarget =
|
|
31
|
+
targetLang === 'zh' || targetLang === 'en'
|
|
32
|
+
? targetLang
|
|
33
|
+
: isChinese
|
|
34
|
+
? 'en'
|
|
35
|
+
: 'zh';
|
|
30
36
|
const sourceLang = isChinese ? 'zh' : 'en';
|
|
31
37
|
|
|
32
38
|
// 注意:这里返回指令让 LLM 来做实际翻译
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// 卡片 / 工具 registry 一致性守护测试(自注册闭环版)。
|
|
2
2
|
// 通过 registerAll(ctx) 收集真实注册状态,替代原手写 registry 静态断言。
|
|
3
|
+
// v2 出卡协议:tool→card 绑定(allowedTools/cardResolver)退场——出卡由工具出参
|
|
4
|
+
// cards 字段运行期声明,registry 仅余 cardType 注册与 dataSchema 校验职责。
|
|
3
5
|
// 注:C 方案下卡片渲染在 client(React 组件),server 无 template.html,故移除原 C12(模板占位符 ∈ schema)。
|
|
4
6
|
import { describe, it, expect } from 'vitest';
|
|
5
7
|
import { diagnosePlugins } from '@tbox.cn/agent-plugin/doctor';
|
|
6
|
-
import { createServerContext,
|
|
8
|
+
import { createServerContext, createPluginIntegration } from '@tbox.cn/app-sdk/server';
|
|
7
9
|
import { registerAll } from '../src/modules.js';
|
|
8
10
|
|
|
9
11
|
describe('工具 / 卡片 registry 一致性(自注册闭环)', () => {
|
|
@@ -15,24 +17,22 @@ describe('工具 / 卡片 registry 一致性(自注册闭环)', () => {
|
|
|
15
17
|
}
|
|
16
18
|
});
|
|
17
19
|
|
|
18
|
-
it('C3:
|
|
20
|
+
it('C3: 每张卡 meta.cardType === ctx.cards 的 key', async () => {
|
|
19
21
|
const ctx = createServerContext();
|
|
20
22
|
await registerAll(ctx);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
expect(Object.keys(cards)).toContain(binding.cardType);
|
|
23
|
+
for (const [key, meta] of Object.entries(ctx.cards.getAll())) {
|
|
24
|
+
expect((meta as { cardType?: string }).cardType).toBe(key);
|
|
24
25
|
}
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
it('C3:
|
|
28
|
+
it('C3: 插件配置诊断(无卡片绑定,v2 协议下 bindings 恒空)', async () => {
|
|
28
29
|
const ctx = createServerContext();
|
|
29
30
|
await registerAll(ctx);
|
|
30
|
-
const cards = ctx.cards.getAll();
|
|
31
31
|
const diagnostics = diagnosePlugins({
|
|
32
32
|
config: createPluginIntegration().readConfig(),
|
|
33
|
-
bindings:
|
|
33
|
+
bindings: [],
|
|
34
34
|
staticToolIds: Object.keys(ctx.tools.getAll()),
|
|
35
|
-
cardTypes: Object.keys(cards),
|
|
35
|
+
cardTypes: Object.keys(ctx.cards.getAll()),
|
|
36
36
|
apiKeyConfigured: true,
|
|
37
37
|
});
|
|
38
38
|
expect(diagnostics.filter((item) => item.level === 'error')).toEqual([]);
|
|
@@ -44,12 +44,4 @@ describe('工具 / 卡片 registry 一致性(自注册闭环)', () => {
|
|
|
44
44
|
warnings: [],
|
|
45
45
|
});
|
|
46
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
47
|
});
|