dsh-origin-bridge 0.4.10 → 0.4.11
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/README.en.md +193 -0
- package/README.md +9 -5
- package/binio.py +10 -1
- package/edge_checks.py +13 -0
- package/originlab.py +21 -4
- package/package.json +2 -1
- package/server.py +1 -1
- package/test_binio.py +19 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="icon.svg" width="128" height="128" alt="Origin Bridge">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Origin Bridge</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="README.md">中文说明</a> · <b>English</b>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
Drive <b>local</b> Origin / OriginPro from chat in plain language:<br>
|
|
13
|
+
hand it a <code>.dat</code> / <code>.csv</code> / Excel / electrochemical-workstation <code>.bin</code> file,<br>
|
|
14
|
+
get back a publication-ready image and an <code>.opju</code> project you can <b>keep editing in Origin</b>.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<sub>27 tools · <b>1136/1136 full-chain tests passing</b> · data never leaves the machine · workstation <code>.txt</code> / <code>.bin</code> supported</sub>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="#requirements"><img alt="platform" src="https://img.shields.io/badge/platform-Windows%20%2B%20Origin%202021%2B-blue"></a>
|
|
23
|
+
<img alt="tests" src="https://img.shields.io/badge/tests-1136%2F1136%20passing-brightgreen">
|
|
24
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-lightgrey">
|
|
25
|
+
<a href="https://github.com/angelky7/dsh-origin-bridge/actions/workflows/offline-checks.yml"><img alt="offline checks" src="https://github.com/angelky7/dsh-origin-bridge/actions/workflows/offline-checks.yml/badge.svg"></a>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
## Why
|
|
29
|
+
|
|
30
|
+
Existing attempts at wiring Origin into chat either upload your data to a cloud or are 200-line toys. Origin Bridge is the opposite:
|
|
31
|
+
|
|
32
|
+
- **Data never leaves the machine.** No API keys, no cloud component. dsh starts a local Python process that drives the Origin on your machine through OriginLab's official [`originpro`](https://pypi.org/project/originpro/) package.
|
|
33
|
+
- **Every step carries evidence.** Origin's X-Functions routinely "succeed" without doing anything. Here, plotting reads the axis range back and compares it against the data span; exporting verifies existence, size and the file-header magic; styling is read back item by item — anything that cannot be read back lands in a `failed` list instead of pretending to succeed.
|
|
34
|
+
- **27 tools, not 74.** Each one has a measured use, and each description states its measured limits, so the model does not step into traps.
|
|
35
|
+
- **1000+ real tests, not a claim.** 1136 full-chain cases run against a real stdio server + local OriginPro — 1136 passed / 0 failed: every tool has at least 15 cases (up to 135), grouped into five side-effect layers (L0 pure parsing 301, L1 read-only COM 63, L2 session-mutating 531, L3 on-disk 163, L4 process-level 78). Prompts are written the way a person talks, and every case is validated against the live `tools/list` schema first — so a failure can only be a product bug, never a miswritten test. Another **409** offline/unit/edge checks ship **inside the package** (asciiio 84, binio 15, lifecycle 42, smoke 129, edge 30, paths 35, json 29, readfile 12, labtalk 33; binio adds per-point vendor cross-checks when a real workstation export is present) and you can re-run them yourself; the 1136-case bank holds prompts and synthetic data, so it stays a development-side asset and is not distributed.
|
|
36
|
+
|
|
37
|
+
## What it can do
|
|
38
|
+
|
|
39
|
+
| You say | It does |
|
|
40
|
+
|---|---|
|
|
41
|
+
| "Plot `D:\data\spectrum.dat` as a line-symbol plot, X axis "Time (s)", export to `D:\out`" | `origin_figure`: import → plot → axis titles → PNG + .opju |
|
|
42
|
+
| "Put these two files on one graph with a double Y axis" | `origin_layer` (right Y) + `origin_plot` (plot into a named layer) |
|
|
43
|
+
| "Add a legend to this spectrum, top right" | `origin_legend` (text / position / visibility all read back) |
|
|
44
|
+
| "Fit this EIS data" | `origin_fit` (gauss / lorentz / voigt / expdec1… presets, parameters can be fixed) |
|
|
45
|
+
| "Here is a cv.bin from a CHI workstation" | `origin_import` reads the binary; the potential axis is reconstructed from the header's method parameters |
|
|
46
|
+
| "Workstation txt export with a wall of parameters first" | `origin_import` skips the header, treats the row next to the data (`Potential/V, Current/A`) as column names, splits units into column Units, keeps parameters in `metadata` |
|
|
47
|
+
| "Make an XPS figure" | `origin_chart_render` (40 scientific presets: axis titles / plot kind / suggested fit) |
|
|
48
|
+
| "Show this matrix as a heatmap" | `origin_matrix` (heat_map / cmap / mesh / contline, Origin's own templates) |
|
|
49
|
+
|
|
50
|
+
Sources: `.dat` `.csv` `.txt` `.tsv` (`origin_read_file` parses these only; `.xls/.xlsx/.wks` are refused with `unsupported_file_type` pointing at `origin_import`; delimiter sniffing; encodings UTF-8 / GBK / **UTF-16 with or without BOM** / cp1252; multi-row headers; Fortran exponents; `NA`/`-`; decorated numbers like `<0.001`, `2.5±0.1`, `25 mV`, `1,234.5`, `(500)`, `2.5*` — each unwrapping states in `warnings` what it substituted), `.xls` `.xlsx`, electrochemical-workstation `.txt` exports (CH Instruments and friends; the parameter preamble lands in `metadata`), and workstation binary `.bin` files whose header starts `80 F2 1B 00`.
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
Windows, Origin/OriginPro **2021 or newer** (hard floor from `originpro`; 2018 measured as unusable), Python 3.10+.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
### With dsh (recommended)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
dsh plugin --profile desktop add dsh-origin-bridge
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Installs exactly **one package** (the host's `@deepseek-ai/dsh-mcp-client` is an optional peer, so no second loader copy lands in the profile). If the install fails with `ERR_PNPM_NO_MATCHING_VERSION`, that is a **lagging npm mirror**, not a missing release: check `npm config get registry` and either point at the official registry once,
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
dsh plugin --profile desktop add dsh-origin-bridge --registry=https://registry.npmjs.org/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
or install an offline bundle without touching any registry:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
dsh plugin --profile desktop add file:C:\path\to\dsh-origin-bridge
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
npm only distributes the files — **your Python environment is your job**. The dependencies must go into the interpreter dsh actually launches:
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
pip install originpro numpy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Two failure modes behave differently (measured, do not guess):
|
|
83
|
+
|
|
84
|
+
- **Interpreter present, `originpro` missing**: all 27 tools still register, and every call returns `code=missing_originpro` with the reason and `isError: true` — visible and actionable.
|
|
85
|
+
- **No `python` on PATH** (the patch's default command): the bridge process never starts. `failOnStartupError: false` keeps dsh alive, but that round has **no tools registered and no error**. Set `ORIGIN_BRIDGE_PYTHON` first.
|
|
86
|
+
|
|
87
|
+
Origin not running does **not** affect registration: the COM connection is built on the first tool call.
|
|
88
|
+
|
|
89
|
+
If the dependencies live in a venv, or you use a different profile, set these user environment variables (then reopen the terminal and fully restart dsh):
|
|
90
|
+
|
|
91
|
+
```powershell
|
|
92
|
+
setx ORIGIN_BRIDGE_PYTHON "C:\path\to\.venv\Scripts\python.exe"
|
|
93
|
+
setx ORIGIN_BRIDGE_HOME "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-origin-bridge"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Fully quit dsh and reopen; in a new chat say "list tools whose names contain origin" — you should see the 27 `mcp__origin_bridge__origin_*` tools. Uninstall: `dsh plugin --profile desktop remove dsh-origin-bridge`.
|
|
97
|
+
|
|
98
|
+
### Any other stdio MCP client (Claude Desktop, Cursor, Codex…)
|
|
99
|
+
|
|
100
|
+
`npm install dsh-origin-bridge` ships a launcher at `bin/origin-bridge.mjs`. Point your client's `command` at it (or at `node_modules/dsh-origin-bridge/bin/origin-bridge.mjs`), keep `serverName: origin_bridge`, and the tool names are `mcp__origin_bridge__origin_*`. The launcher picks the interpreter from `ORIGIN_BRIDGE_PYTHON`, then a `.venv` next to the package, then `python` on PATH — and exits with a one-line error instead of a stack trace if none can be spawned.
|
|
101
|
+
|
|
102
|
+
### From source (to run tests or hack)
|
|
103
|
+
|
|
104
|
+
```powershell
|
|
105
|
+
cd C:\path\to\dsh-origin-bridge
|
|
106
|
+
python -m venv .venv
|
|
107
|
+
.\.venv\Scripts\Activate.ps1
|
|
108
|
+
pip install -r requirements.txt
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The Origin-free suites run anywhere; `smoke_test.py` and `edge_checks.py` drive a real Origin:
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
.\.venv\Scripts\python.exe smoke_test.py # 129 checks, needs Origin
|
|
115
|
+
.\.venv\Scripts\python.exe edge_checks.py # 30 checks, needs Origin
|
|
116
|
+
.\.venv\Scripts\python.exe test_asciiio.py # 84 checks, Origin-free
|
|
117
|
+
.\.venv\Scripts\python.exe test_binio.py # 15 checks, Origin-free; cross-checks point-for-point against a vendor .txt when one exists
|
|
118
|
+
.\.venv\Scripts\python.exe test_lifecycle.py # 42 checks, Origin-free
|
|
119
|
+
.\.venv\Scripts\python.exe test_paths.py # 35 checks, Origin-free
|
|
120
|
+
.\.venv\Scripts\python.exe test_json_safety.py # 29 checks, Origin-free
|
|
121
|
+
.\.venv\Scripts\python.exe test_readfile.py # 12 checks, Origin-free
|
|
122
|
+
.\.venv\Scripts\python.exe test_labtalk_guard.py # 33 checks, Origin-free
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For dsh registration from a source checkout, run `install_dsh.ps1` (backs up your profile patch, writes the insert block, validates the YAML afterwards) — see the [Chinese README](README.md) for the manual YAML and the three things that must be exactly right.
|
|
126
|
+
|
|
127
|
+
## The 27 tools
|
|
128
|
+
|
|
129
|
+
| Tool | Purpose |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `origin_status` | Connection check, Origin version, open pages. **Call this first** |
|
|
132
|
+
| `origin_read_file` | Parse a file without touching Origin: encoding, delimiter, header rows, per-column long name/units/valid rows, a 3-value `sample` per column; refuses `.xls/.xlsx/.wks`; `.bin` gets the technique/points/method/axis-source route |
|
|
133
|
+
| `origin_import` | Import `.dat/.csv/.txt/.tsv/.xls/.xlsx/.wks` and workstation `.bin`; returns a `ws-N` handle and the column list |
|
|
134
|
+
| `origin_write` | Build a sheet from inline data: `columns={name:[values…]}` or `headers`+`rows` |
|
|
135
|
+
| `origin_formula` | Set a column formula (Fx); returns computed rows and first/last values |
|
|
136
|
+
| `origin_plot` | Single or **multiple** curves; `template` uses Origin's own graph templates; `graph`+`layer` plots into a named layer |
|
|
137
|
+
| `origin_layer` | Add a layer (right Y, stacked panels); returns the name Origin gave it as proof |
|
|
138
|
+
| `origin_legend` | Legend from column long names or verbatim entries; 13 positions; text/visibility/coordinates read back |
|
|
139
|
+
| `origin_style` | Axis titles, **log axes**, ranges and steps, per-curve color/width/symbol/size/fill/transparency — **each item read back** |
|
|
140
|
+
| `origin_annotate` | Text labels and reference lines in data coordinates |
|
|
141
|
+
| `origin_matrix` | Grid data → matrix → **heatmap / contour / 3D colored surface** |
|
|
142
|
+
| `origin_fit` | `linear`; presets `gauss/lorentz/voigt/expdec1/expdec2/sine/power/logistic/boltzmann/doseresp/cubic`; or `nlfitsing`+`func` |
|
|
143
|
+
| `origin_chart_presets` | 40 scientific chart presets (XPS/XRD/UV-Vis/fluorescence/CV/Arrhenius…) |
|
|
144
|
+
| `origin_chart_preset` | One preset's full contract: required columns, default titles, suggested fit |
|
|
145
|
+
| `origin_chart_check` | Dry-run a preset's column expectations against your data, no Origin involved |
|
|
146
|
+
| `origin_chart_render` | One call: preset → styled figure (titles/kind/legend/image/opju) |
|
|
147
|
+
| `origin_export` | Export with existence/size/magic verification; PNG dimensions are read back |
|
|
148
|
+
| `origin_view` | Render the graph and return it **as MCP image content the model can see** |
|
|
149
|
+
| `origin_save_project` | Save an editable `.opju` project |
|
|
150
|
+
| `origin_open` | Open an existing project |
|
|
151
|
+
| `origin_inspect` | Pages / layers / plot counts / live handles — **look before you guess indices** |
|
|
152
|
+
| `origin_close_pages` | Clean up accumulated pages (the root cause of stale pointers in long sessions); has `dry_run` |
|
|
153
|
+
| `origin_labtalk` | Escape hatch: raw LabTalk with mandatory read-back; a deny list blocks destructive statements (advisory, not a sandbox) |
|
|
154
|
+
| `origin_exit` | Quit Origin, optionally saving first |
|
|
155
|
+
| `origin_instances` | List Origin instances: windowless background leftovers vs foreground windows, marking which this process started |
|
|
156
|
+
| `origin_reclaim` | Reclaim leftovers — **windowless only**; anything with a window is listed for you to close yourself |
|
|
157
|
+
| `origin_figure` | Main path: import → plot → titles → legend → image + opju in one call |
|
|
158
|
+
|
|
159
|
+
## Workstation `.bin` files
|
|
160
|
+
|
|
161
|
+
Files whose header starts `80 F2 1B 00` and name `Cyclic Voltammetry` / `A.C. Impedance` / `Amperometric i-t Curve` (CHI660E-class exports) end in **one** float32 array — the measured channel (current in A). **The potential axis is not in the file.** Origin Bridge reads the method block (Init/High/Low E, step, segments) from the header, rebuilds the triangle-wave axis, and **only accepts the reconstruction when the point count matches the file exactly** — otherwise it degrades to `axis=none` and says why, never plotting against a wrong X. i-t / CA / CP get a time axis from the sample interval; EIS frequency tables are a different parameter set and are reported as unavailable rather than guessed. The reconstruction was verified point-for-point against the vendor's own `.txt` exports (5 files × 600 points, 0 mismatches).
|
|
162
|
+
|
|
163
|
+
## Known limits
|
|
164
|
+
|
|
165
|
+
- `originpro` needs Origin **2021+**; OriginPro 2018C connects but imports 0 rows and `add_plot` kills the connector.
|
|
166
|
+
- Nonlinear fitting needs an **OriginPro** license; the standard edition gets an explicit `fit_function_unavailable`.
|
|
167
|
+
- `.bin` covers the `80 F2 1B 00` workstation format only; EIS .bin has the measured column only. Other vendors' binaries (Gamry / Autolab / Pine) are not attempted.
|
|
168
|
+
- Network paths are refused (`bad_path`): no UNC, no unmapped drives. Local paths with Chinese names, spaces, 200+ characters, `tiff→tif` renaming and auto-created directories all verified on real hardware.
|
|
169
|
+
- No concurrent-session protection: two chat sessions driving one Origin will stomp each other.
|
|
170
|
+
- Long sessions accumulate pages; past ~200 the per-plot cost grows (0.7 s → 2.8 s at 300 pages). The engine warns every 50 new pages and `origin_close_pages` / `origin_exit` reset the counter.
|
|
171
|
+
- A windowless Origin is the *normal* state for a dsh-driven instance, so nothing ever kills instances implicitly: `origin_reclaim` acts only on windowless ones, and only when you call it.
|
|
172
|
+
- Statistics (t-test / ANOVA / PCA / survival) are not provided — `originpro` doesn't offer them either.
|
|
173
|
+
|
|
174
|
+
## Troubleshooting
|
|
175
|
+
|
|
176
|
+
| Symptom | First check |
|
|
177
|
+
|---|---|
|
|
178
|
+
| Tools missing in dsh | Is the block a `- insert:` entry; is `name:` exactly `@deepseek-ai/dsh-mcp-client`; did you fully restart dsh; does `python server.py --tools` list 27 |
|
|
179
|
+
| `connection_error` | Origin ≥ 2021? `pip show originpro`? Exactly one `Origin64.exe` in Task Manager? |
|
|
180
|
+
| "Instance limit exceeded" | `origin_instances` to see them, `origin_reclaim` to close windowless leftovers; windows with titles may be your own unsaved work |
|
|
181
|
+
| `import_empty` | `origin_read_file` first; space-aligned and multi-header files are handled, so this means the file truly has no numeric rows |
|
|
182
|
+
| `com_timeout` | Origin stuck on a modal dialog / license popup |
|
|
183
|
+
| Image missing | `origin_inspect` → `plots` count; Chinese/space/long paths are verified, no need to ASCII-fy |
|
|
184
|
+
| `bad_path` / `target_locked` | The path is wrong (directory as filename, unmapped drive) or the target exists read-only; the bridge will not overwrite |
|
|
185
|
+
| Install fails `ERR_PNPM_NO_MATCHING_VERSION` | Mirror lag: add `--registry=https://registry.npmjs.org/` once, or use a `file:` offline bundle |
|
|
186
|
+
|
|
187
|
+
The [Chinese README](README.md) also carries a maintainer table of **30+ measured originpro/Origin traps** (silent no-ops, the real `plotxy` codes, `set_formula`'s actual 0-based index, COM thread affinity, the missing-value sentinel, the export filter rewriting `#`…) — if you maintain anything that drives Origin from Python, read it.
|
|
188
|
+
|
|
189
|
+
## Trademark & license
|
|
190
|
+
|
|
191
|
+
This is an independent third-party tool, **not affiliated with, endorsed by or sponsored by** OriginLab Corporation. Origin and OriginPro are trademarks of OriginLab Corporation; this tool drives Origin the normal way through OriginLab's publicly published `originpro` Python package and distributes none of OriginLab's binaries, icons or documentation.
|
|
192
|
+
|
|
193
|
+
MIT © Origin Bridge Contributors
|
package/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Origin Bridge</h1>
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>中文说明</b> · <a href="README.en.md">English</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
7
11
|
<p align="center">
|
|
8
12
|
用自然语言驱动<b>本机</b> Origin / OriginPro:<br>
|
|
9
13
|
丢给它一个 <code>.dat</code> / <code>.csv</code> / Excel / 电化学工作站 <code>.bin</code>,<br>
|
|
@@ -36,7 +40,7 @@
|
|
|
36
40
|
- **数据不出本机。** 没有 API key、没有云端组件,dsh 拉起一个本地 Python 进程,通过 OriginLab 官方的 [`originpro`](https://pypi.org/project/originpro/) 驱动你机器上的 Origin。
|
|
37
41
|
- **每一步都有证据。** Origin 的 X-Function 普遍"返回成功但什么都没发生"。这里画图后读回轴范围与数据跨度比对,导出后校验文件存在/体积/文件头魔数,样式逐项读回——读不回的进 `failed` 列表,不会假装成功。
|
|
38
42
|
- **不是 74 个工具,是 27 个。** 每个都有实测用途,工具描述里写着实测限制,模型不会踩坑。
|
|
39
|
-
- **1000+ 次真实测试,不是自称。** **
|
|
43
|
+
- **1000+ 次真实测试,不是自称。** **1136 例**全链路用例跑在真实 stdio 服务器 + 本机 OriginPro 上,**1136 通过 / 0 失败**:27 个工具每个至少 15 例、最多 135 例,按副作用分五层(L0 纯解析 301、L1 只读 COM 63、L2 改会话 531、L3 落盘 163、L4 进程级 78)。提示词全部按人话写,跑之前先用在线 `tools/list` schema 校验用例本身,所以失败只会是产品问题,不会是题目写错。另有 **409 项**离线/单元/边界检查(asciiio 84、binio 15、lifecycle 42、smoke 129、edge 30、paths 35、json 29、readfile 12、labtalk 33;binio 在机器上有真实工作站导出时会追加逐点对账项)**随包发布,你可以直接复跑**;那 1136 例题库含提示词与模拟数据,是开发侧资产,不随包分发。originpro 的十几个文档陷阱全部记录在案并绕开。
|
|
40
44
|
|
|
41
45
|
## 它能做什么
|
|
42
46
|
|
|
@@ -127,9 +131,9 @@ pip install -r requirements.txt # 国内网络慢就加 -i https://pypi.tun
|
|
|
127
131
|
|
|
128
132
|
```powershell
|
|
129
133
|
.\.venv\Scripts\python.exe smoke_test.py # 129 项,看到 ALL GOOD 才算过
|
|
130
|
-
.\.venv\Scripts\python.exe edge_checks.py #
|
|
134
|
+
.\.venv\Scripts\python.exe edge_checks.py # 30 项边界
|
|
131
135
|
.\.venv\Scripts\python.exe test_asciiio.py # 84 项,不需要 Origin
|
|
132
|
-
.\.venv\Scripts\python.exe test_binio.py #
|
|
136
|
+
.\.venv\Scripts\python.exe test_binio.py # 15 项,不需要 Origin;有真实 .bin 时会跟厂商 .txt 逐点对账
|
|
133
137
|
.\.venv\Scripts\python.exe test_lifecycle.py # 42 项,不需要 Origin:实例分类/回收/掉线重连的安全边界
|
|
134
138
|
.\.venv\Scripts\python.exe test_paths.py # 35 项,不需要 Origin:导出/保存目标路径的规范化与拒绝
|
|
135
139
|
.\.venv\Scripts\python.exe test_json_safety.py # 29 项,不需要 Origin:返回值必须是严格 JSON
|
|
@@ -137,7 +141,7 @@ pip install -r requirements.txt # 国内网络慢就加 -i https://pypi.tun
|
|
|
137
141
|
.\.venv\Scripts\python.exe test_labtalk_guard.py # 33 项,不需要 Origin:LabTalk 黑名单匹配
|
|
138
142
|
```
|
|
139
143
|
|
|
140
|
-
上面这
|
|
144
|
+
上面这 409 项随包发布,你可以自己复跑。另外那 **1136 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
|
|
141
145
|
|
|
142
146
|
### 不是 dsh,是别的 MCP 客户端
|
|
143
147
|
|
|
@@ -327,7 +331,7 @@ dsh-origin-bridge/
|
|
|
327
331
|
├── icon.svg # 卡片图标(scripts/make_icon.py 生成)
|
|
328
332
|
├── locale/*.json # dsh 卡片显示名与描述
|
|
329
333
|
├── scripts/make_preview.py # 重新生成 docs/preview.png(关掉图例后才导出)
|
|
330
|
-
└── test_*.py / smoke_test.py / edge_checks.py #
|
|
334
|
+
└── test_*.py / smoke_test.py / edge_checks.py # 409 项离线与单元检查(随包发布)
|
|
331
335
|
```
|
|
332
336
|
|
|
333
337
|
开发侧另有 `cases/` + `run_cases.py` 的 1135 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
|
package/binio.py
CHANGED
|
@@ -123,10 +123,14 @@ def expected_points(params, positive_first=True):
|
|
|
123
123
|
step = params["interval"]
|
|
124
124
|
if high <= low or step <= 0:
|
|
125
125
|
return 0
|
|
126
|
+
# segments is a raw float from the header with no plausibility filter of its
|
|
127
|
+
# own; a garbage value like 1e11 would make this loop for days on the COM
|
|
128
|
+
# thread and wedge the whole bridge. Real methods are two-digit.
|
|
129
|
+
segments = min(max(1, int(params["segments"])), 100000)
|
|
126
130
|
total = 0
|
|
127
131
|
e = init
|
|
128
132
|
d = 1 if positive_first else -1
|
|
129
|
-
for _ in range(
|
|
133
|
+
for _ in range(segments):
|
|
130
134
|
limit = high if d > 0 else low
|
|
131
135
|
steps = int(round(abs(limit - e) / step))
|
|
132
136
|
if steps <= 0:
|
|
@@ -152,6 +156,11 @@ def read_bin(path):
|
|
|
152
156
|
buf = fh.read()
|
|
153
157
|
if not buf.startswith(MAGIC):
|
|
154
158
|
raise BinFormatError("文件头不是 %s,这不是电化学工作站的 .bin" % MAGIC.hex())
|
|
159
|
+
if len(buf) < 16:
|
|
160
|
+
# A file with the magic but nothing else must fail HERE: _u32(buf, 4)
|
|
161
|
+
# below would otherwise raise a bare struct.error that the callers
|
|
162
|
+
# (they catch BinFormatError) report as internal_error.
|
|
163
|
+
raise BinFormatError("文件只有 %d 字节,头部都不完整,不是有效的 .bin" % len(buf))
|
|
155
164
|
tag, name, build = _technique(buf)
|
|
156
165
|
nseg = _u32(buf, 4)
|
|
157
166
|
total, start = _find_stream(buf)
|
package/edge_checks.py
CHANGED
|
@@ -149,6 +149,19 @@ def main():
|
|
|
149
149
|
check("exit then auto-reconnect", ex.get("exited") and after.get("connected") is True,
|
|
150
150
|
(ex.get("exited"), after.get("connected"), after.get("code")))
|
|
151
151
|
|
|
152
|
+
# 9b) matrix arg validation happens before any COM work, so a garbage grid
|
|
153
|
+
# must come back as named bad_arguments even when Origin is wedged.
|
|
154
|
+
bad = rpc.tool("origin_matrix", {"rows": [[1, 2], [3, "x"]]}, timeout=60)
|
|
155
|
+
check("matrix text cell -> bad_arguments",
|
|
156
|
+
bad.get("code") == "bad_arguments" and bad.get("ok") is False,
|
|
157
|
+
(bad.get("code"), str(bad.get("message"))[:60]))
|
|
158
|
+
bad = rpc.tool("origin_matrix", {"rows": [[1, 2]], "xy_range": "0 1 0 1"}, timeout=60)
|
|
159
|
+
check("matrix string xy_range -> bad_arguments",
|
|
160
|
+
bad.get("code") == "bad_arguments", (bad.get("code"), str(bad.get("message"))[:60]))
|
|
161
|
+
bad = rpc.tool("origin_matrix", {"rows": [[1, 2]], "xy_range": [0, 1, 0]}, timeout=60)
|
|
162
|
+
check("matrix 3-value xy_range -> bad_arguments",
|
|
163
|
+
bad.get("code") == "bad_arguments", (bad.get("code"), str(bad.get("message"))[:60]))
|
|
164
|
+
|
|
152
165
|
rpc.close()
|
|
153
166
|
|
|
154
167
|
# 10) the node launcher shipped in bin/ must actually start the server
|
package/originlab.py
CHANGED
|
@@ -1584,6 +1584,13 @@ def fit(worksheet, x=1, y=2, kind="linear", func="", fixed=None, starts=None,
|
|
|
1584
1584
|
% sorted(FIT_PRESETS))
|
|
1585
1585
|
if kind == "nlfitsing" and str(func).lower() in FIT_PRESETS:
|
|
1586
1586
|
func = FIT_PRESETS[str(func).lower()]
|
|
1587
|
+
for name, pair in (bounds or {}).items():
|
|
1588
|
+
# A wrong-typed pair used to be dropped silently: the fit ran with no
|
|
1589
|
+
# bounds and only a lo/hi:null note hinted at it.
|
|
1590
|
+
if not isinstance(pair, (list, tuple)) or not pair:
|
|
1591
|
+
raise OriginError("bad_arguments",
|
|
1592
|
+
"bounds 的值要是 [下界, 上界],参数 %s 收到 %r" % (name, pair),
|
|
1593
|
+
['如 {"y0": [0, null]},null 表示不设该侧'])
|
|
1587
1594
|
|
|
1588
1595
|
def run():
|
|
1589
1596
|
op = origin()
|
|
@@ -1928,6 +1935,19 @@ def matrix_plot(rows, template="heat_map", book_name="", graph_name="",
|
|
|
1928
1935
|
widths = {len(r) for r in rows if isinstance(r, (list, tuple))}
|
|
1929
1936
|
if len(widths) != 1 or not widths.pop():
|
|
1930
1937
|
raise OriginError("bad_arguments", "矩阵每行宽度必须一致且非空")
|
|
1938
|
+
for r in rows:
|
|
1939
|
+
for v in r:
|
|
1940
|
+
# A text cell used to die as float('x') inside run() and surface as
|
|
1941
|
+
# internal_error; type-check it here instead, before any COM work.
|
|
1942
|
+
if v is not None and not isinstance(v, (int, float)):
|
|
1943
|
+
raise OriginError("bad_arguments",
|
|
1944
|
+
"rows 里有不是数字的格子:%r(缺值请写 null)" % (v,))
|
|
1945
|
+
if xy_range is not None:
|
|
1946
|
+
ok_xy = isinstance(xy_range, (list, tuple)) and len(xy_range) == 4 and all(
|
|
1947
|
+
v is None or isinstance(v, (int, float)) for v in xy_range)
|
|
1948
|
+
if not ok_xy:
|
|
1949
|
+
raise OriginError("bad_arguments",
|
|
1950
|
+
"xy_range 要 4 个数:[x1,x2,y1,y2],收到 %r" % (xy_range,))
|
|
1931
1951
|
|
|
1932
1952
|
def run():
|
|
1933
1953
|
import numpy as np
|
|
@@ -1938,10 +1958,7 @@ def matrix_plot(rows, template="heat_map", book_name="", graph_name="",
|
|
|
1938
1958
|
ms = mb[0]
|
|
1939
1959
|
ms.from_np(arr, mv=np.nan)
|
|
1940
1960
|
if xy_range:
|
|
1941
|
-
|
|
1942
|
-
if len(xy) != 4:
|
|
1943
|
-
raise OriginError("bad_arguments", "xy_range 要 4 个数:[x1,x2,y1,y2]")
|
|
1944
|
-
ms.xymap = xy
|
|
1961
|
+
ms.xymap = [float(v) for v in xy_range]
|
|
1945
1962
|
mapped = _safe(lambda: list(ms.xymap), [])
|
|
1946
1963
|
gr = op.new_graph(graph_name or "", template=tpl)
|
|
1947
1964
|
if gr is None:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-origin-bridge",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "在对话里驱动本机 Origin / OriginPro:导入 .dat/.csv/.txt 与电化学工作站 .bin,绘图、拟合、改样式,导出图片并保存可在 Origin 里继续编辑的 .opju 工程。dsh 插件,27 个 MCP 工具。第三方工具,与 OriginLab Corporation 无关联、未获认可。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"requirements.txt",
|
|
62
62
|
"sample.dat",
|
|
63
63
|
"README.md",
|
|
64
|
+
"README.en.md",
|
|
64
65
|
"LICENSE"
|
|
65
66
|
],
|
|
66
67
|
"dsh": {
|
package/server.py
CHANGED
package/test_binio.py
CHANGED
|
@@ -110,6 +110,25 @@ try:
|
|
|
110
110
|
except binio.BinFormatError as exc:
|
|
111
111
|
check("non-workstation .bin rejected", "文件头" in str(exc), str(exc)[:60])
|
|
112
112
|
|
|
113
|
+
p4 = os.path.join(FIX, "truncated.bin")
|
|
114
|
+
with open(p4, "wb") as fh:
|
|
115
|
+
fh.write(binio.MAGIC + b"\x02\x00") # magic + 2 bytes: too short for _u32(buf, 4)
|
|
116
|
+
try:
|
|
117
|
+
binio.read_bin(p4)
|
|
118
|
+
check("truncated .bin raises BinFormatError", False, "no error raised")
|
|
119
|
+
except binio.BinFormatError as exc:
|
|
120
|
+
check("truncated .bin raises BinFormatError", "不完整" in str(exc), str(exc)[:60])
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
check("truncated .bin raises BinFormatError", False, repr(exc)[:80])
|
|
123
|
+
|
|
124
|
+
# A garbage header must not hang the caller: segments is a raw header float.
|
|
125
|
+
import time
|
|
126
|
+
t0 = time.time()
|
|
127
|
+
n = binio.expected_points({"init": 0.0, "high": 1.0, "low": -1.0,
|
|
128
|
+
"segments": 1e11, "interval": 0.01}, True)
|
|
129
|
+
check("garbage segments is clamped, not a day-long loop",
|
|
130
|
+
time.time() - t0 < 5.0 and n > 0, (n, round(time.time() - t0, 3)))
|
|
131
|
+
|
|
113
132
|
|
|
114
133
|
def txt_rows(fn):
|
|
115
134
|
rows = []
|