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 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+ 次真实测试,不是自称。** **1135 例**全链路用例跑在真实 stdio 服务器 + 本机 OriginPro 上,**1135 通过 / 0 失败**:27 个工具每个至少 15 例、最多 135 例,按副作用分五层(L0 纯解析 301、L1 只读 COM 63、L2 改会话 530、L3 落盘 163、L4 进程级 78)。提示词全部按人话写,跑之前先用在线 `tools/list` schema 校验用例本身,所以失败只会是产品问题,不会是题目写错。另有 **420 项**离线/单元/边界检查(asciiio 84、binio 30、lifecycle 42、smoke 129、edge 26、paths 35、json 29、readfile 12、labtalk 33)**随包发布,你可以直接复跑**;那 1135 例题库含提示词与模拟数据,是开发侧资产,不随包分发。originpro 的十几个文档陷阱全部记录在案并绕开。
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 # 26 项边界
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 # 30 项,不需要 Origin;有真实 .bin 时会跟厂商 .txt 逐点对账
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
- 上面这 420 项随包发布,你可以自己复跑。另外那 **1135 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
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 # 420 项离线与单元检查(随包发布)
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(max(1, int(params["segments"]))):
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
- xy = [float(v) for v in xy_range]
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.10",
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
@@ -20,7 +20,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
20
20
  import originlab # noqa: E402
21
21
 
22
22
  SERVER_NAME = "origin-bridge"
23
- SERVER_VERSION = "0.4.10"
23
+ SERVER_VERSION = "0.4.11"
24
24
  PROTOCOL = "2024-11-05"
25
25
 
26
26
 
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 = []