agentworkshop 0.7.19 → 0.7.21

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/package.json CHANGED
@@ -1,148 +1,148 @@
1
- {
2
- "name": "agentworkshop",
3
- "version": "0.7.19",
4
- "type": "module",
5
- "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
- "license": "PolyForm-Noncommercial-1.0.0",
7
- "exports": {
8
- ".": {
9
- "types": "./sdk/index.d.mts",
10
- "default": "./sdk/index.mjs"
11
- },
12
- "./sdk": {
13
- "types": "./sdk/index.d.mts",
14
- "default": "./sdk/index.mjs"
15
- },
16
- "./sdk/client": {
17
- "types": "./sdk/client.d.mts",
18
- "default": "./sdk/client.mjs"
19
- },
20
- "./package.json": "./package.json"
21
- },
22
- "bin": {
23
- "aw": "bin/aw.mjs",
24
- "agentworkshop": "bin/aw.mjs"
25
- },
26
- "files": [
27
- ".output",
28
- "bin",
29
- "cli",
30
- "sdk",
31
- "app",
32
- "server",
33
- "shared",
34
- "i18n",
35
- "public",
36
- "scripts",
37
- ".AgentWorkShop/prompts",
38
- "config.yml",
39
- "nuxt.config.ts",
40
- "tsconfig.json",
41
- "uno.config.ts",
42
- "docker-compose.yml",
43
- ".env.example",
44
- "README-zh.md",
45
- "docs/cli.md",
46
- "docs/plugins.md",
47
- "docs/sdk.md",
48
- "docs/tui.md",
49
- "tui"
50
- ],
51
- "packageManager": "pnpm@11.9.0",
52
- "engines": {
53
- "node": ">=23.4.0"
54
- },
55
- "scripts": {
56
- "dev": "node bin/aw.mjs dev",
57
- "build": "node bin/aw.mjs build",
58
- "start": "node bin/aw.mjs start",
59
- "generate": "nuxt generate",
60
- "preview": "node bin/aw.mjs start",
61
- "cli": "node bin/aw.mjs",
62
- "tui": "node tui/aw-tui.mjs",
63
- "postinstall": "node scripts/home-bootstrap.mjs",
64
- "prepublishOnly": "node bin/aw.mjs build",
65
- "lint": "eslint .",
66
- "lint:fix": "eslint . --fix",
67
- "typecheck": "nuxt typecheck",
68
- "prepare": "husky",
69
- "game:test": "tsx scripts/test-agent-brain.ts && tsx scripts/test-game-session.ts",
70
- "test:api-live": "node scripts/api-live-e2e.mjs",
71
- "game:verify": "node scripts/verify-game.mjs",
72
- "game:build-assets": "node scripts/build-player-sheet.mjs",
73
- "game:build-wuwa": "node scripts/build-wuwa-assets.mjs",
74
- "game:build-models": "node scripts/build-character-models.mjs",
75
- "game:build-world": "node scripts/build-world-map.mjs",
76
- "game:build-model3d": "node scripts/build-model-3d.mjs"
77
- },
78
- "lint-staged": {
79
- "*.{js,mjs,cjs,ts,vue}": "eslint --fix"
80
- },
81
- "dependencies": {
82
- "@ant-design-vue/nuxt": "^1.4.6",
83
- "@ant-design/icons-vue": "^7.0.1",
84
- "@earendil-works/pi-tui": "^0.84.4",
85
- "@fontsource-variable/fraunces": "^5.3.0",
86
- "@fontsource-variable/geist": "^5.3.0",
87
- "@fontsource-variable/geist-mono": "^5.3.0",
88
- "@fontsource-variable/inter": "^5.3.0",
89
- "@fontsource/eb-garamond": "^5.3.0",
90
- "@fontsource/ibm-plex-mono": "^5.3.0",
91
- "@iconify-json/tabler": "^1.2.0",
92
- "@modelcontextprotocol/sdk": "^1.30.0",
93
- "@nuxtjs/i18n": "^9.0.0",
94
- "@pinia/nuxt": "^0.11.0",
95
- "@unocss/nuxt": "^66.0.0",
96
- "@unocss/preset-attributify": "^66.0.0",
97
- "@unocss/preset-icons": "^66.7.5",
98
- "@unocss/preset-typography": "^66.0.0",
99
- "@vueuse/core": "^13.0.0",
100
- "@vueuse/nuxt": "^13.0.0",
101
- "@xterm/addon-fit": "^0.11.0",
102
- "@xterm/xterm": "^6.0.0",
103
- "ant-design-vue": "^4.2.6",
104
- "axios": "^1.7.9",
105
- "echarts": "^5.6.0",
106
- "h3": "2.0.1-rc.26",
107
- "js-yaml": "^4.1.0",
108
- "minio": "^8.0.7",
109
- "modbus-serial": "^8.0.25",
110
- "mqtt": "^5.15.2",
111
- "node-opcua": "^2.178.0",
112
- "nuxt": "^4.0.0",
113
- "nuxt-auth-utils": "^0.5.0",
114
- "pg": "^8.23.0",
115
- "phaser": "^4.0.0",
116
- "pinia": "^3.0.0",
117
- "pinia-plugin-persistedstate": "^4.2.0",
118
- "sqlite-vec": "^0.1.9",
119
- "three": "^0.185.1",
120
- "unocss": "^66.0.0",
121
- "unplugin-icons": "^22.0.0",
122
- "vue": "^3.5.13",
123
- "vue-echarts": "^7.0.3",
124
- "vue-router": "^4.5.0",
125
- "zod": "^3.24.1"
126
- },
127
- "devDependencies": {
128
- "@commitlint/cli": "^19.6.1",
129
- "@commitlint/config-conventional": "^19.6.0",
130
- "@nuxt/eslint": "^1.0.0",
131
- "@types/js-yaml": "^4.0.9",
132
- "@types/node": "^26.2.0",
133
- "@types/three": "^0.185.4",
134
- "eslint": "^9.17.0",
135
- "husky": "^9.1.7",
136
- "lint-staged": "^15.3.0",
137
- "pngjs": "^7.0.0",
138
- "puppeteer-core": "^25.5.0",
139
- "tsx": "^4.23.12",
140
- "typescript": "^5.7.3",
141
- "vue-tsc": "^3.3.10"
142
- },
143
- "pnpm": {
144
- "ignoredBuiltDependencies": [
145
- "@serialport/bindings-cpp"
146
- ]
147
- }
148
- }
1
+ {
2
+ "name": "agentworkshop",
3
+ "version": "0.7.21",
4
+ "type": "module",
5
+ "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
+ "license": "PolyForm-Noncommercial-1.0.0",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./sdk/index.d.mts",
10
+ "default": "./sdk/index.mjs"
11
+ },
12
+ "./sdk": {
13
+ "types": "./sdk/index.d.mts",
14
+ "default": "./sdk/index.mjs"
15
+ },
16
+ "./sdk/client": {
17
+ "types": "./sdk/client.d.mts",
18
+ "default": "./sdk/client.mjs"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "bin": {
23
+ "aw": "bin/aw.mjs",
24
+ "agentworkshop": "bin/aw.mjs"
25
+ },
26
+ "files": [
27
+ ".output",
28
+ "bin",
29
+ "cli",
30
+ "sdk",
31
+ "app",
32
+ "server",
33
+ "shared",
34
+ "i18n",
35
+ "public",
36
+ "scripts",
37
+ ".AgentWorkShop/prompts",
38
+ "config.yml",
39
+ "nuxt.config.ts",
40
+ "tsconfig.json",
41
+ "uno.config.ts",
42
+ "docker-compose.yml",
43
+ ".env.example",
44
+ "README-zh.md",
45
+ "docs/cli.md",
46
+ "docs/plugins.md",
47
+ "docs/sdk.md",
48
+ "docs/tui.md",
49
+ "tui"
50
+ ],
51
+ "packageManager": "pnpm@11.9.0",
52
+ "engines": {
53
+ "node": ">=23.4.0"
54
+ },
55
+ "scripts": {
56
+ "dev": "node bin/aw.mjs dev",
57
+ "build": "node bin/aw.mjs build",
58
+ "start": "node bin/aw.mjs start",
59
+ "generate": "nuxt generate",
60
+ "preview": "node bin/aw.mjs start",
61
+ "cli": "node bin/aw.mjs",
62
+ "tui": "node tui/aw-tui.mjs",
63
+ "postinstall": "node scripts/home-bootstrap.mjs",
64
+ "prepublishOnly": "node bin/aw.mjs build",
65
+ "lint": "eslint .",
66
+ "lint:fix": "eslint . --fix",
67
+ "typecheck": "nuxt typecheck",
68
+ "prepare": "husky",
69
+ "game:test": "tsx scripts/test-agent-brain.ts && tsx scripts/test-game-session.ts",
70
+ "test:api-live": "node scripts/api-live-e2e.mjs",
71
+ "game:verify": "node scripts/verify-game.mjs",
72
+ "game:build-assets": "node scripts/build-player-sheet.mjs",
73
+ "game:build-wuwa": "node scripts/build-wuwa-assets.mjs",
74
+ "game:build-models": "node scripts/build-character-models.mjs",
75
+ "game:build-world": "node scripts/build-world-map.mjs",
76
+ "game:build-model3d": "node scripts/build-model-3d.mjs"
77
+ },
78
+ "lint-staged": {
79
+ "*.{js,mjs,cjs,ts,vue}": "eslint --fix"
80
+ },
81
+ "dependencies": {
82
+ "@ant-design-vue/nuxt": "^1.4.6",
83
+ "@ant-design/icons-vue": "^7.0.1",
84
+ "@earendil-works/pi-tui": "^0.84.4",
85
+ "@fontsource-variable/fraunces": "^5.3.0",
86
+ "@fontsource-variable/geist": "^5.3.0",
87
+ "@fontsource-variable/geist-mono": "^5.3.0",
88
+ "@fontsource-variable/inter": "^5.3.0",
89
+ "@fontsource/eb-garamond": "^5.3.0",
90
+ "@fontsource/ibm-plex-mono": "^5.3.0",
91
+ "@iconify-json/tabler": "^1.2.0",
92
+ "@modelcontextprotocol/sdk": "^1.30.0",
93
+ "@nuxtjs/i18n": "^9.0.0",
94
+ "@pinia/nuxt": "^0.11.0",
95
+ "@unocss/nuxt": "^66.0.0",
96
+ "@unocss/preset-attributify": "^66.0.0",
97
+ "@unocss/preset-icons": "^66.7.5",
98
+ "@unocss/preset-typography": "^66.0.0",
99
+ "@vueuse/core": "^13.0.0",
100
+ "@vueuse/nuxt": "^13.0.0",
101
+ "@xterm/addon-fit": "^0.11.0",
102
+ "@xterm/xterm": "^6.0.0",
103
+ "ant-design-vue": "^4.2.6",
104
+ "axios": "^1.7.9",
105
+ "echarts": "^5.6.0",
106
+ "h3": "2.0.1-rc.26",
107
+ "js-yaml": "^4.1.0",
108
+ "minio": "^8.0.7",
109
+ "modbus-serial": "^8.0.25",
110
+ "mqtt": "^5.15.2",
111
+ "node-opcua": "^2.178.0",
112
+ "nuxt": "^4.0.0",
113
+ "nuxt-auth-utils": "^0.5.0",
114
+ "pg": "^8.23.0",
115
+ "phaser": "^4.0.0",
116
+ "pinia": "^3.0.0",
117
+ "pinia-plugin-persistedstate": "^4.2.0",
118
+ "sqlite-vec": "^0.1.9",
119
+ "three": "^0.185.1",
120
+ "unocss": "^66.0.0",
121
+ "unplugin-icons": "^22.0.0",
122
+ "vue": "^3.5.13",
123
+ "vue-echarts": "^7.0.3",
124
+ "vue-router": "^4.5.0",
125
+ "zod": "^3.24.1"
126
+ },
127
+ "devDependencies": {
128
+ "@commitlint/cli": "^19.6.1",
129
+ "@commitlint/config-conventional": "^19.6.0",
130
+ "@nuxt/eslint": "^1.0.0",
131
+ "@types/js-yaml": "^4.0.9",
132
+ "@types/node": "^26.2.0",
133
+ "@types/three": "^0.185.4",
134
+ "eslint": "^9.17.0",
135
+ "husky": "^9.1.7",
136
+ "lint-staged": "^15.3.0",
137
+ "pngjs": "^7.0.0",
138
+ "puppeteer-core": "^25.5.0",
139
+ "tsx": "^4.23.12",
140
+ "typescript": "^5.7.3",
141
+ "vue-tsc": "^3.3.10"
142
+ },
143
+ "pnpm": {
144
+ "ignoredBuiltDependencies": [
145
+ "@serialport/bindings-cpp"
146
+ ]
147
+ }
148
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * 多 Harness 真实端到端 E2E(生产实例 :3001,真实 Modbus TCP 模拟器)
3
+ *
4
+ * 四引擎并行,各自独立 Channel/Worker/绑定,场景互不重叠:
5
+ * omp → 闭环控制:dcw_control → daq_query → dcw_judge keep (OMP-CLOSEDLOOP-OK)
6
+ * codex → 数据控制:dcw_control 写真实寄存器 → dcw_journal 账本 (CODEX-WRITE-OK)
7
+ * dsh → 数据采集:daq_query 真实采样 + line_context 归属 (DSH-DAQ-OK)
8
+ * opencode → Recipe 写入回退:recipe_update → recipe_rollback (OC-RECIPE-OK)
9
+ * 服务端状态断言:codex 写后 PLC 回读=目标;opencode 版本净增 2;产线启停生命周期。
10
+ *
11
+ * 用法:NO_PROXY='127.0.0.1,localhost' node scripts/_dbg-multiharness-live-e2e.mjs [base]
12
+ */
13
+ const BASE = process.argv[2] ?? 'http://127.0.0.1:3001'
14
+ const TAG = `mh${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 sleep = ms => new Promise(r => setTimeout(r, ms))
22
+ const { createRequire } = await import('node:module')
23
+
24
+ const api = async (method, path, { body, token, timeoutMs } = {}) => {
25
+ const res = await fetch(`${BASE}${path}`, {
26
+ method,
27
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
28
+ body: body === undefined ? undefined : JSON.stringify(body),
29
+ signal: AbortSignal.timeout(timeoutMs ?? 30_000),
30
+ })
31
+ return { status: res.status, ...(await res.json().catch(() => ({}))) }
32
+ }
33
+ const invoke = (token, agentId, tool, args) => api('POST', '/api/workshop/agent-tools/invoke', { body: { agentId, tool, args }, token })
34
+
35
+ const ENGINES = [
36
+ { key: 'omp', harness: 'omp', cfg: { provider: 'zhipu-coding-plan', model: 'glm-5.3-flash' } },
37
+ { key: 'codex', harness: 'codex', cfg: { model: 'glm-5.3-flash', approvalPolicy: 'never' } },
38
+ { key: 'dsh', harness: 'dsh', cfg: { provider: 'ustc', model: 'glm-5.3-flash' } },
39
+ { key: 'opencode', harness: 'opencode', cfg: { model: 'zhipuai-coding-plan/glm-5.3-flash' } },
40
+ ]
41
+
42
+ async function pollTask(token, id, timeoutMs) {
43
+ const deadline = Date.now() + timeoutMs
44
+ let state = ''
45
+ while (Date.now() < deadline) {
46
+ for (let i = 0; i < 3; i++) {
47
+ try {
48
+ const me = await api('GET', `/api/workshop/tasks/${id}`, { token })
49
+ state = me.data?.state ?? ''
50
+ break
51
+ }
52
+ catch { await sleep(3000) }
53
+ }
54
+ if (['COMPLETED', 'FAILED', 'CANCELED'].includes(state)) return state
55
+ await sleep(6000)
56
+ }
57
+ return state || 'RUNNING'
58
+ }
59
+
60
+ async function gatherTaskBlob(token, channelId, taskId) {
61
+ const parts = await Promise.all([
62
+ api('GET', `/api/workshop/tasks/${taskId}`, { token }),
63
+ api('GET', `/api/workshop/channels/${channelId}/messages?limit=200`, { token }),
64
+ api('GET', `/api/workshop/channels/${channelId}/events?limit=400`, { token }),
65
+ ])
66
+ return parts.map(p => JSON.stringify(p.data ?? {})).join('')
67
+ }
68
+
69
+ async function main() {
70
+ console.log(`━━━ 多 Harness 真实 E2E @ ${BASE} (tag=${TAG}) ━━━`)
71
+ // 清理上一轮遗留(异常中断未清理的 mh-* 产线)
72
+ try {
73
+ const d = (await api('GET', '/api/workshop/dcw', { token })).data
74
+ for (const l of d.lines.filter(x => x.name.startsWith('mh-line-'))) {
75
+ for (const r of d.recipes.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/dcw/recipes/${r.id}`, { token })
76
+ for (const p of d.products.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/dcw/products/${p.id}`, { token })
77
+ for (const n of d.nodes.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/dcw/${n.id}`, { token })
78
+ for (const n of (await api('GET', '/api/workshop/daq', { token })).data.nodes.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/daq/${n.id}`, { token })
79
+ await api('DELETE', `/api/workshop/dcw/lines/${l.id}`, { token })
80
+ }
81
+ for (const c of (await api('GET', '/api/workshop/channels', { token })).data.filter(x => x.name.startsWith('mh-'))) {
82
+ await api('DELETE', `/api/workshop/channels/${c.id}?purge=1`, { token })
83
+ }
84
+ }
85
+ catch { /* 首轮无遗留 */ }
86
+
87
+ const login = await api('POST', '/api/users/login', { body: { email: 'admin@awshop.local', password: 'admin123' } })
88
+ const token = login.data?.token
89
+ ok(Boolean(token), 'admin 登录')
90
+ if (!token) process.exit(1)
91
+
92
+ // ════ 夹具:产线 + 节点 + 产品/配方 ════
93
+ const line = (await api('POST', '/api/workshop/dcw/lines', { body: { name: `mh-line-${TAG}` }, token })).data?.line
94
+ const nodeA = (await api('POST', '/api/workshop/dcw', {
95
+ body: { name: `mh-press-${TAG}`, templateRef: 'temp-sp', driver: 'modbus-tcp', driverConfig: { host: '127.0.0.1', port: 1502, unitId: 1, register: 40021, dataType: 'float32', byteOrder: 'big' }, unit: 'MPa', min: 0, max: 5, lineId: line.id },
96
+ token,
97
+ })).data?.node
98
+ const daq = (await api('POST', '/api/workshop/daq', {
99
+ body: { name: `mh-daq-${TAG}`, templateRef: 'pressure-tx', driver: 'modbus-tcp', driverConfig: { host: '127.0.0.1', port: 1502, unitId: 1, register: 40001, registerType: 'holding', dataType: 'float32', scale: 1, byteOrder: 'big' }, unit: 'MPa', min: 0, max: 5, intervalMs: 2000, lineId: line.id },
100
+ token,
101
+ })).data?.node
102
+ const product = (await api('POST', '/api/workshop/dcw/products', { body: { name: `mh-prod-${TAG}`, lineId: line.id }, token })).data?.product
103
+ const recipe = (await api('POST', '/api/workshop/dcw/recipes', {
104
+ body: { name: `mh-recipe-${TAG}`, productId: product.id, params: [{ nodeId: nodeA.id, value: 0.8, min: 0.6, max: 1.1 }] },
105
+ token,
106
+ })).data?.recipe
107
+ ok(Boolean(line?.id && nodeA?.id && daq?.id && recipe?.id), `夹具:产线/节点/数采/配方(${recipe?.id} v${recipe?.version})`)
108
+
109
+ // 产线管理生命周期:开跑
110
+ const start = await api('POST', `/api/workshop/dcw/lines/${line.id}/start`, { body: { recipeId: recipe.id }, token })
111
+ ok(start.status === 200, '[产线管理] 开跑(绑定批次)')
112
+ await sleep(4000)
113
+ const daqSnap = (await api('GET', '/api/workshop/daq', { token })).data
114
+ const daqLive = daqSnap.nodes.find(n => n.id === daq.id)
115
+ ok(daqLive?.value != null, `[产线管理] 数采真实值流入(${daqLive?.name}=${daqLive?.value}${daqLive?.unit ?? ''})`)
116
+
117
+ // ════ 四引擎并行组队 + 绑定 ════
118
+ const members = {}
119
+ for (const e of ENGINES) {
120
+ const ch = (await api('POST', '/api/workshop/channels', {
121
+ body: { name: `mh-${e.key}-${TAG}`, leadAgent: { name: `lead-${TAG}`, harness: 'mock', config: { delayMs: 60 } } },
122
+ token,
123
+ })).data
124
+ const tpl = await api('POST', '/api/workshop/agents', {
125
+ body: { name: `mh-${e.key}-${TAG}`, harness: e.harness, config: { ...e.cfg, systemPromptPrefix: '你是产线操作员:严格按任务步骤执行,只做要求的事,完成后立即调用 complete_task。' } },
126
+ token,
127
+ })
128
+ const join = await api('POST', `/api/workshop/channels/${ch.channelId}/agents`, { body: { agentId: tpl.data.id, role: 'worker' }, token })
129
+ const instId = join.data?.id
130
+ await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: nodeA.id, kind: 'dcw', mode: 'auto' }, token })
131
+ await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: daq.id, kind: 'daq', mode: 'auto' }, token })
132
+ members[e.key] = { channelId: ch.channelId, instId, tplId: tpl.data.id }
133
+ ok(Boolean(instId), `[${e.harness}] worker 建号+绑定`)
134
+ }
135
+
136
+ // ════ 分场景任务(并行下发)════
137
+ const tasks = {}
138
+ const scenario = (key, body) => api('POST', `/api/workshop/channels/${members[key].channelId}/tasks`, { body: { ...body, assigneeId: members[key].instId }, token })
139
+
140
+ tasks.omp = (await scenario('omp', {
141
+ title: `mh-loop-omp-${TAG}`,
142
+ parts: [{ text: `闭环调优(不要用其它节点):
143
+ 1. dcw_control(node_id="${nodeA.id}", value=0.82, hypothesis="提压观察");
144
+ 2. 等待 3 秒后 daq_query(node_id="${daq.id}", last_minutes=5);
145
+ 3. 依据采样证据 dcw_judge 对该优化记录落 keep;
146
+ 4. 交付原样包含一行:OMP-CLOSEDLOOP-OK,并附设定值与采样值。
147
+ 完成后 complete_task。` }],
148
+ })).data?.id
149
+ tasks.codex = (await scenario('codex', {
150
+ title: `mh-write-codex-${TAG}`,
151
+ parts: [{ text: `数据控制操作:
152
+ 1. dcw_control(node_id="${nodeA.id}", value=0.78) 下发压力设定;
153
+ 2. dcw_journal(node_id="${nodeA.id}") 查看参数账本;
154
+ 3. 交付原样包含一行:CODEX-WRITE-OK,附设定值。
155
+ 完成后 complete_task。` }],
156
+ })).data?.id
157
+ tasks.dsh = (await scenario('dsh', {
158
+ title: `mh-daq-dsh-${TAG}`,
159
+ parts: [{ text: `数据采集操作(不要下发任何控制):
160
+ 1. line_context 查看你负责的产线/产品/配方;
161
+ 2. daq_query(node_id="${daq.id}", last_minutes=10) 获取真实采样;
162
+ 3. 交付原样包含一行:DSH-DAQ-OK,并引用一个采样数值。
163
+ 完成后 complete_task。` }],
164
+ })).data?.id
165
+ const verBefore = (await api('GET', `/api/workshop/dcw/recipes/${recipe.id}/versions`, { token })).data?.versions ?? []
166
+ const vBefore = verBefore[verBefore.length - 1]?.version ?? 1
167
+ tasks.opencode = (await scenario('opencode', {
168
+ title: `mh-recipe-oc-${TAG}`,
169
+ parts: [{ text: `Recipe 写入回退操作:
170
+ 1. recipe_update(recipe_id="${recipe.id}", params=[{node_id:"${nodeA.id}", value:0.88}], reason="数采验证窗口稳定,固化最佳值");
171
+ 2. recipe_rollback(recipe_id="${recipe.id}", version=${vBefore}, reason="E2E 回退演练");
172
+ 3. recipe_versions 复核;
173
+ 4. 交付原样包含一行:OC-RECIPE-OK,并附保存与回退的版本号。
174
+ 完成后 complete_task。` }],
175
+ })).data?.id
176
+ ok(Object.values(tasks).every(Boolean), '四场景任务并行下发')
177
+
178
+ // ════ 等待全部终态(并行;单次窥探防超时堆积)════
179
+ const states = {}
180
+ const peekTask = async (id) => {
181
+ for (let i = 0; i < 3; i++) {
182
+ try {
183
+ return (await api('GET', `/api/workshop/tasks/${id}`, { token })).data?.state ?? ''
184
+ }
185
+ catch { await sleep(3000) }
186
+ }
187
+ return ''
188
+ }
189
+ const deadline = Date.now() + 16 * 60_000
190
+ while (Date.now() < deadline && Object.keys(states).length < ENGINES.length) {
191
+ for (const e of ENGINES) {
192
+ if (states[e.key]) continue
193
+ const st = await peekTask(tasks[e.key])
194
+ if (['COMPLETED', 'FAILED', 'CANCELED'].includes(st)) states[e.key] = st
195
+ }
196
+ if (Object.keys(states).length < ENGINES.length) await sleep(6000)
197
+ }
198
+
199
+ // ════ 逐引擎断言 ════
200
+ const blobCache = {}
201
+ const blobOf = async (key) => {
202
+ if (!blobCache[key]) blobCache[key] = await gatherTaskBlob(token, members[key].channelId, tasks[key])
203
+ return blobCache[key]
204
+ }
205
+ for (const e of ENGINES) {
206
+ const st = states[e.key]
207
+ ok(st === 'COMPLETED', `[${e.harness}] 任务 COMPLETED(state=${st || 'RUNNING'})`)
208
+ blobCache[e.key] = await gatherTaskBlob(token, members[e.key].channelId, tasks[e.key])
209
+ }
210
+
211
+ const bOmp = await blobOf('omp')
212
+ ok(bOmp.includes('OMP-CLOSEDLOOP-OK'), '[omp] 交付含 OMP-CLOSEDLOOP-OK(真实下发+采样+判定)')
213
+
214
+ const wRead = await api('POST', `/api/workshop/dcw/${nodeA.id}/read`, { body: {}, token })
215
+ const lastVal = wRead.data?.read?.value ?? wRead.data?.read?.eng
216
+ const bCodex = await blobOf('codex')
217
+ ok(bCodex.includes('CODEX-WRITE-OK'), '[codex] 交付含 CODEX-WRITE-OK(真实 Modbus 写入)')
218
+
219
+ const bDsh = await blobOf('dsh')
220
+ ok(bDsh.includes('DSH-DAQ-OK'), '[dsh] 交付含 DSH-DAQ-OK(真实数采)')
221
+
222
+ const versAfter = (await api('GET', `/api/workshop/dcw/recipes/${recipe.id}/versions`, { token })).data?.versions ?? []
223
+ const vAfter = versAfter[versAfter.length - 1]?.version ?? 1
224
+ const agentEntries = versAfter.filter(v => v.by === 'agent')
225
+ ok(vAfter >= vBefore + 2, `[opencode] Recipe 版本净增≥2(${vBefore}→${vAfter}:保存+回退)`)
226
+ ok(agentEntries.some(v => (v.actorName ?? '').includes(`mh-opencode-${TAG}`)), `[opencode] 版本归因含 opencode worker 名(${agentEntries.map(v => v.actorName).join(';') || '∅'})`)
227
+ const bOc = await blobOf('opencode')
228
+ ok(bOc.includes('OC-RECIPE-OK'), '[opencode] 交付含 OC-RECIPE-OK(Recipe 写入+回退)')
229
+ void lastVal
230
+
231
+ // ════ 产线管理:停线生命周期 ════
232
+ const stop = await api('POST', `/api/workshop/dcw/lines/${line.id}/stop`, { body: {}, token })
233
+ ok(stop.status === 200, '[产线管理] 停线')
234
+ await sleep(2500)
235
+ const daqAfterStop = (await api('GET', '/api/workshop/daq', { token })).data.nodes.find(n => n.id === daq.id)
236
+ ok(daqAfterStop?.state === 'offline' || daqAfterStop?.state === 'idle', `[产线管理] 停线后节点状态=${daqAfterStop?.state}(采集门控生效)`)
237
+
238
+ // ════ 清理 ════
239
+ for (const m of Object.values(members)) await api('DELETE', `/api/workshop/channels/${m.channelId}?purge=1`, { token }).catch(() => {})
240
+ for (const m of Object.values(members)) await api('DELETE', `/api/workshop/agents/${m.tplId}`, { token }).catch(() => {})
241
+ await api('DELETE', `/api/workshop/dcw/recipes/${recipe.id}`, { token }).catch(() => {})
242
+ await api('DELETE', `/api/workshop/dcw/products/${product.id}`, { token }).catch(() => {})
243
+ await api('DELETE', `/api/workshop/dcw/${nodeA.id}`, { token }).catch(() => {})
244
+ await api('DELETE', `/api/workshop/daq/${daq.id}`, { token }).catch(() => {})
245
+ await api('DELETE', `/api/workshop/dcw/lines/${line.id}`, { token }).catch(() => {})
246
+
247
+ console.log(`\n━━━ MultiHarness Live E2E: ${pass} passed / ${fail} failed ━━━`)
248
+ process.exit(fail === 0 ? 0 : 1)
249
+ }
250
+
251
+ main().catch((err) => {
252
+ console.error('E2E 异常:', err)
253
+ process.exit(1)
254
+ })