@tbox.cn/app-template-agent 0.10.20 → 0.10.22

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.
@@ -16,11 +16,11 @@ catalogs:
16
16
  specifier: 0.2.0
17
17
  version: 0.2.0
18
18
  '@tbox.cn/app-agent-sdk-server':
19
- specifier: 0.3.0
20
- version: 0.3.0
19
+ specifier: 0.3.1
20
+ version: 0.3.1
21
21
  '@tbox.cn/app-contracts':
22
- specifier: 0.13.1
23
- version: 0.13.1
22
+ specifier: 0.13.3
23
+ version: 0.13.3
24
24
  '@tbox.cn/app-sdk-client':
25
25
  specifier: 0.21.3
26
26
  version: 0.21.3
@@ -28,8 +28,8 @@ catalogs:
28
28
  specifier: 0.26.0
29
29
  version: 0.26.0
30
30
  '@tbox.cn/app-theme-tbox':
31
- specifier: 0.1.4
32
- version: 0.1.4
31
+ specifier: 0.1.5
32
+ version: 0.1.5
33
33
  '@types/express':
34
34
  specifier: 4.17.25
35
35
  version: 4.17.25
@@ -136,10 +136,10 @@ importers:
136
136
  version: 0.2.0(@types/react@18.3.31)
137
137
  '@tbox.cn/app-contracts':
138
138
  specifier: 'catalog:'
139
- version: 0.13.1
139
+ version: 0.13.3
140
140
  '@tbox.cn/app-theme-tbox':
141
141
  specifier: 'catalog:'
142
- version: 0.1.4
142
+ version: 0.1.5
143
143
  react:
144
144
  specifier: 'catalog:'
145
145
  version: 18.3.1
@@ -218,10 +218,10 @@ importers:
218
218
  version: 0.1.0(@mastra/core@1.59.0(ai@6.0.246(zod@3.25.76))(express@4.22.2)(rxjs@7.8.1)(zod@3.25.76))(@tbox-dev-js/sdk@0.1.18)(zod@3.25.76)
219
219
  '@tbox.cn/app-agent-sdk-server':
220
220
  specifier: 'catalog:'
