@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.
- package/AGENTS.md +34 -18
- package/ENV.md +7 -6
- package/README.md +13 -12
- package/apps/client/package.json +1 -0
- package/apps/client/src/business/App.tsx +1 -1
- package/apps/client/src/business/README.md +2 -2
- package/apps/client/src/modules.ts +1 -1
- package/apps/client/vite.config.ts +5 -0
- package/apps/server/src/modules.ts +1 -1
- package/apps/server/tests/cards-registry.test.ts +3 -1
- package/config/README.md +20 -0
- package/config/integrations.json +4 -0
- package/package.json +6 -5
- package/packages/provider-custom/AGENTS.md +16 -30
- package/packages/provider-custom/README.md +4 -3
- package/packages/provider-custom/package.json +2 -2
- package/packages/provider-custom/src/server/example-adapter.ts +58 -0
- package/packages/provider-custom/src/server/index.ts +7 -6
- package/packages/provider-custom/tests/provider-custom.test.ts +7 -9
- package/pnpm-lock.template.yaml +53 -37
- package/pnpm-workspace.template.yaml +5 -5
- package/scripts/check-workspace.mjs +30 -0
- package/scripts/smoke.mjs +287 -0
- package/scripts/verify-generated.mjs +2 -2
- package/INTEGRATION.md +0 -95
- package/packages/provider-custom/INTEGRATION.md +0 -185
package/pnpm-lock.template.yaml
CHANGED
|
@@ -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.
|
|
20
|
-
version: 0.3.
|
|
19
|
+
specifier: 0.3.2
|
|
20
|
+
version: 0.3.2
|
|
21
21
|
'@tbox.cn/app-contracts':
|
|
22
|
-
specifier: 0.13.
|
|
23
|
-
version: 0.13.
|
|
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.
|
|
29
|
-
version: 0.26.
|
|
28
|
+
specifier: 0.26.1
|
|
29
|
+
version: 0.26.1
|
|
30
30
|
'@tbox.cn/app-theme-tbox':
|
|
31
|
-
specifier: 0.1.
|
|
32
|
-
version: 0.1.
|
|
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.
|
|
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.
|
|
139
|
+
version: 0.13.3
|
|
140
140
|
'@tbox.cn/app-theme-tbox':
|
|
141
141
|
specifier: 'catalog:'
|
|
142
|
-
version: 0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1419
|
-
resolution: {integrity: sha512-
|
|
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.
|
|
1447
|
-
resolution: {integrity: sha512-
|
|
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.
|
|
1452
|
-
resolution: {integrity: sha512-
|
|
1461
|
+
'@tbox.cn/app-theme-tbox@0.1.5':
|
|
1462
|
+
resolution: {integrity: sha512-NL3xmPQOuXBf7vWutU3dWLhvhCvQSA6cHGH76qcqBkPSAgUlKacE1G67jOFyWlC9rg5oMWVOdrkniXvhi5v/4w==}
|
|
1453
1463
|
|
|
1454
|
-
'@tbox.cn/app-toolkit@0.8.
|
|
1455
|
-
resolution: {integrity: sha512-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
4243
|
-
'@tbox.cn/app-sdk-core': 0.20.
|
|
4244
|
-
'@tbox.cn/app-toolkit': 0.8.
|
|
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.
|
|
4267
|
+
'@tbox.cn/app-theme-tbox@0.1.5': {}
|
|
4252
4268
|
|
|
4253
|
-
'@tbox.cn/app-toolkit@0.8.
|
|
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.
|
|
44
|
-
"@tbox.cn/app-agent-sdk-server": 0.3.
|
|
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.
|
|
47
|
-
"@tbox.cn/app-contracts-mall": 0.13.
|
|
48
|
-
"@tbox.cn/app-theme-tbox": 0.1.
|
|
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 包),全量校验以 `
|
|
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 装配漂移由
|
|
104
|
+
// (客户端 modules.ts 装配漂移由 CLI module doctor 全量校验兜底)
|
|
105
105
|
const npmModules = readManifestModules();
|
|
106
106
|
const declared = new Set([
|
|
107
107
|
...npmModules
|