@tbox.cn/app-template-agent 0.2.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 +36 -5
- package/README.md +23 -7
- package/apps/client/package.json +3 -2
- package/apps/client/src/app.tsx +28 -0
- package/apps/client/src/cards/CardRenderer.tsx +4 -5
- 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/ToolCallBlock/index.less +29 -0
- package/apps/client/src/components/ToolCallBlock/index.tsx +58 -24
- 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 +62 -92
- package/apps/client/vite.config.ts +46 -26
- package/apps/server/package.json +5 -11
- package/apps/server/src/app.ts +452 -0
- package/apps/server/src/cli/collect-registry.ts +7 -6
- package/apps/server/src/config/deployment.ts +36 -70
- package/apps/server/src/config/integration.ts +81 -0
- package/apps/server/src/dev.ts +81 -0
- package/apps/server/src/index.ts +23 -376
- package/apps/server/src/intent-routes.ts +11 -4
- package/apps/server/src/modules.ts +3 -0
- 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 +97 -23
- 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 +4 -4
- package/packages/module-weather/skills/weather-guide/SKILL.md +3 -0
- package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
- package/packages/module-weather/src/server/index.ts +1 -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 -46
- 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/apps/server/skills/shopping-guide-demo/SKILL.md +0 -21
- package/apps/server/skills/shopping-guide-demo/references/guide/parking.md +0 -11
- package/apps/server/skills/shopping-guide-demo/references/mall-directory.md +0 -17
- package/apps/server/skills/shopping-guide-demo/references/promotion.md +0 -10
- package/apps/server/src/config/feature-gate.ts +0 -17
- package/apps/server/tests/feature-gate.test.ts +0 -30
- package/config/deployments/development.json +0 -27
- 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
|
+
* Integrations 配置面 loader(integrations v4:绑定组整组继承 + catalog 派生 + 部署实例注册表)。
|
|
3
|
+
* - `config/integrations.json` 存在即生效(模板缺省 = 全槽 mock,裸跑即完整演示);
|
|
4
|
+
* `TBOX_INTEGRATIONS_FILE` 仅覆盖路径(显式指定真实部署素材)。
|
|
5
|
+
* - 单文件真源(v4):部署拓扑(instances 注册表 + defaultInstanceId)与 provider 绑定同文件——
|
|
6
|
+
* 旧 `config/deployments/development.json` 已退役,stale 防线在装载链 fail-fast
|
|
7
|
+
* (mall 家族形态判定:含 malls 键;support 独立体系不受影响)。
|
|
8
|
+
* - zod 单源 = SDK parseIntegrationsConfig(断言 → strict 校验;拼错键启动即报);
|
|
9
|
+
* 控制平面投影 = contracts-mall mallControlPlaneOf(恒返回——无表 = 空平面)。
|
|
10
|
+
* - 本壳只保留 I/O:占位展开(real-env 统一配置链——缺变量 fail-fast,错误含文件与
|
|
11
|
+
* 缺失清单)+ DEFAULT_PATH 双态锚点探测(打包形态私有知识,不入 SDK)。
|
|
12
|
+
* - 与 base 模板同构(双生无同步机制——公共能力先行落位,漂移由生成应用 verify 守护)。
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { expandEnvVars, parseIntegrationsConfig } from '@tbox.cn/app-sdk/server';
|
|
18
|
+
import type { IntegrationsConfig } from '@tbox.cn/app-contracts';
|
|
19
|
+
import { mallControlPlaneOf } from '@app/contracts-mall/server';
|
|
20
|
+
import type { MallDeploymentControlPlane } from '@app/contracts-mall';
|
|
21
|
+
|
|
22
|
+
// 默认路径 = 应用根 config/integrations.json。双态锚点探测(bundle 打平后 import.meta.url 层级漂移):
|
|
23
|
+
// dev 态本文件在 src/config/(四层到根);tsup bundle 态打平进 dist/(三层到根)。
|
|
24
|
+
// 与 CONFIG_DIR(index.ts __dirname 三层,src|dist 同层)不同——本文件 dev 态深一层,逐层向上探测
|
|
25
|
+
// 首个含 config/integrations.json 的应用根(上限四层,两态收敛)。
|
|
26
|
+
const DEFAULT_PATH = (() => {
|
|
27
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
for (let i = 0; i < 4; i++) {
|
|
29
|
+
const candidate = path.join(dir, 'config', 'integrations.json');
|
|
30
|
+
if (existsSync(candidate)) return candidate;
|
|
31
|
+
dir = path.dirname(dir);
|
|
32
|
+
}
|
|
33
|
+
// 未探测到 → 四层基准路径(loadIntegrations existsSync 判缺席)
|
|
34
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../config/integrations.json');
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
export interface LoadedIntegrations {
|
|
38
|
+
config: IntegrationsConfig;
|
|
39
|
+
source: string;
|
|
40
|
+
/** mall 控制平面投影(恒在场——无参考表 = 空平面;装配链零拓扑分支) */
|
|
41
|
+
controlPlane: MallDeploymentControlPlane;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* v4 stale 防线(G1 限 mall 家族):旧部署控制面文件在场且含 malls 键 → fail-fast
|
|
46
|
+
* (零消费方——残留即静默丢拓扑,必须显式迁移);不含 malls 键(非 mall 形态)不阻断。
|
|
47
|
+
* baseDir 缺省 = 模块位置锚点(双态收敛同 DEFAULT_PATH);测试注入临时根。
|
|
48
|
+
*/
|
|
49
|
+
export function assertNoStaleDeploymentControlPlane(baseDir?: string): void {
|
|
50
|
+
let dir = baseDir ?? path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
for (let i = 0; i < 4; i++) {
|
|
52
|
+
const candidate = path.join(dir, 'config', 'deployments', 'development.json');
|
|
53
|
+
if (existsSync(candidate)) {
|
|
54
|
+
try {
|
|
55
|
+
const stale = JSON.parse(readFileSync(candidate, 'utf8')) as { malls?: unknown };
|
|
56
|
+
if (stale && typeof stale === 'object' && 'malls' in stale) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'部署控制面已并入 config/integrations.json(instances 注册表 + defaultInstanceId)——' +
|
|
59
|
+
'请迁移后删除 config/deployments/development.json(malls → instances[].id/attributes,defaultMallId → defaultInstanceId)',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e instanceof Error && e.message.includes('已并入')) throw e;
|
|
64
|
+
// 旧文件非法 JSON 不在本防线职责(旧体系已退役)——不阻断启动
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
dir = path.dirname(dir);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 读取并校验 integrations 配置(文件缺席 → undefined = 槽位解析不启用,模块走探测链) */
|
|
73
|
+
export function loadIntegrations(envPath?: string): LoadedIntegrations | undefined {
|
|
74
|
+
assertNoStaleDeploymentControlPlane();
|
|
75
|
+
const p = envPath ?? process.env.TBOX_INTEGRATIONS_FILE ?? DEFAULT_PATH;
|
|
76
|
+
if (!p || !existsSync(p)) return undefined;
|
|
77
|
+
// 顺序固定:展开(占位符)→ 断言 → zod(后两步在 parseIntegrationsConfig 内,防线单点)
|
|
78
|
+
const raw = JSON.parse(expandEnvVars(readFileSync(p, 'utf8'), process.env, p));
|
|
79
|
+
const config = parseIntegrationsConfig(raw);
|
|
80
|
+
return { config, source: p, controlPlane: mallControlPlaneOf(config) };
|
|
81
|
+
}
|
|
@@ -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,388 +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
|
-
loadLocalSkills,
|
|
21
|
-
resolveSkillMode,
|
|
22
|
-
resolvePreToolTimeout,
|
|
23
|
-
buildMessageInputs,
|
|
24
|
-
} from '@tbox.cn/app-sdk/server';
|
|
25
|
-
import { installInspector, injectScript, isReady } from '@tbox.cn/app-sdk/platform';
|
|
26
|
-
import { registerAppTools } from './app-tools.js';
|
|
27
|
-
import { APP_PRE_TOOLS } from './pre-tools.js';
|
|
28
|
-
import { INTENT_ROUTES, createAppIntentDispatcher } from './intent-routes.js';
|
|
29
|
-
import { loadDeploymentControlPlane } from './config/deployment.js';
|
|
30
|
-
import { createFeatureGate } from './config/feature-gate.js';
|
|
31
|
-
// 011 S2/S3:mall 装配原语(条件装配——未配置部署文件时零引用)
|
|
32
|
-
import {
|
|
33
|
-
createScopeSessionRegistry,
|
|
34
|
-
createActionGrantService,
|
|
35
|
-
createComponentActionDispatcher,
|
|
36
|
-
createEffectRuntime,
|
|
37
|
-
createProviderRegistry,
|
|
38
|
-
createAuthenticatedTransportFactory,
|
|
39
|
-
createAuthStrategyRegistry,
|
|
40
|
-
createEnvironmentSecretProvider,
|
|
41
|
-
createInMemoryStateStore,
|
|
42
|
-
attachCardActionTokens,
|
|
43
|
-
} from '@tbox.cn/app-sdk/server';
|
|
9
|
+
import express from 'express';
|
|
10
|
+
import { createLogger } from '@tbox.cn/app-sdk/server';
|
|
11
|
+
import { app, start } from './app.js';
|
|
44
12
|
|
|
45
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
-
const
|
|
47
|
-
const HOST = process.env.HOST || '0.0.0.0';
|
|
48
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
49
|
-
|
|
50
|
-
const AI_MODEL_NAME = process.env.APP_AI_MODEL_NAME || '';
|
|
51
|
-
|
|
52
|
-
const AGENT_CONFIG = {
|
|
53
|
-
instructions: [
|
|
54
|
-
'你是一个通用 AI 助手。可以回答用户问题,也可以调用工具获取信息。用简洁、清晰的语言回复。',
|
|
55
|
-
'调用业务工具时正常按其 schema 传参,拿到结果后用自然语言回复用户;',
|
|
56
|
-
'是否向用户呈现卡片由系统自动决定,你不需要也不应该主动渲染 UI。',
|
|
57
|
-
].join('\n'),
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// 平台能力装配(003/006 平台轴,已下沉 @tbox.cn/app-sdk/server)
|
|
61
|
-
const auth = createAuthRuntime();
|
|
62
|
-
const { getModel } = createModelProvider();
|
|
63
|
-
const pluginTools = createPluginIntegration();
|
|
64
|
-
|
|
65
|
-
/** 懒初始化单例 ConversationService */
|
|
66
|
-
let _conversationService: ConversationService | null = null;
|
|
67
|
-
function getConversationService(): ConversationService | null {
|
|
68
|
-
if (!_conversationService) {
|
|
69
|
-
const apiKey = process.env.TBOX_API_KEY;
|
|
70
|
-
if (!apiKey) return null;
|
|
71
|
-
_conversationService = new ConversationService(apiKey);
|
|
72
|
-
}
|
|
73
|
-
return _conversationService;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 进程级最后防线
|
|
77
|
-
process.on('unhandledRejection', (reason) => {
|
|
78
|
-
console.error('[server] unhandledRejection:', reason);
|
|
79
|
-
});
|
|
80
|
-
process.on('uncaughtException', (err) => {
|
|
81
|
-
console.error('[server] uncaughtException:', err);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const app = express();
|
|
85
|
-
app.use(cors());
|
|
86
|
-
app.use(express.json());
|
|
14
|
+
const serverLogger = createLogger('server');
|
|
87
15
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (auth.config.devLoginEnabled) {
|
|
93
|
-
console.warn(
|
|
94
|
-
'[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`,
|
|
95
20
|
);
|
|
96
21
|
}
|
|
97
22
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const ctx = createServerContext({
|
|
106
|
-
...(deployment
|
|
107
|
-
? {
|
|
108
|
-
state: createInMemoryStateStore(),
|
|
109
|
-
runtime: {
|
|
110
|
-
// 011 D10/S5/评审 M2:门控 = 执行期护栏——组件未声明 enabledFeatures = 全启用
|
|
111
|
-
// (与 MallDeploymentComponent 语义一致;声明了 → 白名单包含才放行)
|
|
112
|
-
isFeatureEnabled: createFeatureGate(deployment.controlPlane.deployment.components),
|
|
113
|
-
config: deployment.controlPlane as unknown as Record<string, unknown>,
|
|
114
|
-
},
|
|
115
|
-
providers: createProviderRegistry({
|
|
116
|
-
instances: deployment.controlPlane.providerInstances,
|
|
117
|
-
// 011 评审 7.1:credentialType 真源参数化——providerTemplates 声明优先,
|
|
118
|
-
// 缺省回退从实例派生 + joycity-c-signed-v1(向后兼容既有部署配置)
|
|
119
|
-
templates:
|
|
120
|
-
deployment.controlPlane.providerTemplates ??
|
|
121
|
-
deployment.controlPlane.providerInstances.map((i: { providerTemplateId: string }) => ({
|
|
122
|
-
providerTemplateId: i.providerTemplateId,
|
|
123
|
-
credentialType: 'joycity-c-signed-v1',
|
|
124
|
-
})),
|
|
125
|
-
transportFactory: createAuthenticatedTransportFactory({
|
|
126
|
-
strategies: authStrategies,
|
|
127
|
-
secretProvider: createEnvironmentSecretProvider({
|
|
128
|
-
source: process.env as Record<string, string | undefined>,
|
|
129
|
-
allowedHeaderNames: ['content-type', 'sign', 'timestamp', 'publickey', 'x-api-key'],
|
|
130
|
-
production: isProduction,
|
|
131
|
-
}),
|
|
132
|
-
}),
|
|
133
|
-
}),
|
|
134
|
-
}
|
|
135
|
-
: {}),
|
|
136
|
-
authStrategies,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// 全局 /api 鉴权(H2 挂载顺序契约):除 public(login/dev-login/health)外一律要求 Bearer token;
|
|
140
|
-
// optional 模式缺失 token → 匿名身份(写通道仍被 action gateway 拒绝)
|
|
141
|
-
// ⚠️ 必须先于 /api/m、/api/action、createPlatformApiRouter 挂载,保证全 API 面受鉴权约束
|
|
142
|
-
app.use('/api', auth.middleware);
|
|
143
|
-
|
|
144
|
-
app.use('/api/m', ctx.routes.mount());
|
|
145
|
-
|
|
146
|
-
// 业务动作网关(004 D3/C2):POST /api/action — SDK 黑盒唯一写入口(幂等/防重放/签名槽位;
|
|
147
|
-
// 006 D11:匿名写拒绝,不随 AUTH_MODE 变)
|
|
148
|
-
app.use('/api/action', ctx.actions.middleware());
|
|
149
|
-
|
|
150
|
-
// API 路由(平台路由面:auth/health/tbox/conversation + 404 兜底;卡片恢复内部化)
|
|
151
|
-
app.use('/api', createPlatformApiRouter({ ctx, auth }));
|
|
152
|
-
|
|
153
|
-
// 从 ServerContext 构造 ChatEngine(自注册闭环组装端)
|
|
154
|
-
async function createEngine(
|
|
155
|
-
extra?: { cardActionAttacher: (payload: import('@tbox.cn/app-contracts').TboxCardPayload, sessionId: string) => Promise<import('@tbox.cn/app-contracts').TboxCardPayload> },
|
|
156
|
-
) {
|
|
157
|
-
// 009:应用级本地 skills(SKILL.md 目录格式,dev/dist 路径恒等 = src/dist 平级 + __dirname 绝对解析)
|
|
158
|
-
const appSkills = await loadLocalSkills(path.resolve(__dirname, '../skills'));
|
|
159
|
-
const skillMode = resolveSkillMode(process.env.TBOX_SKILL_MODE);
|
|
160
|
-
// 010:单次解析(引擎选项与启动日志同源,非法值只 warn 一次)
|
|
161
|
-
const preToolTimeoutMs = resolvePreToolTimeout(process.env.TBOX_PRE_TOOL_TIMEOUT_MS);
|
|
162
|
-
|
|
163
|
-
const engine = await createEngineFromContext(ctx, {
|
|
164
|
-
instructions: AGENT_CONFIG.instructions,
|
|
165
|
-
model: getModel(AI_MODEL_NAME),
|
|
166
|
-
traceExporter: tboxTraceExporter,
|
|
167
|
-
modelName: AI_MODEL_NAME,
|
|
168
|
-
fileUrlPrefix: TBOX_FILE_URL_PREFIX,
|
|
169
|
-
// 009:双模式全局配置(accelerated 默认:本地路由命中 → reminder 注入当前轮,零额外 LLM 轮次;
|
|
170
|
-
// progressive:全量注册 InlineSkill,模型渐进 skill_read)
|
|
171
|
-
skillMode,
|
|
172
|
-
skills: appSkills,
|
|
173
|
-
// 010:前置工具加速(应用级声明 + 单工具预算;TBOX_PRE_TOOL_TIMEOUT_MS=0 整体禁用)
|
|
174
|
-
preTools: APP_PRE_TOOLS,
|
|
175
|
-
preToolTimeoutMs,
|
|
176
|
-
// 011 B3:卡片动作 token 签发器(mall 装配时注入)
|
|
177
|
-
cardActionAttacher: extra?.cardActionAttacher,
|
|
178
|
-
// B21:意图路由接线(业务意图表 + handlerDispatch)
|
|
179
|
-
intentRoutes: INTENT_ROUTES,
|
|
180
|
-
handlerDispatch: createAppIntentDispatcher(ctx),
|
|
181
|
-
loadPlugins: async () => {
|
|
182
|
-
const loaded = await pluginTools.loadTools({
|
|
183
|
-
reservedToolIds: Object.keys(ctx.tools.getAll()),
|
|
184
|
-
});
|
|
185
|
-
return { tools: loaded.tools, references: loaded.references };
|
|
186
|
-
},
|
|
187
|
-
saveMessage: async ({ conversationId, query, answer, mediaItems, outerBusinessId, cards, skills, preTools }) => {
|
|
188
|
-
const agentId = process.env.TBOX_APP_ID;
|
|
189
|
-
const service = getConversationService();
|
|
190
|
-
if (!service || !agentId) return undefined;
|
|
191
|
-
const imageUrls = extractImageUrls(mediaItems, TBOX_FILE_URL_PREFIX);
|
|
192
|
-
// P3 W1:卡片快照随消息持久化(MessageEmbedded,004 D1)。
|
|
193
|
-
// 009 D10:skill 加载记录随消息持久化(inputs.skills,前端历史表达重建)。
|
|
194
|
-
// 010:前置工具记录随消息持久化(inputs.preTools,前端历史表达重建)。
|
|
195
|
-
// inputs「非空才加」组装规则已下沉 SDK buildMessageInputs(010 S3)。
|
|
196
|
-
// 若平台拒绝 inputs(V1 验证失败),此处切换为 extraParams 字符串或 FileCardPersistenceBackend 落盘。
|
|
197
|
-
const inputs = buildMessageInputs({ cards, skills, preTools });
|
|
198
|
-
return await service.saveMessage({
|
|
199
|
-
conversationId,
|
|
200
|
-
agentId,
|
|
201
|
-
query,
|
|
202
|
-
answer,
|
|
203
|
-
...(outerBusinessId && { outerBusinessId }),
|
|
204
|
-
...(imageUrls.length > 0 && {
|
|
205
|
-
multiModalInputs: { imageUrls },
|
|
206
|
-
}),
|
|
207
|
-
...(inputs && { inputs }),
|
|
208
|
-
});
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// 009:启动日志诊断(仅名称/描述/路由词,不含 SOP 内容)
|
|
213
|
-
console.log(
|
|
214
|
-
`[SKILL] mode=${skillMode} skills=${engine.listSkills().map((s) => s.name).join(',') || '(none)'}`,
|
|
215
|
-
);
|
|
216
|
-
// 010:前置工具诊断(仅 toolId/描述/路由形态,不含 decide 逻辑)
|
|
217
|
-
console.log(
|
|
218
|
-
`[PRETOOL] timeoutMs=${preToolTimeoutMs} preTools=${engine.listPreTools().map((d) => d.toolId).join(',') || '(none)'}`,
|
|
219
|
-
);
|
|
220
|
-
return engine;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (isProduction) {
|
|
224
|
-
const clientDistPath = path.join(__dirname, '../../client/dist');
|
|
225
|
-
app.use(express.static(clientDistPath));
|
|
226
|
-
app.get('*', (req, res) => {
|
|
227
|
-
if (!req.path.startsWith('/api')) {
|
|
228
|
-
res.sendFile(path.join(clientDistPath, 'index.html'));
|
|
229
|
-
} else {
|
|
230
|
-
res.status(404).json({ error: 'Not Found' });
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
} else {
|
|
234
|
-
// 开发环境:Vite middlewareMode 单进程
|
|
235
|
-
const { createServer: createViteServer } = await import('vite');
|
|
236
|
-
const { readFile } = await import('fs/promises');
|
|
237
|
-
const clientRoot = path.resolve(__dirname, '../../client');
|
|
238
|
-
const vite = await createViteServer({
|
|
239
|
-
root: clientRoot,
|
|
240
|
-
server: { middlewareMode: true },
|
|
241
|
-
// custom:不添加 Vite 自己的 SPA fallback / 404 处理器,
|
|
242
|
-
// 由下方 app.use('*') 统一处理 HTML 路由,避免 Vite 的 404 先返回。
|
|
243
|
-
appType: 'custom',
|
|
244
|
-
optimizeDeps: { entries: [] },
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Vite 中间件:处理静态资源、模块转换、HMR
|
|
248
|
-
app.use(vite.middlewares);
|
|
249
|
-
|
|
250
|
-
// HTML 路由兜底:非 /api、/ws 的请求一律返回 index.html(经 Vite 转换 + inspector 注入)
|
|
251
|
-
app.use('*', async (req, res, next) => {
|
|
252
|
-
if (req.originalUrl.startsWith('/api') || req.originalUrl.startsWith('/ws')) {
|
|
253
|
-
return next();
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
const rawHtml = await readFile(path.join(clientRoot, 'index.html'), 'utf-8');
|
|
257
|
-
const template = await vite.transformIndexHtml(req.originalUrl, rawHtml);
|
|
258
|
-
const html = isReady() ? injectScript(template) : template;
|
|
259
|
-
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
|
|
260
|
-
} catch (e) {
|
|
261
|
-
next(e);
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const httpServer = createServer(app);
|
|
267
|
-
|
|
268
|
-
async function start() {
|
|
269
|
-
// 011 S6:mall 装配 + 生产环境缺动作授权密钥 → 启动期提示(签发路径运行时兜底报错)
|
|
270
|
-
if (deployment && isProduction && !process.env.ACTION_TOKEN_SECRET && !process.env.AUTH_TOKEN_SECRET) {
|
|
271
|
-
console.warn('[ACTION] 生产环境未配置 ACTION_TOKEN_SECRET/AUTH_TOKEN_SECRET——卡片写动作 token 签发将失败');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// 注册业务模块(自注册闭环)
|
|
275
|
-
await registerAll(ctx);
|
|
276
|
-
|
|
277
|
-
// 模板演示工具(apps 级工具,P3 建议迁入模块):不参与模块体系,经统一注册面注册
|
|
278
|
-
// (010:start() 与 cli/collect-registry.ts 双处同调,防注册面失同步)
|
|
279
|
-
registerAppTools(ctx);
|
|
280
|
-
|
|
281
|
-
// 011 F2/E2:mall 装配(条件)——scope 会话 + 动作授权先行(engine attacher 依赖)
|
|
282
|
-
const scopeSessions: ReturnType<typeof createScopeSessionRegistry> | undefined = deployment
|
|
283
|
-
? createScopeSessionRegistry({
|
|
284
|
-
extractScopeKey: deployment.assembly.extractScopeKey,
|
|
285
|
-
validateScope: deployment.assembly.validateScope,
|
|
286
|
-
buildScope: deployment.assembly.buildScope,
|
|
287
|
-
})
|
|
288
|
-
: undefined;
|
|
289
|
-
const grants = scopeSessions ? createActionGrantService({ store: ctx.state }) : undefined;
|
|
290
|
-
|
|
291
|
-
// 011 B3:卡片动作 token 签发(build → attach → send;sessionId 引擎登记、scope 经 sessions 查)
|
|
292
|
-
const cardActionAttacher = grants && scopeSessions
|
|
293
|
-
? async (
|
|
294
|
-
payload: import('@tbox.cn/app-contracts').TboxCardPayload,
|
|
295
|
-
sessionId: string,
|
|
296
|
-
): Promise<import('@tbox.cn/app-contracts').TboxCardPayload> => {
|
|
297
|
-
const session = scopeSessions.get(sessionId);
|
|
298
|
-
if (!session) return payload;
|
|
299
|
-
return attachCardActionTokens({
|
|
300
|
-
payload,
|
|
301
|
-
actions: ctx.cardActions,
|
|
302
|
-
grants: grants,
|
|
303
|
-
sessionId: session.sessionId,
|
|
304
|
-
scope: session.scopeFlat,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
: undefined;
|
|
308
|
-
const engine = await createEngine(cardActionAttacher ? { cardActionAttacher } : undefined);
|
|
309
|
-
|
|
310
|
-
// 011 F2/E2:组件动作分发(依赖 scopeSessions/grants)
|
|
311
|
-
let componentActions: ReturnType<typeof createComponentActionDispatcher> | undefined;
|
|
312
|
-
if (deployment && scopeSessions && grants) {
|
|
313
|
-
componentActions = createComponentActionDispatcher({
|
|
314
|
-
grants,
|
|
315
|
-
sessions: scopeSessions,
|
|
316
|
-
state: ctx.state,
|
|
317
|
-
actions: ctx.cardActions,
|
|
318
|
-
cardRegistry: ctx.cards.getAll(),
|
|
319
|
-
// 011 E5/J7:alipay 授权宿主处理器——二期占位(authCode 回传通道未实现)
|
|
320
|
-
effects: createEffectRuntime({
|
|
321
|
-
handleAlipayAuthorization: async () => {
|
|
322
|
-
console.warn('[ALIPAY] 授权回调通道二期(authCode 回传 + resultActionId 补签未实现),授权效果未执行');
|
|
323
|
-
},
|
|
324
|
-
}),
|
|
325
|
-
extractScopeKey: deployment.assembly.extractScopeKey,
|
|
326
|
-
});
|
|
327
|
-
for (const action of ctx.cardActions.getAll().values()) {
|
|
328
|
-
componentActions.register(action);
|
|
329
|
-
}
|
|
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' });
|
|
330
29
|
}
|
|
331
|
-
|
|
332
|
-
const chatWss = createWsServer(engine, {
|
|
333
|
-
auth: auth.wsAuth,
|
|
334
|
-
...(scopeSessions
|
|
335
|
-
? {
|
|
336
|
-
scopeSessions,
|
|
337
|
-
...(componentActions ? { componentActions } : {}),
|
|
338
|
-
chatConfig: {
|
|
339
|
-
quickCommands: [
|
|
340
|
-
{ label: '找美食', prompt: '帮我找商场里的美食' },
|
|
341
|
-
{ label: '会员积分', prompt: '我的会员积分是多少' },
|
|
342
|
-
],
|
|
343
|
-
enableImageUpload: true,
|
|
344
|
-
},
|
|
345
|
-
}
|
|
346
|
-
: {}),
|
|
347
|
-
});
|
|
348
|
-
const ttsWss = createTtsProxy();
|
|
349
|
-
|
|
350
|
-
httpServer.on('error', (err: NodeJS.ErrnoException) => {
|
|
351
|
-
if (err.code === 'EADDRINUSE') {
|
|
352
|
-
console.error(`[server] Port ${PORT} is already in use.`);
|
|
353
|
-
} else if (err.code === 'EACCES') {
|
|
354
|
-
console.error(`[server] Permission denied while binding ${HOST}:${PORT}.`);
|
|
355
|
-
} else {
|
|
356
|
-
console.error('[server] Failed to start HTTP server:', err);
|
|
357
|
-
}
|
|
358
|
-
process.exit(1);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
httpServer.on('upgrade', (request, socket, head) => {
|
|
362
|
-
socket.on('error', (e) => console.warn('[upgrade] socket error:', e.message));
|
|
363
|
-
const { pathname } = new URL(request.url || '/', 'http://localhost');
|
|
364
|
-
|
|
365
|
-
if (pathname === '/ws') {
|
|
366
|
-
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
|
367
|
-
chatWss.emit('connection', ws, request);
|
|
368
|
-
});
|
|
369
|
-
} else if (pathname === '/ws/tts') {
|
|
370
|
-
ttsWss.handleUpgrade(request, socket, head, (ws) => {
|
|
371
|
-
ttsWss.emit('connection', ws, request);
|
|
372
|
-
});
|
|
373
|
-
} else {
|
|
374
|
-
socket.destroy();
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
httpServer.listen(PORT, HOST, () => {
|
|
379
|
-
console.log(`🚀 Server running at http://${HOST}:${PORT}`);
|
|
380
|
-
console.log(`📡 WebSocket endpoint: ws://${HOST}:${PORT}/ws`);
|
|
381
|
-
console.log(`🔊 TTS endpoint: ws://${HOST}:${PORT}/ws/tts`);
|
|
382
|
-
});
|
|
383
|
-
}
|
|
30
|
+
});
|
|
384
31
|
|
|
385
32
|
start().catch((err) => {
|
|
386
|
-
|
|
33
|
+
serverLogger.error('Failed to start:', err);
|
|
387
34
|
process.exit(1);
|
|
388
|
-
});
|
|
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
|
}
|
|
@@ -11,11 +11,14 @@ import type { ServerContext, InferModuleCards } from '@tbox.cn/app-sdk/server';
|
|
|
11
11
|
// SDK 装配框架(非模块行,CLI 增量维护保留非标行)
|
|
12
12
|
import { registerServerModules } from '@tbox.cn/app-sdk/server';
|
|
13
13
|
import { serverModule as moduleWeather } from '@app/module-weather/server';
|
|
14
|
+
import { serverModule as providerCustom } from '@app/provider-custom/server';
|
|
14
15
|
// @tbox:imports-end
|
|
15
16
|
|
|
16
17
|
// @tbox:register-begin
|
|
18
|
+
// base 模板出厂预装 provider-custom(自定义 API 落点)+ weather 演示模块
|
|
17
19
|
const modules = [
|
|
18
20
|
moduleWeather,
|
|
21
|
+
providerCustom,
|
|
19
22
|
];
|
|
20
23
|
// @tbox:register-end
|
|
21
24
|
|