@tbox.cn/app-template-agent 0.2.0 → 0.7.5

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 (81) hide show
  1. package/.gitignore +6 -0
  2. package/.tbox/app.json +12 -3
  3. package/AGENTS.md +17 -6
  4. package/ENV.md +36 -5
  5. package/README.md +23 -7
  6. package/apps/client/package.json +3 -2
  7. package/apps/client/src/app.tsx +28 -0
  8. package/apps/client/src/cards/CardRenderer.tsx +4 -5
  9. package/apps/client/src/components/ActionBar/DebugButton.tsx +5 -1
  10. package/apps/client/src/components/ActionBar/index.tsx +7 -1
  11. package/apps/client/src/components/ChatInput/index.less +54 -0
  12. package/apps/client/src/components/ChatInput/index.tsx +33 -9
  13. package/apps/client/src/components/ChatInput/voice/useVoicePress.ts +20 -1
  14. package/apps/client/src/components/MessageBubble/index.less +12 -1
  15. package/apps/client/src/components/MessageBubble/index.tsx +16 -8
  16. package/apps/client/src/components/PullDownIndicator/index.less +1 -1
  17. package/apps/client/src/components/PullDownIndicator/index.tsx +3 -4
  18. package/apps/client/src/components/ToolCallBlock/index.less +29 -0
  19. package/apps/client/src/components/ToolCallBlock/index.tsx +58 -24
  20. package/apps/client/src/hooks/useMediaUpload.ts +7 -2
  21. package/apps/client/src/main.tsx +4 -1
  22. package/apps/client/src/pages/index/index.tsx +35 -13
  23. package/apps/client/src/platform/AgentChatApp.tsx +5 -1
  24. package/apps/client/src/store/chat-config.ts +12 -1
  25. package/apps/client/src/store/chat.ts +62 -92
  26. package/apps/client/vite.config.ts +46 -26
  27. package/apps/server/package.json +5 -11
  28. package/apps/server/src/app.ts +452 -0
  29. package/apps/server/src/cli/collect-registry.ts +7 -6
  30. package/apps/server/src/config/deployment.ts +36 -70
  31. package/apps/server/src/config/integration.ts +81 -0
  32. package/apps/server/src/dev.ts +81 -0
  33. package/apps/server/src/index.ts +23 -376
  34. package/apps/server/src/intent-routes.ts +11 -4
  35. package/apps/server/src/modules.ts +3 -0
  36. package/apps/server/tests/cards-registry.test.ts +9 -17
  37. package/apps/server/tests/deployment-loader.test.ts +140 -0
  38. package/apps/server/tests/intent-dispatcher.test.ts +58 -0
  39. package/apps/server/tests/tool-runtime.test.ts +97 -23
  40. package/apps/server/tsup.config.ts +15 -2
  41. package/config/chat.json +4 -1
  42. package/config/llm_provider.json +3 -1
  43. package/package.json +19 -11
  44. package/packages/contracts-mall/package.json +30 -0
  45. package/packages/contracts-mall/src/index.ts +1 -0
  46. package/packages/contracts-mall/src/runtime.ts +1 -0
  47. package/packages/contracts-mall/src/server/index.ts +1 -0
  48. package/packages/contracts-mall/tsconfig.json +7 -0
  49. package/packages/module-weather/package.json +4 -4
  50. package/packages/module-weather/skills/weather-guide/SKILL.md +3 -0
  51. package/packages/module-weather/src/server/cards/weather/meta.ts +0 -1
  52. package/packages/module-weather/src/server/index.ts +1 -0
  53. package/packages/module-weather/src/server/tool.ts +24 -24
  54. package/packages/provider-custom/AGENTS.md +33 -0
  55. package/packages/provider-custom/LICENSE +21 -0
  56. package/packages/provider-custom/README.md +8 -0
  57. package/packages/provider-custom/package.json +62 -0
  58. package/packages/provider-custom/src/index.ts +5 -0
  59. package/packages/provider-custom/src/server/index.ts +40 -0
  60. package/packages/provider-custom/tbox.module.json +29 -0
  61. package/packages/provider-custom/tests/provider-custom.test.ts +17 -0
  62. package/packages/provider-custom/tsconfig.json +7 -0
  63. package/packages/provider-custom/tsup.config.ts +18 -0
  64. package/pnpm-lock.template.yaml +6297 -0
  65. package/pnpm-workspace.template.yaml +45 -46
  66. package/scripts/build.sh +6 -12
  67. package/scripts/check-workspace.mjs +11 -10
  68. package/scripts/stop.sh +48 -14
  69. package/scripts/verify-generated.mjs +4 -4
  70. package/tsconfig.base.json +1 -0
  71. package/apps/client/babel-plugins/code-inspect.cjs +0 -110
  72. package/apps/client/src/hooks/usePullDownHistory.ts +0 -97
  73. package/apps/server/skills/shopping-guide-demo/SKILL.md +0 -21
  74. package/apps/server/skills/shopping-guide-demo/references/guide/parking.md +0 -11
  75. package/apps/server/skills/shopping-guide-demo/references/mall-directory.md +0 -17
  76. package/apps/server/skills/shopping-guide-demo/references/promotion.md +0 -10
  77. package/apps/server/src/config/feature-gate.ts +0 -17
  78. package/apps/server/tests/feature-gate.test.ts +0 -30
  79. package/config/deployments/development.json +0 -27
  80. package/scripts/check-inspector.sh +0 -91
  81. /package/packages/module-weather/{tbox.component.json → tbox.module.json} +0 -0
