agentworkshop 0.7.46 → 0.7.48
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/.output/.AgentWorkShop/aml-python/__pycache__/amlkit.cpython-312.pyc +0 -0
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/6f524f3b-53e1-4df9-84e6-84d8058b9f16.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +955 -858
- package/.output/server/package.json +1 -1
- package/README-zh.md +7 -2
- package/README.md +7 -2
- package/config.yml +1 -1
- package/docs/aml.md +2 -0
- package/docs/cli.en.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/full-test-plan.md +1 -1
- package/docs/plugins.en.md +1 -1
- package/docs/plugins.md +1 -1
- package/docs/sdk.en.md +1 -1
- package/docs/sdk.md +1 -1
- package/package.json +1 -1
- package/scripts/_aw-packaged-start.cjs +22 -0
- package/scripts/_aw0746-stage12.mjs +94 -0
- package/scripts/_aw0746-stage34.mjs +136 -0
- package/scripts/_aw0746-stage5.mjs +144 -0
- package/scripts/_aw0746-stage6-aml.mjs +177 -0
- package/scripts/_aw0746-stage6b-train-sweep.mjs +145 -0
- package/scripts/_aw0746-stage6c-jobsmoke.mjs +69 -0
- package/scripts/_aw0746-stage7-twin.mjs +267 -0
- package/scripts/_aw0746-train-strong.py +142 -0
- package/scripts/_aw0747-readme-rows.mjs +32 -0
- package/scripts/_aw0748-failclosed-probe.mjs +90 -0
- package/scripts/_plc-sim-detached.cjs +22 -0
- package/server/services/workshop/agents/industrial/twin-tools.ts +85 -10
- package/server/services/workshop/aml/job-orchestrator/start-job.ts +140 -124
- package/server/services/workshop/aml/python/__pycache__/amlkit.cpython-312.pyc +0 -0
- package/server/services/workshop/aml/twin/physics-runtime.ts +16 -1
- package/server/services/workshop/aml/twin/repo.ts +9 -4
- package/.output/public/_nuxt/builds/meta/7827fe9f-b4c2-461f-b1fb-c0c7da7d66b8.json +0 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.7.46/0.7.47 打包系统验收 · 阶段 6:AML 真实训练 → 门禁 → 晋升 → 投入使用
|
|
3
|
+
* 1) 保证注塑产线有 ≥3 个已完结批次(采样成行)
|
|
4
|
+
* 2) 构建数据集快照(隔离三元组 + 控制/目标/特征节点)
|
|
5
|
+
* 3) 提交**真实训练作业**(平台参考实现 train-example.py 作为内联 code;uv venv + torch)
|
|
6
|
+
* 4) 评估门禁 → 模型注册 → 晋升 production(ONNX 深检)→ predict 影子参考(投入使用)
|
|
7
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 node scripts/_aw0746-stage6-aml.mjs
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
|
|
11
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
12
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
13
|
+
const REPO = process.env.AW_REPO ?? process.cwd()
|
|
14
|
+
|
|
15
|
+
let pass = 0
|
|
16
|
+
const fails = []
|
|
17
|
+
const check = (name, ok, detail = '') => {
|
|
18
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
19
|
+
if (ok) pass += 1
|
|
20
|
+
else fails.push(name)
|
|
21
|
+
}
|
|
22
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
23
|
+
const j = async (method, path, body, token) => {
|
|
24
|
+
const r = await fetch(BASE + path, {
|
|
25
|
+
method,
|
|
26
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
27
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
28
|
+
signal: AbortSignal.timeout(300_000),
|
|
29
|
+
})
|
|
30
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const token = (await j('POST', '/api/users/login', { email: ADMIN.email, password: ADMIN.password })).data?.token
|
|
34
|
+
console.log(`\n═══ 阶段 6:AML 真实训练与投入使用 @ ${BASE} ═══`)
|
|
35
|
+
|
|
36
|
+
// ── 1. 产线与三元组 ──
|
|
37
|
+
const dcw = (await j('GET', '/api/workshop/dcw', undefined, token)).data
|
|
38
|
+
const line = (dcw.lines ?? []).find(l => /injection-aw0746/i.test(String(l.name)))
|
|
39
|
+
const product = (dcw.products ?? []).find(p => p.lineId === line.id)
|
|
40
|
+
const recipe = (dcw.recipes ?? []).find(r => r.lineId === line.id)
|
|
41
|
+
check('隔离三元组可解析(line/product/recipe)', Boolean(line && product && recipe), `${line.id} / ${product?.id} / ${recipe?.id}`)
|
|
42
|
+
|
|
43
|
+
const daq = (await j('GET', '/api/workshop/daq', undefined, token)).data
|
|
44
|
+
const nodes = (daq.nodes ?? []).filter(n => n.lineId === line.id)
|
|
45
|
+
const find = (arr, re) => arr.find(n => re.test(n.name))
|
|
46
|
+
// 数据集 nodes **只能是数采(DAQ)节点**:控制量取"可控量的数采观测通道"
|
|
47
|
+
// (模具温度 / 注射压力 / 节拍),目标 = 制品克重,特征 = 熔温·飞边·缩痕·熔压。
|
|
48
|
+
// 数控(DCW)节点是写通道,不属于 daq_samples,提交会被 AML_NODE_MISSING 拒绝。
|
|
49
|
+
const ctrl = [find(nodes, /mold-temp-pv/), find(nodes, /inj-pressure-pv/), find(nodes, /cycle-time/)].filter(Boolean)
|
|
50
|
+
const specNodes = [
|
|
51
|
+
...ctrl.map(n => ({ nodeId: n.id, role: 'control' })),
|
|
52
|
+
{ nodeId: find(nodes, /part-weight/).id, role: 'target' },
|
|
53
|
+
...[find(nodes, /melt-temp-pv/), find(nodes, /flash-rate/), find(nodes, /sink-mark/), find(nodes, /melt-pressure-pv/)].filter(Boolean).map(n => ({ nodeId: n.id, role: 'feature' })),
|
|
54
|
+
]
|
|
55
|
+
check('数据集节点编组(control/target/feature 均为 DAQ 通道)', specNodes.filter(n => n.role === 'control').length >= 2 && specNodes.some(n => n.role === 'target'), `nodes=${specNodes.length} ctrl=${ctrl.map(n => n.name).join('/')}`)
|
|
56
|
+
|
|
57
|
+
// ── 2. 保证 ≥4 个已完结批次(每轮 3 分钟采样) ──
|
|
58
|
+
// 关跑必须**无条件**先停:`/api/workshop/dcw` 的产线投影不含 activeRunId,
|
|
59
|
+
// 靠它判断"是否在跑"会漏停 → 后续 start 409(实测踩过),批次数量永远上不去。
|
|
60
|
+
// 复跑优化:`AW_DATASET_ID=<已建数据集>` 时跳过采样直接进入训练(调参迭代不再等 12 分钟)。
|
|
61
|
+
const reuseDatasetId = process.env.AW_DATASET_ID ?? ''
|
|
62
|
+
const BATCHES = Number(process.env.AW_BATCHES ?? 4)
|
|
63
|
+
if (!reuseDatasetId) {
|
|
64
|
+
console.log(` · 造批次:停线 → 开跑 → 采样 3 分钟 → 停线,重复 ${BATCHES} 轮`)
|
|
65
|
+
for (let i = 1; i <= BATCHES; i += 1) {
|
|
66
|
+
await j('POST', `/api/workshop/dcw/lines/${line.id}/stop`, {}, token)
|
|
67
|
+
await sleep(2500)
|
|
68
|
+
const st = await j('POST', `/api/workshop/dcw/lines/${line.id}/start`, { recipeId: recipe.id }, token)
|
|
69
|
+
console.log(` 批次 ${i}: start status=${st.status} ${st.code ?? ''} ${st.message ?? 'ok'}`)
|
|
70
|
+
await sleep(180_000)
|
|
71
|
+
}
|
|
72
|
+
await j('POST', `/api/workshop/dcw/lines/${line.id}/stop`, {}, token)
|
|
73
|
+
await sleep(5000)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── 3. 数据集快照 ──
|
|
77
|
+
// beatMs 必须贴近真实采样间隔(实测产线运行中原样约 1.0 s/点):5 s 的拍格会把
|
|
78
|
+
// 3 分钟批次压成 ~27 行,行数门禁(≥500)必然不达标;1 s 拍格 → 单批次约 170 行。
|
|
79
|
+
const ds = reuseDatasetId
|
|
80
|
+
? { code: 0, data: { id: reuseDatasetId } }
|
|
81
|
+
: await j('POST', '/api/workshop/aml/datasets', {
|
|
82
|
+
lineId: line.id,
|
|
83
|
+
productId: product.id,
|
|
84
|
+
recipeId: recipe.id,
|
|
85
|
+
nodes: specNodes,
|
|
86
|
+
beatMs: Number(process.env.AW_BEAT_MS ?? 1000),
|
|
87
|
+
window: { historySteps: 6, horizonSteps: 4 },
|
|
88
|
+
cleaning: { hampelK: 3, maxInterpMs: 15_000, maxDropRatio: 0.5 },
|
|
89
|
+
split: { valRatio: 0.2, testRatio: 0.2, seed: 42 },
|
|
90
|
+
purpose: 'mpc_surrogate',
|
|
91
|
+
note: '0.7.46/0.7.47 打包系统验收:注塑克重窗口闭环',
|
|
92
|
+
}, token)
|
|
93
|
+
const datasetId = ds.data?.id ?? ds.data?.dataset?.id
|
|
94
|
+
const dsRow = ds.data?.dataset ?? ds.data
|
|
95
|
+
if (reuseDatasetId) {
|
|
96
|
+
const d = (await j('GET', `/api/workshop/aml/datasets/${reuseDatasetId}`, undefined, token)).data
|
|
97
|
+
const row = d?.dataset ?? d
|
|
98
|
+
Object.assign(dsRow, { rowCount: row?.rowCount, runIds: row?.runIds ?? row?.runs ?? [] })
|
|
99
|
+
console.log(` · 复用数据集 ${reuseDatasetId} rows=${dsRow.rowCount} runs=${dsRow.runIds?.length}`)
|
|
100
|
+
}
|
|
101
|
+
check('数据集快照构建(真实打标样本)', Boolean(datasetId), `dataset=${datasetId} rows=${dsRow?.rowCount ?? '?'} runs=${dsRow?.runIds?.length ?? '?'} ${ds.code !== 0 ? `${ds.code}: ${ds.message}` : ''}`)
|
|
102
|
+
check('行数与批次满足门禁下限(≥500 行 / ≥3 批次)', Number(dsRow?.rowCount ?? 0) >= 500 && (dsRow?.runIds?.length ?? 0) >= 3, `rows=${dsRow?.rowCount} runs=${dsRow?.runIds?.length}`)
|
|
103
|
+
|
|
104
|
+
// ── 4. 真实训练作业 ──
|
|
105
|
+
const trainCode = readFileSync(`${REPO}/server/services/workshop/aml/python/train-example.py`, 'utf8')
|
|
106
|
+
const trainParams = process.env.AW_TRAIN_PARAMS
|
|
107
|
+
? JSON.parse(process.env.AW_TRAIN_PARAMS)
|
|
108
|
+
: { lr: 0.01, epochs: 30, hidden: 64, batch: 64 }
|
|
109
|
+
console.log(` · 训练超参:${JSON.stringify(trainParams)}`)
|
|
110
|
+
const job = await j('POST', '/api/workshop/aml/jobs', {
|
|
111
|
+
datasetId,
|
|
112
|
+
purpose: 'mpc_surrogate',
|
|
113
|
+
jobKind: 'supervised',
|
|
114
|
+
code: trainCode,
|
|
115
|
+
params: trainParams,
|
|
116
|
+
seed: 42,
|
|
117
|
+
changeNote: '0.7.46 acceptance: baseline one-step model',
|
|
118
|
+
}, token)
|
|
119
|
+
const jobId = job.data?.id ?? job.data?.job?.id
|
|
120
|
+
check('训练作业提交(内联平台参考实现)', Boolean(jobId), `job=${jobId} ${job.message ?? ''}`)
|
|
121
|
+
|
|
122
|
+
console.log(' … 真实训练中(uv venv + torch,最长 15 分钟)…')
|
|
123
|
+
let jobRow = null
|
|
124
|
+
const deadline = Date.now() + 15 * 60_000
|
|
125
|
+
while (Date.now() < deadline) {
|
|
126
|
+
await sleep(10_000)
|
|
127
|
+
const row = (await j('GET', `/api/workshop/aml/jobs/${jobId}`, undefined, token)).data
|
|
128
|
+
jobRow = row?.job ?? row
|
|
129
|
+
process.stdout.write(`\r status=${jobRow?.status ?? '?'} ${String(jobRow?.error ?? '').slice(0, 40)} `)
|
|
130
|
+
if (jobRow && ['done', 'failed', 'canceled'].includes(jobRow.status)) break
|
|
131
|
+
}
|
|
132
|
+
console.log('')
|
|
133
|
+
check('训练作业跑完(status=done)', jobRow?.status === 'done', `status=${jobRow?.status} err=${jobRow?.error ?? ''}`)
|
|
134
|
+
const logs = await j('GET', `/api/workshop/aml/jobs/${jobId}/logs`, undefined, token)
|
|
135
|
+
const logText = JSON.stringify(logs.data ?? '').slice(0, 4000)
|
|
136
|
+
check('训练日志可见 torch/ONNX 产出痕迹', /onnx|epoch|loss|torch/i.test(logText), logText.replace(/\\n/g, ' ').slice(0, 200))
|
|
137
|
+
|
|
138
|
+
// ── 5. 门禁 + 模型注册 ──
|
|
139
|
+
const exps = await j('GET', `/api/workshop/aml/experiments?datasetId=${datasetId}`, undefined, token)
|
|
140
|
+
const expList = Array.isArray(exps.data) ? exps.data : (exps.data?.experiments ?? exps.data?.items ?? [])
|
|
141
|
+
const exp = expList[0]
|
|
142
|
+
const gates = exp?.gates ?? exp?.gateReport ?? {}
|
|
143
|
+
console.log(` 门禁:${JSON.stringify(gates).slice(0, 400)}`)
|
|
144
|
+
check('门禁报告产出(逐项可读)', Object.keys(gates).length > 0 || exp?.gatesPassed !== undefined, Object.keys(gates).slice(0, 8).join(','))
|
|
145
|
+
|
|
146
|
+
const models = await j('GET', '/api/workshop/aml/models', undefined, token)
|
|
147
|
+
const modelList = Array.isArray(models.data) ? models.data : (models.data?.models ?? models.data?.items ?? [])
|
|
148
|
+
const model = modelList.find(m => m.experimentId === exp?.id) ?? modelList[0]
|
|
149
|
+
check('模型已注册(registry 出现候选)', Boolean(model?.id), `model=${model?.id} stage=${model?.stage}`)
|
|
150
|
+
|
|
151
|
+
// ── 6. 晋升 production(投入使用)──
|
|
152
|
+
let promoted = null
|
|
153
|
+
if (model?.id) {
|
|
154
|
+
const p = await j('POST', `/api/workshop/aml/models/${model.id}/promote`, { toStage: 'production' }, token)
|
|
155
|
+
promoted = p
|
|
156
|
+
check('晋升 production(ONNX 深检 + 权限门)', p.code === 0 || p.status === 200, `status=${p.status} ${p.message ?? ''}`)
|
|
157
|
+
const after = (await j('GET', '/api/workshop/aml/models', undefined, token)).data
|
|
158
|
+
const listAfter = Array.isArray(after) ? after : (after?.models ?? after?.items ?? [])
|
|
159
|
+
const m2 = listAfter.find(x => x.id === model.id)
|
|
160
|
+
check('注册表 stage=production(模型已投入生产)', m2?.stage === 'production', `stage=${m2?.stage}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── 7. 预测引用(真正用起来)──
|
|
164
|
+
if (model?.id) {
|
|
165
|
+
const hist = Array.from({ length: 6 }, () => specNodes.map(() => 0.5))
|
|
166
|
+
const pred = await j('POST', `/api/workshop/aml/models/${model.id}/predict`, {
|
|
167
|
+
history: hist,
|
|
168
|
+
controls: [[0.5, 0.5, 0.5]],
|
|
169
|
+
steps: 4,
|
|
170
|
+
}, token)
|
|
171
|
+
const arr = pred.data?.prediction ?? pred.prediction
|
|
172
|
+
const flat = Array.isArray(arr) ? arr.flat(3).filter(v => typeof v === 'number') : []
|
|
173
|
+
check('影子预测可用(what-if 投入使用)', pred.status === 200 && flat.length > 0, `points=${flat.length} first=${flat.slice(0, 3).map(v => v.toFixed?.(4) ?? v).join(',')}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`\n★ 阶段 6:${pass} 通过 / ${fails.length} 失败${fails.length ? ` (${fails.join('; ')})` : ''}`)
|
|
177
|
+
console.log(JSON.stringify({ lineId: line.id, datasetId, jobId, modelId: model?.id, jobStatus: jobRow?.status, gatesPassed: exp?.gatesPassed, promoteStatus: promoted?.status }, null, 1))
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.7.46/0.7.47 打包系统验收 · 阶段 6b:训练调参冲刺(复用已采样的数据集,不再等 12 分钟)
|
|
3
|
+
* 逐档提交真实训练作业,直到单步 NRMSE 门禁(G1 ≤ 0.10)通过;通过则晋升 production + 影子预测。
|
|
4
|
+
* 每档都是"平台内联 code"路径:第 1/2 档用平台参考实现换超参,第 3 档用带 z-score 标准化的加强实现。
|
|
5
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 [AW_DATASET_ID=ds-xxx] node scripts/_aw0746-stage6b-train-sweep.mjs
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
|
|
9
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
10
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
11
|
+
const REPO = process.env.AW_REPO ?? process.cwd()
|
|
12
|
+
|
|
13
|
+
const j = async (method, path, body, token) => {
|
|
14
|
+
const r = await fetch(BASE + path, {
|
|
15
|
+
method,
|
|
16
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
17
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
18
|
+
signal: AbortSignal.timeout(300_000),
|
|
19
|
+
})
|
|
20
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
21
|
+
}
|
|
22
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
23
|
+
const token = (await j('POST', '/api/users/login', { email: ADMIN.email, password: ADMIN.password })).data?.token
|
|
24
|
+
console.log(`\n═══ 阶段 6b:训练调参冲刺 @ ${BASE} ═══`)
|
|
25
|
+
|
|
26
|
+
const dcw = (await j('GET', '/api/workshop/dcw', undefined, token)).data
|
|
27
|
+
const line = (dcw.lines ?? []).find(l => /injection-aw0746/i.test(String(l.name)))
|
|
28
|
+
const product = (dcw.products ?? []).find(p => p.lineId === line.id)
|
|
29
|
+
const recipe = (dcw.recipes ?? []).find(r => r.lineId === line.id)
|
|
30
|
+
const daq = (await j('GET', '/api/workshop/daq', undefined, token)).data
|
|
31
|
+
const find = (arr, re) => arr.find(n => re.test(n.name))
|
|
32
|
+
const nodes = (daq.nodes ?? []).filter(n => n.lineId === line.id)
|
|
33
|
+
|
|
34
|
+
async function buildDataset(window) {
|
|
35
|
+
const ds = await j('POST', '/api/workshop/aml/datasets', {
|
|
36
|
+
lineId: line.id,
|
|
37
|
+
productId: product.id,
|
|
38
|
+
recipeId: recipe.id,
|
|
39
|
+
nodes: [
|
|
40
|
+
{ nodeId: find(nodes, /mold-temp-pv/).id, role: 'control' },
|
|
41
|
+
{ nodeId: find(nodes, /inj-pressure-pv/).id, role: 'control' },
|
|
42
|
+
{ nodeId: find(nodes, /cycle-time/).id, role: 'control' },
|
|
43
|
+
{ nodeId: find(nodes, /part-weight/).id, role: 'target' },
|
|
44
|
+
...[find(nodes, /melt-temp-pv/), find(nodes, /flash-rate/), find(nodes, /sink-mark/), find(nodes, /melt-pressure-pv/)].filter(Boolean).map(n => ({ nodeId: n.id, role: 'feature' })),
|
|
45
|
+
],
|
|
46
|
+
beatMs: 1000,
|
|
47
|
+
window,
|
|
48
|
+
cleaning: { hampelK: 3, maxInterpMs: 15_000, maxDropRatio: 0.5 },
|
|
49
|
+
split: { valRatio: 0.2, testRatio: 0.2, seed: 42 },
|
|
50
|
+
purpose: 'mpc_surrogate',
|
|
51
|
+
note: `0.7.46 验收:调参冲刺 window=${window.historySteps}/${window.horizonSteps}`,
|
|
52
|
+
}, token)
|
|
53
|
+
return ds.data?.id
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function rowOf(datasetId) {
|
|
57
|
+
const d = (await j('GET', `/api/workshop/aml/datasets/${datasetId}`, undefined, token)).data
|
|
58
|
+
const row = d?.dataset ?? d
|
|
59
|
+
return { id: datasetId, rows: Number(row?.rowCount ?? 0), runs: JSON.parse(row?.runIdsJson ?? '[]').length }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function trainAndWait(datasetId, code, params, note, minutes = 16) {
|
|
63
|
+
const job = await j('POST', '/api/workshop/aml/jobs', { datasetId, purpose: 'mpc_surrogate', jobKind: 'supervised', code, params, seed: 42, changeNote: note }, token)
|
|
64
|
+
const jobId = job.data?.id ?? job.data?.job?.id
|
|
65
|
+
if (!jobId) return { status: 'submit-failed', message: job.message, jobId: null }
|
|
66
|
+
const deadline = Date.now() + minutes * 60_000
|
|
67
|
+
let row = null
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
await sleep(10_000)
|
|
70
|
+
row = (await j('GET', `/api/workshop/aml/jobs/${jobId}`, undefined, token)).data
|
|
71
|
+
row = row?.job ?? row
|
|
72
|
+
process.stdout.write(`\r ${jobId} status=${row?.status ?? '?'} `)
|
|
73
|
+
if (row && ['done', 'failed', 'canceled'].includes(row.status)) break
|
|
74
|
+
}
|
|
75
|
+
console.log('')
|
|
76
|
+
return { jobId, status: row?.status, error: row?.error ?? '' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function gatesOf(datasetId, jobId) {
|
|
80
|
+
const exps = ((await j('GET', `/api/workshop/aml/experiments?datasetId=${datasetId}`, undefined, token)).data?.experiments ?? [])
|
|
81
|
+
// 实验列表按创建升序返回,必须按 jobId 精确匹配本次作业(否则读到旧档的指标)
|
|
82
|
+
const exp = exps.find(e => e.jobId === jobId) ?? exps.at(-1)
|
|
83
|
+
const metrics = JSON.parse(exp?.metricsJson || '{}')
|
|
84
|
+
return { exp, metrics, gates: exp?.gates ?? exp?.gateReport ?? null }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── 档位定义 ──
|
|
88
|
+
const refCode = readFileSync(`${REPO}/server/services/workshop/aml/python/train-example.py`, 'utf8')
|
|
89
|
+
const strongCode = readFileSync(`${REPO}/scripts/_aw0746-train-strong.py`, 'utf8')
|
|
90
|
+
let datasetId = process.env.AW_DATASET_ID ?? ''
|
|
91
|
+
|
|
92
|
+
const attempts = [
|
|
93
|
+
{ label: '参考实现 + 更大容量/更久训练(lr3e-3,e200,h128)', code: refCode, params: { lr: 0.003, epochs: 200, hidden: 128, batch: 64 }, window: null },
|
|
94
|
+
{ label: '参考实现 + 全连接加宽(lr1e-3,e300,h256,b128)', code: refCode, params: { lr: 0.001, epochs: 300, hidden: 256, batch: 128 }, window: null },
|
|
95
|
+
{ label: '加强实现(z-score + 2 层 + best-val 回滚,lr3e-3,e400,h128)', code: strongCode, params: { lr: 0.003, epochs: 400, hidden: 128, batch: 64 }, window: { historySteps: 10, horizonSteps: 4 } },
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
let winner = null
|
|
99
|
+
for (const [i, a] of attempts.entries()) {
|
|
100
|
+
if (a.window) {
|
|
101
|
+
datasetId = await buildDataset(a.window)
|
|
102
|
+
console.log(` · 重建数据集(window=${a.window.historySteps}/${a.window.horizonSteps}) → ${datasetId}`)
|
|
103
|
+
}
|
|
104
|
+
const info = await rowOf(datasetId)
|
|
105
|
+
if (info.rows < 500) {
|
|
106
|
+
console.log(` ✗ 数据集行数不足:${JSON.stringify(info)}`)
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
console.log(`\n 档 ${i + 1}:${a.label}\n dataset=${datasetId} rows=${info.rows} runs=${info.runs} params=${JSON.stringify(a.params)}`)
|
|
110
|
+
const run = await trainAndWait(datasetId, a.code, a.params, `0.7.46 sweep #${i + 1}: ${a.label}`)
|
|
111
|
+
const { exp, metrics, gates } = await gatesOf(datasetId, run.jobId)
|
|
112
|
+
const g1 = Number(metrics.oneStepTest?.nrmse ?? NaN)
|
|
113
|
+
const g2 = Number(metrics.rolloutTest?.nrmse ?? NaN)
|
|
114
|
+
const g3 = Number(metrics.oneStepVal?.nrmse ?? NaN)
|
|
115
|
+
console.log(` job=${run.jobId} ${run.status}${run.error ? ` err=${run.error}` : ''}`)
|
|
116
|
+
console.log(` oneStep(test)=${g1.toFixed(4)} oneStep(val)=${g3.toFixed(4)} rollout=${g2.toFixed(4)} gatesPassed=${gates?.passed}`)
|
|
117
|
+
if (run.status === 'done' && Number.isFinite(g1) && g1 <= 0.10) {
|
|
118
|
+
winner = { attempt: i + 1, datasetId, jobId: run.jobId, exp, metrics }
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
console.log(` ✗ 未过 G1(实测 ${Number.isFinite(g1) ? g1.toFixed(4) : 'n/a'} / 上限 0.10),进入下一档`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!winner) {
|
|
125
|
+
console.log('\n★ 阶段 6b:全部档位未过 G1 门禁')
|
|
126
|
+
process.exit(2)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`\n ✓ 档 ${winner.attempt} 通过门禁:G1=${Number(winner.metrics.oneStepTest.nrmse).toFixed(4)}`)
|
|
130
|
+
const models = ((await j('GET', '/api/workshop/aml/models', undefined, token)).data?.models ?? [])
|
|
131
|
+
const model = models.find(m => m.experimentId === winner.exp?.id) ?? models[0]
|
|
132
|
+
console.log(` · 模型注册:${model?.id} stage=${model?.stage}`)
|
|
133
|
+
const p = await j('POST', `/api/workshop/aml/models/${model.id}/promote`, { toStage: 'production' }, token)
|
|
134
|
+
console.log(` · 晋升 production:status=${p.status} ${p.message ?? ''}`)
|
|
135
|
+
const after = ((await j('GET', '/api/workshop/aml/models', undefined, token)).data?.models ?? []).find(x => x.id === model.id)
|
|
136
|
+
console.log(` · 复核 stage=${after?.stage}`)
|
|
137
|
+
|
|
138
|
+
const hist = Array.from({ length: Number(winner.metrics.ioSpec?.historySteps ?? 10) }, () => winner.metrics.ioSpec.allNodes.map(n => Number(winner.metrics.ioSpec.norm.x.mean[winner.metrics.ioSpec.allNodes.indexOf(n)] ?? 0)))
|
|
139
|
+
const pred = await j('POST', `/api/workshop/aml/models/${model.id}/predict`, { history: hist, controls: [[0.5, 0.5, 0.5]], steps: 4 }, token)
|
|
140
|
+
const arr = pred.data?.prediction ?? pred.prediction
|
|
141
|
+
const flat = Array.isArray(arr) ? arr.flat(3).filter(v => typeof v === 'number') : []
|
|
142
|
+
console.log(` · 影子预测:status=${pred.status} points=${flat.length} first=${flat.slice(0, 3).map(v => v.toFixed?.(4) ?? v).join(',')}`)
|
|
143
|
+
|
|
144
|
+
console.log(`\n★ 阶段 6b:通过(dataset=${winner.datasetId} job=${winner.jobId} model=${model.id} stage=${after?.stage})`)
|
|
145
|
+
console.log(JSON.stringify({ datasetId: winner.datasetId, jobId: winner.jobId, modelId: model.id, oneStepTest: winner.metrics.oneStepTest?.nrmse, rolloutTest: winner.metrics.rolloutTest?.nrmse, stage: after?.stage, predictPoints: flat.length }, null, 1))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.7.47 打包系统验收 · 阶段 6c:AML 作业链路冒烟(内联 code + 新 sidecar 落盘)
|
|
3
|
+
* 只验证"作业能不能真的跑起来":短训练(3 epoch)→ 作业到达终态 → 日志/门禁/工件可见。
|
|
4
|
+
* 门禁是否达标由阶段 6b 的完整训练负责,这里不重复等 10 分钟。
|
|
5
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 node scripts/_aw0746-stage6c-jobsmoke.mjs
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
|
|
9
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
10
|
+
const REPO = process.env.AW_REPO ?? process.cwd()
|
|
11
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
12
|
+
let pass = 0
|
|
13
|
+
const fails = []
|
|
14
|
+
const check = (name, ok, detail = '') => {
|
|
15
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
16
|
+
if (ok) pass += 1
|
|
17
|
+
else fails.push(name)
|
|
18
|
+
}
|
|
19
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
20
|
+
const j = async (method, path, body, token) => {
|
|
21
|
+
const r = await fetch(BASE + path, {
|
|
22
|
+
method,
|
|
23
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
24
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
25
|
+
signal: AbortSignal.timeout(300_000),
|
|
26
|
+
})
|
|
27
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
28
|
+
}
|
|
29
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
30
|
+
console.log(`\n═══ 阶段 6c:AML 作业链路冒烟 @ ${BASE} ═══`)
|
|
31
|
+
|
|
32
|
+
const datasets = ((await j('GET', '/api/workshop/aml/datasets', undefined, token)).data?.datasets ?? [])
|
|
33
|
+
const ds = datasets.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))[0]
|
|
34
|
+
check('存在可复用数据集快照', Boolean(ds?.id), `dataset=${ds?.id} rows=${ds?.rowCount} runs=${JSON.parse(ds?.runIdsJson ?? '[]').length}`)
|
|
35
|
+
|
|
36
|
+
const code = readFileSync(`${REPO}/server/services/workshop/aml/python/train-example.py`, 'utf8')
|
|
37
|
+
const job = await j('POST', '/api/workshop/aml/jobs', {
|
|
38
|
+
datasetId: ds.id,
|
|
39
|
+
purpose: 'mpc_surrogate',
|
|
40
|
+
jobKind: 'supervised',
|
|
41
|
+
code,
|
|
42
|
+
params: { lr: 0.01, epochs: 3, hidden: 32, batch: 64 },
|
|
43
|
+
seed: 42,
|
|
44
|
+
changeNote: '0.7.47 acceptance: inline-code job smoke',
|
|
45
|
+
}, token)
|
|
46
|
+
const jobId = job.data?.id ?? job.data?.job?.id
|
|
47
|
+
check('内联 code 作业被接受(不再要求 workspace/train.py)', Boolean(jobId) && !/train\.py 不存在/.test(String(job.message ?? '')), `job=${jobId} ${job.message ?? 'ok'}`)
|
|
48
|
+
|
|
49
|
+
let row = null
|
|
50
|
+
const deadline = Date.now() + 10 * 60_000
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
await sleep(8000)
|
|
53
|
+
const r = await j('GET', `/api/workshop/aml/jobs/${jobId}`, undefined, token)
|
|
54
|
+
row = r.data?.job ?? r.data
|
|
55
|
+
process.stdout.write(`\r status=${row?.status ?? '?'} `)
|
|
56
|
+
if (row && ['done', 'failed', 'canceled'].includes(row.status)) break
|
|
57
|
+
}
|
|
58
|
+
console.log('')
|
|
59
|
+
check('作业到达终态(python 运行时真实执行)', ['done', 'failed'].includes(row?.status), `status=${row?.status} err=${String(row?.error ?? '').slice(0, 90)}`)
|
|
60
|
+
const logs = await j('GET', `/api/workshop/aml/jobs/${jobId}/logs`, undefined, token)
|
|
61
|
+
const logText = JSON.stringify(logs.data ?? '')
|
|
62
|
+
check('训练日志可见(epoch / onnx 痕迹)', /epoch|onnx|train-example done/i.test(logText), logText.slice(0, 180))
|
|
63
|
+
const exps = ((await j('GET', `/api/workshop/aml/experiments?datasetId=${ds.id}`, undefined, token)).data?.experiments ?? [])
|
|
64
|
+
const exp = exps.find(e => e.jobId === jobId)
|
|
65
|
+
const gates = exp?.gates
|
|
66
|
+
check('实验与门禁报告落库(逐项可读)', Boolean(exp?.id) && Array.isArray(gates?.checks) && gates.checks.length >= 4, `exp=${exp?.id} checks=${gates?.checks?.length} passed=${gates?.passed}`)
|
|
67
|
+
|
|
68
|
+
console.log(`\n★ 阶段 6c:${pass} 通过 / ${fails.length} 失败${fails.length ? ` (${fails.join('; ')})` : ''}`)
|
|
69
|
+
console.log(JSON.stringify({ datasetId: ds.id, jobId, status: row?.status, gatesPassed: gates?.passed }, null, 1))
|