dsh-labscript 0.2.0 → 0.2.1

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/docs/README.zh.md CHANGED
@@ -1,57 +1,57 @@
1
- # dsh-labscript(中文说明)
2
-
3
- 在 DeepSeek Harness(DSH)里控制 labscript-suite 实验。只用 stock 接口:
4
- runmanager remote(面板 globals、n_shots、engage、abort、run_shots、
5
- error_in_globals)+ shot HDF5。不需要改 BLACS/lyse;实验室私有的分析、协议、
6
- hard limits 全部通过配置接入。
7
-
8
- ## 工具(9 个)
9
-
10
- | 工具 | 作用 |
11
- |---|---|
12
- | labs_doctor | 只读诊断:python、numpy/h5py、labscript/runmanager、路径、审批、版本 |
13
- | labs_status | 读面板:labscript、globals、n_shots、error_in_globals、run_shots、pending 进度 |
14
- | labs_set | 写面板(标量 / raw 表达式 / reps),写前全量快照;不运行 |
15
- | labs_engage | 提交当前面板,立即返回 n_shots_at_submit 与 expected_n_shots |
16
- | labs_wait | 等待 pending run(可超时);若是协议则收尾:postprocess → finish 写回 → restore |
17
- | labs_abort | 中止当前提交(不排队,不会被 labs_wait 堵住) |
18
- | labs_data | 读 run 目录 HDF5:首发 globals、按扫描轴的逐发表格 |
19
- | labs_protocol | list / show / run YAML 固定操作协议 |
20
- | labs_restore | 用最近一次写前快照恢复面板(默认只恢复该次写过的键) |
21
-
22
- 配套 runtime skill(labscript):先看现场、稳定流程优先用协议、提交后可以结束回合、
23
- 同一时间只跑一个、不猜面板状态;新装或报配置错误时先跑 labs_doctor。
24
-
25
- ## 安装与配置
26
-
27
- - 要求:DSH >= 0.2、Node ^22.19/24;Python >= 3.9。mock 模式只要 numpy+h5py;
28
- 真机用 labscript-suite 环境。详见 docs/INSTALL.zh.md。
29
- - 默认 backend 是 mock-labscript;真机必须显式配置 backend: labscript。
30
- - 安装后在 DSH 插件页填 backend、python、runmanager host/port、protocolsDir;
31
- 其余高级配置(scriptsFile、hard limits、postprocess、approval、pathMap)写在
32
- profile 的 cordis.patch.yml。
33
-
34
- ## 协议 YAML
35
-
36
- name: demo_scan
37
- labscript: /path/to/sequence.py # 或 joint_script: 登记名
38
- set: { do_acquire: true, amplitude: 0.5 }
39
- scan: { duration_test_us: np.linspace(0, 8.0, 5) } # 表达式/数字/列表
40
- reps: 2
41
- postprocess: demo.peaks # 全局配置一次,协议里只写名字
42
- finish:
43
- restore: true
44
- panel: { duration_test_us: fit.center }
45
- history: { demo.peaks.center_us: fit.center }
46
-
47
- postprocess 是后处理:跑完后调用配置的 Python 函数(entry + pythonPaths),
48
- 返回 dict 供 finish 写回。旧协议里的 analysis: 字段当作 postprocess 别名。
49
-
50
- ## 边界
51
-
52
- - 真机 engage / protocol run 会请求 DSH 审批(一次),abort 和面板写入不请求;
53
- 插件不是硬件联锁。
54
- - labscript abort 只停止编译/提交新的 shot,已进 BLACS 队列或正在跑的 shot 不会被停。
55
- - 完成判定基于 HDF5 数量、大小稳定和最后一发可读;BLACS paused 会报
56
- compiled_not_executed 诊断。
57
-
1
+ # dsh-labscript(中文说明)
2
+
3
+ 在 DeepSeek Harness(DSH)里控制 labscript-suite 实验。只用 stock 接口:
4
+ runmanager remote(面板 globals、n_shots、engage、abort、run_shots、
5
+ error_in_globals)+ shot HDF5。不需要改 BLACS/lyse;实验室私有的分析、协议、
6
+ hard limits 全部通过配置接入。
7
+
8
+ ## 工具(9 个)
9
+
10
+ | 工具 | 作用 |
11
+ |---|---|
12
+ | labs_doctor | 只读诊断:python、numpy/h5py、labscript/runmanager、路径、审批、版本 |
13
+ | labs_status | 读面板:labscript、globals、n_shots、error_in_globals、run_shots、pending 进度 |
14
+ | labs_set | 写面板(标量 / raw 表达式 / reps),写前全量快照;不运行 |
15
+ | labs_engage | 提交当前面板,立即返回 n_shots_at_submit 与 expected_n_shots |
16
+ | labs_wait | 等待 pending run(可超时);若是协议则收尾:postprocess → finish 写回 → restore |
17
+ | labs_abort | 中止当前提交(不排队,不会被 labs_wait 堵住) |
18
+ | labs_data | 读 run 目录 HDF5:首发 globals、按扫描轴的逐发表格 |
19
+ | labs_protocol | list / show / run YAML 固定操作协议 |
20
+ | labs_restore | 用最近一次写前快照恢复面板(默认只恢复该次写过的键) |
21
+
22
+ 配套 runtime skill(labscript):先看现场、稳定流程优先用协议、提交后可以结束回合、
23
+ 同一时间只跑一个、不猜面板状态;新装或报配置错误时先跑 labs_doctor。
24
+
25
+ ## 安装与配置
26
+
27
+ - 要求:DSH >= 0.2、Node ^22.19/24;Python >= 3.9。mock 模式只要 numpy+h5py;
28
+ 真机用 labscript-suite 环境。详见 docs/INSTALL.zh.md。
29
+ - 默认 backend 是 mock-labscript;真机必须显式配置 backend: labscript。
30
+ - 安装后在 DSH 插件页填 backend、python、runmanager host/port、protocolsDir;
31
+ 其余高级配置(scriptsFile、hard limits、postprocess、approval、pathMap)写在
32
+ profile 的 cordis.patch.yml。
33
+
34
+ ## 协议 YAML
35
+
36
+ name: demo_scan
37
+ labscript: /path/to/sequence.py # 或 joint_script: 登记名
38
+ set: { do_acquire: true, amplitude: 0.5 }
39
+ scan: { duration_test_us: np.linspace(0, 8.0, 5) } # 表达式/数字/列表
40
+ reps: 2
41
+ postprocess: demo.peaks # 全局配置一次,协议里只写名字
42
+ finish:
43
+ restore: true
44
+ panel: { duration_test_us: fit.center }
45
+ history: { demo.peaks.center_us: fit.center }
46
+
47
+ postprocess 是后处理:跑完后调用配置的 Python 函数(entry + pythonPaths),
48
+ 返回 dict 供 finish 写回。旧协议里的 analysis: 字段当作 postprocess 别名。
49
+
50
+ ## 边界
51
+
52
+ - 真机 engage / protocol run 会请求 DSH 审批(一次),abort 和面板写入不请求;
53
+ 插件不是硬件联锁。
54
+ - labscript abort 只停止编译/提交新的 shot,已进 BLACS 队列或正在跑的 shot 不会被停。
55
+ - 完成判定基于 HDF5 数量、大小稳定和最后一发可读;BLACS paused 会报
56
+ compiled_not_executed 诊断。
57
+
@@ -1,33 +1,33 @@
1
- # Two ways to configure dsh-labscript in a DSH profile.
2
- #
3
- # 1) Hardware-free development / examples (built-in simulator):
4
- - id: dsh-labscript
5
- name: dsh-labscript
6
- config:
7
- python: /path/to/python3
8
- backend: mock-labscript
9
- protocolsDir: /path/to/your/protocols
10
- mock:
11
- stateDir: ~/.dsh-labscript/mock
12
- postprocess:
13
- entry: postprocess_demo:analyze
14
- pythonPaths: [/path/to/dsh-labscript/examples]
15
-
16
- # 2) Real labscript-suite (adjust host/port to your runmanager):
17
- # - id: dsh-labscript
18
- # name: dsh-labscript
19
- # config:
20
- # python: /path/to/labscript-suite/python
21
- # backend: labscript
22
- # runmanager: { host: 127.0.0.1, port: 42523, timeoutS: 10 }
23
- # protocolsDir: /path/to/your/protocols
24
- # scriptsFile: /path/to/scripts.json
25
- # hardLimitsFile: /path/to/hard_limits.json
26
- # calibrationLog: /path/to/calibration.jsonl
27
- # pathMap:
28
- # - { remotePrefix: "E:/Experiments", localPrefix: "//lab-pc/Experiments" }
29
- # reps: { global: dummy_iterator }
30
- # postprocess:
31
- # entry: my_lab.analysis:analyze
32
- # pythonPaths: [/path/to/my_lab]
33
- # approval: { engage: true, protocolRun: true }
1
+ # Two ways to configure dsh-labscript in a DSH profile.
2
+ #
3
+ # 1) Hardware-free development / examples (built-in simulator):
4
+ - id: dsh-labscript
5
+ name: dsh-labscript
6
+ config:
7
+ python: /path/to/python3
8
+ backend: mock-labscript
9
+ protocolsDir: /path/to/your/protocols
10
+ mock:
11
+ stateDir: ~/.dsh-labscript/mock
12
+ postprocess:
13
+ entry: postprocess_demo:analyze
14
+ pythonPaths: [/path/to/dsh-labscript/examples]
15
+
16
+ # 2) Real labscript-suite (adjust host/port to your runmanager):
17
+ # - id: dsh-labscript
18
+ # name: dsh-labscript
19
+ # config:
20
+ # python: /path/to/labscript-suite/python
21
+ # backend: labscript
22
+ # runmanager: { host: 127.0.0.1, port: 42523, timeoutS: 10 }
23
+ # protocolsDir: /path/to/your/protocols
24
+ # scriptsFile: /path/to/scripts.json
25
+ # hardLimitsFile: /path/to/hard_limits.json
26
+ # calibrationLog: /path/to/calibration.jsonl
27
+ # pathMap:
28
+ # - { remotePrefix: "E:/Experiments", localPrefix: "//lab-pc/Experiments" }
29
+ # reps: { global: dummy_iterator }
30
+ # postprocess:
31
+ # entry: my_lab.analysis:analyze
32
+ # pythonPaths: [/path/to/my_lab]
33
+ # approval: { engage: true, protocolRun: true }
@@ -1,19 +1,19 @@
1
- """Example postprocess entry: after a run, return the numbers that finish.panel
2
- and finish.history write back. Replace the fake model with your lab analysis.
3
-
4
- Signature: analyze(run_folder=..., scan_global=..., reps=..., name=...)
5
- Returns a JSON-serializable dict.
6
- """
7
-
8
-
9
- def analyze(run_folder, scan_global=None, reps=None, name=None):
10
- # Placeholder numbers so the write-back path is demonstrable end to end.
11
- return {
12
- "fit": {"center": 2.5, "pi_duration_us": 1.25, "r2": 0.99},
13
- "meta": {
14
- "run_folder": run_folder,
15
- "scan_global": scan_global,
16
- "reps": reps,
17
- "name": name,
18
- },
19
- }
1
+ """Example postprocess entry: after a run, return the numbers that finish.panel
2
+ and finish.history write back. Replace the fake model with your lab analysis.
3
+
4
+ Signature: analyze(run_folder=..., scan_global=..., reps=..., name=...)
5
+ Returns a JSON-serializable dict.
6
+ """
7
+
8
+
9
+ def analyze(run_folder, scan_global=None, reps=None, name=None):
10
+ # Placeholder numbers so the write-back path is demonstrable end to end.
11
+ return {
12
+ "fit": {"center": 2.5, "pi_duration_us": 1.25, "r2": 0.99},
13
+ "meta": {
14
+ "run_folder": run_folder,
15
+ "scan_global": scan_global,
16
+ "reps": reps,
17
+ "name": name,
18
+ },
19
+ }
@@ -1,18 +1,18 @@
1
- # Demo fixed-operation protocol. It runs against the built-in mock-labscript
2
- # backend without hardware; replace labscript with your sequence path and the
3
- # panel keys with your own globals for real experiments.
4
- name: demo_scan
5
- labscript: /path/to/your/sequence.py
6
- set:
7
- do_acquire: true
8
- amplitude: 0.5
9
- scan:
10
- duration_test_us: np.linspace(0.0, 8.0, 5)
11
- reps: 2
12
- postprocess: demo.peaks
13
- finish:
14
- restore: true
15
- panel:
16
- duration_test_us: fit.center
17
- history:
18
- demo.peaks.center_us: fit.center
1
+ # Demo fixed-operation protocol. It runs against the built-in mock-labscript
2
+ # backend without hardware; replace labscript with your sequence path and the
3
+ # panel keys with your own globals for real experiments.
4
+ name: demo_scan
5
+ labscript: /path/to/your/sequence.py
6
+ set:
7
+ do_acquire: true
8
+ amplitude: 0.5
9
+ scan:
10
+ duration_test_us: np.linspace(0.0, 8.0, 5)
11
+ reps: 2
12
+ postprocess: demo.peaks
13
+ finish:
14
+ restore: true
15
+ panel:
16
+ duration_test_us: fit.center
17
+ history:
18
+ demo.peaks.center_us: fit.center
package/lib/client.js CHANGED
@@ -1,193 +1,193 @@
1
- /**
2
- * dsh-labscript -- browser half.
3
- *
4
- * A pre-built client bundle: DSH serves it as-is and expects the
5
- * window.__ModuleLoader__.load({ id, factory }) wrapper. It registers one small
6
- * configuration page into the official Plugins page (plugins.bundle.config).
7
- * The page only edits backend, python, runmanager host/port and protocolsDir;
8
- * everything else stays in the profile config. It talks to this plugin's own
9
- * routes under /dsh-labscript/api (GET/POST config, POST doctor).
10
- */
11
- window.__ModuleLoader__.load({
12
- id: 'dsh-labscript',
13
- factory: (require) => {
14
- const module = { exports: {} }
15
- const exports = module.exports
16
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
17
-
18
- const React = require('react')
19
- const h = React.createElement
20
- const { useEffect, useState } = React
21
-
22
- const API = '/dsh-labscript/api'
23
- const STYLE_ID = 'dsh-labscript/plugin-config.css'
24
-
25
- const CSS = [
26
- '.lsc{max-width:720px;display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:20px}',
27
- '.lsc-section{border:1px solid #d9d9d9;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:8px}',
28
- '.lsc-section h3{margin:0;font-size:14px;font-weight:600}',
29
- '.lsc-row{display:flex;flex-direction:column;gap:4px}',
30
- '.lsc label{font-weight:500}',
31
- '.lsc input,.lsc select{width:100%;box-sizing:border-box;padding:6px 8px;border-radius:6px;border:1px solid #d9d9d9;background:#fff;color:inherit;font:inherit}',
32
- '.lsc .hint{color:#8a8a8a;font-size:12px;line-height:18px}',
33
- '.lsc-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}',
34
- '.lsc button{padding:5px 12px;border-radius:6px;border:1px solid #d9d9d9;background:#f5f5f5;color:inherit;cursor:pointer;font:inherit}',
35
- '.lsc button.primary{background:#2b6cb0;border-color:transparent;color:#fff}',
36
- '.lsc button:disabled{opacity:.55;cursor:not-allowed}',
37
- '.lsc-msg{padding:8px 10px;border-radius:6px;white-space:pre-wrap;font-size:12px;line-height:18px}',
38
- '.lsc-msg.ok{background:rgba(22,163,74,.1);color:#16a34a}',
39
- '.lsc-msg.err{background:rgba(220,38,38,.1);color:#dc2626}',
40
- '.lsc-check{display:flex;gap:8px;font-size:12px;line-height:18px}',
41
- '.lsc-check b{min-width:88px;font-weight:600}',
42
- '.lsc-check.ok b{color:#16a34a}',
43
- '.lsc-check.warn b{color:#ea580c}',
44
- '.lsc-check.fail b{color:#dc2626}',
45
- ].join(String.fromCharCode(10))
46
-
47
- function ensureStyle() {
48
- if (document.getElementById(STYLE_ID) !== null) return
49
- const style = document.createElement('style')
50
- style.id = STYLE_ID
51
- style.textContent = CSS
52
- document.head.appendChild(style)
53
- }
54
-
55
- async function apiFetch(path, options) {
56
- const response = await fetch(API + path, Object.assign({ headers: { 'Content-Type': 'application/json' } }, options || {}))
57
- let payload
58
- try {
59
- payload = await response.json()
60
- } catch (error) {
61
- throw new Error('接口返回了非 JSON 内容(HTTP ' + response.status + ')')
62
- }
63
- if (!response.ok || payload.ok === false) throw new Error(payload.error || ('请求失败(HTTP ' + response.status + ')'))
64
- return payload
65
- }
66
-
67
- function Row(props) {
68
- return h('div', { className: 'lsc-row' },
69
- h('label', null, props.label),
70
- props.children,
71
- props.hint === undefined ? null : h('span', { className: 'hint' }, props.hint),
72
- )
73
- }
74
-
75
- function ConfigPage() {
76
- const [loading, setLoading] = useState(true)
77
- const [configFile, setConfigFile] = useState('')
78
- const [version, setVersion] = useState('')
79
- const [backend, setBackend] = useState('mock-labscript')
80
- const [python, setPython] = useState('')
81
- const [host, setHost] = useState('127.0.0.1')
82
- const [port, setPort] = useState(42523)
83
- const [protocolsDir, setProtocolsDir] = useState('')
84
- const [busy, setBusy] = useState('')
85
- const [message, setMessage] = useState(null)
86
- const [checks, setChecks] = useState(null)
87
-
88
- const load = async () => {
89
- setLoading(true)
90
- try {
91
- const payload = await apiFetch('/config')
92
- setVersion(payload.version || '')
93
- setConfigFile(payload.configFile || '')
94
- const current = payload.config || {}
95
- setBackend(current.backend || 'mock-labscript')
96
- setPython(current.python || '')
97
- setHost((current.runmanager && current.runmanager.host) || '127.0.0.1')
98
- setPort((current.runmanager && current.runmanager.port) || 42523)
99
- setProtocolsDir(current.protocolsDir || '')
100
- } catch (error) {
101
- setMessage({ kind: 'err', text: error.message })
102
- } finally {
103
- setLoading(false)
104
- }
105
- }
106
-
107
- useEffect(() => {
108
- ensureStyle()
109
- load()
110
- }, [])
111
-
112
- const save = async () => {
113
- setBusy('save')
114
- setMessage(null)
115
- try {
116
- await apiFetch('/config', {
117
- method: 'POST',
118
- body: JSON.stringify({
119
- backend,
120
- python,
121
- runmanager: { host, port: Number(port) },
122
- protocolsDir,
123
- }),
124
- })
125
- setMessage({ kind: 'ok', text: '已保存;下一次工具调用生效,不需要重启。' })
126
- } catch (error) {
127
- setMessage({ kind: 'err', text: error.message })
128
- } finally {
129
- setBusy('')
130
- }
131
- }
132
-
133
- const doctor = async () => {
134
- setBusy('doctor')
135
- setMessage(null)
136
- setChecks(null)
137
- try {
138
- const payload = await apiFetch('/doctor', { method: 'POST', body: '{}' })
139
- setChecks(payload.checks || [])
140
- } catch (error) {
141
- setMessage({ kind: 'err', text: error.message })
142
- } finally {
143
- setBusy('')
144
- }
145
- }
146
-
147
- const checkRows = (checks || []).map((check, index) => h('div', { key: index, className: 'lsc-check ' + check.status },
148
- h('b', null, check.status),
149
- h('span', null, check.name + ': ' + check.detail + (check.hint ? ' -- ' + check.hint : '')),
150
- ))
151
-
152
- return h('div', { className: 'lsc' },
153
- h('div', null, h('h3', null, 'dsh-labscript'), h('span', { className: 'hint' }, 'v' + version + (configFile ? ' · ' + configFile : ''))),
154
- h('section', { className: 'lsc-section' },
155
- h('h3', null, '连接'),
156
- h(Row, { label: 'backend' },
157
- h('select', { value: backend, onChange: (event) => setBackend(event.target.value) },
158
- h('option', { value: 'mock-labscript' }, 'mock-labscript(模拟器)'),
159
- h('option', { value: 'labscript' }, 'labscript(真实硬件)'),
160
- ),
161
- ),
162
- h(Row, { label: 'python', hint: '真机填 labscript-suite 环境的 python;模拟器只需 numpy+h5py。' }, h('input', { type: 'text', value: python, onChange: (event) => setPython(event.target.value) })),
163
- h(Row, { label: 'runmanager host' }, h('input', { type: 'text', value: host, onChange: (event) => setHost(event.target.value) })),
164
- h(Row, { label: 'runmanager port' }, h('input', { type: 'number', value: port, onChange: (event) => setPort(event.target.value) })),
165
- ),
166
- h('section', { className: 'lsc-section' },
167
- h('h3', null, '实验'),
168
- h(Row, { label: 'protocolsDir', hint: '协议 YAML 目录;留空则协议列表为空。' }, h('input', { type: 'text', value: protocolsDir, onChange: (event) => setProtocolsDir(event.target.value) })),
169
- h('span', { className: 'hint' }, 'scriptsFile、hard limits、postprocess、approval、pathMap 等高级配置仍写在 profile 的 cordis.patch.yml。'),
170
- ),
171
- h('div', { className: 'lsc-actions' },
172
- h('button', { className: 'primary', disabled: loading || busy !== '', onClick: save }, busy === 'save' ? '保存中…' : '保存'),
173
- h('button', { disabled: loading || busy !== '', onClick: doctor }, busy === 'doctor' ? '诊断中…' : '运行诊断'),
174
- ),
175
- message === null ? null : h('div', { className: 'lsc-msg ' + message.kind }, message.text),
176
- checks === null ? null : h('div', { className: 'lsc-section' }, h('h3', null, '诊断结果'), checkRows),
177
- )
178
- }
179
-
180
- function apply(ctx) {
181
- ctx.effect(
182
- () => ctx.slots.inject('plugins.bundle.config', () =>
183
- ctx.slots.register({ name: 'plugins.bundle.config', key: 'dsh-labscript' }, ConfigPage)),
184
- 'dsh-labscript: plugin configuration page',
185
- )
186
- }
187
-
188
- exports.apply = apply
189
- exports.inject = ['slots']
190
- exports.ConfigPage = ConfigPage
191
- return module.exports
192
- },
193
- })
1
+ /**
2
+ * dsh-labscript -- browser half.
3
+ *
4
+ * A pre-built client bundle: DSH serves it as-is and expects the
5
+ * window.__ModuleLoader__.load({ id, factory }) wrapper. It registers one small
6
+ * configuration page into the official Plugins page (plugins.bundle.config).
7
+ * The page only edits backend, python, runmanager host/port and protocolsDir;
8
+ * everything else stays in the profile config. It talks to this plugin's own
9
+ * routes under /dsh-labscript/api (GET/POST config, POST doctor).
10
+ */
11
+ window.__ModuleLoader__.load({
12
+ id: 'dsh-labscript',
13
+ factory: (require) => {
14
+ const module = { exports: {} }
15
+ const exports = module.exports
16
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
17
+
18
+ const React = require('react')
19
+ const h = React.createElement
20
+ const { useEffect, useState } = React
21
+
22
+ const API = '/dsh-labscript/api'
23
+ const STYLE_ID = 'dsh-labscript/plugin-config.css'
24
+
25
+ const CSS = [
26
+ '.lsc{max-width:720px;display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:20px}',
27
+ '.lsc-section{border:1px solid #d9d9d9;border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:8px}',
28
+ '.lsc-section h3{margin:0;font-size:14px;font-weight:600}',
29
+ '.lsc-row{display:flex;flex-direction:column;gap:4px}',
30
+ '.lsc label{font-weight:500}',
31
+ '.lsc input,.lsc select{width:100%;box-sizing:border-box;padding:6px 8px;border-radius:6px;border:1px solid #d9d9d9;background:#fff;color:inherit;font:inherit}',
32
+ '.lsc .hint{color:#8a8a8a;font-size:12px;line-height:18px}',
33
+ '.lsc-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}',
34
+ '.lsc button{padding:5px 12px;border-radius:6px;border:1px solid #d9d9d9;background:#f5f5f5;color:inherit;cursor:pointer;font:inherit}',
35
+ '.lsc button.primary{background:#2b6cb0;border-color:transparent;color:#fff}',
36
+ '.lsc button:disabled{opacity:.55;cursor:not-allowed}',
37
+ '.lsc-msg{padding:8px 10px;border-radius:6px;white-space:pre-wrap;font-size:12px;line-height:18px}',
38
+ '.lsc-msg.ok{background:rgba(22,163,74,.1);color:#16a34a}',
39
+ '.lsc-msg.err{background:rgba(220,38,38,.1);color:#dc2626}',
40
+ '.lsc-check{display:flex;gap:8px;font-size:12px;line-height:18px}',
41
+ '.lsc-check b{min-width:88px;font-weight:600}',
42
+ '.lsc-check.ok b{color:#16a34a}',
43
+ '.lsc-check.warn b{color:#ea580c}',
44
+ '.lsc-check.fail b{color:#dc2626}',
45
+ ].join(String.fromCharCode(10))
46
+
47
+ function ensureStyle() {
48
+ if (document.getElementById(STYLE_ID) !== null) return
49
+ const style = document.createElement('style')
50
+ style.id = STYLE_ID
51
+ style.textContent = CSS
52
+ document.head.appendChild(style)
53
+ }
54
+
55
+ async function apiFetch(path, options) {
56
+ const response = await fetch(API + path, Object.assign({ headers: { 'Content-Type': 'application/json' } }, options || {}))
57
+ let payload
58
+ try {
59
+ payload = await response.json()
60
+ } catch (error) {
61
+ throw new Error('接口返回了非 JSON 内容(HTTP ' + response.status + ')')
62
+ }
63
+ if (!response.ok || payload.ok === false) throw new Error(payload.error || ('请求失败(HTTP ' + response.status + ')'))
64
+ return payload
65
+ }
66
+
67
+ function Row(props) {
68
+ return h('div', { className: 'lsc-row' },
69
+ h('label', null, props.label),
70
+ props.children,
71
+ props.hint === undefined ? null : h('span', { className: 'hint' }, props.hint),
72
+ )
73
+ }
74
+
75
+ function ConfigPage() {
76
+ const [loading, setLoading] = useState(true)
77
+ const [configFile, setConfigFile] = useState('')
78
+ const [version, setVersion] = useState('')
79
+ const [backend, setBackend] = useState('mock-labscript')
80
+ const [python, setPython] = useState('')
81
+ const [host, setHost] = useState('127.0.0.1')
82
+ const [port, setPort] = useState(42523)
83
+ const [protocolsDir, setProtocolsDir] = useState('')
84
+ const [busy, setBusy] = useState('')
85
+ const [message, setMessage] = useState(null)
86
+ const [checks, setChecks] = useState(null)
87
+
88
+ const load = async () => {
89
+ setLoading(true)
90
+ try {
91
+ const payload = await apiFetch('/config')
92
+ setVersion(payload.version || '')
93
+ setConfigFile(payload.configFile || '')
94
+ const current = payload.config || {}
95
+ setBackend(current.backend || 'mock-labscript')
96
+ setPython(current.python || '')
97
+ setHost((current.runmanager && current.runmanager.host) || '127.0.0.1')
98
+ setPort((current.runmanager && current.runmanager.port) || 42523)
99
+ setProtocolsDir(current.protocolsDir || '')
100
+ } catch (error) {
101
+ setMessage({ kind: 'err', text: error.message })
102
+ } finally {
103
+ setLoading(false)
104
+ }
105
+ }
106
+
107
+ useEffect(() => {
108
+ ensureStyle()
109
+ load()
110
+ }, [])
111
+
112
+ const save = async () => {
113
+ setBusy('save')
114
+ setMessage(null)
115
+ try {
116
+ await apiFetch('/config', {
117
+ method: 'POST',
118
+ body: JSON.stringify({
119
+ backend,
120
+ python,
121
+ runmanager: { host, port: Number(port) },
122
+ protocolsDir,
123
+ }),
124
+ })
125
+ setMessage({ kind: 'ok', text: '已保存;下一次工具调用生效,不需要重启。' })
126
+ } catch (error) {
127
+ setMessage({ kind: 'err', text: error.message })
128
+ } finally {
129
+ setBusy('')
130
+ }
131
+ }
132
+
133
+ const doctor = async () => {
134
+ setBusy('doctor')
135
+ setMessage(null)
136
+ setChecks(null)
137
+ try {
138
+ const payload = await apiFetch('/doctor', { method: 'POST', body: '{}' })
139
+ setChecks(payload.checks || [])
140
+ } catch (error) {
141
+ setMessage({ kind: 'err', text: error.message })
142
+ } finally {
143
+ setBusy('')
144
+ }
145
+ }
146
+
147
+ const checkRows = (checks || []).map((check, index) => h('div', { key: index, className: 'lsc-check ' + check.status },
148
+ h('b', null, check.status),
149
+ h('span', null, check.name + ': ' + check.detail + (check.hint ? ' -- ' + check.hint : '')),
150
+ ))
151
+
152
+ return h('div', { className: 'lsc' },
153
+ h('div', null, h('h3', null, 'dsh-labscript'), h('span', { className: 'hint' }, 'v' + version + (configFile ? ' · ' + configFile : ''))),
154
+ h('section', { className: 'lsc-section' },
155
+ h('h3', null, '连接'),
156
+ h(Row, { label: 'backend' },
157
+ h('select', { value: backend, onChange: (event) => setBackend(event.target.value) },
158
+ h('option', { value: 'mock-labscript' }, 'mock-labscript(模拟器)'),
159
+ h('option', { value: 'labscript' }, 'labscript(真实硬件)'),
160
+ ),
161
+ ),
162
+ h(Row, { label: 'python', hint: '真机填 labscript-suite 环境的 python;模拟器只需 numpy+h5py。' }, h('input', { type: 'text', value: python, onChange: (event) => setPython(event.target.value) })),
163
+ h(Row, { label: 'runmanager host' }, h('input', { type: 'text', value: host, onChange: (event) => setHost(event.target.value) })),
164
+ h(Row, { label: 'runmanager port' }, h('input', { type: 'number', value: port, onChange: (event) => setPort(event.target.value) })),
165
+ ),
166
+ h('section', { className: 'lsc-section' },
167
+ h('h3', null, '实验'),
168
+ h(Row, { label: 'protocolsDir', hint: '协议 YAML 目录;留空则协议列表为空。' }, h('input', { type: 'text', value: protocolsDir, onChange: (event) => setProtocolsDir(event.target.value) })),
169
+ h('span', { className: 'hint' }, 'scriptsFile、hard limits、postprocess、approval、pathMap 等高级配置仍写在 profile 的 cordis.patch.yml。'),
170
+ ),
171
+ h('div', { className: 'lsc-actions' },
172
+ h('button', { className: 'primary', disabled: loading || busy !== '', onClick: save }, busy === 'save' ? '保存中…' : '保存'),
173
+ h('button', { disabled: loading || busy !== '', onClick: doctor }, busy === 'doctor' ? '诊断中…' : '运行诊断'),
174
+ ),
175
+ message === null ? null : h('div', { className: 'lsc-msg ' + message.kind }, message.text),
176
+ checks === null ? null : h('div', { className: 'lsc-section' }, h('h3', null, '诊断结果'), checkRows),
177
+ )
178
+ }
179
+
180
+ function apply(ctx) {
181
+ ctx.effect(
182
+ () => ctx.slots.inject('plugins.bundle.config', () =>
183
+ ctx.slots.register({ name: 'plugins.bundle.config', key: 'dsh-labscript' }, ConfigPage)),
184
+ 'dsh-labscript: plugin configuration page',
185
+ )
186
+ }
187
+
188
+ exports.apply = apply
189
+ exports.inject = ['slots']
190
+ exports.ConfigPage = ConfigPage
191
+ return module.exports
192
+ },
193
+ })