@@ -0,0 +1,452 @@
1
+ /**
2
+ * 装配主体(模块级单例):express 链、/api 路由、createEngine、WS、start()/listen、进程防线。
3
+ * 客户端供给由入口决定——静态产物见 index.ts(唯一 tsup entry),vite dev 见 dev.ts(仅 tsx)。
4
+ * import 本模块即完成装配(与旧单文件入口时序等价:listen 仍在 start() 内最后发生)。
5
+ */
6
+ import express from 'express';
7
+ import cors from 'cors';
8
+ import { createServer, type IncomingMessage } from 'http';
9
+ import type { Duplex } from 'node:stream';
10
+ import path from 'path';
11
+ import { existsSync } from 'node:fs';
12
+ import { fileURLToPath } from 'url';
13
+ import { registerAll } from './modules.js';
14
+
15
+ // ===== 模板日志(@tbox.cn/app-sdk/server createLogger:本地时间 + LEVEL + [tag] + req/conv 尾缀) =====
16
+ import { createLogger } from '@tbox.cn/app-sdk/server';
17
+ const serverLogger = createLogger('server');
18
+ const authLogger = createLogger('AUTH');
19
+ const skillLogger = createLogger('SKILL');
20
+ const preToolLogger = createLogger('PRETOOL');
21
+ const actionLogger = createLogger('ACTION');
22
+ const upgradeLogger = createLogger('upgrade');
23
+ import {
24
+ createWsServer,
25
+ createServerContext,
26
+ createEngineFromContext,
27
+ loadProviderCatalog,
28
+ tboxTraceExporter,
29
+ extractImageUrls,
30
+ TBOX_FILE_URL_PREFIX,
31
+ createAuthRuntime,
32
+ createPlatformApiRouter,
33
+ createTtsProxy,
34
+ createModelProvider,
35
+ createPluginIntegration,
36
+ ConversationService,
37
+ loadLocalSkills,
38
+ resolveSkillMode,
39
+ resolvePreToolTimeout,
40
+ resolveToolDebug,
41
+ resolveEarlyToolExecution,
42
+ resolveMemoryStoreGuarded,
43
+ withRecallCache,
44
+ buildMessageInputs,
45
+ loadChatConfig,
46
+ loadAgentPromptConfig,
47
+ toWsChatConfig,
48
+ installTboxDev,
49
+ } from '@tbox.cn/app-sdk/server';
50
+ import { installInspector } from '@tbox.cn/app-sdk/platform';
51
+ import { registerAppTools } from './app-tools.js';
52
+ import { APP_PRE_TOOLS } from './pre-tools.js';
53
+ import { INTENT_ROUTES, createAppIntentDispatcher } from './intent-routes.js';
54
+ import { deploymentFromIntegrations, buildDefaultMallContext } from './config/deployment.js';
55
+ import { loadIntegrations } from './config/integration.js';
56
+ // 011 S2/S3:mall 装配原语(条件装配——未配置部署文件时零引用)
57
+ import {
58
+ createScopeSessionRegistry,
59
+ createActionGrantService,
60
+ createModuleActionDispatcher,
61
+ createEffectRuntime,
62
+ createIntegrationsRegistry,
63
+ createAuthStrategyRegistry,
64
+ createEnvironmentSecretProvider,
65
+ attachCardActionTokens,
66
+ } from '@tbox.cn/app-sdk/server';
67
+
68
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
69
+ // chat.json 配置目录(credentials 同款锚定三元式):dev 态 TBOX_CONFIG_DIR 指档位目录,
70
+ // 否则应用根 config/。src|dist → server → apps → 应用根(与 mall entry 锚点同构)。
71
+ const CONFIG_DIR = process.env.TBOX_CONFIG_DIR
72
+ ? path.resolve(process.env.TBOX_CONFIG_DIR)
73
+ : path.resolve(__dirname, '../../../config');
74
+ const PORT = Number(process.env.PORT) || 8000;
75
+ const HOST = process.env.HOST || '0.0.0.0';
76
+ const isProduction = process.env.NODE_ENV === 'production';
77
+ // 018 tool-debug:模块级单次解析双喂(engine 发射门控 + ws-server HELLO_ACK 旗标同值同源;
78
+ // 非法值只 warn 一次——对齐 skillMode/preToolTimeout 解析先例)
79
+ const toolDebug = resolveToolDebug(process.env.TBOX_TOOL_DEBUG, process.env.NODE_ENV);
80
+ // PR-2 尽早工具执行(缺省 off):多工具轮参数流完即预执行白名单查询工具,
81
+ // 收益依赖 config/llm_provider.json parallelToolCalls 开启;单工具轮行为等价现状
82
+ const earlyToolExecution = resolveEarlyToolExecution(process.env.TBOX_EARLY_TOOL_EXECUTION);
83
+
84
+ /** 应用根探测(apps/server cwd → 逐层向上找 .tbox/app.json;dev/bundle 双态收敛) */
85
+ function appRoot(): string {
86
+ let dir = path.resolve(process.cwd());
87
+ for (let i = 0; i < 4; i++) {
88
+ if (existsSync(path.join(dir, '.tbox', 'app.json'))) return dir;
89
+ const parent = path.dirname(dir);
90
+ if (parent === dir) break;
91
+ dir = parent;
92
+ }
93
+ return path.resolve(process.cwd());
94
+ }
95
+
96
+ const AI_MODEL_NAME = process.env.APP_AI_MODEL_NAME || '';
97
+
98
+ // agent 提示词(config/agent.jsonc;未配置 = SDK 默认兜底;读取一次,改文件需重启生效)
99
+ const AGENT_PROMPT = loadAgentPromptConfig(CONFIG_DIR);
100
+
101
+ // 平台能力装配(003/006 平台轴,已下沉 @tbox.cn/app-sdk/server)。
102
+ // 011 L1 + F2/F3:装配序 integrations → deployment 派生 → auth(v4:部署拓扑并入
103
+ // integrations.json 单文件真源——integrations 先装,deployment 由其投影派生;auth 构造期
104
+ // 直引 validateContext)。上下文统一化 Stage 2:auth 的 validateContext 域校验注入(HTTP
105
+ // body.context 伪冒防线:mallId 形状校验 → 403)与 scopeDefaults 同源。
106
+ const integrationsConfig = loadIntegrations();
107
+ const deployment = integrationsConfig ? deploymentFromIntegrations(integrationsConfig) : undefined;
108
+ const auth = createAuthRuntime(undefined, {
109
+ // validateContext 只在全局面生效(login/dev-login 的 body.context 走 login-routes 独立归一化)
110
+ ...(deployment ? { validateContext: deployment.assembly.validateScope } : {}),
111
+ });
112
+ const { getModel } = createModelProvider();
113
+ const pluginTools = createPluginIntegration();
114
+
115
+ // 017:长期记忆(默认 inmemory 开启;TBOX_MEMORY_STORE=off 逃生舱 / knowledge 需 KB_ID+DOC_ID+API_KEY)。
116
+ // 单实例双消费:engine(recall + 三工具)与 /api/memory 管理面共享同一存储。
117
+ // 017 M4:KB 校验三态(TBOX_MEMORY_KB_VALIDATE:1=异步降级不阻塞启动 / strict=同步 fail-fast /
118
+ // 未设置不校验)——gated store 统一收口,装配面恒定(INV-D),降级只发生在调用执行时。
119
+ const memoryBase = await resolveMemoryStoreGuarded({
120
+ store: process.env.TBOX_MEMORY_STORE,
121
+ kbId: process.env.TBOX_MEMORY_KB_ID,
122
+ kbDocId: process.env.TBOX_MEMORY_KB_DOC_ID,
123
+ kbBaseUrl: process.env.TBOX_MEMORY_KB_BASE_URL,
124
+ kbCookie: process.env.TBOX_MEMORY_KB_COOKIE,
125
+ validate: process.env.TBOX_MEMORY_KB_VALIDATE,
126
+ validateTimeoutMs: process.env.TBOX_MEMORY_KB_VALIDATE_TIMEOUT_MS,
127
+ });
128
+ // B4:recall 缓存装饰器(list 60s TTL + 空结果负缓存短路——零记忆用户 recall <10ms)。
129
+ // 组合序:withRecallCache 包 gated 外层(gate 失败穿透不污染缓存);单实例双消费
130
+ // (engine recall + 三工具 + /api/memory 管理面)共享 wrapped 实例——save 写失效钩子生效。
131
+ const memory = {
132
+ ...memoryBase,
133
+ ...(memoryBase.store ? { store: withRecallCache(memoryBase.store) } : {}),
134
+ };
135
+
136
+ /** 懒初始化单例 ConversationService */
137
+ let _conversationService: ConversationService | null = null;
138
+ function getConversationService(): ConversationService | null {
139
+ if (!_conversationService) {
140
+ const apiKey = process.env.TBOX_API_KEY;
141
+ if (!apiKey) return null;
142
+ _conversationService = new ConversationService(apiKey);
143
+ }
144
+ return _conversationService;
145
+ }
146
+
147
+ // 进程级最后防线
148
+ process.on('unhandledRejection', (reason) => {
149
+ serverLogger.error('unhandledRejection:', reason);
150
+ });
151
+ process.on('uncaughtException', (err) => {
152
+ serverLogger.error('uncaughtException:', err);
153
+ });
154
+
155
+ export const app = express();
156
+ app.use(cors());
157
+ app.use(express.json());
158
+
159
+ // 🔒 平台基础设施 — 禁止 AI 删除或修改(@infrastructure-locked)
160
+ installInspector(app);
161
+
162
+ // 非 prod 默认开启 dev-login:对暴露公网的实例给出可见警示(生产必须 NODE_ENV=production)
163
+ if (auth.config.devLoginEnabled) {
164
+ authLogger.warn(
165
+ 'dev-login 已开启(非 production 默认)。若本实例暴露公网,请设 NODE_ENV=production 或 AUTH_DEV_LOGIN_DISABLED=true。',
166
+ );
167
+ }
168
+
169
+ // 自注册闭环:模块路由挂载到 /api/m/<moduleId>/(在 apiRouter 之前,避免被 /api 兜底 404 截获)
170
+ // ctx 为模块级单例:registerAll 在 start() 内执行,ApiRouteRegistry 共享 root 保证延迟注册即时生效
171
+ // 011 L1 + F2/F3:控制面纯两键(malls + defaultMallId)——旧 ProviderRegistry 链已拆除,
172
+ // provider 绑定唯一真源 = config/integrations.json(槽位轴;与 mall 模板同构);
173
+ // F2:scopeDefaults 中立注入 + authExchanges 同引用(provider 包 register 期自注册换码器)
174
+ const providerCatalog = loadProviderCatalog(appRoot());
175
+ const scopeDefaults = deployment?.controlPlane.defaultMallId
176
+ ? { defaultMallId: deployment.controlPlane.defaultMallId }
177
+ : undefined;
178
+ // 011 B2:可变认证策略注册表——模块 register 期注册(厂商签名策略),transportFactory 同引用
179
+ const authStrategies = createAuthStrategyRegistry();
180
+
181
+ // provider 拆包 F2/F3:integrations 槽位解析轴(config/integrations.json 存在即生效;
182
+ // 缺席 → undefined,模块槽解析与换码链 fail-fast 显性暴露)
183
+ // instanceIdOf 注入 mall 语义(SDK 零领域铁律——mallId 派生归模板层)
184
+ // root config.miniAppId(016 默认商圈兜底拼装用;与 mall 模板同构——env 注入形态无参考表可查)
185
+ const rootMiniAppId =
186
+ typeof integrationsConfig?.config.config?.miniAppId === 'string' && integrationsConfig.config.config.miniAppId
187
+ ? integrationsConfig.config.config.miniAppId
188
+ : undefined;
189
+ const integrations = integrationsConfig
190
+ ? createIntegrationsRegistry({
191
+ integrations: integrationsConfig.config,
192
+ // catalog 真源 = 应用根 .tbox/app.json(上方无条件装载)
193
+ catalog: providerCatalog,
194
+ authStrategies,
195
+ secretProvider: createEnvironmentSecretProvider({
196
+ source: process.env as Record<string, string | undefined>,
197
+ // 文件通道(v2):config/credentials/ 凭据文件优先(ref = 文件名 stem 直引),env 兜底。
198
+ // dev 态 TBOX_CONFIG_DIR 指档位目录 → files 指档位凭据目录;PR-2 前目录缺席零行为。
199
+ ...(process.env.TBOX_CONFIG_DIR
200
+ ? { files: path.resolve(process.env.TBOX_CONFIG_DIR, 'credentials') }
201
+ : { files: path.resolve(__dirname, '../../../config/credentials') }),
202
+ allowedHeaderNames: ['content-type', 'sign', 'timestamp', 'publickey', 'x-api-key'],
203
+ production: isProduction,
204
+ }),
205
+ instanceIdOf: (scope) => scope.mallId ?? '',
206
+ // F5 观测轴:provider factory 收到的 transport 统一包外部调用日志(→/← path durMs)
207
+ logger: createLogger('provider'),
208
+ })
209
+ : undefined;
210
+
211
+ const ctx = createServerContext({
212
+ authStrategies,
213
+ authExchanges: auth.exchanges,
214
+ ...(memory.store ? { memory: memory.store } : {}),
215
+ ...(integrations ? { integrations } : {}),
216
+ ...(scopeDefaults ? { scopeDefaults } : {}),
217
+ });
218
+
219
+ // 全局 /api 鉴权(H2 挂载顺序契约):除 public(login/dev-login/health)外一律要求 Bearer token;
220
+ // optional 模式缺失 token → 匿名身份(写通道仍被 action gateway 拒绝)
221
+ // ⚠️ 必须先于 /api/m、/api/action、createPlatformApiRouter 挂载,保证全 API 面受鉴权约束
222
+ app.use('/api', auth.middleware);
223
+
224
+ app.use('/api/m', ctx.routes.mount());
225
+
226
+ // 业务动作网关(004 D3/C2):POST /api/action — SDK 黑盒唯一写入口(幂等/防重放/签名槽位;
227
+ // 006 D11:匿名写拒绝,不随 AUTH_MODE 变)
228
+ app.use('/api/action', ctx.actions.middleware());
229
+
230
+ // API 路由(平台路由面:auth/health/tbox/conversation [+memory] + 404 兜底;卡片恢复内部化)
231
+ // 017:装配记忆时挂载 /api/memory 管理面(与引擎同源 store)
232
+ app.use('/api', createPlatformApiRouter({ ctx, auth, ...(memory.store ? { memory: memory.store } : {}) }));
233
+
234
+ // tbox dev 工具族(/_tbox:卡片预览 API + 页面族;dev-only 自门控——NODE_ENV=production
235
+ // 零挂载;import 期注册先于 dev.ts 运行期 vite middleware,路由序恒胜)
236
+ installTboxDev(app, { ctx, auth });
237
+
238
+ // 从 ServerContext 构造 ChatEngine(自注册闭环组装端)
239
+ async function createEngine(
240
+ extra?: { cardActionAttacher: (payload: import('@tbox.cn/app-contracts').TboxCardPayload, sessionId: string) => Promise<import('@tbox.cn/app-contracts').TboxCardPayload> },
241
+ ) {
242
+ // 009:应用级本地 skills(SKILL.md 目录格式,dev/dist 路径恒等 = src/dist 平级 + __dirname 绝对解析)
243
+ const appSkills = await loadLocalSkills(path.resolve(__dirname, '../skills'));
244
+ const skillMode = resolveSkillMode(process.env.TBOX_SKILL_MODE);
245
+ // 010:单次解析(引擎选项与启动日志同源,非法值只 warn 一次)
246
+ const preToolTimeoutMs = resolvePreToolTimeout(process.env.TBOX_PRE_TOOL_TIMEOUT_MS);
247
+
248
+ const engine = await createEngineFromContext(ctx, {
249
+ instructions: AGENT_PROMPT.instructions,
250
+ model: getModel(AI_MODEL_NAME),
251
+ traceExporter: tboxTraceExporter,
252
+ modelName: AI_MODEL_NAME,
253
+ fileUrlPrefix: TBOX_FILE_URL_PREFIX,
254
+ // 009:双模式全局配置(accelerated 默认:本地路由命中 → reminder 注入当前轮,零额外 LLM 轮次;
255
+ // progressive:全量注册 InlineSkill,模型渐进 skill_read)
256
+ skillMode,
257
+ skills: appSkills,
258
+ // 010:前置工具加速(应用级声明 + 单工具预算;TBOX_PRE_TOOL_TIMEOUT_MS=0 整体禁用)
259
+ preTools: APP_PRE_TOOLS,
260
+ preToolTimeoutMs,
261
+ // 018 tool-debug:工具调试视图(memory/silent preTool 前端表达;缺省关)
262
+ toolDebug,
263
+ // PR-2 尽早工具执行(缺省 off;env TBOX_EARLY_TOOL_EXECUTION=1 开启)
264
+ earlyToolExecution,
265
+ // 017:长期记忆(默认 inmemory;off 时 enabled=false 不装配)
266
+ ...(memory.enabled && memory.store ? { memory: { store: memory.store } } : {}),
267
+ // 011 B3:卡片动作 token 签发器(mall 装配时注入)
268
+ cardActionAttacher: extra?.cardActionAttacher,
269
+ // B21:意图路由接线(业务意图表 + handlerDispatch)
270
+ intentRoutes: INTENT_ROUTES,
271
+ handlerDispatch: createAppIntentDispatcher(ctx),
272
+ loadPlugins: async () => {
273
+ const loaded = await pluginTools.loadTools({
274
+ reservedToolIds: Object.keys(ctx.tools.getAll()),
275
+ });
276
+ return { tools: loaded.tools, references: loaded.references };
277
+ },
278
+ saveMessage: async ({ conversationId, query, answer, mediaItems, outerBusinessId, cards, skills, preTools }) => {
279
+ const agentId = process.env.TBOX_APP_ID;
280
+ const service = getConversationService();
281
+ if (!service || !agentId) return undefined;
282
+ const imageUrls = extractImageUrls(mediaItems, TBOX_FILE_URL_PREFIX);
283
+ // P3 W1:卡片快照随消息持久化(MessageEmbedded,004 D1)。
284
+ // 009 D10:skill 加载记录随消息持久化(inputs.skills,前端历史表达重建)。
285
+ // 010:前置工具记录随消息持久化(inputs.preTools,前端历史表达重建)。
286
+ // inputs「非空才加」组装规则已下沉 SDK buildMessageInputs(010 S3)。
287
+ // 若平台拒绝 inputs(V1 验证失败),此处切换为 extraParams 字符串或 FileCardPersistenceBackend 落盘。
288
+ const inputs = buildMessageInputs({ cards, skills, preTools });
289
+ return await service.saveMessage({
290
+ conversationId,
291
+ agentId,
292
+ query,
293
+ answer,
294
+ ...(outerBusinessId && { outerBusinessId }),
295
+ ...(imageUrls.length > 0 && {
296
+ multiModalInputs: { imageUrls },
297
+ }),
298
+ ...(inputs && { inputs }),
299
+ });
300
+ },
301
+ });
302
+
303
+ // 009:启动日志诊断(仅名称/描述/路由词,不含 SOP 内容)
304
+ skillLogger.info(
305
+ `mode=${skillMode} skills=${engine.listSkills().map((s) => s.name).join(',') || '(none)'}`,
306
+ );
307
+ // 010:前置工具诊断(仅 toolId/描述/路由形态,不含 decide 逻辑)
308
+ preToolLogger.info(
309
+ `timeoutMs=${preToolTimeoutMs} preTools=${engine.listPreTools().map((d) => d.toolId).join(',') || '(none)'}`,
310
+ );
311
+ // 017:记忆诊断(装配形态 + 能力面 + 校验模式;recall 配置行由 SDK [MEMORY] 构造期日志输出)
312
+ const memoryLogger = createLogger('MEMORY');
313
+ memoryLogger.info(
314
+ `store=${memory.mode}${memory.store?.capabilities ? ` caps=${Object.entries(memory.store.capabilities).filter(([, v]) => v).map(([k]) => k).join(',') || 'none'}` : ''} validate=${memory.validateMode ?? 'off'}`,
315
+ );
316
+ return engine;
317
+ }
318
+
319
+ const httpServer = createServer(app);
320
+
321
+ /** dev 态升级扩展点(dev.ts 注册):返回 true = 已认领,app 路由跳过。生产 bundle 恒空。 */
322
+ export type UpgradeDelegate = (request: IncomingMessage, socket: Duplex, head: Buffer) => boolean;
323
+ let upgradeDelegate: UpgradeDelegate | undefined;
324
+ export function registerUpgradeDelegate(delegate: UpgradeDelegate): void {
325
+ upgradeDelegate = delegate;
326
+ }
327
+
328
+ export async function start() {
329
+ // 011 S6:mall 装配 + 生产环境缺动作授权密钥 → 启动期提示(签发路径运行时兜底报错)
330
+ if (deployment && isProduction && !process.env.ACTION_TOKEN_SECRET && !process.env.AUTH_TOKEN_SECRET) {
331
+ actionLogger.warn('生产环境未配置 ACTION_TOKEN_SECRET/AUTH_TOKEN_SECRET——卡片动作授权装配将 fail-fast 拒绝启动');
332
+ }
333
+
334
+ // 注册业务模块(自注册闭环)
335
+ await registerAll(ctx);
336
+
337
+ // 模板演示工具(apps 级工具,P3 建议迁入模块):不参与模块体系,经统一注册面注册
338
+ // (010:start() 与 cli/collect-registry.ts 双处同调,防注册面失同步)
339
+ registerAppTools(ctx);
340
+
341
+ // 011 F2/E2:mall 装配(条件)——scope 会话 + 动作授权先行(engine attacher 依赖)
342
+ const scopeSessions: ReturnType<typeof createScopeSessionRegistry> | undefined = deployment
343
+ ? createScopeSessionRegistry({
344
+ extractScopeKey: deployment.assembly.extractScopeKey,
345
+ validateScope: deployment.assembly.validateScope,
346
+ buildScope: deployment.assembly.buildScope,
347
+ })
348
+ : undefined;
349
+ const grants = scopeSessions ? createActionGrantService({ store: ctx.state }) : undefined;
350
+
351
+ // 011 B3:卡片动作 token 签发(build → attach → send;sessionId 引擎登记、scope 经 sessions 查)
352
+ const cardActionAttacher = grants && scopeSessions
353
+ ? async (
354
+ payload: import('@tbox.cn/app-contracts').TboxCardPayload,
355
+ sessionId: string,
356
+ ): Promise<import('@tbox.cn/app-contracts').TboxCardPayload> => {
357
+ const session = scopeSessions.get(sessionId);
358
+ if (!session) return payload;
359
+ return attachCardActionTokens({
360
+ payload,
361
+ actions: ctx.cardActions,
362
+ grants: grants,
363
+ sessionId: session.sessionId,
364
+ scope: session.scopeFlat,
365
+ });
366
+ }
367
+ : undefined;
368
+ const engine = await createEngine(cardActionAttacher ? { cardActionAttacher } : undefined);
369
+
370
+ // 011 F2/E2:模块动作分发(依赖 scopeSessions/grants)
371
+ let moduleActions: ReturnType<typeof createModuleActionDispatcher> | undefined;
372
+ if (deployment && scopeSessions && grants) {
373
+ moduleActions = createModuleActionDispatcher({
374
+ grants,
375
+ sessions: scopeSessions,
376
+ state: ctx.state,
377
+ actions: ctx.cardActions,
378
+ cardRegistry: ctx.cards.getAll(),
379
+ // 015 P3:effect 运行时纯投影(payment/authorization 回调补签在 dispatcher step7,
380
+ // 客户端执行在 SDK host-effect-runner——模板零装配效果链)
381
+ effects: createEffectRuntime(),
382
+ extractScopeKey: deployment.assembly.extractScopeKey,
383
+ });
384
+ for (const action of ctx.cardActions.getAll().values()) {
385
+ moduleActions.register(action);
386
+ }
387
+ }
388
+
389
+ // chat.json 真读(原 scope 分支内 demo 硬编码移除,快捷指令迁入 config/chat.json——与 mall 同构:
390
+ // 配置单一真源 + settings 运行时下发;chatConfig 无条件传递,非 scope 部署亦发 ack)
391
+ const loadedChatConfig = loadChatConfig(CONFIG_DIR);
392
+ const chatWss = createWsServer(engine, {
393
+ auth: auth.wsAuth,
394
+ // 018 tool-debug:HELLO_ACK 旗标(仅开启时携带;与 engine.toolDebug 同值同源)
395
+ toolDebug,
396
+ ...(scopeSessions
397
+ ? {
398
+ scopeSessions,
399
+ ...(moduleActions ? { moduleActions } : {}),
400
+ // 016:默认商圈兜底——无 URL 参数访问时服务端补 HELLO context(显式参数优先);
401
+ // root config.miniAppId 优先(读源统一,回落 malls[].miniAppId;与 mall 模板同构)
402
+ ...(deployment
403
+ ? { defaultContext: buildDefaultMallContext(deployment.controlPlane, rootMiniAppId) }
404
+ : {}),
405
+ }
406
+ : {}),
407
+ chatConfig: toWsChatConfig(loadedChatConfig),
408
+ });
409
+ const ttsWss = createTtsProxy();
410
+
411
+ httpServer.on('error', (err: NodeJS.ErrnoException) => {
412
+ if (err.code === 'EADDRINUSE') {
413
+ serverLogger.error(`Port ${PORT} is already in use.`);
414
+ } else if (err.code === 'EACCES') {
415
+ serverLogger.error(`Permission denied while binding ${HOST}:${PORT}.`);
416
+ } else {
417
+ serverLogger.error('Failed to start HTTP server:', err);
418
+ }
419
+ process.exit(1);
420
+ });
421
+
422
+ httpServer.on('upgrade', (request, socket, head) => {
423
+ socket.on('error', (e) => upgradeLogger.warn('socket error:', e.message));
424
+ if (upgradeDelegate?.(request, socket, head)) return;
425
+ const { pathname } = new URL(request.url || '/', 'http://localhost');
426
+
427
+ if (pathname === '/ws') {
428
+ chatWss.handleUpgrade(request, socket, head, (ws) => {
429
+ chatWss.emit('connection', ws, request);
430
+ });
431
+ } else if (pathname === '/ws/tts') {
432
+ // 通道门与 UI 同值同源(chat.json enableTTS 单源):关态硬拒 upgrade,
433
+ // 防服务端开着 TTS 代理但 UI 已隐藏按钮的僵尸路径;destroy 于 handleUpgrade 之前
434
+ if (!loadedChatConfig.enableTTS) {
435
+ upgradeLogger.warn('TTS disabled (chat.json enableTTS:false) — /ws/tts upgrade rejected');
436
+ socket.destroy();
437
+ return;
438
+ }
439
+ ttsWss.handleUpgrade(request, socket, head, (ws) => {
440
+ ttsWss.emit('connection', ws, request);
441
+ });
442
+ } else {
443
+ socket.destroy();
444
+ }
445
+ });
446
+
447
+ httpServer.listen(PORT, HOST, () => {
448
+ serverLogger.info(`🚀 Server running at http://${HOST}:${PORT}`);
449
+ serverLogger.info(`📡 WebSocket endpoint: ws://${HOST}:${PORT}/ws`);
450
+ serverLogger.info(`🔊 TTS endpoint: ws://${HOST}:${PORT}/ws/tts`);
451
+ });
452
+ }
@@ -1,15 +1,17 @@
1
- import { createServerContext, buildToolCardBindings } from '@tbox.cn/app-sdk/server';
1
+ import { createServerContext } from '@tbox.cn/app-sdk/server';
2
+ import type { ToolCardBinding } from '@tbox.cn/agent-plugin/cards';
2
3
  import { registerAll } from '../modules.js';
