@tbox.cn/app-template-agent 0.10.21 → 0.10.23

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,20 +16,20 @@ 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.1
20
- version: 0.3.1
19
+ specifier: 0.3.2
20
+ version: 0.3.2
21
21
  '@tbox.cn/app-contracts':
22
- specifier: 0.13.2
23
- version: 0.13.2
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
27
27
  '@tbox.cn/app-sdk-server':
28
- specifier: 0.26.0
29
- version: 0.26.0
28
+ specifier: 0.26.1
29
+ version: 0.26.1
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
@@ -103,7 +103,7 @@ importers:
103
103
  devDependencies:
104
104
  '@tbox.cn/app-sdk-server':
105
105
  specifier: 'catalog:'
106
- version: 0.26.0
106
+ version: 0.26.1
107
107
  eslint:
108
108
  specifier: 'catalog:'
109
109
  version: 9.39.5
@@ -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.2
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
@@ -150,6 +150,9 @@ importers:
150
150
  specifier: 'catalog:'
151
151
  version: 4.5.7(@types/react@18.3.31)(react@18.3.1)
152
152
  devDependencies:
153
+ '@tbox.cn/app-agent-sdk-server':
154
+ specifier: 'catalog:'
155
+ version: 0.3.2(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
153
156
  '@types/node':
154
157
  specifier: 'catalog:'
155
158
  version: 24.13.3
@@ -191,7 +194,7 @@ importers:
191
194
  version: 0.21.3
192
195
  '@tbox.cn/app-sdk-server':
193
196
  specifier: 'catalog:'
194
- version: 0.26.0
197
+ version: 0.26.1
195
198
  typescript:
196
199
  specifier: 'catalog:'
197
200
  version: 5.9.3
@@ -218,10 +221,10 @@ importers:
218
221
  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
222
  '@tbox.cn/app-agent-sdk-server':
220
223
  specifier: 'catalog:'
221
- version: 0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
224
+ version: 0.3.2(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
222
225
  '@tbox.cn/app-contracts':
223
226
  specifier: 'catalog:'
224
- version: 0.13.2
227
+ version: 0.13.3
225
228
  express:
226
229
  specifier: 'catalog:'
227
230
  version: 4.22.2
@@ -261,7 +264,7 @@ importers:
261
264
  version: 0.2.0(@types/react@18.3.31)
262
265
  '@tbox.cn/app-contracts':
263
266
  specifier: 'catalog:'
264
- version: 0.13.2
267
+ version: 0.13.3
265
268
  antd-mobile:
266
269
  specifier: 5.42.3
267
270
  version: 5.42.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -310,7 +313,7 @@ importers:
310
313
  dependencies:
311
314
  '@tbox.cn/app-contracts':
312
315
  specifier: 'catalog:'
313
- version: 0.13.2
316
+ version: 0.13.3
314
317
  zod:
315
318
  specifier: 3.25.76
316
319
  version: 3.25.76
@@ -341,10 +344,10 @@ importers:
341
344
  version: 0.2.0(@types/react@18.3.31)
342
345
  '@tbox.cn/app-agent-sdk-server':
343
346
  specifier: 'catalog:'
344
- version: 0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
347
+ version: 0.3.2(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
345
348
  '@tbox.cn/app-contracts':
346
349
  specifier: 'catalog:'
347
- version: 0.13.2
350
+ version: 0.13.3
348
351
  react:
349
352
  specifier: 18.3.1
350
353
  version: 18.3.1
@@ -384,11 +387,14 @@ importers:
384
387
  version: 0.2.0(@types/react@18.3.31)
385
388
  '@tbox.cn/app-agent-sdk-server':
386
389
  specifier: 'catalog:'
387
- version: 0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
390
+ version: 0.3.2(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)
388
391
  '@tbox.cn/app-contracts':
389
392
  specifier: 'catalog:'
390
- version: 0.13.2
393
+ version: 0.13.3
391
394
  devDependencies:
395
+ '@app/contracts-mall':
396
+ specifier: workspace:*
397
+ version: link:../contracts-mall
392
398
  '@types/node':
393
399
  specifier: 24.13.3
394
400
  version: 24.13.3
@@ -1415,8 +1421,8 @@ packages:
1415
1421
  resolution: {integrity: sha512-yxK5vLuERzlebTwiw8WxU6e9rm7F1L9tVW4aSUGsIxeU0y8VtRcrh7y8xKbrlXR4WL4XrmYZ6ZoDyOuro9sn5A==}
1416
1422
  engines: {node: '>=20.0.0'}
1417
1423
 
1418
- '@tbox.cn/app-agent-sdk-server@0.3.1':
1419
- resolution: {integrity: sha512-VWAlG+Efjyo6Wwj4QPKO9N4DPjhG20z3t9cGjzjW8ZvUFU7hQ7NngbFh7Z7IXtYrXJCGpjJSGtMLJ9AgBk67iQ==}
1424
+ '@tbox.cn/app-agent-sdk-server@0.3.2':
1425
+ resolution: {integrity: sha512-NrKZNjc6xJgkF2EXnGApOnVS52HMllDmtNnd8GjQh9qfiJfkvzKSsceCUu2Ek75tTjY5axBGJP5sel1+N2N07g==}
1420
1426
  engines: {node: '>=20.0.0'}
1421
1427
 
1422
1428
  '@tbox.cn/app-contracts@0.13.0':
@@ -1431,6 +1437,10 @@ packages:
1431
1437
  resolution: {integrity: sha512-0lY9g29ice7xYbmV5XDnEiMmM70jvSzeK2cciUZVpzHgGykWqZiPRlqXIGkIRY7uObDQ1cICHC4mS67HuNQ8Jg==}
1432
1438
  engines: {node: '>=20.0.0'}
1433
1439
 
1440
+ '@tbox.cn/app-contracts@0.13.3':
1441
+ resolution: {integrity: sha512-P16LFd+ZzIASrujs6fyaOEwRnsLg7Lh5k1qOVDNblmohrvUbVIOK5ntn+7mupJxnjK7SY43RUT4mtWtfM85/rQ==}
1442
+ engines: {node: '>=20.0.0'}
1443
+
1434
1444
  '@tbox.cn/app-sdk-client@0.21.3':
1435
1445
  resolution: {integrity: sha512-64y22k5gzCc9eEQ0IJA1izxPaosNEmoCJ4YrsfHOoHYq2UPJCCmP7RIu3Rn4stPZ4tcmRdMdAi2y5499KObYHw==}
1436
1446
  engines: {node: '>=20.0.0'}
@@ -1443,16 +1453,16 @@ packages:
1443
1453
  resolution: {integrity: sha512-HLLRg1y2jB32uJPL3/a387eABdNZUxrmkGoJwIv44XV9B94DXmTgb9vxYzbyBa0FazxXG0WudyzFnmH5eeqqrw==}
1444
1454
  engines: {node: '>=20.0.0'}
1445
1455
 
1446
- '@tbox.cn/app-sdk-server@0.26.0':
1447
- resolution: {integrity: sha512-VpV3sf4K/2MEsnkuK5EqZUyf9KoSVHeJUm4Al0M6R4Wumg6183apnxB2f38S5wuP30JSJShN/ww63EzRniTrjw==}
1456
+ '@tbox.cn/app-sdk-server@0.26.1':
1457
+ resolution: {integrity: sha512-spg0aF6nmt9Nbxz8ofRYyojOpeOWcYI3mfTevzVJrhfzKmQ3WufpmP/lwLHuQkF2WEVI33dhVQQum7TFCck3+Q==}
1448
1458
  engines: {node: '>=20.0.0'}
1449
1459
  hasBin: true
1450
1460
 
1451
- '@tbox.cn/app-theme-tbox@0.1.4':
1452
- resolution: {integrity: sha512-VF2fjTBFKJ6x/wvM6/T0qi7dmoiN1stNIT2xDerYbBBLCggr7f+H0v6VtuFMSed6tirlcpAlqtBDOctxudHJlQ==}
1461
+ '@tbox.cn/app-theme-tbox@0.1.5':
1462
+ resolution: {integrity: sha512-NL3xmPQOuXBf7vWutU3dWLhvhCvQSA6cHGH76qcqBkPSAgUlKacE1G67jOFyWlC9rg5oMWVOdrkniXvhi5v/4w==}
1453
1463
 
1454
- '@tbox.cn/app-toolkit@0.8.0':
1455
- resolution: {integrity: sha512-urefCf5DCWbH9wM8dSGavA9MNdY4gx7LY16+s9jIlwhahq83mfrCP2h9ombn44ljMrv04G7mnb3vMtNeWs5/Ug==}
1464
+ '@tbox.cn/app-toolkit@0.8.3':
1465
+ resolution: {integrity: sha512-i+rz8tUwndygwCw/T5dp/59p2JKW+Xpfy2UznKOY8vuKbqbe2ANCUTDUuNf8IDuyfvy158Iw/+5H/hLVNAD/3w==}
1456
1466
  engines: {node: '>=20.0.0'}
1457
1467
 
1458
1468
  '@types/babel__core@7.20.5':
@@ -4175,7 +4185,7 @@ snapshots:
4175
4185
  - '@types/react'
4176
4186
  - immer
4177
4187
 
4178
- '@tbox.cn/app-agent-sdk-server@0.3.1(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)':
4188
+ '@tbox.cn/app-agent-sdk-server@0.3.2(@tbox-dev-js/sdk@0.1.18)(rxjs@7.8.1)':
4179
4189
  dependencies:
4180
4190
  '@ag-ui/core': 0.0.47
4181
4191
  '@ai-sdk/openai-compatible': 2.0.68(zod@3.25.76)
@@ -4183,9 +4193,9 @@ snapshots:
4183
4193
  '@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)
4184
4194
  '@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)
4185
4195
  '@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)
4186
- '@tbox.cn/app-contracts': 0.13.2
4196
+ '@tbox.cn/app-contracts': 0.13.3
4187
4197
  '@tbox.cn/app-sdk-core': 0.20.5
4188
- '@tbox.cn/app-sdk-server': 0.26.0
4198
+ '@tbox.cn/app-sdk-server': 0.26.1
4189
4199
  ai: 6.0.246(zod@3.25.76)
4190
4200
  express: 4.22.2
4191
4201
  gray-matter: 4.0.3
@@ -4221,6 +4231,12 @@ snapshots:
4221
4231
  react: 18.3.1
4222
4232
  zod: 3.25.76
4223
4233
 
4234
+ '@tbox.cn/app-contracts@0.13.3':
4235
+ dependencies:
4236
+ '@ag-ui/core': 0.0.47
4237
+ react: 18.3.1
4238
+ zod: 3.25.76
4239
+
4224
4240
  '@tbox.cn/app-sdk-client@0.21.3':
4225
4241
  dependencies:
4226
4242
  '@tbox.cn/app-contracts': 0.13.0
@@ -4236,21 +4252,21 @@ snapshots:
4236
4252
  dependencies:
4237
4253
  '@tbox.cn/app-contracts': 0.13.2
4238
4254
 
4239
- '@tbox.cn/app-sdk-server@0.26.0':
4255
+ '@tbox.cn/app-sdk-server@0.26.1':
4240
4256
  dependencies:
4241
4257
  '@ag-ui/core': 0.0.47
4242
- '@tbox.cn/app-contracts': 0.13.1
4243
- '@tbox.cn/app-sdk-core': 0.20.4
4244
- '@tbox.cn/app-toolkit': 0.8.0
4258
+ '@tbox.cn/app-contracts': 0.13.3
4259
+ '@tbox.cn/app-sdk-core': 0.20.5
4260
+ '@tbox.cn/app-toolkit': 0.8.3
4245
4261
  express: 4.22.2
4246
4262
  jose: 6.2.8
4247
4263
  zod: 3.25.76
4248
4264
  transitivePeerDependencies:
4249
4265
  - supports-color
4250
4266
 
4251
- '@tbox.cn/app-theme-tbox@0.1.4': {}
4267
+ '@tbox.cn/app-theme-tbox@0.1.5': {}
4252
4268
 
4253
- '@tbox.cn/app-toolkit@0.8.0':
4269
+ '@tbox.cn/app-toolkit@0.8.3':
4254
4270
  dependencies:
4255
4271
  ajv: 8.20.0
4256
4272
  zod: 3.25.76
@@ -40,12 +40,12 @@ catalog:
40
40
  "@tbox.cn/agent-plugin": 0.1.0
41
41
  "@tbox.cn/app-sdk-core": 0.20.5
42
42
  "@tbox.cn/app-sdk-client": 0.21.3
43
- "@tbox.cn/app-sdk-server": 0.26.0
44
- "@tbox.cn/app-agent-sdk-server": 0.3.1
43
+ "@tbox.cn/app-sdk-server": 0.26.1
44
+ "@tbox.cn/app-agent-sdk-server": 0.3.2
45
45
  "@tbox.cn/app-agent-sdk-client": 0.2.0
46
- "@tbox.cn/app-contracts": 0.13.2
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