agentworkshop 0.7.17 → 0.7.18

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 (35) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{BJem8_v5.js → BOJaHO5V.js} +4 -4
  3. package/.output/public/_nuxt/B_C3GLsD.js +1 -0
  4. package/.output/public/_nuxt/_id_.IbmpkhH4.css +1 -0
  5. package/.output/public/_nuxt/builds/latest.json +1 -1
  6. package/.output/public/_nuxt/builds/meta/253c093e-3336-4451-9f11-08061bf22f01.json +1 -0
  7. package/.output/public/_nuxt/{entry.BNApuuEW.css → entry.DRUzDwmX.css} +1 -1
  8. package/.output/server/chunks/_/nitro.mjs +723 -668
  9. package/.output/server/chunks/build/{_id_-DvJZ5Qis.mjs → _id_-Bs-euJPu.mjs} +178 -131
  10. package/.output/server/chunks/build/_id_-Bs-euJPu.mjs.map +1 -0
  11. package/.output/server/chunks/routes/renderer.mjs +1 -1
  12. package/.output/server/chunks/virtual/entry.mjs +9 -1
  13. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  14. package/.output/server/package.json +1 -1
  15. package/app/pages/dcw/[id].vue +56 -6
  16. package/i18n/locales/en.ts +4 -0
  17. package/i18n/locales/zh-CN.ts +4 -0
  18. package/package.json +1 -1
  19. package/scripts/_dbg-recipe-stale-e2e.mjs +135 -0
  20. package/server/data/daqs.json +203 -66
  21. package/server/data/dcw-lines.json +7 -28
  22. package/server/data/dcw-products.json +7 -7
  23. package/server/data/dcw-recipes.json +28 -14
  24. package/server/data/dcw-rollback.json +108 -9
  25. package/server/data/dcw-runs.json +430 -230
  26. package/server/data/dcw-writes.json +48 -0
  27. package/server/data/dcws.json +444 -352
  28. package/server/data/line-runs.json +23 -13
  29. package/server/services/workshop/agents/industrial-tools.ts +52 -18
  30. package/server/services/workshop/dcw/dcw-controller.ts +11 -1
  31. package/server/services/workshop/dcw/dcw-recipe.repo.ts +21 -6
  32. package/.output/public/_nuxt/DtM4RX5T.js +0 -1
  33. package/.output/public/_nuxt/_id_.pF-B9O6u.css +0 -1
  34. package/.output/public/_nuxt/builds/meta/6a0a88de-639e-4e7b-b7c1-7f4034e04787.json +0 -1
  35. package/.output/server/chunks/build/_id_-DvJZ5Qis.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop-prod",
3
- "version": "0.7.17",
3
+ "version": "0.7.18",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
@@ -389,6 +389,26 @@ const visibleRecipes = computed(() =>
389
389
  )
390
390
  const productName = (id: string): string => dcw.products.find(p => p.id === id)?.name ?? id
391
391
 
392
+ // ---------- 配方参数节点失效态(已删除/已停用/已取消绑定;灰化 + 徽标) ----------
393
+ interface StaleRef { code: 'deleted' | 'disabled' | 'unbound', label: string }
394
+ function paramStatus(nodeId: string, lineId: string): StaleRef | null {
395
+ const node = dcw.nodes.find(n => n.id === nodeId)
396
+ if (!node) return { code: 'deleted', label: t('dcwDetail.staleDeleted') }
397
+ if (!node.enabled) return { code: 'disabled', label: t('dcwDetail.staleDisabled') }
398
+ if (lineId && node.lineId !== lineId) return { code: 'unbound', label: t('dcwDetail.staleUnbound') }
399
+ return null
400
+ }
401
+ function paramNodeName(nodeId: string): string {
402
+ return dcw.nodes.find(n => n.id === nodeId)?.name ?? nodeId
403
+ }
404
+ function daqWindowStatus(nodeId: string, lineId: string): StaleRef | null {
405
+ const node = daq.nodes.find(n => n.id === nodeId)
406
+ if (!node) return { code: 'deleted', label: t('dcwDetail.staleDeleted') }
407
+ if (!node.enabled) return { code: 'disabled', label: t('dcwDetail.staleDisabled') }
408
+ if (lineId && node.lineId !== lineId) return { code: 'unbound', label: t('dcwDetail.staleUnbound') }
409
+ return null
410
+ }
411
+
392
412
  // ---------- 配方版本历史(整体修改变更记录;来源=用户/Agent/系统 + 操作者 + 回退) ----------