3
4
  import { registerAppTools } from '../app-tools.js';
4
5
 
5
6
  export interface RegistryState {
6
7
  staticToolIds: string[];
7
8
  cardTypes: string[];
8
- bindings: ReturnType<typeof buildToolCardBindings>;
9
+ /** v2 出卡协议:tool→card 静态绑定退场(出卡由工具出参 cards 运行期声明),恒空数组 */
10
+ bindings: ToolCardBinding[];
9
11
  }
10
12
 
11
13
  /**
12
- * 从自注册闭环收集当前注册状态(静态工具 id / cardType / tool→card 绑定)。
14
+ * 从自注册闭环收集当前注册状态(静态工具 id / cardType)。
13
15
  * 供 plugin-doctor / plugin-inspect 复用——替代已删除的手写 tools/registry.ts + cards/registry.ts。
14
16
  * 与运行时(apps/server/src/index.ts start())保持一致的注册面:registerAll + registerAppTools
15
17
  * (010:apps 级演示工具统一注册面,双处同调防失同步)。
@@ -18,10 +20,9 @@ export async function collectRegistryState(): Promise<RegistryState> {
18
20
  const ctx = createServerContext();
19
21
  await registerAll(ctx);
20
22
  registerAppTools(ctx); // 与运行时一致(translate + clock 为 apps 级演示工具)
21
- const cards = ctx.cards.getAll();
22
23
  return {
23
24
  staticToolIds: Object.keys(ctx.tools.getAll()),
24
- cardTypes: Object.keys(cards),
25
- bindings: buildToolCardBindings(cards),
25
+ cardTypes: Object.keys(ctx.cards.getAll()),
26
+ bindings: [],
26
27
  };
27
28
  }
@@ -1,81 +1,47 @@
1
1
  /**
2
- * 部署控制平面 loader(011 L2:模板装配点专用)。
3
- * - DEPLOYMENT_CONTROL_PLANE_PATH 存在 → 读取 + zod 校验 + 无内联密钥断言;
4
- * - 未配置 → 返回 undefined(模板退化为通用助手形态,非 mall 部署)。
2
+ * mall 装配派生层(integrations v4:部署拓扑并入 integrations.json——单文件真源;
3
+ * 与 mall 模板同构,base 无 mallName 包装链)。
4
+ * - 旧 `config/deployments/development.json` 已退役:stale 防线在 integration.ts 装载链
5
+ * (在场且含 malls 键 → error 迁移指引;support 独立体系不受影响);
6
+ * - controlPlane 来源 = integration.ts 的 mallControlPlaneOf 投影(attributes 赋义 zod
7
+ * 单源上收 contracts-mall——本文件不再内嵌 schema);
5
8
  * - mall 语义集中在 contracts-mall(TIER1);apps 只做装配组合(011 L6 checklist)。
6
9
  */