221
- version: 0.3.0(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
221
+ version: 0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
222
222
  '@tbox.cn/app-contracts':
223
223
  specifier: 'catalog:'
224
- version: 0.13.1
224
+ version: 0.13.3
225
225
  express:
226
226
  specifier: 'catalog:'
227
227
  version: 4.22.2
@@ -261,7 +261,7 @@ importers:
261
261
  version: 0.2.0(@types/react@18.3.31)
262
262
  '@tbox.cn/app-contracts':
263
263
  specifier: 'catalog:'
264
- version: 0.13.1
264
+ version: 0.13.3
265
265
  antd-mobile:
266
266
  specifier: 5.42.3
267
267
  version: 5.42.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -310,7 +310,7 @@ importers:
310
310
  dependencies:
311
311
  '@tbox.cn/app-contracts':
312
312
  specifier: 'catalog:'
313
- version: 0.13.1
313
+ version: 0.13.3
314
314
  zod:
315
315
  specifier: 3.25.76
316
316
  version: 3.25.76
@@ -341,10 +341,10 @@ importers:
341
341
  version: 0.2.0(@types/react@18.3.31)
342
342
  '@tbox.cn/app-agent-sdk-server':
343
343
  specifier: 'catalog:'
344
- version: 0.3.0(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
344
+ version: 0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
345
345
  '@tbox.cn/app-contracts':
346
346
  specifier: 'catalog:'
347
- version: 0.13.1
347
+ version: 0.13.3
348
348
  react:
349
349
  specifier: 18.3.1
350
350
  version: 18.3.1
@@ -384,11 +384,14 @@ importers:
384
384
  version: 0.2.0(@types/react@18.3.31)
385
385
  '@tbox.cn/app-agent-sdk-server':
386
386
  specifier: 'catalog:'
387
- version: 0.3.0(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
387
+ version: 0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
388
388
  '@tbox.cn/app-contracts':
389
389
  specifier: 'catalog:'
390
- version: 0.13.1
390
+ version: 0.13.3
391
391
  devDependencies:
392
+ '@app/contracts-mall':
393
+ specifier: workspace:*
394
+ version: link:../contracts-mall
392
395
  '@types/node':
393
396
  specifier: 24.13.3
394
397
  version: 24.13.3
@@ -1415,8 +1418,8 @@ packages:
1415
1418
  resolution: {integrity: sha512-yxK5vLuERzlebTwiw8WxU6e9rm7F1L9tVW4aSUGsIxeU0y8VtRcrh7y8xKbrlXR4WL4XrmYZ6ZoDyOuro9sn5A==}
1416
1419
  engines: {node: '>=20.0.0'}
1417
1420
 
1418
- '@tbox.cn/app-agent-sdk-server@0.3.0':
1419
- resolution: {integrity: sha512-thnnPwA0Tkb3yf+HxmeIEDyji6myIJVKvyhzn0WWuVJN7mg9I6+juTimxwRNhqnpZBjue3kExMw75+MvTineYA==}
1421
+ '@tbox.cn/app-agent-sdk-server@0.3.1':
1422
+ resolution: {integrity: sha512-VWAlG+Efjyo6Wwj4QPKO9N4DPjhG20z3t9cGjzjW8ZvUFU7hQ7NngbFh7Z7IXtYrXJCGpjJSGtMLJ9AgBk67iQ==}
1420
1423
  engines: {node: '>=20.0.0'}
1421
1424
 
1422
1425
  '@tbox.cn/app-contracts@0.13.0':
@@ -1427,6 +1430,14 @@ packages:
1427
1430
  resolution: {integrity: sha512-Cty4qoknyihli6ZMlxOqqTgRqwg0NOh0WOMfIWdB4rHua0QzZkP1jUoeffq5imnpIh7vXdfLhDpv/JwTrHd1XQ==}
1428
1431
  engines: {node: '>=20.0.0'}
1429
1432
 
1433
+ '@tbox.cn/app-contracts@0.13.2':
1434
+ resolution: {integrity: sha512-0lY9g29ice7xYbmV5XDnEiMmM70jvSzeK2cciUZVpzHgGykWqZiPRlqXIGkIRY7uObDQ1cICHC4mS67HuNQ8Jg==}
1435
+ engines: {node: '>=20.0.0'}
1436
+
1437
+ '@tbox.cn/app-contracts@0.13.3':
1438
+ resolution: {integrity: sha512-P16LFd+ZzIASrujs6fyaOEwRnsLg7Lh5k1qOVDNblmohrvUbVIOK5ntn+7mupJxnjK7SY43RUT4mtWtfM85/rQ==}
1439
+ engines: {node: '>=20.0.0'}
1440
+
1430
1441
  '@tbox.cn/app-sdk-client@0.21.3':
1431
1442
  resolution: {integrity: sha512-64y22k5gzCc9eEQ0IJA1izxPaosNEmoCJ4YrsfHOoHYq2UPJCCmP7RIu3Rn4stPZ4tcmRdMdAi2y5499KObYHw==}
1432
1443
  engines: {node: '>=20.0.0'}
@@ -1435,13 +1446,17 @@ packages:
1435
1446
  resolution: {integrity: sha512-4kcoxnZnPgsO0RwAsjsodOXRhZNQcr9r3INN3Z03/JR2e3E+izkW2YHsgGcdbVSVX81s8gZrkaBbE+xco3wSqg==}
1436
1447
  engines: {node: '>=20.0.0'}
1437
1448
 
1449
+ '@tbox.cn/app-sdk-core@0.20.5':
1450
+ resolution: {integrity: sha512-HLLRg1y2jB32uJPL3/a387eABdNZUxrmkGoJwIv44XV9B94DXmTgb9vxYzbyBa0FazxXG0WudyzFnmH5eeqqrw==}
1451
+ engines: {node: '>=20.0.0'}
1452
+
1438
1453
  '@tbox.cn/app-sdk-server@0.26.0':
1439
1454
  resolution: {integrity: sha512-VpV3sf4K/2MEsnkuK5EqZUyf9KoSVHeJUm4Al0M6R4Wumg6183apnxB2f38S5wuP30JSJShN/ww63EzRniTrjw==}
1440
1455
  engines: {node: '>=20.0.0'}
1441
1456
  hasBin: true
1442
1457
 
1443
- '@tbox.cn/app-theme-tbox@0.1.4':
1444
- resolution: {integrity: sha512-VF2fjTBFKJ6x/wvM6/T0qi7dmoiN1stNIT2xDerYbBBLCggr7f+H0v6VtuFMSed6tirlcpAlqtBDOctxudHJlQ==}
1458
+ '@tbox.cn/app-theme-tbox@0.1.5':
1459
+ resolution: {integrity: sha512-NL3xmPQOuXBf7vWutU3dWLhvhCvQSA6cHGH76qcqBkPSAgUlKacE1G67jOFyWlC9rg5oMWVOdrkniXvhi5v/4w==}
1445
1460
 
1446
1461
  '@tbox.cn/app-toolkit@0.8.0':
1447
1462
  resolution: {integrity: sha512-urefCf5DCWbH9wM8dSGavA9MNdY4gx7LY16+s9jIlwhahq83mfrCP2h9ombn44ljMrv04G7mnb3vMtNeWs5/Ug==}
@@ -4167,7 +4182,7 @@ snapshots:
4167
4182
  - '@types/react'
4168
4183
  - immer
4169
4184
 
4170
- '@tbox.cn/app-agent-sdk-server@0.3.0(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)':
4185
+ '@tbox.cn/app-agent-sdk-server@0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)':
4171
4186
  dependencies:
4172
4187
  '@ag-ui/core': 0.0.47
4173
4188
  '@ai-sdk/openai-compatible': 2.0.68(zod@3.25.76)
@@ -4175,8 +4190,8 @@ snapshots:
4175
4190
  '@mastra/core': 1.59.0(ai@6.0.246(zod@3.25.76))(express@4.22.2)(rxjs@7.8.1)(zod@3.25.76)
4176
4191
  '@mastra/observability': 1.16.5(@mastra/core@1.59.0(ai@6.0.246(zod@3.25.76))(express@4.22.2)(rxjs@7.8.1)(zod@3.25.76))(zod@3.25.76)
4177
4192
  '@tbox.cn/agent-plugin': 0.1.0(@mastra/core@1.59.0(ai@6.0.246(zod@3.25.76))(express@4.22.2)(rxjs@7.8.1)(zod@3.25.76))(@tbox-dev-js/sdk@0.1.18)(zod@3.25.76)
4178
- '@tbox.cn/app-contracts': 0.13.1
4179
- '@tbox.cn/app-sdk-core': 0.20.4
4193
+ '@tbox.cn/app-contracts': 0.13.2
4194
+ '@tbox.cn/app-sdk-core': 0.20.5
4180
4195
  '@tbox.cn/app-sdk-server': 0.26.0
4181
4196
  ai: 6.0.246(zod@3.25.76)
4182
4197
  express: 4.22.2
@@ -4207,6 +4222,18 @@ snapshots:
4207
4222
  react: 18.3.1
4208
4223
  zod: 3.25.76
4209
4224
 
4225
+ '@tbox.cn/app-contracts@0.13.2':
4226
+ dependencies:
4227
+ '@ag-ui/core': 0.0.47
4228
+ react: 18.3.1
4229
+ zod: 3.25.76
4230
+
4231
+ '@tbox.cn/app-contracts@0.13.3':
4232
+ dependencies:
4233
+ '@ag-ui/core': 0.0.47
4234
+ react: 18.3.1
4235
+ zod: 3.25.76
4236
+
4210
4237
  '@tbox.cn/app-sdk-client@0.21.3':
4211
4238
  dependencies:
4212
4239
  '@tbox.cn/app-contracts': 0.13.0
@@ -4218,6 +4245,10 @@ snapshots:
4218
4245
  dependencies:
4219
4246
  '@tbox.cn/app-contracts': 0.13.0
4220
4247
 
4248
+ '@tbox.cn/app-sdk-core@0.20.5':
4249
+ dependencies:
4250
+ '@tbox.cn/app-contracts': 0.13.2
4251
+
4221
4252
  '@tbox.cn/app-sdk-server@0.26.0':
4222
4253
  dependencies:
4223
4254
  '@ag-ui/core': 0.0.47
@@ -4230,7 +4261,7 @@ snapshots:
4230
4261
  transitivePeerDependencies:
4231
4262
  - supports-color
4232
4263
 
4233
- '@tbox.cn/app-theme-tbox@0.1.4': {}
4264
+ '@tbox.cn/app-theme-tbox@0.1.5': {}
4234
4265
 
4235
4266
  '@tbox.cn/app-toolkit@0.8.0':
4236
4267
  dependencies:
@@ -38,14 +38,14 @@ catalog:
38
38
  ws: 8.21.3
39
39
  "@types/ws": 8.18.1
40
40
  "@tbox.cn/agent-plugin": 0.1.0
41
- "@tbox.cn/app-sdk-core": 0.20.4
41
+ "@tbox.cn/app-sdk-core": 0.20.5
42
42
  "@tbox.cn/app-sdk-client": 0.21.3
43
43
  "@tbox.cn/app-sdk-server": 0.26.0
44
- "@tbox.cn/app-agent-sdk-server": 0.3.0
44
+ "@tbox.cn/app-agent-sdk-server": 0.3.1
45
45
  "@tbox.cn/app-agent-sdk-client": 0.2.0
46
- "@tbox.cn/app-contracts": 0.13.1
47
- "@tbox.cn/app-contracts-mall": 0.13.1
48
- "@tbox.cn/app-theme-tbox": 0.1.4
46
+ "@tbox.cn/app-contracts": 0.13.3
47
+ "@tbox.cn/app-contracts-mall": 0.13.2
48
+ "@tbox.cn/app-theme-tbox": 0.1.5
49
49
  zustand: 4.5.7
50
50
  eslint: 9.39.5
51
51
  typescript-eslint: 8.67.0
@@ -30,6 +30,30 @@ const nestedLockfiles = new Set([
30
30
  ]);
31
31
  const viteConfigPattern = /^vite\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
32
32
 
33
+ /**
34
+ * CLI 裸简写防线:应用文档里不得出现 `tbox-app <cmd>`(agent 照抄后 PATH 无 bin → 命令失败)。
35
+ * 唯一合法形态 = `npx --yes @tbox.cn/app-cli <cmd>`;`tbox-app-dev`(私有 CLI)不匹配。
36
+ */
37
+ const bareCliPattern = /\btbox-app\s+(?:module|provider|theme|starter|new|create|schema|skills|doctor|miniapp-container)\b/g;
38
+ const cliDocFiles = [
39
+ 'AGENTS.md',
40
+ 'README.md',
41
+ 'ENV.md',
42
+ 'TEMPLATE.md',
43
+ 'apps/client/src/business/README.md',
44
+ ];
45
+
46
+ async function checkCliShorthand(findings) {
47
+ for (const rel of cliDocFiles) {
48
+ const file = path.join(projectDir, rel);
49
+ if (!existsSync(file)) continue;
50
+ const text = await readFile(file, 'utf8');
51
+ for (const match of text.matchAll(bareCliPattern)) {
52
+ findings.bareCliShorthands.push(`${rel}: ${match[0]}`);
53
+ }
54
+ }
55
+ }
56
+
33
57
  function relativeDir(filePath) {
34
58
  const relative = path.relative(projectDir, path.dirname(filePath));
35
59
  return relative === '' ? '.' : relative.split(path.sep).join('/');
@@ -95,6 +119,10 @@ function printFailure(findings) {
95
119
  console.error('\npackages/* 缺少 tbox.module.json(contracts / app-sdk / shared 除外):');
96
120
  for (const file of findings.missingModuleDescriptors) console.error(` - ${file}`);
97
121
  }
122
+ if (findings.bareCliShorthands.length > 0) {
123
+ console.error('\n检测到 CLI 裸简写(须写全 npx --yes @tbox.cn/app-cli <cmd>):');
124
+ for (const item of findings.bareCliShorthands) console.error(` - ${item}`);
125
+ }
98
126
 
99
127
  console.error('');
100
128
  process.exitCode = 1;
@@ -147,10 +175,12 @@ async function main() {
147
175
  nestedLockfiles: [],
148
176
  detachedViteConfigs: [],
149
177
  missingModuleDescriptors: [],
178
+ bareCliShorthands: [],
150
179
  };
151
180
 
152
181
  await walk(projectDir, findings);
153
182
  await checkModuleDescriptors(projectDir, findings);
183
+ await checkCliShorthand(findings);
154
184
 
155
185
  if (Object.values(findings).some((items) => items.length > 0)) {
156
186
  printFailure(findings);
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * smoke.mjs — 一次性运行时冒烟(三级回路第二级)。
4
+ *
5
+ * 四验:boot(/api/health 200 + status:ok)→ browser 登录 → /api/auth/me → 进程组清理。
6
+ * 设计约束(与平台沙箱共存):
7
+ * - 静态入口 src/index.ts(不走 dev.ts——dev 面引入 vite/token 分面,非本脚本职责);
8
+ * - NODE_ENV=development(SDK 安全姿态);端口由 OS 分配(仅覆盖 PORT——apps/server 唯一消费该键);
9
+ * - detached 进程组 + SIGTERM→SIGKILL,零残留;
10
+ * - 路径锚定脚本位置(任意 cwd 可执行);
11
+ * - 登录 403 AUTH_BROWSER_LOGIN_DISABLED = 合法配置(SKIP,不判失败);
12
+ * - fail-soft 配置降级不阻断 boot:stderr/stdout 尾部固定 dump,提示人工判读 WARN/ERROR。
13
+ *
14
+ * 用法:pnpm smoke / node scripts/smoke.mjs [--self-test]
15
+ */
16
+ import { spawn } from 'node:child_process';
17
+ import { existsSync } from 'node:fs';
18
+ import { createServer } from 'node:net';
19
+ import path from 'node:path';
20
+ import process from 'node:process';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const APP_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const SERVER_DIR = path.join(APP_ROOT, 'apps', 'server');
25
+ const KNOWN_WARNINGS = ['client/dist 缺失'];
26
+ const BOOT_TIMEOUT_MS = 30_000;
27
+ const TOTAL_TIMEOUT_MS = 60_000;
28
+ const LOG_LIMIT = 200_000;
29
+
30
+ /** OS 分配空闲端口(listen 0 → 取 port → close)。 */
31
+ export function pickPort() {
32
+ return new Promise((resolve, reject) => {
33
+ const server = createServer();
34
+ server.unref();
35
+ server.once('error', reject);
36
+ server.listen(0, '127.0.0.1', () => {
37
+ const address = server.address();
38
+ const port = typeof address === 'object' && address !== null ? address.port : NaN;
39
+ server.close(() => (Number.isInteger(port) && port > 0 ? resolve(port) : reject(new Error('无法分配端口'))));
40
+ });
41
+ });
42
+ }
43
+
44
+ /** 端口合法性(禁固定 dev/stage 端口——互踩防线;非法直接 throw)。 */
45
+ export function assertValidPort(port) {
46
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error(`非法端口: ${port}`);
47
+ if (port === 8000 || port === 8001) throw new Error(`冒烟端口不可复用固定端口 ${port}(OS 分配)`);
48
+ }
49
+
50
+ /** 日志尾部(末 maxLines 行;超长截断)。 */
51
+ export function tailLines(text, maxLines = 100) {
52
+ if (!text) return '';
53
+ const lines = text.split(/\r?\n/);
54
+ if (lines.length <= maxLines) return text;
55
+ return lines.slice(-maxLines).join('\n');
56
+ }
57
+
58
+ function appendLog(state, chunk) {
59
+ state.log += chunk;
60
+ if (state.log.length > LOG_LIMIT) state.log = state.log.slice(-LOG_LIMIT);
61
+ }
62
+
63
+ function sleep(ms) {
64
+ return new Promise((resolve) => setTimeout(resolve, ms));
65
+ }
66
+
67
+ async function requestJson(url, init = {}) {
68
+ const response = await fetch(url, { ...init, signal: AbortSignal.timeout(5_000) });
69
+ let body = null;
70
+ try {
71
+ body = await response.json();
72
+ } catch {
73
+ body = null;
74
+ }
75
+ return { status: response.status, body };
76
+ }
77
+
78
+ /** 进程组终止(detached spawn 的反向操作;ESRCH 容忍)。 */
79
+ function killGroup(pid, signal) {
80
+ if (!pid) return;
81
+ try {
82
+ process.kill(process.platform === 'win32' ? pid : -pid, signal);
83
+ } catch (error) {
84
+ if (error.code !== 'ESRCH') throw error;
85
+ }
86
+ }
87
+
88
+ async function waitForHealth(port, child, state, spawnErrorOf = () => undefined) {
89
+ const deadline = Date.now() + BOOT_TIMEOUT_MS;
90
+ const url = `http://127.0.0.1:${port}/api/health`;
91
+ while (Date.now() < deadline) {
92
+ const spawnError = spawnErrorOf();
93
+ if (spawnError) throw new Error(`server 启动失败:${spawnError.message}`);
94
+ if (child.exitCode !== null) {
95
+ throw new Error(`server 在健康检查前退出(code=${child.exitCode})\n${tailLines(state.log)}`);
96
+ }
97
+ try {
98
+ const { status, body } = await requestJson(url);
99
+ if (status === 200 && body?.status === 'ok') return;
100
+ throw new Error(`/api/health 非预期响应: ${status} ${JSON.stringify(body)}`);
101
+ } catch (error) {
102
+ if (String(error.message).includes('/api/health 非预期响应')) throw error;
103
+ await sleep(250);
104
+ }
105
+ }
106
+ throw new Error(`/api/health 超时(${BOOT_TIMEOUT_MS}ms)\n${tailLines(state.log)}`);
107
+ }
108
+
109
+ async function loginMatrix(port) {
110
+ const login = await requestJson(`http://127.0.0.1:${port}/api/auth/login`, {
111
+ method: 'POST',
112
+ headers: { 'content-type': 'application/json' },
113
+ body: JSON.stringify({ method: 'browser' }),
114
+ });
115
+ if (login.status === 200 && typeof login.body?.token === 'string') {
116
+ const me = await requestJson(`http://127.0.0.1:${port}/api/auth/me`, {
117
+ headers: { authorization: `Bearer ${login.body.token}` },
118
+ });
119
+ if (me.status !== 200 || !me.body?.identity) {
120
+ throw new Error(`/api/auth/me 校验失败: ${me.status} ${JSON.stringify(me.body)}`);
121
+ }
122
+ return 'PASS';
123
+ }
124
+ if (login.status === 403 && login.body?.code === 'AUTH_BROWSER_LOGIN_DISABLED') {
125
+ return 'SKIP(browser 登录关闭——合法配置)';
126
+ }
127
+ throw new Error(`browser 登录失败: ${login.status} ${JSON.stringify(login.body)}`);
128
+ }
129
+
130
+ export async function runSmoke(opts = {}) {
131
+ const totalTimeoutMs = opts.timeoutMs ?? TOTAL_TIMEOUT_MS;
132
+ if (opts.probe) {
133
+ const port = await pickPort();
134
+ assertValidPort(port);
135
+ return { ok: true, reason: `probe ok(port=${port})` };
136
+ }
137
+ if (!existsSync(path.join(SERVER_DIR, 'node_modules', 'tsx'))) {
138
+ return { ok: false, reason: 'apps/server/node_modules 下的 tsx 缺席——先执行 pnpm install' };
139
+ }
140
+ let port;
141
+ try {
142
+ port = await pickPort();
143
+ assertValidPort(port);
144
+ } catch (error) {
145
+ return { ok: false, reason: error.message };
146
+ }
147
+
148
+ const state = { log: '' };
149
+ let child = null;
150
+ let watchdog;
151
+ const cleanup = async () => {
152
+ if (!child || child.exitCode !== null) return;
153
+ killGroup(child.pid, 'SIGTERM');
154
+ const deadline = Date.now() + 2_000;
155
+ while (child.exitCode === null && Date.now() < deadline) await sleep(50);
156
+ if (child.exitCode === null) killGroup(child.pid, 'SIGKILL');
157
+ };
158
+ const onSignal = () => {
159
+ void cleanup().finally(() => process.exit(130));
160
+ };
161
+ process.once('SIGINT', onSignal);
162
+ process.once('SIGTERM', onSignal);
163
+
164
+ const MAX_ATTEMPTS = 3;
165
+ let lastError = null;
166
+ try {
167
+ if (totalTimeoutMs > 0) {
168
+ watchdog = setTimeout(() => {
169
+ console.error(`⏱️ 冒烟总超时 ${totalTimeoutMs}ms——强制终止 server 进程组`);
170
+ void cleanup().finally(() => process.exit(1));
171
+ }, totalTimeoutMs);
172
+ watchdog.unref();
173
+ }
174
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
175
+ state.log = '';
176
+ const startedAt = Date.now();
177
+ child = spawn(process.execPath, ['--import', 'tsx', 'src/index.ts'], {
178
+ cwd: SERVER_DIR,
179
+ env: { ...process.env, PORT: String(port), NODE_ENV: 'development' },
180
+ detached: process.platform !== 'win32',
181
+ stdio: ['ignore', 'pipe', 'pipe'],
182
+ });
183
+ let spawnError;
184
+ child.stdout.on('data', (chunk) => appendLog(state, chunk.toString()));
185
+ child.stderr.on('data', (chunk) => appendLog(state, chunk.toString()));
186
+ child.once('error', (error) => {
187
+ spawnError = error;
188
+ appendLog(state, `spawn error: ${error.message}\n`);
189
+ });
190
+
191
+ console.log(`[smoke] 启动 server(port=${port}, entry=src/index.ts${attempt > 1 ? `,第 ${attempt} 次尝试` : ''})…`);
192
+ try {
193
+ await waitForHealth(port, child, state, () => spawnError);
194
+ console.log(`[smoke] ✓ health(${Date.now() - startedAt}ms)`);
195
+
196
+ const login = await loginMatrix(port);
197
+ console.log(`[smoke] ✓ login/me:${login}`);
198
+
199
+ const warningLines = state.log.split(/\r?\n/).filter((line) => /\b(WARN|ERROR)\b/.test(line));
200
+ const knownWarnings = warningLines.filter((line) => KNOWN_WARNINGS.some((known) => line.includes(known)));
201
+ const warnings = warningLines.filter((line) => !KNOWN_WARNINGS.some((known) => line.includes(known)));
202
+ if (knownWarnings.length > 0) {
203
+ console.log('[smoke] 已知告警([known],不判失败):');
204
+ for (const line of knownWarnings.slice(0, 5)) console.log(` [known] ${line}`);
205
+ }
206
+ if (warnings.length > 0) {
207
+ console.log('[smoke] 运行期 WARN/ERROR(前 20 行):');
208
+ for (const line of warnings.slice(0, 20)) console.log(` ${line}`);
209
+ }
210
+ return { ok: true, reason: 'health + login + me 全通过' };
211
+ } catch (error) {
212
+ lastError = error;
213
+ const portBusy = /EADDRINUSE|already in use/i.test(state.log);
214
+ await cleanup();
215
+ child = null;
216
+ if (portBusy && attempt < MAX_ATTEMPTS) {
217
+ try {
218
+ port = await pickPort();
219
+ assertValidPort(port);
220
+ } catch (portError) {
221
+ return { ok: false, reason: `端口重分配失败:${portError.message}` };
222
+ }
223
+ console.log(`[smoke] 端口被占用——改用 ${port} 重试(${attempt}/${MAX_ATTEMPTS})`);
224
+ continue;
225
+ }
226
+ break;
227
+ }
228
+ }
229
+ return {
230
+ ok: false,
231
+ reason: `${lastError?.message ?? '未知失败'}\n\n--- server 日志尾部 ---\n${tailLines(state.log)}`,
232
+ };
233
+ } finally {
234
+ if (watchdog) clearTimeout(watchdog);
235
+ process.removeListener('SIGINT', onSignal);
236
+ process.removeListener('SIGTERM', onSignal);
237
+ await cleanup();
238
+ }
239
+ }
240
+
241
+ function selfTest() {
242
+ const failures = [];
243
+ const eq = (label, actual, expected) => {
244
+ if (actual !== expected) failures.push(`${label}: ${JSON.stringify(actual)} !== ${JSON.stringify(expected)}`);
245
+ };
246
+ eq('tailLines short', tailLines('a\nb'), 'a\nb');
247
+ eq('tailLines trim', tailLines('1\n2\n3', 2), '2\n3');
248
+ eq('tailLines empty', tailLines(''), '');
249
+ for (const bad of [0, -1, 8000, 8001, 70000, 1.5, NaN]) {
250
+ let threw = false;
251
+ try {
252
+ assertValidPort(bad);
253
+ } catch {
254
+ threw = true;
255
+ }
256
+ if (!threw) failures.push(`assertValidPort(${bad}) 应 throw`);
257
+ }
258
+ assertValidPort(8123);
259
+ if (failures.length > 0) {
260
+ for (const failure of failures) console.error(`[smoke:self-test] ${failure}`);
261
+ return false;
262
+ }
263
+ console.log('[smoke:self-test] ok');
264
+ return true;
265
+ }
266
+
267
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
268
+ if (isMain) {
269
+ if (process.argv.includes('--self-test')) {
270
+ if (!selfTest()) process.exit(1);
271
+ const probe = await runSmoke({ probe: true });
272
+ if (!probe.ok) {
273
+ console.error(`[smoke:self-test] ${probe.reason}`);
274
+ process.exit(1);
275
+ }
276
+ console.log(`[smoke:self-test] ${probe.reason}`);
277
+ process.exit(0);
278
+ }
279
+ const result = await runSmoke();
280
+ if (result.ok) {
281
+ console.log(`✅ smoke 通过:${result.reason}`);
282
+ console.log('[smoke] 提示:检查运行期 WARN/ERROR(fail-soft 配置降级在此可见)');
283
+ } else {
284
+ console.error(`❌ smoke 失败:${result.reason}`);
285
+ process.exitCode = 1;
286
+ }
287
+ }
@@ -4,7 +4,7 @@
4
4
  * ① 装配文件与模块声明一致(漂移 = 0,服务端 modules.ts)
5
5
  * ② 2 装配文件双锚点完整(imports + register 标记齐全)
6
6
  * ③ 装配文件无裸露 import(全部 import 行在锚区内;import type 豁免)
7
- * 自包含实现(模板独立于 CLI 包),全量校验以 `tbox-app module doctor` / `tbox-app module diff` 为准。
7
+ * 自包含实现(模板独立于 CLI 包),全量校验以 CLI `module doctor` / `module diff` 为准。
8
8
  */
9
9
  import { existsSync, readFileSync } from 'node:fs';
10
10
  import { readdir } from 'node:fs/promises';
@@ -101,7 +101,7 @@ async function main() {
101
101
 
102
102
  // ① 漂移 = 0:声明(manifest + 本地模块)vs 服务端 modules.ts 实际装配
103
103
  // 契约包(无 tbox.module.json / 无 serverModule)不参与 modules.ts 装配,不计入声明
104
- // (客户端 modules.ts 装配漂移由 tbox-app module doctor 全量校验兜底)
104
+ // (客户端 modules.ts 装配漂移由 CLI module doctor 全量校验兜底)
105
105
  const npmModules = readManifestModules();
106
106
  const declared = new Set([
107
107
  ...npmModules
package/INTEGRATION.md DELETED
@@ -1,95 +0,0 @@
1
- # INTEGRATION —— 自定义服务集成指引(真源)
2
-
3
- 本文件是应用「自定义服务集成」指引的唯一真源;发现入口 = 根 `AGENTS.md`
4
- 「自定义服务集成(provider-custom)」节(该节为指针,勿在其下扩展内容)。
5
-
6
- **自定义 API** = 服务的真实后端由你自己对接:声明住在 `config/integrations.json`,adapter
7
- 代码住在 `packages/provider-custom/`。服务后端三选一:Mock(演示数据,装 provider-mock
8
- 即用)/ 自定义 API(你自己的后端接口——本文件流程)/ 服务商集成(官方 provider 包,
9
- `module add` + `provider bind`)。
10
-
11
- ## 任务路由
12
-
13
- | 你的任务 | 按此读取 |
14
- |---|---|
15
- | 为一个服务接入 | `packages/contracts-mall/src/services/<域>.ts`(Port 定义)+ `packages/provider-custom/INTEGRATION.md` |
16
- | 为某模块全部服务接入 | `packages/<module>/INTEGRATION.md` + `packages/<module>/tbox.module.json` + 该域 Port 文件 + `packages/provider-custom/INTEGRATION.md` |
17
- | 应用全部服务接入 | `tbox-app module list` 后逐模块按上行 |
18
- | 实施与排障 | `packages/provider-custom/INTEGRATION.md`(声明/骨架/注册/凭据/完成判据全链) |
19
-
20
- ## 四步流程
21
-
22
- 收到「为某服务/模块/应用接入自定义 API」(或「对接我的 API」「先要接口清单」等同义
23
- 说法)的请求时按四步走:
24
-
25
- 1. **说明并产出接入材料清单**(规范见下节)。目标不在已装模块服务槽词汇内 → 说明未装
26
- 该模块,引导 `module add` 或 `new module`,不进入后续步骤。用户已给出的信息不重复问。
27
- 未实施前调用报 `PROVIDER_NOT_IMPLEMENTED` 属预期中间态。
28
- 2. **落声明**:目标槽已有内联声明则跳过;否则直接编辑 `config/integrations.json`
29
- (CLI provider 命令仅支持字符串厂商)按「声明落点」表写入。
30
- 3. **实施 adapter**:按 `packages/provider-custom/INTEGRATION.md` 实施。
31
- 4. **验收**:`pnpm typecheck && pnpm build` 通过 + `npx --yes @tbox.cn/app-cli module
32
- doctor`;平台重启后该服务的调用不再报 `PROVIDER_NOT_IMPLEMENTED`(resolveService
33
- 惰性——调用时观测;doctor 对通配槽凭据不设防,运行时判据为最终裁判)。
34
-
35
- ### 第 1 步:接入材料清单规范
36
-
37
- 清单回答**「你的后端需要提供什么」**——从用户需要准备的材料出发;不解释槽/Port/registry/
38
- 级联等平台内部机制(用户追问除外)。
39
-
40
- **信息源(按序读,勿凭空编造)**:
41
-
42
- - 槽位清单与必选性 → `packages/<module>/tbox.module.json`(`declare.consumes` 消费需求、
43
- `declare.owns` 服务归属);
44
- - 接口签名细节 → 消费模块 `src/server/ports.ts` 或 `packages/contracts-mall/src/services/<域>.ts`
45
- (每个 Port 方法 = 后端需提供的一个接口);
46
- - 该模块接入语义(顺序/依赖/可选降级)→ `packages/<module>/INTEGRATION.md`。
47
-
48
- **清单交付项**(每服务一份,模块/应用维度 = 逐服务罗列):
49
-
50
- - 服务槽:展示名 + 一句业务说明;
51
- - 每个接口(Port 方法):接口语义(查询什么/写入什么);入参中来自平台的定位键
52
- (如 userId / plateNo——平台传入,你的后端按它定位用户/车辆);返回**必选**字段与
53
- **可选**字段;可选方法(`?` 结尾)缺席时的产品降级行为;
54
- - 连接材料:API 地址(baseUrl)、认证方式(静态请求头 → `static-headers-v1`;Bearer/
55
- 自定义签名 → 自定义认证策略)、其他可选连接参数(超时等);
56
- - 依赖与顺序:接入该服务前需要先接入的其他服务/模块;`*.snapshot` 聚合槽由底层服务
57
- 派生——落声明时跳过;
58
- - 材料收集后仍缺的信息(baseUrl / 认证 / 密钥放置方式)明确列出请用户补充。
59
-
60
- **纯询问即停**:用户仅询问「需要提供哪些接口/材料」(未给连接信息)→ 产出清单即停,
61
- 等用户补充材料后再落声明与实施。密钥等敏感值不写进 prompt/代码/明文——经平台凭据
62
- (`credentialRef`)配置,见 `packages/provider-custom/INTEGRATION.md`「认证与凭据」。
63
-
64
- **中文名称寻址**:用户用中文展示名指代(如「会员积分」模块)→ 经 `packages/*/tbox.module.json`
65
- 的 `name`/`id` 或 `tbox-app module list` 解析到模块;解析歧义时向用户确认。
66
-
67
- ## 声明落点
68
-
69
- | 维度 | 绑定(单点) | 激活 | 共享连接参数落点 |
70
- |---|---|---|---|
71
- | 指定服务 | `services.<slot>.provider = "provider-custom"` | 节点本身 | 槽/实例级 `config` |
72
- | 指定模块 | `modules[<install id>].provider = "provider-custom"`(**单点作用于全部 ownedServices**) | owned 槽补 `{}` 通配节点 | `modules[<install id>].config` |
73
- | 应用 | root `provider = "provider-custom"` | 同左逐槽 | root `config` |
74
-
75
- 推荐 CLI(等价路径):`tbox-app provider use provider-custom --module <install id>`(模块)/
76
- `provider use provider-custom`(应用)/ `provider bind <slot> provider-custom`(服务)。
77
-
78
- 关键形态约束:
79
-
80
- - **绑定与激活正交**:provider 选择单点落绑定层;激活 = `services[<slot>]` 节点在场
81
- (`{}` 空槽 = 通配激活,实例差异化再补 instances 键)。只写绑定不写激活 → 服务保持
82
- `service-not-configured`(诚实暴露,非错误)。
83
- - **内联 provider 声明(对象形态)已弃用(v8)**:存量兼容读;迁移 =
84
- `tbox-app provider migrate --write`(内联 → provider-custom 统一落点,迁移后 ⚠ 待接入,
85
- 按 provider-custom/INTEGRATION.md 实施转 ok)。
86
- - 完整字段语义(各层级联、instances 激活等)单源 = `@tbox.cn/app-contracts` 的
87
- integrations-config(zod 校验,与运行时同一解析面)。
88
- - **provider 名三处一致**(声明/注册/凭据)规则与失败信号见
89
- `packages/provider-custom/INTEGRATION.md`「provider 名三处一致」。
90
-
91
- ## 验收
92
-
93
- - `pnpm typecheck && pnpm build` 通过 + `npx --yes @tbox.cn/app-cli module doctor`;
94
- - 平台重启后该服务的调用不再报 `PROVIDER_NOT_IMPLEMENTED`(resolveService 惰性——
95
- 调用时观测;doctor 对通配槽凭据不设防,运行时判据为最终裁判)。