393
413
  interface RecipeVersionRow {
394
414
  version: number
@@ -493,6 +513,7 @@ const recipeOpen = ref(false)
493
513
  const recipeEditing = ref<string | null>(null)
494
514
  const recipeSaving = ref(false)
495
515
  const recipeError = ref('')
516
+ const recipeStaleNote = ref('')
496
517
  const recipeForm = reactive({
497
518
  productId: '',
498
519
  name: '',
@@ -520,10 +541,15 @@ function openRecipeEdit(id: string): void {
520
541
  if (!r) return
521
542
  recipeEditing.value = id
522
543
  recipeError.value = ''
544
+ // 失效参数自动清理:节点已删除的参数行直接剔除(保存会被后端归一化拒绝);
545
+ // 停用/解绑节点参数保留可编辑,状态由 select 旁徽标标示
546
+ const live = r.params.filter(p => dcw.nodes.some(n => n.id === p.nodeId))
547
+ const dropped = r.params.length - live.length
548
+ recipeStaleNote.value = dropped > 0 ? t('dcwDetail.staleDropped', { p0: dropped }) : ''
523
549
  recipeForm.productId = r.productId
524
550
  recipeForm.name = r.name
525
551
  recipeForm.description = r.description
526
- recipeForm.params = r.params.map(p => ({
552
+ recipeForm.params = live.map(p => ({
527
553
  nodeId: p.nodeId,
528
554
  value: p.value,
529
555
  min: p.min ?? '',
@@ -1428,16 +1454,25 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1428
1454
  v-for="(p, i) in r.params"
1429
1455
  :key="i"
1430
1456
  class="param-chip"
1457
+ :class="{ stale: paramStatus(p.nodeId, r.lineId) }"
1458
+ :title="paramStatus(p.nodeId, r.lineId)?.label ?? ''"
1431
1459
  >
1432
- {{ (dcw.templates.find(t => t.key === (p.templateRef ?? '').replace('dcw-', ''))?.ch ?? p.templateRef) }} = {{ p.value }}
1460
+ {{ paramNodeName(p.nodeId) }} = {{ p.value }}<span
1461
+ v-if="paramStatus(p.nodeId, r.lineId)"
1462
+ class="chip-stale-tag"
1463
+ >{{ paramStatus(p.nodeId, r.lineId)!.label }}</span>
1433
1464
  </span>
1434
1465
  <span
1435
1466
  v-for="(w, i) in r.daqWindows"
1436
1467
  :key="`w-${i}`"
1437
1468
  class="param-chip daqwin"
1438
- :title="$t('dcwDetail.k1l11api013')"
1469
+ :class="{ stale: daqWindowStatus(w.nodeId, r.lineId) }"
1470
+ :title="daqWindowStatus(w.nodeId, r.lineId)?.label ?? $t('dcwDetail.k1l11api013')"
1439
1471
  >
1440
- ◎ {{ daqNodeCh(w.nodeId) }} ∈ [{{ w.min ?? '-∞' }}, {{ w.max ?? '+∞' }}]
1472
+ ◎ {{ daqNodeCh(w.nodeId) }} ∈ [{{ w.min ?? '-∞' }}, {{ w.max ?? '+∞' }}]<span
1473
+ v-if="daqWindowStatus(w.nodeId, r.lineId)"
1474
+ class="chip-stale-tag"
1475
+ >{{ daqWindowStatus(w.nodeId, r.lineId)!.label }}</span>
1441
1476
  </span>
1442
1477
  </div>
1443
1478
  <div class="recipe-actions">
@@ -1733,6 +1768,10 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1733
1768
  {{ n.name }}({{ dcwTemplateRefCh(n.templateRef) }} · {{ n.min }}~{{ n.max }} {{ n.unit }})
1734
1769
  </option>
1735
1770
  </select>
1771
+ <span
1772
+ v-if="paramStatus(p.nodeId, lineId)"
1773
+ class="row-stale"
1774
+ >{{ paramStatus(p.nodeId, lineId)!.label }}</span>
1736
1775
  </label>
1737
1776
  <label class="f">
1738
1777
  <span>{{ $t('dcwDetail.k3renp2098') }}<em>*</em></span>
@@ -1832,6 +1871,12 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1832
1871
  >
1833
1872
  {{ $t('dcwDetail.kv1de1p105') }}
1834
1873
  </button>
1874
+ <p
1875
+ v-if="recipeStaleNote"
1876
+ class="m-note"
1877
+ >
1878
+ {{ recipeStaleNote }}
1879
+ </p>
1835
1880
  <p
1836
1881
  v-if="recipeError"
1837
1882
  class="m-err"
@@ -1867,12 +1912,12 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1867
1912
  style="max-width: 760px;"
1868
1913
  >
1869
1914
  <div class="modal-head">
1870
- <h3>
1915
+ <h3>
1871
1916
  {{ t('dcwDetail.histTitle') }}<span
1872
1917
  v-if="verRecipe"
1873
1918
  class="dim"
1874
1919
  style="margin-left: 8px; font-size: 12px;"
1875
- >{{ verRecipe.name }} · v{{ verRecipe.version }}</span>
1920
+ >{{ verRecipe.name }} · v{{ verRecipe.version }}</span>
1876
1921
  </h3>
1877
1922
  <button
1878
1923
  class="mini-btn"
@@ -2471,6 +2516,11 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
2471
2516
  .test-result.good { color: var(--tone-success-dot); }
2472
2517
  .test-result.bad { color: var(--tone-danger-dot); }
2473
2518
  .m-err { margin: 8px 0 0; font-size: 12px; color: var(--tone-danger-dot); }
2519
+ .m-note { margin: 8px 0 0; font-size: 12px; color: var(--ink-faint); }
2520
+ /* 失效配方参数:灰化 + 徽标 */
2521
+ .param-chip.stale { color: var(--ink-faint); background: color-mix(in srgb, var(--ink) 5%, transparent); border-color: var(--divider-hair); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--ink-faint) 60%, transparent); }
2522
+ .chip-stale-tag { margin-left: 5px; padding: 0 5px; font-size: 9.5px; font-style: normal; color: var(--tone-danger-dot); background: color-mix(in srgb, var(--tone-danger-dot) 8%, transparent); border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 30%, transparent); border-radius: var(--radius-chip); text-decoration: none; }
2523
+ .row-stale { flex: none; padding: 1px 7px; font-size: 10.5px; color: var(--tone-danger-dot); border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 30%, transparent); border-radius: var(--radius-chip); }
2474
2524
  .m-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
2475
2525
  .aw-pill.outline { color: var(--ink); background: var(--paper-raised); border-color: var(--line-strong); }
2476
2526
  .err { margin-top: 14px; font-size: 13px; color: var(--tone-danger-dot); }
@@ -840,6 +840,10 @@ export default {
840
840
  k1bphrb3048: 'Delete failed',
841
841
  },
842
842
  dcwDetail: {
843
+ staleDeleted: 'deleted',
844
+ staleDisabled: 'disabled',
845
+ staleUnbound: 'unbound',
846
+ staleDropped: 'Removed {p0} stale param(s) (node deleted); re-add with a live node if needed',
843
847
  histBtn: 'History',
844
848
  histTitle: 'Recipe Version History',
845
849
  histLoading: 'Loading…',
@@ -839,6 +839,10 @@ export default {
839
839
  k1bphrb3048: '删除失败',
840
840
  },
841
841
  dcwDetail: {
842
+ staleDeleted: '已删除',
843
+ staleDisabled: '已停用',
844
+ staleUnbound: '已取消绑定',
845
+ staleDropped: '已自动移除 {p0} 条失效参数(节点已删除);如需恢复请重新添加有效节点',
842
846
  histBtn: '版本历史',
843
847
  histTitle: '配方版本历史',
844
848
  histLoading: '加载中…',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop",
3
- "version": "0.7.17",
3
+ "version": "0.7.18",
4
4
  "type": "module",
5
5
  "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 配方失效节点(停用/解绑/删除)一致性 E2E(隔离实例 :3021)
3
+ *
4
+ * 验证:
5
+ * 1) 配方一键下发:A 正常下发;B(停用)/C(解绑)/D(删除)三类失效参数逐一跳过并记明原因
6
+ * 2) 手动 REST 写停用节点 → 409(单点门控)
7
+ * 3) Agent dcw_control 停用节点 → 明确「已停用」报错
8
+ * 4) Agent recipe_update:触到停用/解绑节点被拒;正常参数保存时自动剪除基线中的失效参数并告知
9
+ * 5) recipe_rollback 回退到含失效节点的历史版本 → 剪枝成功,版本描述记录剔除
10
+ * 6) line_context 参数行带 [已停用/已取消绑定/已删除] 标记
11
+ * 用法:NO_PROXY='127.0.0.1,localhost' node scripts/_dbg-recipe-stale-e2e.mjs [base]
12
+ */
13
+ const BASE = process.argv[2] ?? 'http://127.0.0.1:3021'
14
+ const TAG = `st${Math.random().toString(36).slice(2, 6)}`
15
+ let pass = 0
16
+ let fail = 0
17
+ const ok = (cond, name, extra = '') => {
18
+ if (cond) { pass++; console.log(` ✓ ${name}`) }
19
+ else { fail++; console.log(` ✗ ${name} ${extra}`) }
20
+ }
21
+ const api = async (method, path, { body, token } = {}) => {
22
+ const res = await fetch(`${BASE}${path}`, {
23
+ method,
24
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
25
+ body: body === undefined ? undefined : JSON.stringify(body),
26
+ signal: AbortSignal.timeout(30_000),
27
+ })
28
+ return { status: res.status, ...(await res.json().catch(() => ({}))) }
29
+ }
30
+ const invoke = (token, agentId, tool, args) => api('POST', '/api/workshop/agent-tools/invoke', { body: { agentId, tool, args }, token })
31
+
32
+ async function main() {
33
+ console.log(`━━━ 配方失效节点一致性 E2E @ ${BASE} (tag=${TAG}) ━━━`)
34
+ const login = await api('POST', '/api/users/login', { body: { email: 'admin', password: 'admin123' } })
35
+ const token = login.data?.token
36
+ ok(Boolean(token), 'admin 登录')
37
+
38
+ // ── 夹具:产线 + 4 节点 + 产品 + 配方(4 参数)──
39
+ const line = (await api('POST', '/api/workshop/dcw/lines', { body: { name: `st-line-${TAG}` }, token })).data?.line
40
+ const mkNode = async (name) => (await api('POST', '/api/workshop/dcw', {
41
+ body: { name: `${name}-${TAG}`, templateRef: 'temp-sp', driver: 'mock', driverConfig: { key: `${name}-${TAG}` }, min: 0, max: 100, unit: '℃', lineId: line.id },
42
+ token,
43
+ })).data?.node
44
+ const na = await mkNode('st-a')
45
+ const nb = await mkNode('st-b')
46
+ const nc = await mkNode('st-c')
47
+ const nd = await mkNode('st-d')
48
+ const product = (await api('POST', '/api/workshop/dcw/products', { body: { name: `st-prod-${TAG}`, lineId: line.id }, token })).data?.product
49
+ const recipe = (await api('POST', '/api/workshop/dcw/recipes', {
50
+ body: { name: `st-recipe-${TAG}`, productId: product.id, params: [
51
+ { nodeId: na.id, value: 40 },
52
+ { nodeId: nb.id, value: 41 },
53
+ { nodeId: nc.id, value: 42 },
54
+ { nodeId: nd.id, value: 43 },
55
+ ] },
56
+ token,
57
+ })).data?.recipe
58
+ ok(Boolean(na?.id && nb?.id && nc?.id && nd?.id && recipe?.id), `夹具:4 节点 + 配方 ${recipe?.id}`)
59
+
60
+ // ── 三态变异:B 停用 / C 解绑 / D 删除 ──
61
+ await api('PATCH', `/api/workshop/dcw/${nb.id}`, { body: { enabled: false }, token })
62
+ await api('PATCH', `/api/workshop/dcw/${nc.id}`, { body: { lineId: '' }, token })
63
+ await api('DELETE', `/api/workshop/dcw/${nd.id}`, { token })
64
+
65
+ // ── 1. 一键下发:失效参数逐一跳过 ──
66
+ const apply = await api('POST', `/api/workshop/dcw/recipes/${recipe.id}/apply`, { body: {}, token })
67
+ ok(apply.status === 200, '一键下发执行')
68
+ const results = apply.data?.run?.results ?? []
69
+ const byNode = Object.fromEntries(results.map(r => [r.nodeId ?? 'deleted', r]))
70
+ ok(byNode[na.id]?.ok === true, 'A 正常节点照常下发')
71
+ ok((byNode[nb.id]?.message ?? '').includes('已停用') && byNode[nb.id]?.ok === false, `B 停用跳过(${byNode[nb.id]?.message ?? '∅'})`)
72
+ ok((byNode[nc.id]?.message ?? '').includes('已取消绑定') && byNode[nc.id]?.ok === false, `C 解绑跳过(${byNode[nc.id]?.message ?? '∅'})`)
73
+ ok(Object.values(byNode).some(r => (r.message ?? '').includes('已删除') && r.ok === false), `D 删除跳过(${(results.find(r => !r.nodeId)?.message) ?? '∅'})`)
74
+ ok((apply.data?.run?.results ?? []).every(r => results.some(x => x === r)), 'run.results 完整保留跳过原因')
75
+
76
+ // ── 2. 手动 REST 写停用节点 → 409 ──
77
+ const manual = await api('POST', `/api/workshop/dcw/${nb.id}/write`, { body: { value: 10 }, token })
78
+ ok(manual.status === 409, `手动写停用节点 409(实际 ${manual.status} ${manual.message ?? ''})`)
79
+
80
+ // ── 3. Agent 组建 + 直调 ──
81
+ const ch = (await api('POST', '/api/workshop/channels', {
82
+ body: { name: `st-${TAG}`, leadAgent: { name: `lead-${TAG}`, harness: 'mock', config: { delayMs: 60 } } },
83
+ token,
84
+ })).data
85
+ const tpl = await api('POST', '/api/workshop/agents', { body: { name: `stw-${TAG}`, harness: 'omp', config: { provider: 'zhipu-coding-plan', model: 'glm-5.3-flash' } }, token })
86
+ const join = await api('POST', `/api/workshop/channels/${ch.channelId}/agents`, { body: { agentId: tpl.data.id, role: 'worker' }, token })
87
+ const instId = join.data?.id
88
+ for (const n of [na, nb, nc]) await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: n.id, kind: 'dcw', mode: 'auto' }, token })
89
+ ok(Boolean(instId), 'omp worker 建号+绑定')
90
+
91
+ const wB = await invoke(token, instId, 'dcw_control', { node_id: nb.id, value: 10 })
92
+ ok(wB.data?.result?.isError === true && wB.data?.result?.text.includes('已停用'), '[agent] dcw_control 停用节点明确报错', wB.data?.result?.text?.slice(0, 80))
93
+
94
+ // ── 4. recipe_update:触雷拒绝 + 自动剪枝 ──
95
+ const uB = await invoke(token, instId, 'recipe_update', { recipe_id: recipe.id, params: [{ node_id: nb.id, value: 70 }], reason: '触雷测试' })
96
+ ok(uB.data?.result?.isError === true && uB.data?.result?.text.includes('已停用'), '[agent] recipe_update 停用节点被拒', uB.data?.result?.text?.slice(0, 80))
97
+ const uC = await invoke(token, instId, 'recipe_update', { recipe_id: recipe.id, params: [{ node_id: nc.id, value: 70 }], reason: '触雷测试' })
98
+ ok(uC.data?.result?.isError === true && uC.data?.result?.text.includes('已取消绑定'), '[agent] recipe_update 解绑节点被拒', uC.data?.result?.text?.slice(0, 80))
99
+
100
+ const uA = await invoke(token, instId, 'recipe_update', { recipe_id: recipe.id, params: [{ node_id: na.id, value: 77 }], reason: '正常参数保存(应自动剔除 C/D 失效参数)' })
101
+ ok(uA.data?.result?.text?.includes('v2') && uA.data?.result?.text?.includes('剔除'), '[agent] 正常保存 → v2 + 剔除失效参数告知', uA.data?.result?.text?.slice(0, 120))
102
+ const cur = (await api('GET', '/api/workshop/dcw', { token })).data?.recipes?.find(r => r.id === recipe.id)
103
+ ok(cur?.params?.length === 2 && cur?.params?.some(p => p.nodeId === na.id) && cur?.params?.some(p => p.nodeId === nb.id), `基线剪枝后参数=A+B(实际 ${cur?.params?.length} 条)`)
104
+
105
+ // ── 5. 回退到含失效节点的 v1 → 剪枝成功 ──
106
+ const rb = await api('POST', `/api/workshop/dcw/recipes/${recipe.id}/revert`, { body: { version: 1, reason: '回退到含失效节点的初版' }, token })
107
+ ok(rb.status === 200 && rb.data?.recipe?.version === 3, `回退 v1 → v3 成功(实际 v${rb.data?.recipe?.version})`)
108
+ ok((rb.data?.recipe?.description ?? '').includes('剔除') || (rb.data?.recipe?.paramsHistory ?? []).some(h => (h.description ?? '').includes('剔除')), '回退版本描述记录剔除')
109
+ ok((rb.data?.recipe?.params ?? []).every(p => p.nodeId !== nd.id && p.nodeId !== nc.id), '回退后不含已删除/已解绑节点参数')
110
+
111
+ // ── 6. line_context 状态标记 ──
112
+ const lc = await invoke(token, instId, 'line_context', {})
113
+ if (lc.data?.result?.text?.includes('已停用') && lc.data?.result?.text?.includes('参数不下发')) ok(true, '[agent] line_context 标记停用参数')
114
+ else {
115
+ ok(false, '[agent] line_context 标记停用参数')
116
+ console.log(' ── line_context 实际输出 ──')
117
+ console.log(lc.data?.result?.text ?? JSON.stringify(lc))
118
+ }
119
+
120
+ // ── 清理 ──
121
+ await api('DELETE', `/api/workshop/channels/${ch.channelId}?purge=1`, { token }).catch(() => {})
122
+ await api('DELETE', `/api/workshop/agents/${tpl.data.id}`, { token }).catch(() => {})
123
+ await api('DELETE', `/api/workshop/dcw/recipes/${recipe.id}`, { token }).catch(() => {})
124
+ await api('DELETE', `/api/workshop/dcw/products/${product.id}`, { token }).catch(() => {})
125
+ for (const n of [na, nb, nc]) await api('DELETE', `/api/workshop/dcw/${n.id}`, { token }).catch(() => {})
126
+ await api('DELETE', `/api/workshop/dcw/lines/${line.id}`, { token }).catch(() => {})
127
+
128
+ console.log(`\n━━━ StaleRef E2E: ${pass} passed / ${fail} failed ━━━`)
129
+ process.exit(fail === 0 ? 0 : 1)
130
+ }
131
+
132
+ main().catch((err) => {
133
+ console.error('E2E 异常:', err)
134
+ process.exit(1)
135
+ })