7
- import { readFileSync } from 'node:fs';
8
- import { z } from 'zod';
9
- import {
10
- createMallRuntimeAssembly,
11
- type MallDeploymentControlPlane,
12
- type MallRuntimeAssembly,
13
- } from '@tbox.cn/app-contracts-mall';
10
+ import { createLogger } from '@tbox.cn/app-sdk/server';
11
+ import { createMallRuntimeAssembly, type MallDeploymentControlPlane, type MallRuntimeAssembly } from '@app/contracts-mall';
12
+ import type { LoadedIntegrations } from './integration.js';
14
13
 
15
- const providerInstanceSchema = z.object({
16
- providerInstanceId: z.string().min(1),
17
- providerTemplateId: z.string().min(1),
18
- scope: z.record(z.string(), z.string()).optional(),
19
- config: z.record(z.string(), z.unknown()).optional(),
20
- credentialRef: z.string().optional(),
21
- credentialVersion: z.string().optional(),
22
- enabled: z.boolean(),
23
- });
24
-
25
- const controlPlaneSchema = z.object({
26
- deployment: z.object({
27
- miniAppId: z.string().min(1),
28
- agentId: z.string().min(1),
29
- components: z.array(
30
- z.object({
31
- componentId: z.string().min(1),
32
- enabledFeatures: z.array(z.string()).optional(),
33
- optional: z.boolean().optional(),
34
- }),
35
- ),
36
- }),
37
- malls: z.array(z.object({ mallId: z.string().min(1), name: z.string().min(1) })),
38
- providerTemplates: z
39
- .array(
40
- z.object({
41
- providerTemplateId: z.string().min(1),
42
- credentialType: z.string().min(1),
43
- }),
44
- )
45
- .optional(),
46
- providerInstances: z.array(providerInstanceSchema),
47
- });
48
-
49
- /** 无内联密钥断言(011 L2):命中敏感键名即拒(部署配置只放引用不放机密) */
50
- const SECRET_KEY_PATTERN = /api[-_]?key|private[-_]?key|secret|password|token|credential/i;
51
-
52
- function assertNoInlineSecrets(config: unknown, path = '$'): void {
53
- if (Array.isArray(config)) {
54
- config.forEach((item, i) => assertNoInlineSecrets(item, `${path}[${i}]`));
55
- return;
56
- }
57
- if (config && typeof config === 'object') {
58
- for (const [k, v] of Object.entries(config as Record<string, unknown>)) {
59
- if (SECRET_KEY_PATTERN.test(k)) {
60
- throw new Error(`部署配置含疑似内联密钥字段:${path}.${k}(机密只允许经 credentialRef 引用环境变量)`);
61
- }
62
- assertNoInlineSecrets(v, `${path}.${k}`);
63
- }
64
- }
65
- }
14
+ const deploymentLogger = createLogger('DEPLOYMENT');
66
15
 
