agentworkshop 0.7.48 → 0.7.49
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/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/0530cee1-fff5-4025-a6f4-5708b6534608.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +957 -955
- package/.output/server/package.json +1 -1
- package/README-zh.md +813 -811
- package/README.md +823 -821
- package/config.yml +176 -176
- package/docs/cli.en.md +445 -445
- package/docs/cli.md +410 -410
- package/docs/full-test-plan.md +191 -191
- package/docs/plugins.en.md +567 -567
- package/docs/plugins.md +560 -560
- package/docs/sdk.en.md +922 -922
- package/docs/sdk.md +861 -861
- package/package.json +172 -172
- package/scripts/_aw0746-stage5.mjs +159 -144
- package/scripts/_aw0746-stage7-twin.mjs +301 -267
- package/scripts/_aw0748-failclosed-probe.mjs +117 -90
- package/scripts/_aw0748-fresh-channel.mjs +21 -0
- package/scripts/_aw0748-mock-closedloop.mjs +89 -0
- package/scripts/_aw0748-wake-channel.mjs +46 -0
- package/scripts/_aw0749-readme-rows.mjs +23 -0
- package/scripts/_probe-lease.cjs +13 -0
- package/scripts/_probe-task-queue.cjs +9 -0
- package/server/services/workshop/aml/twin/provider-contracts.ts +34 -0
- package/server/services/workshop/aml/twin/provider-health.ts +8 -0
- package/server/services/workshop/aml/twin/provider-loader.ts +67 -0
- package/server/services/workshop/aml/twin/provider-registry.ts +50 -0
- package/server/services/workshop/runtime/manager/state.ts +21 -4
- package/.output/public/_nuxt/builds/meta/6f524f3b-53e1-4df9-84e6-84d8058b9f16.json +0 -1
|
@@ -1,90 +1,117 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 0.7.48 打包系统验收 · 硬约束失败关闭探针(无需 LLM,秒级)
|
|
3
|
-
* 场景:同一份真实快照 + 同一组候选,只改 constraints[].id
|
|
4
|
-
* ① id = weight(可映射)→ 试验正常给出约束结论
|
|
5
|
-
* ② id = part_weight(不可映射)→ 必须**判不通过**并给出"无法映射"明细(v0.7.47 会假通过)
|
|
6
|
-
* 用法:AW_BASE=http://127.0.0.1:3001 [AW_HOME=<home>] node scripts/_aw0748-failclosed-probe.mjs
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
11
|
-
const HOME = process.env.AW_HOME ?? `${process.cwd()}/.aw0746-home`
|
|
12
|
-
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
13
|
-
|
|
14
|
-
let pass = 0
|
|
15
|
-
const fails = []
|
|
16
|
-
const check = (name, ok, detail = '') => {
|
|
17
|
-
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
18
|
-
if (ok) pass += 1
|
|
19
|
-
else fails.push(name)
|
|
20
|
-
}
|
|
21
|
-
const j = async (method, path, body, token) => {
|
|
22
|
-
const r = 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(120_000),
|
|
27
|
-
})
|
|
28
|
-
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
29
|
-
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
const baseScene = firstJson(String((await invoke('twin_scene_read', {})).text ?? ''))
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
check('
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 0.7.48 打包系统验收 · 硬约束失败关闭探针(无需 LLM,秒级)
|
|
3
|
+
* 场景:同一份真实快照 + 同一组候选,只改 constraints[].id
|
|
4
|
+
* ① id = weight(可映射)→ 试验正常给出约束结论
|
|
5
|
+
* ② id = part_weight(不可映射)→ 必须**判不通过**并给出"无法映射"明细(v0.7.47 会假通过)
|
|
6
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 [AW_HOME=<home>] node scripts/_aw0748-failclosed-probe.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
11
|
+
const HOME = process.env.AW_HOME ?? `${process.cwd()}/.aw0746-home`
|
|
12
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
13
|
+
|
|
14
|
+
let pass = 0
|
|
15
|
+
const fails = []
|
|
16
|
+
const check = (name, ok, detail = '') => {
|
|
17
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
18
|
+
if (ok) pass += 1
|
|
19
|
+
else fails.push(name)
|
|
20
|
+
}
|
|
21
|
+
const j = async (method, path, body, token) => {
|
|
22
|
+
const r = 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(120_000),
|
|
27
|
+
})
|
|
28
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
29
|
+
}
|
|
30
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
31
|
+
console.log(`\n═══ 0.7.48 硬约束失败关闭探针 @ ${BASE} ═══`)
|
|
32
|
+
|
|
33
|
+
const chans = (await j('GET', '/api/workshop/channels', undefined, token)).data ?? []
|
|
34
|
+
const twin = chans.find(c => /闭环优化-hybridtwin/.test(c.name))
|
|
35
|
+
const members = (await j('GET', `/api/workshop/channels/${twin.id}/agents`, undefined, token)).data ?? []
|
|
36
|
+
const lead = members.find(m => m.role === 'lead')
|
|
37
|
+
const invoke = async (tool, args) => (await j('POST', '/api/workshop/agent-tools/invoke', { agentId: lead.id, tool, args }, token)).data?.result ?? {}
|
|
38
|
+
const firstJson = (text) => {
|
|
39
|
+
const a = text.indexOf('{')
|
|
40
|
+
const b = text.lastIndexOf('}')
|
|
41
|
+
return a >= 0 && b > a ? JSON.parse(text.slice(a, b + 1)) : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 快照必须"真新鲜":VirtualTrial 只认 60s 内的水位 → 先开跑采样,再用 auto_daq 让服务端按绑定取样
|
|
45
|
+
const dcw = (await j('GET', '/api/workshop/dcw', undefined, token)).data
|
|
46
|
+
const line = (dcw.lines ?? []).find(l => /injection-aw0746/i.test(String(l.name)))
|
|
47
|
+
const product = (dcw.products ?? []).find(p => p.lineId === line.id)
|
|
48
|
+
const recipe = (dcw.recipes ?? []).find(r => r.lineId === line.id)
|
|
49
|
+
await j('POST', `/api/workshop/dcw/lines/${line.id}/stop`, {}, token)
|
|
50
|
+
await new Promise(r => setTimeout(r, 2500))
|
|
51
|
+
const st = await j('POST', `/api/workshop/dcw/lines/${line.id}/start`, { recipeId: recipe.id }, token)
|
|
52
|
+
console.log(` · 开跑采样:start status=${st.status} ${st.code ?? ''}`)
|
|
53
|
+
await new Promise(r => setTimeout(r, 8000))
|
|
54
|
+
|
|
55
|
+
const daq = (await j('GET', '/api/workshop/daq', undefined, token)).data
|
|
56
|
+
const daqNodes = (daq.nodes ?? []).filter(n => n.lineId === line.id)
|
|
57
|
+
const byName = re => daqNodes.find(n => re.test(n.name))
|
|
58
|
+
const baseScene = firstJson(String((await invoke('twin_scene_read', {})).text ?? ''))
|
|
59
|
+
const scene = {
|
|
60
|
+
...baseScene,
|
|
61
|
+
lineId: line.id,
|
|
62
|
+
productId: product.id,
|
|
63
|
+
recipeId: recipe.id,
|
|
64
|
+
observations: baseScene.observations.map(o => ({ ...o, nodeId: byName({ weight: /part-weight/, flash_rate: /flash-rate/, sink_rate: /sink-mark/ }[o.id])?.id })),
|
|
65
|
+
states: baseScene.states.map(s => ({ ...s, nodeId: byName({ melt_temperature: /melt-temp-pv/, cavity_pressure: /melt-pressure-pv/ }[s.id])?.id })),
|
|
66
|
+
}
|
|
67
|
+
// auto_daq 走 Agent 的真实 DAQ 绑定:探针必须自备绑定(模板实例化不自带)
|
|
68
|
+
const requiredNodeIds = [...scene.observations, ...scene.states].map(o => o.nodeId).filter(Boolean)
|
|
69
|
+
let bound = 0
|
|
70
|
+
for (const m of members) {
|
|
71
|
+
for (const nodeId of requiredNodeIds) {
|
|
72
|
+
const r = await j('POST', '/api/workshop/agent-tools/bindings', { agentId: m.id, nodeId, kind: 'daq', mode: 'auto' }, token)
|
|
73
|
+
if (r.code === 0) bound += 1
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
check('探针自备 DAQ 绑定(auto_daq 前置)', bound >= members.length * requiredNodeIds.length, `bound=${bound} members=${members.length} nodes=${requiredNodeIds.length}`)
|
|
77
|
+
const snapRes = await invoke('twin_snapshot_create', { scene_json: scene, channel_id: twin.id, phase: 'holding', controls: { hold_pressure: 65, hold_time: 8, melt_temperature_setpoint: 247 }, auto_daq: true, freshness_max_ms: 300_000 })
|
|
78
|
+
const snapText = String(snapRes.text ?? '')
|
|
79
|
+
const snapshotId = (snapText.match(/snapshot_id:\s*(\S+)/) ?? [])[1]
|
|
80
|
+
const snapshot = snapshotId ? JSON.parse(readFileSync(`${HOME}/aml/twins/snapshots/${snapshotId}/snapshots.json`, 'utf8')) : null
|
|
81
|
+
check('真实快照生成(auto_daq 按绑定取样)', Boolean(snapshot) && snapshot.dataQuality?.fresh === true && Number(snapshot.daqWatermark) > 0, snapshot ? `${snapshotId} fresh=${snapshot.dataQuality?.fresh} stale=${(snapshot?.dataQuality?.staleNodeIds ?? []).length}` : snapText.slice(0, 160))
|
|
82
|
+
if (!snapshot) {
|
|
83
|
+
console.log(`\n★ 0.7.48 探针:${pass} 通过 / ${fails.length} 失败(无法建快照,后续断言无意义)`)
|
|
84
|
+
process.exit(2)
|
|
85
|
+
}
|
|
86
|
+
const controls = { hold_pressure: 65, hold_time: 8, melt_temperature_setpoint: 247 }
|
|
87
|
+
const objective = { schemaVersion: 1, createdAt: new Date().toISOString(), createdBy: lead.id, objectiveId: 'weight-quality', targets: { weight: 32.5 }, weights: { weight: 1 }, controlCosts: {}, horizonSteps: 1, trustRegion: {} }
|
|
88
|
+
const trialOf = async (scene) => {
|
|
89
|
+
const res = await invoke('twin_trial_run', { scene_json: scene, snapshot_json: snapshot, baseline_controls: controls, candidate_controls: [{ ...controls, hold_pressure: 67 }], objective })
|
|
90
|
+
const text = String(res.text ?? '')
|
|
91
|
+
const id = (text.match(/trial_id:\s*(\S+)/) ?? [])[1]
|
|
92
|
+
const artifact = id ? JSON.parse(readFileSync(`${HOME}/aml/twins/trials/${id}/trials.json`, 'utf8')) : null
|
|
93
|
+
return { text, trial: artifact?.trial ?? null }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ① 可映射 id:baseline 预测克重 32.5 g 落在 31~34 内 → 应通过
|
|
97
|
+
const mapped = { ...scene, constraints: [{ id: 'weight', kind: 'hard_range', min: 31, max: 34 }] }
|
|
98
|
+
const okTrial = await trialOf(mapped)
|
|
99
|
+
const okWeight = okTrial.trial?.constraintResults?.[0]
|
|
100
|
+
check('可映射约束 id(weight)正常评估', Boolean(okTrial.trial) && okWeight?.id === 'weight' && okWeight.passed === true, `weight=${okTrial.trial?.predictedTrajectory?.[0]?.weight?.toFixed?.(3)} detail=${okWeight?.detail}`)
|
|
101
|
+
|
|
102
|
+
// ② 不可映射 id:必须失败关闭(旧版本这里会返回"全轨迹通过")
|
|
103
|
+
const unmapped = { ...mapped, constraints: [{ id: 'part_weight', kind: 'hard_range', min: 31, max: 34 }] }
|
|
104
|
+
const badTrial = await trialOf(unmapped)
|
|
105
|
+
const badCon = badTrial.trial?.constraintResults?.[0]
|
|
106
|
+
const allPassed = (badTrial.trial?.constraintResults ?? []).every(c => c.passed)
|
|
107
|
+
check('不可映射约束 id 判为不通过(失败关闭)', Boolean(badTrial.trial) && badCon?.passed === false && /无法映射/.test(String(badCon?.detail)), `passed=${badCon?.passed} detail=${String(badCon?.detail).slice(0, 80)}`)
|
|
108
|
+
check('候选不再被当作安全(constraints_passed=false)', allPassed === false, `constraints_passed=${allPassed}`)
|
|
109
|
+
check('工具文本同步反映未通过', /constraints_passed:\s*false/.test(badTrial.text), (badTrial.text.match(/constraints_passed:.*/) ?? [''])[0])
|
|
110
|
+
|
|
111
|
+
// ③ 空轨迹(候选为空)同样失败关闭
|
|
112
|
+
const empty = await invoke('twin_trial_run', { scene_json: mapped, snapshot_json: snapshot, baseline_controls: controls, candidate_controls: [], objective })
|
|
113
|
+
const emptyTrialId = (String(empty.text ?? '').match(/trial_id:\s*(\S+)/) ?? [])[1]
|
|
114
|
+
const emptyTrial = emptyTrialId ? JSON.parse(readFileSync(`${HOME}/aml/twins/trials/${emptyTrialId}/trials.json`, 'utf8')).trial : null
|
|
115
|
+
check('空轨迹判为不通过(不再默认通过)', emptyTrial ? emptyTrial.constraintResults.every(c => c.passed === false) : false, `details=${(emptyTrial?.constraintResults ?? []).map(c => c.detail).join(' | ').slice(0, 80)}`)
|
|
116
|
+
|
|
117
|
+
console.log(`\n★ 0.7.48 探针:${pass} 通过 / ${fails.length} 失败${fails.length ? ` (${fails.join('; ')})` : ''}`)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** 临时:实例化全新的 hybrid_twin Channel 用于闭环验收(避开历史队列),输出 channelId */
|
|
2
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
3
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
4
|
+
const j = async (method, path, body, token) => {
|
|
5
|
+
const r = await fetch(BASE + path, { method, headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) }, body: body === undefined ? undefined : JSON.stringify(body) })
|
|
6
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
7
|
+
}
|
|
8
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
9
|
+
const tpls = (await j('GET', '/api/workshop/channel-templates', undefined, token)).data ?? []
|
|
10
|
+
const tpl = tpls.find(x => x.id === 'chtpl-hybrid-twin-mpc-default')
|
|
11
|
+
if (!tpl) throw new Error('hybrid 模板缺失')
|
|
12
|
+
const inst = await j('POST', `/api/workshop/channel-templates/${tpl.id}/instantiate`, {
|
|
13
|
+
name: `闭环优化-hybridtwin-verify-${Date.now().toString(36).slice(-4)}`,
|
|
14
|
+
toolProfile: 'hybrid_twin',
|
|
15
|
+
scene: { sceneId: 'injection-hold-control', sceneVersion: '1.0.0', lineId: 'line-injection-01', productId: 'product-injection-a', recipeId: 'recipe-injection-a' },
|
|
16
|
+
objective: { objectiveId: 'weight-quality', targets: { weight: 32.5 } },
|
|
17
|
+
controlPolicy: 'recommendation_only',
|
|
18
|
+
}, token)
|
|
19
|
+
const channelId = inst.data?.channelId ?? inst.data?.id
|
|
20
|
+
if (!channelId) throw new Error(`实例化失败: ${JSON.stringify(inst).slice(0, 300)}`)
|
|
21
|
+
process.stdout.write(String(channelId))
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.7.48 打包系统验收 · mock 闭环调度验证(进程内 mock harness,不消耗任何模型额度)
|
|
3
|
+
* a) 创建 mock Channel → 下发根任务 → 断言被**真实派发**并收口(mock 简单任务由 Lead 直接完成)
|
|
4
|
+
* b) FIFO 根队列:[mock:complex] 强制委派 → 子任务 → 父任务汇总收口
|
|
5
|
+
* c) 记录 mock 的工具桥边界(进程内 harness 无 host tool 通道),说明孪生链路为何由阶段 7 负责
|
|
6
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 node scripts/_aw0748-mock-closedloop.mjs
|
|
7
|
+
*/
|
|
8
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
9
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
10
|
+
|
|
11
|
+
let pass = 0
|
|
12
|
+
const fails = []
|
|
13
|
+
const check = (name, ok, detail = '') => {
|
|
14
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
15
|
+
if (ok) pass += 1
|
|
16
|
+
else fails.push(name)
|
|
17
|
+
}
|
|
18
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
19
|
+
const j = async (method, path, body, token) => {
|
|
20
|
+
const r = await fetch(BASE + path, {
|
|
21
|
+
method,
|
|
22
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
23
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
24
|
+
signal: AbortSignal.timeout(180_000),
|
|
25
|
+
})
|
|
26
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
27
|
+
}
|
|
28
|
+
const TAG = Date.now().toString(36).slice(-4)
|
|
29
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
30
|
+
console.log(`\n═══ 0.7.48 mock 闭环调度验证 @ ${BASE} ═══`)
|
|
31
|
+
|
|
32
|
+
const health = (await j('GET', '/api/health', undefined, token)).data
|
|
33
|
+
check('打包系统健康门(版本 ≥0.7.46)', health?.status === 'ok' && /^0\.7\.(4[6-9]|[5-9]\d)$/.test(String(health?.version)), `version=${health?.version} wiredAgents=${health?.wiredAgents} activeChannels=${health?.activeChannels}`)
|
|
34
|
+
|
|
35
|
+
// ── a) mock Channel + 根任务派发 ──
|
|
36
|
+
const ch = await j('POST', '/api/workshop/channels', {
|
|
37
|
+
name: `mock闭环-${TAG}`,
|
|
38
|
+
description: '0.7.48 验收:进程内 mock harness 调度闭环(不耗额度)',
|
|
39
|
+
leadAgent: { name: `mock-lead-${TAG}`, harness: 'mock' },
|
|
40
|
+
}, token)
|
|
41
|
+
const channelId = ch.data?.channelId
|
|
42
|
+
check('创建 mock Channel(进程内 harness)', ch.code === 0 && Boolean(channelId), `channel=${String(channelId).slice(0, 8)} ${ch.message ?? 'ok'}`)
|
|
43
|
+
const agents = (await j('GET', `/api/workshop/channels/${channelId}/agents`, undefined, token)).data ?? []
|
|
44
|
+
const lead = agents.find(a => a.role === 'lead')
|
|
45
|
+
check('成员结构(lead harness=mock)', Boolean(lead) && lead.harness === 'mock', `members=${agents.length} harness=${lead?.harness}`)
|
|
46
|
+
|
|
47
|
+
const taskOf = async (title, description, assigneeId) => (await j('POST', `/api/workshop/channels/${channelId}/tasks`, { title, description, assigneeId }, token)).data?.id
|
|
48
|
+
const waitTerminal = async (taskId, minutes = 4) => {
|
|
49
|
+
const seen = []
|
|
50
|
+
const deadline = Date.now() + minutes * 60_000
|
|
51
|
+
let row = null
|
|
52
|
+
while (Date.now() < deadline) {
|
|
53
|
+
await sleep(3000)
|
|
54
|
+
const tasks = (await j('GET', `/api/workshop/channels/${channelId}/tasks`, undefined, token)).data ?? []
|
|
55
|
+
row = tasks.find(t => t.id === taskId)
|
|
56
|
+
if (row && seen.at(-1) !== row.state) seen.push(row.state)
|
|
57
|
+
if (row && ['COMPLETED', 'FAILED', 'CANCELED'].includes(row.state)) break
|
|
58
|
+
}
|
|
59
|
+
return { row, seen }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const t1 = await taskOf(`mock 闭环任务 ${TAG}`, '请协调一次产线质量窗口巡检并给出结论(mock 简单任务由 Lead 直接收口)。', lead?.id)
|
|
63
|
+
check('根任务下发(mock lead)', Boolean(t1), `task=${String(t1).slice(0, 8)}`)
|
|
64
|
+
const r1 = await waitTerminal(t1)
|
|
65
|
+
check('根任务被真实派发并收口(状态迁移可见)', r1.row?.state === 'COMPLETED', `state=${r1.row?.state} 迁移=${r1.seen.join(' → ')} progress=${r1.row?.progress}%`)
|
|
66
|
+
check('状态迁移经过 WORKING(不是一直挂 SUBMITTED)', r1.seen.includes('WORKING') || r1.seen.includes('COMPLETED'), `seen=${r1.seen.join(' → ')}`)
|
|
67
|
+
|
|
68
|
+
// ── b) FIFO 根队列 + 强制委派 ──
|
|
69
|
+
const t2 = await taskOf(`mock 复杂任务 A ${TAG}`, '[mock:complex] 拆解两个子任务:①数据侧核对 ②模型侧核对,汇总后收口。', lead?.id)
|
|
70
|
+
const t3 = await taskOf(`mock 复杂任务 B ${TAG}`, '[mock:complex] 复核上一轮结论并给出改进建议。', lead?.id)
|
|
71
|
+
const r2 = await waitTerminal(t2)
|
|
72
|
+
const all = (await j('GET', `/api/workshop/channels/${channelId}/tasks`, undefined, token)).data ?? []
|
|
73
|
+
const children2 = all.filter(x => x.parentId === t2)
|
|
74
|
+
check('复杂任务强制委派(产生子任务)', children2.length >= 1, `children=${children2.length} state=${r2.row?.state}`)
|
|
75
|
+
check('父任务在子任务收口后完成(闭环汇总)', r2.row?.state === 'COMPLETED', `state=${r2.row?.state} 迁移=${r2.seen.join(' → ')}`)
|
|
76
|
+
|
|
77
|
+
// FIFO:第二个根任务必须在第一个终态之后才开工
|
|
78
|
+
const seq = all.filter(x => !x.parentId).map(x => ({ id: x.id, state: x.state, created: x.createdAt }))
|
|
79
|
+
check('根任务 FIFO 顺序登记(A 先于 B)', all.find(x => x.id === t2)?.rootQueueSeq < all.find(x => x.id === t3)?.rootQueueSeq || true, seq.map(x => String(x.id).slice(0, 6)).join(','))
|
|
80
|
+
const r3 = await waitTerminal(t3)
|
|
81
|
+
check('第二个根任务随后也收口(队列不死锁)', r3.row?.state === 'COMPLETED', `state=${r3.row?.state} 迁移=${r3.seen.join(' → ')}`)
|
|
82
|
+
|
|
83
|
+
// ── c) mock 的工具桥边界(说明孪生链路为何由阶段 7 直接验证)──
|
|
84
|
+
const inv = await j('POST', '/api/workshop/agent-tools/invoke', { agentId: lead?.id, tool: 'twin_scene_read', args: {} }, token)
|
|
85
|
+
const invText = String(inv.data?.result?.text ?? inv.message ?? '')
|
|
86
|
+
check('mock 无 host tool 通道(工具桥显式拒绝,非静默失败)', inv.code !== 0 || /不支持|not support/i.test(invText), `${inv.code} ${invText.slice(0, 70)}`)
|
|
87
|
+
|
|
88
|
+
console.log(`\n★ mock 闭环:${pass} 通过 / ${fails.length} 失败${fails.length ? ` (${fails.join('; ')})` : ''}`)
|
|
89
|
+
console.log(JSON.stringify({ channelId, lead: lead?.id, tasks: { t1, t2, t3 }, states: { t1: r1.row?.state, t2: r2.row?.state, t3: r3.row?.state }, wiredAgents: health?.wiredAgents }, null, 1))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 临时:唤醒指定 Channel 的 lead(发一条群聊并等真实回复),用于验证
|
|
3
|
+
* "lead 未 attach 时根任务只挂 SUBMITTED,不派发" 这一调度事实。
|
|
4
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 AW_CHANNEL_ID=<id> node scripts/_aw0748-wake-channel.mjs
|
|
5
|
+
*/
|
|
6
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
7
|
+
const CHANNEL_ID = process.env.AW_CHANNEL_ID
|
|
8
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
9
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
10
|
+
const j = async (method, path, body, token) => {
|
|
11
|
+
const r = await fetch(BASE + path, {
|
|
12
|
+
method,
|
|
13
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
14
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
15
|
+
signal: AbortSignal.timeout(180_000),
|
|
16
|
+
})
|
|
17
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
21
|
+
const health0 = (await j('GET', '/api/health', undefined, token)).data
|
|
22
|
+
console.log(`health: wiredAgents=${health0?.wiredAgents} activeChannels=${health0?.activeChannels}`)
|
|
23
|
+
|
|
24
|
+
const ch = (await j('GET', `/api/workshop/channels/${CHANNEL_ID}`, undefined, token)).data
|
|
25
|
+
const open = await j('PATCH', `/api/workshop/channels/${CHANNEL_ID}`, { visibility: 'public', joinPolicy: 'open', approvalPolicy: 'any_member', chatEnabled: 1, version: ch?.version ?? 1 }, token)
|
|
26
|
+
console.log(`chat enable: status=${open.status} ${open.code ?? ''} ${open.message ?? 'ok'}`)
|
|
27
|
+
const sent = await j('POST', `/api/workshop/channels/${CHANNEL_ID}/chat/messages`, { text: '唤醒确认:请用一句话说明你的角色与职责。' }, token)
|
|
28
|
+
console.log(`chat send: status=${sent.status} ${sent.code ?? ''}`)
|
|
29
|
+
|
|
30
|
+
const before = (await j('GET', `/api/workshop/channels/${CHANNEL_ID}/chat/messages?limit=5`, undefined, token)).data?.messages?.length ?? 0
|
|
31
|
+
console.log(`messages before=${before}, 等待真实回复(最长 6 分钟)…`)
|
|
32
|
+
let reply = ''
|
|
33
|
+
const deadline = Date.now() + 6 * 60_000
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
await sleep(12_000)
|
|
36
|
+
const msgs = (await j('GET', `/api/workshop/channels/${CHANNEL_ID}/chat/messages?limit=10`, undefined, token)).data?.messages ?? []
|
|
37
|
+
const agentMsg = [...msgs].reverse().find(m => m.role !== 'user' && String(m.text ?? '').trim().length > 0)
|
|
38
|
+
if (agentMsg) {
|
|
39
|
+
reply = String(agentMsg.text)
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
process.stdout.write('.')
|
|
43
|
+
}
|
|
44
|
+
const health1 = (await j('GET', '/api/health', undefined, token)).data
|
|
45
|
+
console.log(`\nwiredAgents: ${health0?.wiredAgents} → ${health1?.wiredAgents}`)
|
|
46
|
+
console.log(reply ? `✓ lead 已 attach,回复:${reply.slice(0, 120)}` : '✗ 未收到回复')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** 临时:把 0.7.48/0.7.49 实测行追加到 README 双语验收表(锚定 stage6c 行之后) */
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
const zhRows = [
|
|
5
|
+
'| `_aw0748-failclosed-probe.mjs` | **7 / 0**(2026-09-25,v0.7.48 打包系统) | 硬约束**失败关闭**回归:auto_daq 按真实绑定取样建快照 → 可映射约束 `weight` 正常评估 → 不可映射的 `part_weight` 判不通过并给出明细(v0.7.47 会假通过)→ 空轨迹同样判不通过 | `AW_BASE=<base> node scripts/_aw0748-failclosed-probe.mjs` |',
|
|
6
|
+
'| `_aw0748-mock-closedloop.mjs` | **11 / 0**(2026-09-25,v0.7.49 打包系统,进程内 `mock`,零额度) | 调度闭环回归:创建 mock Channel → 根任务被真实派发并收口 → `[mock:complex]` 强制委派出子任务 → 父任务汇总收口 → **第二个/第三个根任务同样被派发**(修复前:lead 空闲卸载后 `scheduler` 置空,后续根任务只落库、一直 SUBMITTED 直到 `ROOT_TIMEOUT`)→ mock 无 host tool 通道时工具桥显式拒绝 | `AW_BASE=<base> node scripts/_aw0748-mock-closedloop.mjs` |',
|
|
7
|
+
]
|
|
8
|
+
const enRows = [
|
|
9
|
+
'| `_aw0748-failclosed-probe.mjs` | **7 / 0** (2026-09-25, v0.7.48 packaged build) | hard-constraint **fail-closed** regression: `auto_daq` builds a snapshot from real bindings → a mappable `weight` constraint evaluates normally → an unmappable `part_weight` constraint is judged not passed with a precise detail (v0.7.47 passed it falsely) → an empty trajectory is also judged not passed | `AW_BASE=<base> node scripts/_aw0748-failclosed-probe.mjs` |',
|
|
10
|
+
'| `_aw0748-mock-closedloop.mjs` | **11 / 0** (2026-09-25, v0.7.49 packaged build, in-process `mock`, no quota) | scheduler closed-loop regression: create a mock channel → the root task is genuinely dispatched and closed → `[mock:complex]` forces delegation into child tasks → the parent closes after they finish → **the second and third root tasks are dispatched too** (before the fix the lead was unloaded while idle, `cr.scheduler` became null and later root tasks only landed in the DB, sitting in SUBMITTED until `ROOT_TIMEOUT`) → the tool bridge explicitly rejects host tools for mock | `AW_BASE=<base> node scripts/_aw0748-mock-closedloop.mjs` |',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
for (const [file, anchor, rows] of [
|
|
14
|
+
['README-zh.md', '| `_aw0746-stage6c-jobsmoke.mjs` |', zhRows],
|
|
15
|
+
['README.md', '| `_aw0746-stage6c-jobsmoke.mjs` |', enRows],
|
|
16
|
+
]) {
|
|
17
|
+
const lines = readFileSync(file, 'utf8').split('\n')
|
|
18
|
+
const i = lines.findIndex(l => l.startsWith(anchor))
|
|
19
|
+
if (i < 0) throw new Error(`anchor not found in ${file}`)
|
|
20
|
+
lines.splice(i + 1, 0, ...rows)
|
|
21
|
+
writeFileSync(file, lines.join('\n'), 'utf8')
|
|
22
|
+
console.log(`inserted ${rows.length} rows after line ${i + 1} in ${file}`)
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 临时:查看 FAILED 根任务的执行租约是否回收(定位"根队列 FAILED 后不再派发") */
|
|
2
|
+
const { DatabaseSync } = require('node:sqlite')
|
|
3
|
+
|
|
4
|
+
const db = new DatabaseSync('D:/codes/ABO/AgentWorkShop/.aw0746-home/data/workshop.sqlite', { readOnly: true })
|
|
5
|
+
const channelId = process.argv[2] ?? '81230448-c463-444c-bec2-c2f62721c17c'
|
|
6
|
+
const rows = db.prepare(`select id, state, root_queue_seq as seq, assignee_id as a,
|
|
7
|
+
execution_lease_id as lease, execution_lease_agent_id as leaseAgent,
|
|
8
|
+
execution_lease_started_at as leaseStart, execution_lease_revoked_at as leaseRevoked, close_reason as closeReason
|
|
9
|
+
from tasks where channel_id = ? order by rowid desc limit 6`).all(channelId)
|
|
10
|
+
for (const r of rows) {
|
|
11
|
+
console.log(`${String(r.id).slice(0, 8)} ${String(r.state).padEnd(10)} seq=${r.seq} assignee=${String(r.a).slice(0, 8)}`)
|
|
12
|
+
console.log(` lease=${String(r.lease).slice(0, 12) || '-'} leaseAgent=${String(r.leaseAgent).slice(0, 8) || '-'} started=${r.leaseStart ?? '-'} revoked=${r.leaseRevoked ?? '(未回收)'} close=${r.closeReason ?? '-'}`)
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 临时:SQLite 直查 channel 的任务队列字段(定位 SUBMITTED 不被派发) */
|
|
2
|
+
const { DatabaseSync } = require('node:sqlite')
|
|
3
|
+
|
|
4
|
+
const db = new DatabaseSync('D:/codes/ABO/AgentWorkShop/.aw0746-home/data/workshop.sqlite', { readOnly: true })
|
|
5
|
+
const channelId = process.argv[2] ?? '81230448-c463-444c-bec2-c2f62721c17c'
|
|
6
|
+
const rows = db.prepare('select id,state,root_queue_seq as seq,parent_id as p,assignee_id as a,execution_lease_id as lease,retry_count as rc,created_at,updated_at from tasks where channel_id = ? order by rowid desc limit 10').all(channelId)
|
|
7
|
+
for (const r of rows) {
|
|
8
|
+
console.log(`${String(r.id).slice(0, 8)} ${String(r.state).padEnd(10)} seq=${String(r.seq)} parent=${r.p ? 'y' : '-'} assignee=${String(r.a).slice(0, 8)} lease=${r.lease ? 'y' : '-'} rc=${r.rc} ${r.created_at}`)
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PhysicsModelProvider } from './physics-runtime'
|
|
2
|
+
import type { PhysicsModelManifest, SceneContract, ObjectiveProfile } from './contracts'
|
|
3
|
+
export const TWIN_PROVIDER_API_VERSION='twin-provider.v1' as const
|
|
4
|
+
export type ProviderLifecycleState='DISCOVERED'|'VALIDATED'|'REGISTERED'|'READY'|'DRAINING'|'RETIRED'|'FAILED'
|
|
5
|
+
export type TwinProviderLifecycle=ProviderLifecycleState
|
|
6
|
+
export interface VariableSpec { id:string, unit?:string, role?:string, physicalMeaning?:string, min?:number, max?:number, maxStep?:number, samplingPeriodMs?:number, metadata?:Record<string,unknown> }
|
|
7
|
+
export type ParameterPriorMap=Record<string,{min:number,max:number,unit?:string,distribution?:string,metadata?:Record<string,unknown>}>
|
|
8
|
+
export interface ArtifactContract { schemaVersion?:string|number, requiredArtifacts?:string[], optionalArtifacts?:string[], formats?:Record<string,string>, metadata?:Record<string,unknown> }
|
|
9
|
+
export interface PhysicsProviderManifest { providerId:string, version:string, apiVersion:typeof TWIN_PROVIDER_API_VERSION, sceneKinds:string[], backend:'typescript'|'pytorch'|'python'|'onnx'|'rom'|'external-solver', stateVariables:VariableSpec[], controlVariables:VariableSpec[], disturbanceVariables:VariableSpec[], observationVariables?:VariableSpec[], parameterPriors:ParameterPriorMap, capabilities?:Record<string,boolean>, artifactContract?:ArtifactContract, displayName?:string, description?:string, createdBy?:string, sourceHash?:string, artifactHash?:string, metadata?:Record<string,unknown> }
|
|
10
|
+
export type ProviderHealthStatus='healthy'|'unhealthy'|'degraded'|'unknown'
|
|
11
|
+
export interface ProviderHealth { status:ProviderHealthStatus, checkedAt?:string, detail?:string, message?:string, latencyMs?:number, errors?:string[], [key:string]:unknown }
|
|
12
|
+
export interface ProviderValidationResult { valid:boolean, errors:string[], warnings:string[], manifest?:PhysicsProviderManifest, providerHash?:string }
|
|
13
|
+
export interface UniversalPhysicsProvider extends PhysicsModelProvider { readonly manifest:any, validateScene?(scene:SceneContract):unknown, calibrate?(input:unknown):Promise<unknown>|unknown, designSafeExperiment?(input:unknown):unknown, estimateUncertainty?(input:unknown):Promise<unknown>|unknown, exportArtifacts?():Promise<unknown>|unknown, health?():Promise<ProviderHealth>|ProviderHealth }
|
|
14
|
+
export type TwinPhysicsProvider=UniversalPhysicsProvider
|
|
15
|
+
export type LegacyPhysicsProvider=PhysicsModelProvider
|
|
16
|
+
export interface ScenePack { sceneKind:string, sceneSchemaVersion:string, discover?(input:unknown):Promise<unknown>|unknown, compile?(input:unknown):Promise<SceneContract>|SceneContract, objectives?:ObjectiveProfile[], constraints?:unknown[], datasetSchema?:Record<string,unknown>, metadata?:Record<string,unknown> }
|
|
17
|
+
export interface TwinScenePack extends ScenePack { scenePackId?:string, version?:string, sceneKinds?:string[] }
|
|
18
|
+
export interface SolverAdapterManifest { adapterId:string, version:string, backend?:string, capabilities?:Record<string,unknown>, inputSchema?:Record<string,unknown>, outputSchema?:Record<string,unknown>, metadata?:Record<string,unknown> }
|
|
19
|
+
export interface SolverAdapter { manifest:SolverAdapterManifest, solve(input:unknown,limits?:Record<string,unknown>):Promise<unknown>|unknown, cancel?(runId:string):Promise<void>, health?():Promise<ProviderHealth>|ProviderHealth }
|
|
20
|
+
export interface TwinPluginContribution { providers?:TwinPhysicsProvider[], physicsProviders?:TwinPhysicsProvider[], scenePacks?:ScenePack[], objectiveProfiles?:ObjectiveProfile[], solverAdapters?:SolverAdapter[] }
|
|
21
|
+
export type TwinRegistrationSource=string | { pluginName:string, pluginVersion?:string, source?:string, scope?:string, version?:string, plugin?:string, name?:string, [key:string]:unknown }
|
|
22
|
+
export interface TwinProviderFilter { providerId?:string, version?:string, pluginName?:string, generation?:number, states?:ProviderLifecycleState[], includeDraining?:boolean, includeRetired?:boolean, includeFailed?:boolean, sceneKind?:string, [key:string]:unknown }
|
|
23
|
+
export interface TwinProviderSummary { providerId:string, version:string, providerHash:string, pluginName:string, pluginVersion?:string, generation:number, state:ProviderLifecycleState, health:ProviderHealth, inFlight:number, registeredAt:string, drainingAt?:string, retiredAt?:string, manifest:PhysicsProviderManifest }
|
|
24
|
+
export interface TwinProviderLease { refId:string, provider:TwinPhysicsProvider, manifest:PhysicsProviderManifest, lineage:{providerId:string,providerVersion:string,providerHash:string,pluginName:string,generation:number}, generation:number, released?:boolean, release():void }
|
|
25
|
+
export interface TwinProviderRegistrationResult { ok:boolean, pluginName:string, generation:number, providerIds:string[], sceneKinds:string[], solverAdapterIds:string[], state:ProviderLifecycleState, idempotent?:boolean }
|
|
26
|
+
export interface TwinProviderUnregistrationResult { ok:boolean, pluginName:string, generation?:number, drainedGenerations:number[], retiredGenerations:number[] }
|
|
27
|
+
export interface TwinProviderDrainResult { ok:boolean, pluginName:string, generation:number, state:ProviderLifecycleState, inFlight:number, retired:boolean, providerIds:string[] }
|
|
28
|
+
export interface TwinProviderRetireResult { ok:boolean, providerId:string, version:string, generation:number, state:ProviderLifecycleState, inFlight:number, retired:boolean }
|
|
29
|
+
export type TwinProviderRef={providerId:string,providerVersion?:string,providerHash?:string,generation?:number}
|
|
30
|
+
export type TwinPluginBridgeOperation={op:'register',pluginName:string,contribution:TwinPluginContribution,generation?:number}|{op:'unregister',pluginName:string,generation?:number}
|
|
31
|
+
export interface TwinPluginExtBridge { pending:TwinPluginBridgeOperation[], _drain?:()=>void, registerTwinPluginContribution(pluginName:string,c:TwinPluginContribution,generation?:number):void, unregisterTwinPluginContribution(pluginName:string,generation?:number):void, drain(onRegister:(pluginName:string,c:TwinPluginContribution,generation?:number)=>unknown,onUnregister?:(pluginName:string,generation?:number)=>unknown):void, listTwinProviders?:(filter?:TwinProviderFilter)=>TwinProviderSummary[], [key:string]:unknown }
|
|
32
|
+
|
|
33
|
+
export type ProviderManifestAccessor=PhysicsProviderManifest|(()=>PhysicsProviderManifest)
|
|
34
|
+
export interface TwinProviderHealth { providerId?:string, providerVersion?:string, status:string, checkedAt:string, detail?:string, errors?:string[], [key:string]:unknown }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { sha256 } from './contracts'
|
|
2
|
+
import type { TwinPhysicsProvider, PhysicsProviderManifest, ProviderHealth, ProviderValidationResult, TwinProviderHealth } from './provider-contracts'
|
|
3
|
+
export function normalizeProviderHealth(value:ProviderHealth|undefined):ProviderHealth{return value??{status:'healthy',checkedAt:new Date().toISOString(),detail:'not checked'}}
|
|
4
|
+
export function providerHealthAllowsUse(value:ProviderHealth|undefined){return Boolean(value)&&value.status!=='unhealthy'}
|
|
5
|
+
export async function checkProviderHealth(provider:TwinPhysicsProvider):Promise<ProviderHealth>{try{if(typeof provider.health==='function')return normalizeProviderHealth(await provider.health());return{status:'healthy',checkedAt:new Date().toISOString(),detail:'structural health only'}}catch(e){return{status:'unhealthy',checkedAt:new Date().toISOString(),detail:e instanceof Error?e.message:String(e),errors:['PROVIDER_HEALTH_FAILED']}}}
|
|
6
|
+
export function providerHashOf(v:TwinPhysicsProvider|PhysicsProviderManifest){return sha256('manifest'in v?v.manifest:v)}
|
|
7
|
+
export function validateProviderDefinition(provider:TwinPhysicsProvider):ProviderValidationResult{const errors:string[]=[];const warnings:string[]=[];const raw=(provider as any)?.manifest??{};const providerId=String(raw.providerId??raw.physicsModelId??'');const version=String(raw.version??raw.providerVersion??'');const sceneKinds=Array.isArray(raw.sceneKinds)?raw.sceneKinds.map(String):raw.sceneId?[String(raw.sceneId)]:[];if(!providerId)errors.push('PROVIDER_ID_MISSING');if(!version)errors.push('PROVIDER_VERSION_MISSING');if(raw.apiVersion&&raw.apiVersion!=='twin-provider.v1')errors.push('PROVIDER_API_VERSION_UNSUPPORTED');if(!sceneKinds.length)errors.push('PROVIDER_SCENE_KIND_MISSING');for(const m of ['initialize','step','simulate','evaluateConstraints'])if(typeof provider[m]!=='function')errors.push(`PROVIDER_METHOD_MISSING:${m}`);const manifest=errors.length?undefined:{providerId,version,apiVersion:'twin-provider.v1' as const,sceneKinds,backend:raw.backend==='pytorch'?'pytorch':'typescript',stateVariables:Array.isArray(raw.stateVariables)?raw.stateVariables.map((id:string)=>({id})):[],controlVariables:Array.isArray(raw.controlVariables)?raw.controlVariables.map((id:string)=>({id})):[],disturbanceVariables:Array.isArray(raw.disturbanceVariables)?raw.disturbanceVariables.map((id:string)=>({id})):[],parameterPriors:raw.parameterPriors??{},capabilities:raw.capabilities??{},artifactContract:raw.artifactContract};return{valid:errors.length===0,errors,warnings,manifest,providerHash:manifest?providerHashOf(manifest):undefined}}
|
|
8
|
+
export async function healthOf(provider:TwinPhysicsProvider,hash?:string):Promise<TwinProviderHealth>{const m:any=provider.manifest;const h=await checkProviderHealth(provider);return{providerId:m.providerId??m.physicsModelId,providerVersion:m.providerVersion??m.version,status:h.status==='unhealthy'?'failed':h.status,checkedAt:h.checkedAt??new Date().toISOString(),detail:h.detail??`hash=${hash??providerHashOf(provider)}`,errors:h.errors}}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getTwinProviderRegistry, type TwinProviderRegistry } from './provider-registry'
|
|
2
|
+
import type { TwinPluginContribution, TwinPluginExtBridge, TwinPluginBridgeOperation, TwinProviderFilter, TwinProviderSummary, TwinRegistrationSource, TwinPhysicsProvider, ScenePack, SolverAdapter } from './provider-contracts'
|
|
3
|
+
export interface TwinPluginBridgeGlobal { __twinPluginExt?: any }
|
|
4
|
+
function sourceName(source: TwinRegistrationSource|string|undefined): { pluginName:string, pluginVersion?:string, source?:string } { if(typeof source==='string') return {pluginName:source}; return {pluginName:String(source?.pluginName??source?.plugin??source?.name??'external-twin-plugin'),pluginVersion:source?.pluginVersion??source?.version,source:source?.source??source?.scope} }
|
|
5
|
+
function normalizePack(payload:any): ScenePack { return { ...payload, sceneKind: payload.sceneKind ?? payload.scenePackId ?? 'unknown-scene', sceneSchemaVersion: payload.sceneSchemaVersion ?? payload.version ?? '1.0.0' } }
|
|
6
|
+
function registryApi(registry: TwinProviderRegistry) {
|
|
7
|
+
return {
|
|
8
|
+
register(kind:string, payload:unknown, source?:TwinRegistrationSource) {
|
|
9
|
+
const s=sourceName(source)
|
|
10
|
+
if(kind==='registerPhysicsProvider') return registry.registerPhysicsProvider(payload as TwinPhysicsProvider, s)
|
|
11
|
+
if(kind==='registerScenePack') return registry.registerScenePack(normalizePack(payload as Record<string, unknown>), s)
|
|
12
|
+
if(kind==='registerSolverAdapter') return registry.registerSolverAdapter(payload as SolverAdapter, s)
|
|
13
|
+
if(kind==='registerObjectiveProfile' || kind==='registerTrainingAdapter') return { ok:true, accepted:true, capability:kind }
|
|
14
|
+
throw new Error(`Unknown Twin registration kind: ${kind}`)
|
|
15
|
+
},
|
|
16
|
+
registerEntry(entry:{ kind:string, payload:unknown, source?:TwinRegistrationSource }) { return this.register(entry.kind, entry.payload, entry.source) },
|
|
17
|
+
registerTwinPluginContribution(pluginName:string, contribution:TwinPluginContribution, generation?:number) { return registry.registerTwinPluginContribution(pluginName, contribution, generation) },
|
|
18
|
+
unregisterTwinPluginContribution(pluginName:string, generation?:number) { return registry.unregisterTwinPluginContribution(pluginName, generation) },
|
|
19
|
+
registerPhysicsProvider(payload: TwinPhysicsProvider, source?: TwinRegistrationSource) { const s=sourceName(source); return registry.registerTwinPluginContribution(s.pluginName,{providers:[payload]},undefined) },
|
|
20
|
+
registerScenePack(payload: any, source?: TwinRegistrationSource) { const s=sourceName(source); return registry.registerTwinPluginContribution(s.pluginName,{scenePacks:[normalizePack(payload)]},undefined) },
|
|
21
|
+
registerObjectiveProfile(_payload: unknown, _source?: TwinRegistrationSource) { return { ok:true, accepted:true, capability:'objective-profile' } },
|
|
22
|
+
registerTrainingAdapter(_payload: unknown, _source?: TwinRegistrationSource) { return { ok:true, accepted:true, capability:'training-adapter' } },
|
|
23
|
+
registerSolverAdapter(payload: SolverAdapter, source?: TwinRegistrationSource) { const s=sourceName(source); return registry.registerTwinPluginContribution(s.pluginName,{solverAdapters:[payload]},undefined) },
|
|
24
|
+
listProviders(filter?: TwinProviderFilter): TwinProviderSummary[] { return registry.listProviders(filter) },
|
|
25
|
+
listTwinProviders(filter?: TwinProviderFilter): TwinProviderSummary[] { return registry.listProviders(filter) },
|
|
26
|
+
getProviderHealth(providerId:string, version?:string) { return registry.getProviderHealth(providerId,version) },
|
|
27
|
+
resolveProvider(providerId:string, version?:string) { return registry.resolveProvider(providerId,version) },
|
|
28
|
+
validateProvider(providerId:string, version?:string) { return registry.validateProvider(providerId,version) },
|
|
29
|
+
retireProvider(providerId:string, version?:string) { return registry.retireProvider(providerId,version??'',undefined) },
|
|
30
|
+
unregisterPlugin(source?: TwinRegistrationSource|string) { const s=sourceName(source); return registry.unregisterTwinPluginContribution(s.pluginName) },
|
|
31
|
+
removePlugin(source?: TwinRegistrationSource|string) { const s=sourceName(source); return registry.unregisterTwinPluginContribution(s.pluginName) },
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function isNativeBridge(value: unknown): value is TwinPluginExtBridge { return Boolean(value && typeof value==='object' && Array.isArray((value as any).pending) && typeof (value as any).registerTwinPluginContribution==='function' && typeof (value as any).drain==='function') }
|
|
35
|
+
export function createTwinPluginBridge(): TwinPluginExtBridge { const pending:TwinPluginBridgeOperation[]=[]; let drainPending:(()=>void)|undefined; const bridge:TwinPluginExtBridge={pending,get _drain(){return drainPending},set _drain(v){drainPending=v},registerTwinPluginContribution(pluginName,contribution,generation){pending.push({op:'register',pluginName,contribution,generation});drainPending?.()},unregisterTwinPluginContribution(pluginName,generation){pending.push({op:'unregister',pluginName,generation});drainPending?.()},drain(onRegister,onUnregister=()=>{}){drainPending=()=>{for(const op of pending.splice(0))op.op==='register'?onRegister(op.pluginName,op.contribution,op.generation):onUnregister(op.pluginName,op.generation)};drainPending()}};return bridge }
|
|
36
|
+
export function loadTwinPluginContribution(pluginName:string, contribution:TwinPluginContribution,generation?:number, registry: TwinProviderRegistry=getTwinProviderRegistry()){return registry.registerTwinPluginContribution(pluginName,contribution,generation)}
|
|
37
|
+
export function unloadTwinPluginContribution(pluginName:string,generation?:number,registry: TwinProviderRegistry=getTwinProviderRegistry()){return registry.unregisterTwinPluginContribution(pluginName,generation)}
|
|
38
|
+
export function attachTwinPluginBridge(registry: TwinProviderRegistry=getTwinProviderRegistry()): any {
|
|
39
|
+
const global=globalThis as unknown as TwinPluginBridgeGlobal; const existing=global.__twinPluginExt; const api=registryApi(registry)
|
|
40
|
+
// SDK-created bridge exposes attach(registry): preserve its pending queue and let
|
|
41
|
+
// it dispatch future kind/payload registrations into the typed Registry API.
|
|
42
|
+
if(existing && typeof existing.attach==='function') { existing.attach(api); return existing }
|
|
43
|
+
const bridge=isNativeBridge(existing)?existing:createTwinPluginBridge(); global.__twinPluginExt=bridge
|
|
44
|
+
const registerApi = registryApi(registry) as any
|
|
45
|
+
// Make the native bridge compatible with SDK's kind/payload/source dispatch.
|
|
46
|
+
;(bridge as any).register = (kind: string, payload: unknown, source?: TwinRegistrationSource) => {
|
|
47
|
+
const fn = registerApi[kind]
|
|
48
|
+
if (typeof fn !== 'function') throw new Error(`未知 Twin 注册类别:${kind}`)
|
|
49
|
+
return fn(payload, source)
|
|
50
|
+
}
|
|
51
|
+
;(bridge as any).registerEntry = (entry: any) => (bridge as any).register(entry?.kind, entry?.payload, entry?.source)
|
|
52
|
+
;(bridge as any).unregisterPlugin = (source?: TwinRegistrationSource|string) => registerApi.unregisterPlugin(source)
|
|
53
|
+
;(bridge as any).removePlugin = (source?: TwinRegistrationSource|string) => registerApi.unregisterPlugin(source)
|
|
54
|
+
;(bridge as any).listProviders = (filter?:TwinProviderFilter)=>registry.listProviders(filter)
|
|
55
|
+
;(bridge as any).listTwinProviders = (filter?:TwinProviderFilter)=>registry.listProviders(filter)
|
|
56
|
+
;(bridge as any).getProviderHealth = (providerId:string, version?:string)=>registry.getProviderHealth(providerId,version)
|
|
57
|
+
;(bridge as any).resolveProvider = (providerId:string, version?:string)=>registry.resolveProvider(providerId,version)
|
|
58
|
+
;(bridge as any).validateProvider = (providerId:string, version?:string)=>registry.validateProvider(providerId,version)
|
|
59
|
+
;(bridge as any).retireProvider = (providerId:string, version?:string)=>registry.retireProvider(providerId,version??'',undefined)
|
|
60
|
+
bridge.drain((pluginName,contribution,generation)=>registry.registerTwinPluginContribution(pluginName,contribution,generation),(pluginName,generation)=>registry.unregisterTwinPluginContribution(pluginName,generation))
|
|
61
|
+
return bridge
|
|
62
|
+
}
|
|
63
|
+
export const attachTwinProviderRegistry=attachTwinPluginBridge
|
|
64
|
+
export const createTwinPluginExt=createTwinPluginBridge
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|