67
16
  export interface LoadedDeployment {
68
17
  controlPlane: MallDeploymentControlPlane;
69
18
  assembly: MallRuntimeAssembly;
70
19
  }
71
20
 
72
- /** 读取并校验部署控制平面(路径未配置 → undefined = 非 mall 部署) */
73
- export function loadDeploymentControlPlane(envPath?: string): LoadedDeployment | undefined {
74
- const p = envPath ?? process.env.DEPLOYMENT_CONTROL_PLANE_PATH;
75
- if (!p) return undefined;
76
- const raw = JSON.parse(readFileSync(p, 'utf8'));
77
- assertNoInlineSecrets(raw);
78
- const parsed = controlPlaneSchema.parse(raw);
79
- const controlPlane = parsed as MallDeploymentControlPlane;
80
- return { controlPlane, assembly: createMallRuntimeAssembly(controlPlane) };
21
+ /** integrations 装载产物 → mall 装配(恒投影——无参考表 = 空平面,装配零拓扑分支) */
22
+ export function deploymentFromIntegrations(loaded: LoadedIntegrations): LoadedDeployment {
23
+ const assembly = createMallRuntimeAssembly(loaded.controlPlane);
24
+ return { controlPlane: loaded.controlPlane, assembly };
25
+ }
26
+
27
+ /**
28
+ * 016:默认商圈 HELLO context(无 URL 参数访问时的服务端兜底;与 mall 模板同构)。
29
+ * v4 mallId-only 注入(D8/D9):miniAppId 是尽力回填的展示值,非会话边界——
30
+ * 读源统一 root config.miniAppId 优先、回落参考表 attributes.miniAppId;双缺不再
31
+ * 阻断注入(此前 warn-refuse 会造成默认商圈静默失效),按 mallId-only 注入。
32
+ * 缺省 defaultMallId → undefined(不兜底)。
33
+ */
34
+ export function buildDefaultMallContext(
35
+ controlPlane: MallDeploymentControlPlane,
36
+ rootMiniAppId?: string,
37
+ ): Record<string, unknown> | undefined {
38
+ if (!controlPlane.defaultMallId) return undefined;
39
+ const mall = controlPlane.malls.find((m) => m.mallId === controlPlane.defaultMallId);
40
+ const miniAppId = rootMiniAppId ?? mall?.miniAppId;
41
+ if (!miniAppId) {
42
+ deploymentLogger.warn(
43
+ `defaultMallId=${controlPlane.defaultMallId} 无 miniAppId 真源(root config 与参考表双缺)——按 mallId-only 注入默认商圈`,
44
+ );
45
+ }
46
+ return { mall: { mallId: controlPlane.defaultMallId, ...(miniAppId ? { miniAppId } : {}) } };
81
47
  }