dsh-origin-bridge 0.4.5 → 0.4.6

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.md CHANGED
@@ -1,303 +1,329 @@
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
- 用自然语言驱动<b>本机</b> Origin / OriginPro:<br>
9
- 丢给它一个 <code>.dat</code> / <code>.csv</code> / Excel / 电化学工作站 <code>.bin</code>,<br>
10
- 拿回一张发表级图片和一个<b>能在 Origin 里继续编辑</b>的 <code>.opju</code> 工程。
11
- </p>
12
-
13
- <p align="center">
14
- <sub>27 个工具 · <b>1079 例全链路测试全部通过</b> · 数据不出本机 · 支持工作站 <code>.txt</code> / <code>.bin</code></sub>
15
- </p>
16
-
17
- <p align="center">
18
- <a href="#安装"><img alt="platform" src="https://img.shields.io/badge/platform-Windows%20%2B%20Origin%202021%2B-blue"></a>
19
- <img alt="tests" src="https://img.shields.io/badge/tests-1079%2F1079%20passing-brightgreen">
20
- <img alt="license" src="https://img.shields.io/badge/license-MIT-lightgrey">
21
- </p>
22
-
23
- <p align="center">
24
- <img src="docs/preview.png" width="640" alt="两条曲线、轴标题齐全的 Origin 成图">
25
- </p>
26
-
27
- > 上图由 `scripts/make_preview.py` 生成:导入 → 画两条曲线 → 配色与符号 → 关掉图例 → 导出 PNG,全部走本插件自己的引擎调用,轴标题与图例可见性都读回校验后才导出。数据是 `sample.dat` 的合成序列,不含任何真实实验数据。
28
-
29
- ---
30
-
31
- ## 为什么是它
32
-
33
- 市面上把 Origin 接进聊天的尝试,要么要求把数据上传到云端,要么是个 200 行的玩具。Origin Bridge 反着来:
34
-
35
- - **数据不出本机。** 没有 API key、没有云端组件,dsh 拉起一个本地 Python 进程,通过 OriginLab 官方的 [`originpro`](https://pypi.org/project/originpro/) 驱动你机器上的 Origin。
36
- - **每一步都有证据。** Origin 的 X-Function 普遍"返回成功但什么都没发生"。这里画图后读回轴范围与数据跨度比对,导出后校验文件存在/体积/文件头魔数,样式逐项读回——读不回的进 `failed` 列表,不会假装成功。
37
- - **不是 74 个工具,是 27 个。** 每个都有实测用途,工具描述里写着实测限制,模型不会踩坑。
38
- - **1000+ 次真实测试,不是自称。** **1079 例**全链路用例跑在真实 stdio 服务器 + 本机 OriginPro 上,**1079 通过 / 0 失败**:27 个工具每个至少 15 例、最多 134 例,按副作用分五层(L0 纯解析 273、L1 只读 COM 61、L2 改会话 512、L3 落盘 155、L4 进程级 78)。提示词全部按人话写,跑之前先用在线 `tools/list` schema 校验用例本身,所以失败只会是产品问题,不会是题目写错。另有 **214 项**离线/单元与边界检查(asciiio 24、binio 30、lifecycle 37、smoke 97、edge 26)**随包发布,你可以直接复跑**;那 1079 例题库含提示词与模拟数据,是开发侧资产,不随包分发。originpro 的十几个文档陷阱全部记录在案并绕开。
39
-
40
- ## 它能做什么
41
-
42
- | 你说 | 它做 |
43
- |---|---|
44
- | "把 `D:\data\spectrum.dat` 画成散点连线,X 轴 Time (s),导出到 `D:\out`" | `origin_figure`:导入 → 画图 → 轴标题 → PNG + .opju |
45
- | "这两个文件画在一张图上,双 Y 轴" | `origin_layer`(右 Y 轴)+ `origin_plot`(往指定层画) |
46
- | "给这条谱图加个图例,放右上角" | `origin_legend`(文字/位置/可见性全部读回) |
47
- | "这组 EIS 数据拟合一下" | `origin_fit`(gauss / lorentz / voigt / expdec1… 预设,参数可固定) |
48
- | "这是 CHI 工作站导出的 cv.bin" | `origin_import` 直接读二进制,电势轴按头部方法参数重建 |
49
- | "这是工作站导出的 txt,前面一大段参数" | `origin_import` 自动跳过参数头,紧贴数据的那行(`Potential/V, Current/A`)当表头,单位拆到列的 Units 里,参数进 `metadata` |
50
- | "做一张 XPS 图" | `origin_chart_render`(40 个科研预设:轴标题/图类型/建议拟合) |
51
- | "热力图显示这个矩阵" | `origin_matrix`(heat_map / cmap / mesh / contline,Origin 自带图模) |
52
-
53
- 数据源支持:`.dat` `.csv` `.txt` `.tsv`(自动嗅探分隔符、GBK、BOM、多行表头、Fortran 指数、NA/-)、`.xls` `.xlsx`、电化学工作站(CH Instruments 等)导出的 `.txt`(几十行仪器说明自动收进 `metadata`,不会把首行日期当成列名),以及头部为 `80 F2 1B 00` 的电化学工作站二进制 `.bin`。
54
-
55
- ## 安装
56
-
57
- 要求:Windows、Origin/OriginPro **2021 或更高**(`originpro` 的硬性下限,实测 2018 不可用)、Python 3.10+。
58
-
59
- ### 方式 A:用 `dsh plugin` 从 npm 装(推荐)
60
-
61
- ```bash
62
- dsh plugin --profile desktop add dsh-origin-bridge
63
- ```
64
-
65
- 两个包名指向同一个包,装哪个都行:
66
-
67
- ```bash
68
- dsh plugin --profile desktop add dsh-origin-bridge # 正式包
69
- dsh plugin --profile desktop add @caob23/dsh-origin-bridge # 转发壳,依赖上面那个
70
- ```
71
-
72
- 本包是 bundle 包(`package.json` 的 `dsh.bundle.patch` 指向根目录的 `cordis.patch.yml`),`dsh plugin` 装完会自动把它加进 profile 的 `dsh.profile.bundles`,**重启 dsh 即加载**,不用再手写补丁。`dsh plugin` 转发给 pnpm,所以 pnpm 要在 PATH 上。
73
-
74
- **npm 只管分发,不管你的 Python 环境**——Origin 依赖必须自己装进 dsh 实际使用的那个解释器:
75
-
76
- ```powershell
77
- pip install originpro numpy # 国内网络慢就加 -i https://pypi.tuna.tsinghua.edu.cn/simple
78
- ```
79
-
80
- 包内补丁的默认取值是 `python`(PATH 上那个)和 `profiles/desktop`。依赖装在 venv 里、或者你用的是别的 profile,就设这两个用户环境变量(设完要重开终端、再彻底重启 dsh):
81
-
82
- ```powershell
83
- setx ORIGIN_BRIDGE_PYTHON "C:\path\to\.venv\Scripts\python.exe"
84
- setx ORIGIN_BRIDGE_HOME "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-origin-bridge"
85
- ```
86
-
87
- 装完自检(路径按你的 profile 名改):
88
-
89
- ```powershell
90
- Test-Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-origin-bridge\server.py"
91
- ```
92
-
93
- 彻底退出 dsh 再打开,新会话里说"列出 origin 开头的工具",应看到 27 个 `mcp__origin_bridge__origin_*`。卸载:
94
-
95
- ```bash
96
- dsh plugin --profile desktop remove dsh-origin-bridge
97
- ```
98
-
99
- > `failOnStartupError: false` 是刻意的:Origin 没开或有模态框卡住 COM 时,只让工具这一轮不注册,不把 dsh 一起拖崩。
100
-
101
- ### 方式 B:本地 clone(要跑测试或改代码时)
102
-
103
- ```powershell
104
- cd C:\path\to\dsh-origin-bridge
105
- python -m venv .venv
106
- .\.venv\Scripts\Activate.ps1
107
- pip install -r requirements.txt # 国内网络慢就加 -i https://pypi.tuna.tsinghua.edu.cn/simple
108
- ```
109
-
110
- 先确认能独立跑通(会自己拉起 Origin):
111
-
112
- ```powershell
113
- .\.venv\Scripts\python.exe smoke_test.py # 97 项,看到 ALL GOOD 才算过
114
- .\.venv\Scripts\python.exe edge_checks.py # 26 项边界
115
- .\.venv\Scripts\python.exe test_asciiio.py # 24 项,不需要 Origin
116
- .\.venv\Scripts\python.exe test_binio.py # 30 项,不需要 Origin;有真实 .bin 时会跟厂商 .txt 逐点对账
117
- .\.venv\Scripts\python.exe test_lifecycle.py # 37 项,不需要 Origin:实例分类/回收/掉线重连的安全边界
118
- ```
119
-
120
- 上面这 214 项随包发布,你可以自己复跑。另外那 **1079 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
121
-
122
- ### 不是 dsh,是别的 MCP 客户端
123
-
124
- npm 装完会生成 `origin-bridge` 这个可执行入口(`bin/origin-bridge.mjs`),stdio 直连,配置里把 `command` 指向它或指向 `node_modules/dsh-origin-bridge/bin/origin-bridge.mjs`,`serverName` 保持 `origin_bridge` 即可,工具名仍是 `mcp__origin_bridge__origin_*`。
125
-
126
- ## 挂进 dsh(只针对方式 B)
127
-
128
- 走方式 A 的话这一段可以整个跳过——`dsh plugin` 已经替你写好了。下面是本地 clone 时的两种接法。
129
-
130
- 自动(推荐,会先备份再写入):
131
-
132
- ```powershell
133
- powershell -ExecutionPolicy Bypass -File .\install_dsh.ps1 -Profile desktop
134
- ```
135
-
136
- 手动:编辑 `C:\Users\<你>\.dsh\profiles\desktop\cordis.patch.yml`(**先备份**),在文件**末尾**追加:
137
-
138
- ```yaml
139
- - insert:
140
- - id: mcp-origin-bridge
141
- name: '@deepseek-ai/dsh-mcp-client'
142
- config:
143
- serverName: origin_bridge
144
- transport: stdio
145
- command: 'C:\path\to\dsh-origin-bridge\.venv\Scripts\python.exe'
146
- args: ['-u', '-X', 'utf8', 'C:\path\to\dsh-origin-bridge\server.py']
147
- env:
148
- PYTHONIOENCODING: utf-8
149
- PYTHONUNBUFFERED: '1'
150
- failOnStartupError: false
151
- toolCallTimeoutMs: 180000
152
- ```
153
-
154
- 然后**彻底退出 dsh 再打开**(配置只在启动时读一次)。新会话里说"列出 origin 开头的工具"应能看到 27 个 `mcp__origin_bridge__origin_*`。
155
-
156
- > 三个必须对的点,都是踩过的坑:
157
- > 1. **必须以 `- insert:` 开头。** 直接写 `- id: 新名字` 是"覆盖下层已存在条目"的语法,对全新 id 会被 dsh **静默忽略**——症状就是改了配置、重启了、什么都没发生。
158
- > 2. `name:` 那行必须是 `@deepseek-ai/dsh-mcp-client`(dsh 的 app 包里自带这个 loader),少了它条目不会被任何加载器接管。
159
- > 3. `serverName` 只能是标识符:工具名是 `mcp__<serverName>__<tool>`,写成 `origin-plot` 这种带连字符的名字会得到没法寻址的工具。
160
- >
161
- > `failOnStartupError: false` 别省:Origin 没装或卡在弹窗时,它保证 dsh 照常启动,只是那一次不注册工具。
162
-
163
- 不想用 `install_dsh.ps1` 也不用 npm 的话,可以把整个目录拷进 `<DSH_HOME>\profiles\<profile>\node_modules\dsh-origin-bridge\`,仓库自带的 `cordis.patch.yml` 就是 bundle 层补丁——但这只是方式 A 的手动等价物,路径与解释器仍靠 `ORIGIN_BRIDGE_HOME` / `ORIGIN_BRIDGE_PYTHON` 指定,正常情况请直接走方式 A。卡片图标与显示名来自 `icon.svg` 和 `locale/en.json`、`locale/zh.json`。
164
-
165
- ## 工具(27 个)
166
-
167
- | 工具 | 用途 |
168
- |---|---|
169
- | `origin_status` | 连接检查、Origin 版本、已开页面。**动手前先调它** |
170
- | `origin_read_file` | 只解析文件不碰 Origin:编码、分隔符、表头行、每列长名/单位/有效行数;`.bin` 走另一条路线给出技术/点数/方法参数/轴来源 |
171
- | `origin_import` | 导入 `.dat/.csv/.txt/.tsv/.xls/.xlsx/.wks` 和工作站二进制 `.bin`,返回 `ws-N` 句柄和列清单 |
172
- | `origin_write` | 内联数据建表:窄表 `columns={列名:[值…]}`,宽表 `headers`+`rows`(一次写完) |
173
- | `origin_formula` | 给列设计算公式(Fx)做派生列,读回算出的行数与首末值 |
174
- | `origin_plot` | 单条或**多条**曲线;`template` 走 Origin 自带图模;`graph`+`layer` 往指定层画 |
175
- | `origin_layer` | 加层(右 Y 轴、上下多面板),返回 Origin 给这层起的名字作为证据 |
176
- | `origin_legend` | 图例:按列长名自动生成或写死,12 个位置,文字/可见性/坐标都读回 |
177
- | `origin_style` | 轴标题、**对数轴**、范围与步长、逐曲线颜色/线宽/符号形状/大小/填充/透明度,**每项读回验证** |
178
- | `origin_annotate` | 图上文字标注与参考线(数据坐标) |
179
- | `origin_matrix` | 网格数据 → 矩阵 → **热力图 / 等高线 / 3D 彩色映射面**(模板驱动) |
180
- | `origin_fit` | `linear`(标准版可用);预设 `gauss/lorentz/voigt/expdec1/expdec2/sine/power/logistic/boltzmann/doseresp/cubic`;或 `nlfitsing`+`func` |
181
- | `origin_chart_presets` | 40 个科研图表预设(XPS/XRD/UV-Vis/荧光/循环伏安/Arrhenius…) |
182
- | `origin_chart_preset` | 单个预设的完整约定:要哪些列、默认轴标题、建议拟合、轴向提示 |
183
- | `origin_chart_check` | 拿数据列名对预设做 dry-run,不碰 Origin |
184
- | `origin_chart_render` | 按预设一步出图(轴标题/图类型/图例/图片/opju) |
185
- | `origin_export` | 出图,带文件存在/体积/文件头校验,PNG 还回读宽高 |
186
- | `origin_view` | 把图渲染成小图以 **MCP image content 回传给模型自己看** |
187
- | `origin_save_project` | 存 `.opju`(可编辑工程,不是截图) |
188
- | `origin_open` | 打开已有工程 |
189
- | `origin_inspect` | 读回页面/图层/曲线数与已分配句柄——**改图前先看它,别猜索引** |
190
- | `origin_close_pages` | 清理攒下来的页面(长会话里这是"无效指针"的根因),支持 dry_run |
191
- | `origin_labtalk` | 逃生舱:原始 LabTalk + 强制读回 |
192
- | `origin_exit` | 关 Origin,可选先存盘 |
193
- | `origin_instances` | 清点机器上的 Origin 实例:后台无窗口(客户端退出后的残留)vs 前台有窗口(可能是用户自己的项目),并标出哪些是本进程起的 |
194
- | `origin_reclaim` | 回收残留实例:**只关没有窗口的**;有窗口的一律不动,只在返回里提示用户自己关。`close_background:false` 只看不动手 |
195
- | `origin_figure` | 主路径:导入→画图→轴标题→图例→图片+opju 一次完成 |
196
-
197
- ## 工作站 `.bin` 是怎么回事
198
-
199
- 那种头部以 `80 F2 1B 00` 开头、写着 `Cyclic Voltammetry` / `A.C. Impedance` / `Amperometric i-t Curve` 的文件(CHI660E 一类工作站导出),尾部是**一个** float32 数组 = 测量通道(电流 A),**电势轴不在文件里**。
200
-
201
- Origin Bridge 的做法:
202
-
203
- 1. 从头部"数据起点前 600 字节"的参数块读 Init/High/Low E、步长、段数;
204
- 2. 按"正向起步的三角波"重建电势轴,**重建点数必须与文件点数完全相等**才采信,否则退回 `axis=none` 并说明原因,绝不拿错的 X 轴画图;
205
- 3. i-t / CA / CP 的时间轴直接由采样间隔展开;EIS 的频率表是另一套参数,明说拿不到,不猜。
206
-
207
- 这条重建规则对着厂商自己导出的 `.txt` 逐点核过:5 个文件、每个 600 点,电势和电流 **0 处不符**。
208
-
209
- ## 设计取舍
210
-
211
- **为什么不用 `mcp` SDK 的 stdio 传输。** SDK 建的是 asyncio `ProactorEventLoop`,其 self-pipe 在 Windows 上回落到 `socketpair()`(127.0.0.1 listen+connect+accept)。防火墙拦回环 accept 的机器上这一步永久阻塞 → initialize 永不返回 → 客户端等满 60 秒超时。`server.py` 用同步换行分隔 JSON-RPC,握手毫秒级,零事件循环依赖。
212
-
213
- **为什么返回句柄而不是 Origin 引用字符串。** `[Book1]Sheet1!` 这类引用对大小写、空格、活动页状态都敏感,写错了不报错只静默失败。引擎内部持有真实对象,把 `ws-1` / `gr-2` 交给模型原样传回。
214
-
215
- **为什么进程探测失败要报 `probe: "unavailable"` 而不是空列表。** `origin_instances` / `origin_reclaim` 靠读进程表区分"没人看的后台实例"和"用户正开着的窗口"。如果 powershell 不可用或超时就让它们返回空集合,调用方读到的是"没有残留、没有前台窗口"——一个假的清白结论。宁可说"我没看清、这次什么都没做",也不能把"未知"报成"没事"。
216
-
217
- **为什么连接时不自动回收既存的无窗口实例。** 被 dsh 驱动的那个 Origin **本身就是无窗口的**:可见性只说明"没有人在屏幕前看它",不说明"没有客户端在用它"。开机时顺手杀掉所有无窗口实例,会直接砍掉用户当前会话正在跑的任务。所以只有显式 `origin_reclaim` 才动手,且只动无窗口的;有窗口的一律留给用户自己关。
218
-
219
- **两条画图路径,各自实测过。** 单 x/单 y 走 LabTalk `plotxy`——那是 Origin 工具栏自己的路径,主题、配色、自动缩放都对。多条曲线 / 指定层 / 指定模板走 `layer.add_plot()` + `layer.rescale()`:`add_plot` 不 rescale 就是 0..10 空轴,但 rescale 之后颜色/符号/线宽全部正确,且**只有它能精确投到某一层**——`plotxy` 给限定范围时返回 True 却画到别的页/层上去了。
220
-
221
- **为什么每次写入都读回。** `plot()` 读回轴范围和数据跨度比对,不符就报 `plot_axis_degenerate`;`origin_export` 校验文件存在、体积、文件头魔数;`style()` / `legend()` 每项读回,读不进的进 `failed`。
222
-
223
- **缺失值消毒。** Origin 用 `-1.23456789e-300` 表示 missing,直接吐给模型会被当成真实拟合参数。`sanitize()` 统一转成 `null`。
224
-
225
- ## originpro 1.1.15 / OriginPro 2024 实测陷阱(维护者必读)
226
-
227
- 这几条都是文档和实际行为不一致,靠读 API 文档写会静默出错:
228
-
229
- | 陷阱 | 实际行为 | 处置 |
230
- |---|---|---|
231
- | `lay.add_plot(...)` | 返回 Plot 对象、不报错,但轴停在 0..10 的**空图** | 只在 `rescale()` 之后使用,并用轴范围读回反证 |
232
- | `plotxy` 的 `plot:=` 代码 | 200 线、201 散点、202 线+符号、203 柱(**不是**常见的 201=line) | `PLOT_CODES` 常量,逐个导图目视确认 |
233
- | `plotxy` 往指定层画 | 给限定范围返回 True,曲线却不在目标层(甚至多出一层重叠轴) | 指定层一律用 `add_plot` + `rescale` |
234
- | `add_layer(t)` | t=0 BottomXLeftY、1 TopX、**2 RightY**、3 LeftY、4 TopXRightY、5 BottomXRightY、6 Inset | `LAYER_TYPES` 名字表 + 读回 `layer.name` 当证据 |
235
- | `plot.set_cmd("w=2.5")` | 设线宽**静默无效**(`get_float('line.width')` 仍 0.5) | 用 `set_float('line.width', v)` 并读回比对 |
236
- | `legend` X-Function | `legend 1;` 把图例写成 "111";`legend -s Graph1` 写成 "Graph1Graph1Graph1";`legend -p br` 不挪图例反而多出写着 "br" 的文本框 | `legend -r;` 自动生成,文字走 `layer.label('Legend').text`,位置写 `Legend.x/Legend.y`(该层**数据坐标**,锚点是图例中心) |
237
- | `obj.is_open` | 含义是"没被最小化/隐藏",**不是**"页面还存在" | `close_pages` 用关闭后重新枚举页面名单作为证据 |
238
- | `op.pages()` | 是**生成器**:迭代第二次静默返回空 | 一律 `list(...)` 落地 |
239
- | `WSheet.set_formula(col, …)` | 文档写 1-offset,实际 **0-based**;传 `idx+1` 在 `SetStrProp` 上抛 NoneType | 传 0-based `idx`,列不够先扩列 |
240
- | `WSheet.from_list2(data)` | 内层列表是**列**不是行 | `write_block` 内部转置 |
241
- | `Axis.scale` / `lay.xscale` | setter 收 `'log10'`,getter 返回**整数** 2 | `SCALE_TYPES` 映射后按整数比对 |
242
- | `lt_exec('layer.x.title$="…"')` | 返回 **False**;字符串赋值类 LabTalk 走不通 | 轴标题走对象模型 `origin_style` |
243
- | `op.NLFit(name)` | **区分大小写**且无别名:`Gauss` 可用,`gauss1`/`Linear`/`Quadratic`/`FRADA` 直接抛 Invalid fitting function | `FIT_PRESETS` 只收逐个构造成功的名字 |
244
- | `op.path('p')` | 返回工程**所在文件夹**,不是工程文件 | 自己用 `_project_file` 记录绑定 |
245
- | `op.save(file)` | 语义是 Save-As;绑定后再存同名返回 False(不是失败) | 同路径改用无参 `op.save()`,并比对 mtime |
246
- | COM 线程亲和 | 连接必须在**专用 COM 线程**上建立;在 stdio 读线程上连接会跨 apartment 复用对象 | `_resolve()` 只查句柄表不碰 COM;有"图工具作为首个调用"的回归项守着 |
247
- | 已销毁对象的 `.name` | 留下 C 层待处理异常,之后一个不相干的 `len()` 才炸成 `SystemError` | 句柄登记时顺手存页面名,清理句柄表不再回摸 COM |
248
- | 桥坏到一定程度 | `op.path()` 抛 "returned a result with an exception set",此时连 `op.exit()` 都跑不动;自动 `detach()/attach()` 会把可诊断的报错变成**进程直接崩** | 不自愈,抛 `connection_error` 并写明要外部重启 Origin |
249
- | 工作站 `.bin` 的数组布局 | 看着像两列(各 N/2),其实厂商 `.txt` 里那 600 行电流正好等于整个数组——只有测量通道 | `binio.py` 按单通道 + 方法参数重建轴,点数相等做门禁 |
250
-
251
- ## 已知边界
252
-
253
- - `originpro` 要求 Origin **2021+**。实测 OriginPro 2018C:能连、能存空工程,但导入返回 0 行、`add_plot` 直接打死连接器。
254
- - 非线性拟合需要 **OriginPro** 授权;标准版调 `origin_fit(kind='gauss')` 会明确报 `fit_function_unavailable`。
255
- - `.bin` 只覆盖 `80 F2 1B 00` 头的工作站导出;**EIS 的频率表在另一套参数里,所以 `.bin` 的 EIS 只有测量列**。Gamry / Autolab / Pine 等其它厂商二进制没做,也不猜。
256
- - 图例位置是"按数据坐标摆放",对数轴上分数位置会偏;没有"避让数据点"的智能摆放。
257
- - 中文/网盘路径导出历史上不稳,导出建议 ASCII 本地路径。
258
- - Origin 以管理员运行而 Python 不是(或反之)时 COM 连不上,两侧权限要一致。
259
- - 长会话攒太多页面会让 COM 桥报"无效指针"。`origin_close_pages` 能清,根治是 `origin_exit` 后重连。
260
- - **Origin 实例不会跟着客户端退出**:originpro 需要时自己拉一个 Origin,Python 进程结束那个实例还在。攒够就会报"实例超过上限"。现在插件在进程退出时回收**自己起的、且没有窗口的**实例(`atexit`),历史残留用 `origin_reclaim` 清;有窗口的实例永远不动,因为那可能是你正开着的项目。
261
- - 统计批处理(t 检验 / ANOVA / PCA / 生存分析)没有,originpro 本身也不提供。
262
- - **无并发保护**:两个会话同时驱动 Origin 会互相踩。
263
- - 40 个图表预设只取元数据;预设自带的 `render_template()` 会先 `op.new()`——那会把用户正在编辑的工程整个丢掉,所以这里换成自己的渲染路径。
264
-
265
- ## 排障
266
-
267
- | 现象 | 先查 |
268
- |---|---|
269
- | dsh 里看不到工具 | 配置是不是漏了 `- insert:`;`name:` 有没有写成 `@deepseek-ai/dsh-mcp-client`;有没有彻底退出重启 dsh;`python server.py --tools` 能否列出 27 个 |
270
- | `connection_error` | Origin 是否 ≥2021;`pip show originpro`;任务管理器里 `Origin64.exe` 是否只有一个 |
271
- | 报"实例超过上限" | `origin_instances` 看有几个、哪些没窗口;`origin_reclaim` 关残留的后台实例;有窗口的那些要用户自己关(可能有未保存的东西) |
272
- | `import_empty` | 先 `origin_read_file` 看结构;空格/多行表头已由自研解析器处理,走到这个报错说明文件本身没有数值行 |
273
- | `com_timeout` | Origin 窗口是否卡在对话框/许可证弹窗 |
274
- | `无效指针` | 页面攒太多:`origin_close_pages`,或 `origin_exit` 让它重开 |
275
- | 图片没出来 | `origin_inspect` 看 `plots` 是否为 0;换 ASCII 路径 |
276
- | 图是空的但没报错 | 看返回里的 `proof_level`;`plot_axis_degenerate` 会主动抛出 |
277
-
278
- ## 给维护者
279
-
280
- ```
281
- dsh-origin-bridge/
282
- ├── server.py # 同步 stdio JSON-RPC,27 个工具的分发与 schema
283
- ├── originlab.py # 引擎:COM 专用线程、句柄表、每个操作及其读回验证
284
- ├── asciiio.py # 健壮的 ASCII 读取(分隔符嗅探 / GBK / 多行表头)
285
- ├── binio.py # 工作站 .bin:单测量通道 + 方法参数重建电势轴
286
- ├── templates_catalog.py # 40 个科研图表预设(纯元数据)
287
- ├── cordis.patch.yml # dsh bundle 补丁(serverName: origin_bridge)
288
- ├── install_dsh.ps1 # 一键装入 dsh(备份 + 写 profile 层 + 自检)
289
- ├── icon.svg # 卡片图标(scripts/make_icon.py 生成)
290
- ├── locale/*.json # dsh 卡片显示名与描述
291
- ├── scripts/make_preview.py # 重新生成 docs/preview.png(关掉图例后才导出)
292
- └── test_*.py / smoke_test.py / edge_checks.py # 214 项离线与单元检查(随包发布)
293
- ```
294
-
295
- 开发侧另有 `cases/` + `run_cases.py` 的 1079 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
296
-
297
- 重新生成图标:`python scripts/make_icon.py`(只需要 numpy,PNG 由 stdlib 手写)。
298
-
299
- ## 商标声明与许可
300
-
301
- 本项目是独立的第三方工具,与 OriginLab Corporation **无关联、无合作、未获认可**。Origin、OriginPro 为 OriginLab Corporation 的商标;本工具通过 OriginLab 官方公开发布的 `originpro` Python 包以正常方式使用其产品,未分发其任何二进制、图标或文档。
302
-
303
- MIT © Origin Bridge Contributors
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
+ 用自然语言驱动<b>本机</b> Origin / OriginPro:<br>
9
+ 丢给它一个 <code>.dat</code> / <code>.csv</code> / Excel / 电化学工作站 <code>.bin</code>,<br>
10
+ 拿回一张发表级图片和一个<b>能在 Origin 里继续编辑</b>的 <code>.opju</code> 工程。
11
+ </p>
12
+
13
+ <p align="center">
14
+ <sub>27 个工具 · <b>1118 例全链路测试全部通过</b> · 数据不出本机 · 支持工作站 <code>.txt</code> / <code>.bin</code></sub>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <a href="#安装"><img alt="platform" src="https://img.shields.io/badge/platform-Windows%20%2B%20Origin%202021%2B-blue"></a>
19
+ <img alt="tests" src="https://img.shields.io/badge/tests-1118%2F1118%20passing-brightgreen">
20
+ <img alt="license" src="https://img.shields.io/badge/license-MIT-lightgrey">
21
+ </p>
22
+
23
+ <p align="center">
24
+ <img src="docs/preview.png" width="640" alt="两条曲线、轴标题齐全的 Origin 成图">
25
+ </p>
26
+
27
+ > 上图由 `scripts/make_preview.py` 生成:导入 → 画两条曲线 → 配色与符号 → 关掉图例 → 导出 PNG,全部走本插件自己的引擎调用,轴标题与图例可见性都读回校验后才导出。数据是 `sample.dat` 的合成序列,不含任何真实实验数据。
28
+
29
+ ---
30
+
31
+ ## 为什么是它
32
+
33
+ 市面上把 Origin 接进聊天的尝试,要么要求把数据上传到云端,要么是个 200 行的玩具。Origin Bridge 反着来:
34
+
35
+ - **数据不出本机。** 没有 API key、没有云端组件,dsh 拉起一个本地 Python 进程,通过 OriginLab 官方的 [`originpro`](https://pypi.org/project/originpro/) 驱动你机器上的 Origin。
36
+ - **每一步都有证据。** Origin 的 X-Function 普遍"返回成功但什么都没发生"。这里画图后读回轴范围与数据跨度比对,导出后校验文件存在/体积/文件头魔数,样式逐项读回——读不回的进 `failed` 列表,不会假装成功。
37
+ - **不是 74 个工具,是 27 个。** 每个都有实测用途,工具描述里写着实测限制,模型不会踩坑。
38
+ - **1000+ 次真实测试,不是自称。** **1118 例**全链路用例跑在真实 stdio 服务器 + 本机 OriginPro 上,**1118 通过 / 0 失败**:27 个工具每个至少 15 例、最多 134 例,按副作用分五层(L0 纯解析 296、L1 只读 COM 61、L2 改会话 520、L3 落盘 163、L4 进程级 78)。提示词全部按人话写,跑之前先用在线 `tools/list` schema 校验用例本身,所以失败只会是产品问题,不会是题目写错。另有 **352 项**离线/单元/边界检查(asciiio 84、binio 30、lifecycle 37、smoke 129、edge 26、paths 28、json 18)**随包发布,你可以直接复跑**;那 1118 例题库含提示词与模拟数据,是开发侧资产,不随包分发。originpro 的十几个文档陷阱全部记录在案并绕开。
39
+
40
+ ## 它能做什么
41
+
42
+ | 你说 | 它做 |
43
+ |---|---|
44
+ | "把 `D:\data\spectrum.dat` 画成散点连线,X 轴 Time (s),导出到 `D:\out`" | `origin_figure`:导入 → 画图 → 轴标题 → PNG + .opju |
45
+ | "这两个文件画在一张图上,双 Y 轴" | `origin_layer`(右 Y 轴)+ `origin_plot`(往指定层画) |
46
+ | "给这条谱图加个图例,放右上角" | `origin_legend`(文字/位置/可见性全部读回) |
47
+ | "这组 EIS 数据拟合一下" | `origin_fit`(gauss / lorentz / voigt / expdec1… 预设,参数可固定) |
48
+ | "这是 CHI 工作站导出的 cv.bin" | `origin_import` 直接读二进制,电势轴按头部方法参数重建 |
49
+ | "这是工作站导出的 txt,前面一大段参数" | `origin_import` 自动跳过参数头,紧贴数据的那行(`Potential/V, Current/A`)当表头,单位拆到列的 Units 里,参数进 `metadata` |
50
+ | "做一张 XPS 图" | `origin_chart_render`(40 个科研预设:轴标题/图类型/建议拟合) |
51
+ | "热力图显示这个矩阵" | `origin_matrix`(heat_map / cmap / mesh / contline,Origin 自带图模) |
52
+
53
+ 数据源支持:`.dat` `.csv` `.txt` `.tsv`(自动嗅探分隔符;编码覆盖 UTF-8 / GBK / **UTF-16(有无 BOM 都行)** / cp1252;多行表头、Fortran 指数、`NA`/`-`;还有 `<0.001` 检出限、`2.5±0.1`、`25 mV`、`1,234.5`、`(500)`、`2.5*` 这类被装饰过的数值——每解一种都会在 `warnings` 里写清代入了什么)、`.xls` `.xlsx`、电化学工作站(CH Instruments 等)导出的 `.txt`(几十行仪器说明自动收进 `metadata`,不会把首行日期当成列名),以及头部为 `80 F2 1B 00` 的电化学工作站二进制 `.bin`。
54
+
55
+ ## 安装
56
+
57
+ 要求:Windows、Origin/OriginPro **2021 或更高**(`originpro` 的硬性下限,实测 2018 不可用)、Python 3.10+。
58
+
59
+ ### 方式 A:用 `dsh plugin` 从 npm 装(推荐)
60
+
61
+ ```bash
62
+ dsh plugin --profile desktop add dsh-origin-bridge
63
+ ```
64
+
65
+ > 只多装 **1 个包**(隔离 profile 实测 pnpm `Packages: +1`,620ms)。宿主自带的 `@deepseek-ai/dsh-mcp-client` 声明成**可选 peer**,不会在插件目录里再塞一份 loader 副本。
66
+
67
+ > 装的时候报 `ERR_PNPM_NO_MATCHING_VERSION`、提示 "latest release is 0.4.x":那是**镜像滞后**,不是包没发出去。dsh 安装完全沿用你本机的 npm 配置(`npm config get registry` 看一眼),实测 0.4.5 发布 2.5 小时后 `registry.npmmirror.com` 上仍只有 0.4.4。两种解法都验证过:
68
+ > ```bash
69
+ > # 1) 显式走官方源(这个参数会透传给 pnpm;24 小时新鲜度门禁会自己写进 minimumReleaseAgeExclude)
70
+ > dsh plugin --profile desktop add dsh-origin-bridge --registry=https://registry.npmjs.org/
71
+ > # 2) 离线包,完全不经过 registry
72
+ > dsh plugin --profile desktop add file:C:\path\to\dsh-origin-bridge
73
+ > ```
74
+
75
+ 两个包名指向同一个包,装哪个都行:
76
+
77
+ ```bash
78
+ dsh plugin --profile desktop add dsh-origin-bridge # 正式包
79
+ dsh plugin --profile desktop add @caob23/dsh-origin-bridge # 转发壳,依赖上面那个
80
+ ```
81
+
82
+ 本包是 bundle 包(`package.json` 的 `dsh.bundle.patch` 指向根目录的 `cordis.patch.yml`),`dsh plugin` 装完会自动把它加进 profile 的 `dsh.profile.bundles`,**重启 dsh 即加载**,不用再手写补丁。`dsh plugin` 转发给 pnpm,所以 pnpm 要在 PATH 上。
83
+
84
+ **npm 只管分发,不管你的 Python 环境**——Origin 依赖必须自己装进 dsh 实际使用的那个解释器:
85
+
86
+ ```powershell
87
+ pip install originpro numpy # 国内网络慢就加 -i https://pypi.tuna.tsinghua.edu.cn/simple
88
+ ```
89
+
90
+ 包内补丁的默认取值是 `python`(PATH 上那个)和 `profiles/desktop`。依赖装在 venv 里、或者你用的是别的 profile,就设这两个用户环境变量(设完要重开终端、再彻底重启 dsh):
91
+
92
+ ```powershell
93
+ setx ORIGIN_BRIDGE_PYTHON "C:\path\to\.venv\Scripts\python.exe"
94
+ setx ORIGIN_BRIDGE_HOME "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-origin-bridge"
95
+ ```
96
+
97
+ 装完自检(路径按你的 profile 名改):
98
+
99
+ ```powershell
100
+ Test-Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-origin-bridge\server.py"
101
+ ```
102
+
103
+ 彻底退出 dsh 再打开,新会话里说"列出 origin 开头的工具",应看到 27 个 `mcp__origin_bridge__origin_*`。卸载:
104
+
105
+ ```bash
106
+ dsh plugin --profile desktop remove dsh-origin-bridge
107
+ ```
108
+
109
+ > `failOnStartupError: false` 是刻意的:Origin 没开或有模态框卡住 COM 时,只让工具这一轮不注册,不把 dsh 一起拖崩。
110
+
111
+ ### 方式 B:本地 clone(要跑测试或改代码时)
112
+
113
+ ```powershell
114
+ cd C:\path\to\dsh-origin-bridge
115
+ python -m venv .venv
116
+ .\.venv\Scripts\Activate.ps1
117
+ pip install -r requirements.txt # 国内网络慢就加 -i https://pypi.tuna.tsinghua.edu.cn/simple
118
+ ```
119
+
120
+ 先确认能独立跑通(会自己拉起 Origin):
121
+
122
+ ```powershell
123
+ .\.venv\Scripts\python.exe smoke_test.py # 129 项,看到 ALL GOOD 才算过
124
+ .\.venv\Scripts\python.exe edge_checks.py # 26 项边界
125
+ .\.venv\Scripts\python.exe test_asciiio.py # 84 项,不需要 Origin
126
+ .\.venv\Scripts\python.exe test_binio.py # 30 项,不需要 Origin;有真实 .bin 时会跟厂商 .txt 逐点对账
127
+ .\.venv\Scripts\python.exe test_lifecycle.py # 37 项,不需要 Origin:实例分类/回收/掉线重连的安全边界
128
+ .\.venv\Scripts\python.exe test_paths.py # 28 项,不需要 Origin:导出/保存目标路径的规范化与拒绝
129
+ .\.venv\Scripts\python.exe test_json_safety.py # 18 项,不需要 Origin:返回值必须是严格 JSON
130
+ ```
131
+
132
+ 上面这 352 项随包发布,你可以自己复跑。另外那 **1118 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
133
+
134
+ ### 不是 dsh,是别的 MCP 客户端
135
+
136
+ npm 装完会生成 `origin-bridge` 这个可执行入口(`bin/origin-bridge.mjs`),stdio 直连,配置里把 `command` 指向它或指向 `node_modules/dsh-origin-bridge/bin/origin-bridge.mjs`,`serverName` 保持 `origin_bridge` 即可,工具名仍是 `mcp__origin_bridge__origin_*`。
137
+
138
+ ## 挂进 dsh(只针对方式 B)
139
+
140
+ 走方式 A 的话这一段可以整个跳过——`dsh plugin` 已经替你写好了。下面是本地 clone 时的两种接法。
141
+
142
+ 自动(推荐,会先备份再写入):
143
+
144
+ ```powershell
145
+ powershell -ExecutionPolicy Bypass -File .\install_dsh.ps1 -Profile desktop
146
+ ```
147
+
148
+ 手动:编辑 `C:\Users\<你>\.dsh\profiles\desktop\cordis.patch.yml`(**先备份**),在文件**末尾**追加:
149
+
150
+ ```yaml
151
+ - insert:
152
+ - id: mcp-origin-bridge
153
+ name: '@deepseek-ai/dsh-mcp-client'
154
+ config:
155
+ serverName: origin_bridge
156
+ transport: stdio
157
+ command: 'C:\path\to\dsh-origin-bridge\.venv\Scripts\python.exe'
158
+ args: ['-u', '-X', 'utf8', 'C:\path\to\dsh-origin-bridge\server.py']
159
+ env:
160
+ PYTHONIOENCODING: utf-8
161
+ PYTHONUNBUFFERED: '1'
162
+ failOnStartupError: false
163
+ toolCallTimeoutMs: 180000
164
+ ```
165
+
166
+ 然后**彻底退出 dsh 再打开**(配置只在启动时读一次)。新会话里说"列出 origin 开头的工具"应能看到 27 个 `mcp__origin_bridge__origin_*`。
167
+
168
+ > 三个必须对的点,都是踩过的坑:
169
+ > 1. **必须以 `- insert:` 开头。** 直接写 `- id: 新名字` 是"覆盖下层已存在条目"的语法,对全新 id 会被 dsh **静默忽略**——症状就是改了配置、重启了、什么都没发生。
170
+ > 2. `name:` 那行必须是 `@deepseek-ai/dsh-mcp-client`(dsh 的 app 包里自带这个 loader),少了它条目不会被任何加载器接管。
171
+ > 3. `serverName` 只能是标识符:工具名是 `mcp__<serverName>__<tool>`,写成 `origin-plot` 这种带连字符的名字会得到没法寻址的工具。
172
+ >
173
+ > `failOnStartupError: false` 别省:Origin 没装或卡在弹窗时,它保证 dsh 照常启动,只是那一次不注册工具。
174
+
175
+ 不想用 `install_dsh.ps1` 也不用 npm 的话,可以把整个目录拷进 `<DSH_HOME>\profiles\<profile>\node_modules\dsh-origin-bridge\`,仓库自带的 `cordis.patch.yml` 就是 bundle 层补丁——但这只是方式 A 的手动等价物,路径与解释器仍靠 `ORIGIN_BRIDGE_HOME` / `ORIGIN_BRIDGE_PYTHON` 指定,正常情况请直接走方式 A。卡片图标与显示名来自 `icon.svg` 和 `locale/en.json`、`locale/zh.json`。
176
+
177
+ ## 工具(27 个)
178
+
179
+ | 工具 | 用途 |
180
+ |---|---|
181
+ | `origin_status` | 连接检查、Origin 版本、已开页面。**动手前先调它** |
182
+ | `origin_read_file` | 只解析文件不碰 Origin:编码、分隔符、表头行、每列长名/单位/有效行数;`.bin` 走另一条路线给出技术/点数/方法参数/轴来源 |
183
+ | `origin_import` | 导入 `.dat/.csv/.txt/.tsv/.xls/.xlsx/.wks` 和工作站二进制 `.bin`,返回 `ws-N` 句柄和列清单 |
184
+ | `origin_write` | 内联数据建表:窄表 `columns={列名:[值…]}`,宽表 `headers`+`rows`(一次写完) |
185
+ | `origin_formula` | 给列设计算公式(Fx)做派生列,读回算出的行数与首末值 |
186
+ | `origin_plot` | 单条或**多条**曲线;`template` 走 Origin 自带图模;`graph`+`layer` 往指定层画 |
187
+ | `origin_layer` | 加层(右 Y 轴、上下多面板),返回 Origin 给这层起的名字作为证据 |
188
+ | `origin_legend` | 图例:按列长名自动生成或写死,13 个位置,文字/可见性/坐标都读回 |
189
+ | `origin_style` | 轴标题、**对数轴**、范围与步长、逐曲线颜色/线宽/符号形状/大小/填充/透明度,**每项读回验证** |
190
+ | `origin_annotate` | 图上文字标注与参考线(数据坐标) |
191
+ | `origin_matrix` | 网格数据 → 矩阵 → **热力图 / 等高线 / 3D 彩色映射面**(模板驱动) |
192
+ | `origin_fit` | `linear`(标准版可用);预设 `gauss/lorentz/voigt/expdec1/expdec2/sine/power/logistic/boltzmann/doseresp/cubic`;或 `nlfitsing`+`func` |
193
+ | `origin_chart_presets` | 40 个科研图表预设(XPS/XRD/UV-Vis/荧光/循环伏安/Arrhenius…) |
194
+ | `origin_chart_preset` | 单个预设的完整约定:要哪些列、默认轴标题、建议拟合、轴向提示 |
195
+ | `origin_chart_check` | 拿数据列名对预设做 dry-run,不碰 Origin |
196
+ | `origin_chart_render` | 按预设一步出图(轴标题/图类型/图例/图片/opju) |
197
+ | `origin_export` | 出图,带文件存在/体积/文件头校验,PNG 还回读宽高 |
198
+ | `origin_view` | 把图渲染成小图以 **MCP image content 回传给模型自己看** |
199
+ | `origin_save_project` | 存 `.opju`(可编辑工程,不是截图) |
200
+ | `origin_open` | 打开已有工程 |
201
+ | `origin_inspect` | 读回页面/图层/曲线数与已分配句柄——**改图前先看它,别猜索引** |
202
+ | `origin_close_pages` | 清理攒下来的页面(长会话里这是"无效指针"的根因),支持 dry_run |
203
+ | `origin_labtalk` | 逃生舱:原始 LabTalk + 强制读回 |
204
+ | `origin_exit` | 关 Origin,可选先存盘 |
205
+ | `origin_instances` | 清点机器上的 Origin 实例:后台无窗口(客户端退出后的残留)vs 前台有窗口(可能是用户自己的项目),并标出哪些是本进程起的 |
206
+ | `origin_reclaim` | 回收残留实例:**只关没有窗口的**;有窗口的一律不动,只在返回里提示用户自己关。`close_background:false` 只看不动手 |
207
+ | `origin_figure` | 主路径:导入→画图→轴标题→图例→图片+opju 一次完成 |
208
+
209
+ ## 工作站 `.bin` 是怎么回事
210
+
211
+ 那种头部以 `80 F2 1B 00` 开头、写着 `Cyclic Voltammetry` / `A.C. Impedance` / `Amperometric i-t Curve` 的文件(CHI660E 一类工作站导出),尾部是**一个** float32 数组 = 测量通道(电流 A),**电势轴不在文件里**。
212
+
213
+ Origin Bridge 的做法:
214
+
215
+ 1. 从头部"数据起点前 600 字节"的参数块读 Init/High/Low E、步长、段数;
216
+ 2. 按"正向起步的三角波"重建电势轴,**重建点数必须与文件点数完全相等**才采信,否则退回 `axis=none` 并说明原因,绝不拿错的 X 轴画图;
217
+ 3. i-t / CA / CP 的时间轴直接由采样间隔展开;EIS 的频率表是另一套参数,明说拿不到,不猜。
218
+
219
+ 这条重建规则对着厂商自己导出的 `.txt` 逐点核过:5 个文件、每个 600 点,电势和电流 **0 处不符**。
220
+
221
+ ## 设计取舍
222
+
223
+ **为什么不用 `mcp` SDK 的 stdio 传输。** SDK 建的是 asyncio `ProactorEventLoop`,其 self-pipe 在 Windows 上回落到 `socketpair()`(127.0.0.1 listen+connect+accept)。防火墙拦回环 accept 的机器上这一步永久阻塞 → initialize 永不返回 → 客户端等满 60 秒超时。`server.py` 用同步换行分隔 JSON-RPC,握手毫秒级,零事件循环依赖。
224
+
225
+ **宿主注入的包走 peerDependencies,不走 dependencies。** 以前 `@deepseek-ai/dsh-mcp-client` 写在 `dependencies` 里,`>=0.1.0-rc.1` 这个范围在 pnpm/npm 的默认语义下**够不到** 0.2.0-rc.2 这种新预发布(同元组才允许比预发布),于是安装器只会去挑 0.1.0-rc.8 那份老 loader:隔离 profile 实测旧形状 `Packages: +101` 并落下 `@deepseek-ai/dsh-mcp-client@0.1.0-rc.8`,新形状 `Packages: +1`、`@deepseek-ai` 目录为空。范围写成 `0.2.0-rc.2 || >=0.1.0-rc.1 <1` 是必要的——宿主门禁读 peer 时带 `includePrerelease`(拿运行中的宿主版本比),pnpm 读同一个字符串时不带,只有这种"精确 + 宽松"的并集两边都满足;改成单一 `>=0.1.0-rc.1` 会在安装器侧退回过期版本。`peerDependenciesMeta.optional: true` 则是让 npm 7+ 连自动装 peer 都不做。**别把它"简化"回 dependencies。**
226
+
227
+ **为什么返回句柄而不是 Origin 引用字符串。** `[Book1]Sheet1!` 这类引用对大小写、空格、活动页状态都敏感,写错了不报错只静默失败。引擎内部持有真实对象,把 `ws-1` / `gr-2` 交给模型原样传回。
228
+
229
+ **为什么进程探测失败要报 `probe: "unavailable"` 而不是空列表。** `origin_instances` / `origin_reclaim` 靠读进程表区分"没人看的后台实例"和"用户正开着的窗口"。如果 powershell 不可用或超时就让它们返回空集合,调用方读到的是"没有残留、没有前台窗口"——一个假的清白结论。宁可说"我没看清、这次什么都没做",也不能把"未知"报成"没事"。
230
+
231
+ **为什么连接时不自动回收既存的无窗口实例。** 被 dsh 驱动的那个 Origin **本身就是无窗口的**:可见性只说明"没有人在屏幕前看它",不说明"没有客户端在用它"。开机时顺手杀掉所有无窗口实例,会直接砍掉用户当前会话正在跑的任务。所以只有显式 `origin_reclaim` 才动手,且只动无窗口的;有窗口的一律留给用户自己关。
232
+
233
+ **两条画图路径,各自实测过。** 单 x/单 y 走 LabTalk `plotxy`——那是 Origin 工具栏自己的路径,主题、配色、自动缩放都对。多条曲线 / 指定层 / 指定模板走 `layer.add_plot()` + `layer.rescale()`:`add_plot` 不 rescale 就是 0..10 空轴,但 rescale 之后颜色/符号/线宽全部正确,且**只有它能精确投到某一层**——`plotxy` 给限定范围时返回 True 却画到别的页/层上去了。
234
+
235
+ **为什么每次写入都读回。** `plot()` 读回轴范围和数据跨度比对,不符就报 `plot_axis_degenerate`;`origin_export` 校验文件存在、体积、文件头魔数;`style()` / `legend()` 每项读回,读不进的进 `failed`。
236
+
237
+ **缺失值消毒。** Origin 用 `-1.23456789e-300` 表示 missing,直接吐给模型会被当成真实拟合参数。`sanitize()` 统一转成 `null`。
238
+
239
+ ## originpro 1.1.15 / OriginPro 2024 实测陷阱(维护者必读)
240
+
241
+ 这几条都是文档和实际行为不一致,靠读 API 文档写会静默出错:
242
+
243
+ | 陷阱 | 实际行为 | 处置 |
244
+ |---|---|---|
245
+ | `lay.add_plot(...)` | 返回 Plot 对象、不报错,但轴停在 0..10 的**空图** | 只在 `rescale()` 之后使用,并用轴范围读回反证 |
246
+ | `plotxy` 的 `plot:=` 代码 | 200 线、201 散点、202 线+符号、203 柱(**不是**常见的 201=line) | `PLOT_CODES` 常量,逐个导图目视确认 |
247
+ | `plotxy` 往指定层画 | 给限定范围返回 True,曲线却不在目标层(甚至多出一层重叠轴) | 指定层一律用 `add_plot` + `rescale` |
248
+ | `add_layer(t)` | t=0 BottomXLeftY、1 TopX、**2 RightY**、3 LeftY、4 TopXRightY、5 BottomXRightY、6 Inset | `LAYER_TYPES` 名字表 + 读回 `layer.name` 当证据 |
249
+ | `plot.set_cmd("w=2.5")` | 设线宽**静默无效**(`get_float('line.width')` 仍 0.5) | 用 `set_float('line.width', v)` 并读回比对 |
250
+ | `legend` X-Function | `legend 1;` 把图例写成 "111";`legend -s Graph1` 写成 "Graph1Graph1Graph1";`legend -p br` 不挪图例反而多出写着 "br" 的文本框 | `legend -r;` 自动生成,文字走 `layer.label('Legend').text`,位置写 `Legend.x/Legend.y`(该层**数据坐标**,锚点是图例中心) |
251
+ | `obj.is_open` | 含义是"没被最小化/隐藏",**不是**"页面还存在" | `close_pages` 用关闭后重新枚举页面名单作为证据 |
252
+ | `op.pages()` | 是**生成器**:迭代第二次静默返回空 | 一律 `list(...)` 落地 |
253
+ | `WSheet.set_formula(col, …)` | 文档写 1-offset,实际 **0-based**;传 `idx+1` 在 `SetStrProp` 上抛 NoneType | 传 0-based `idx`,列不够先扩列 |
254
+ | `WSheet.from_list2(data)` | 内层列表是**列**不是行 | `write_block` 内部转置 |
255
+ | `Axis.scale` / `lay.xscale` | setter 收 `'log10'`,getter 返回**整数** 2 | `SCALE_TYPES` 映射后按整数比对 |
256
+ | `lt_exec('layer.x.title$="…"')` | 返回 **False**;字符串赋值类 LabTalk 走不通 | 轴标题走对象模型 `origin_style` |
257
+ | `op.NLFit(name)` | **区分大小写**且无别名:`Gauss` 可用,`gauss1`/`Linear`/`Quadratic`/`FRADA` 直接抛 Invalid fitting function | `FIT_PRESETS` 只收逐个构造成功的名字 |
258
+ | `op.path('p')` | 返回工程**所在文件夹**,不是工程文件 | 自己用 `_project_file` 记录绑定 |
259
+ | `op.save(file)` | 语义是 Save-As;绑定后再存同名返回 False(不是失败) | 同路径改用无参 `op.save()`,并比对 mtime |
260
+ | COM 线程亲和 | 连接必须在**专用 COM 线程**上建立;在 stdio 读线程上连接会跨 apartment 复用对象 | `_resolve()` 只查句柄表不碰 COM;有"图工具作为首个调用"的回归项守着 |
261
+ | 已销毁对象的 `.name` | 留下 C 层待处理异常,之后一个不相干的 `len()` 才炸成 `SystemError` | 句柄登记时顺手存页面名,清理句柄表不再回摸 COM |
262
+ | 桥坏到一定程度 | `op.path()` 抛 "returned a result with an exception set",此时连 `op.exit()` 都跑不动;自动 `detach()/attach()` 会把可诊断的报错变成**进程直接崩** | 不自愈,抛 `connection_error` 并写明要外部重启 Origin |
263
+ | 工作站 `.bin` 的数组布局 | 看着像两列(各 N/2),其实厂商 `.txt` 里那 600 行电流正好等于整个数组——只有测量通道 | `binio.py` 按单通道 + 方法参数重建轴,点数相等做门禁 |
264
+ | 矩阵图的色标标题 | 色条标题是模板占位文字;`lt_exec` 字符串赋值返回 False,而 originpro 1.1.15 里静态搜 `colorscale / colorbar / cbtitle` **一个 API 都没有**(2026-10-12 查过) | `origin_matrix` 只给模板选择(heat_map / cmap / mesh / contline / contgray / 3d)与 `xy_range`;色标区间、配色、标题都没做,标题要改只能事后在 Origin 里手点——不假装能设 |
265
+ | 导出滤镜对文件名的改写 | 要 `100%_a#b[1].png` 实际写出 `100%_a1b[1].png`:**`#` 被当成编号 token 换成了数字**(`%`、`[]`、空格、中文都活着)。另外 Windows 会静默截掉路径组件尾部的空格/点,于是 `isfile()` 对我们请求的字面路径说 False,而字节确实在盘上 | `_norm_target` 先按 Win32 的规则规范化,`_written_or_mangled` 用 `#+ → \d+` 匹配实际落名的唯一候选、`os.replace` 改回用户要的名字并在 `warnings` 里说明;匹配有歧义就不猜 |
266
+ | 没有文件名的 path | `path=" "` 曾经 `abspath` 到进程 CWD,变成插件目录里一个叫 `.png` 的隐形文件(dsh 下就是 node_modules 安装目录);`path="."` 同理 | `_norm_target` 要求"第一个点之前有真字符",否则 `bad_path`;目录、盘根、UNC、未映射盘一律 `bad_path`;已存在但只读/被占用返回 `target_locked`,不覆盖也不假装成功(`test_paths.py` 28 项离线锁住) |
267
+ | 编码候选顺序 | 仪器"另存为 Unicode 文本"导出的 UTF-16(有/无 BOM)此前直接报 `找不到数值数据行`;而 cp1252 写的 `µ`/`°` 会被 gb18030 **抢先**解成 `祍`/`癈`——它不报错,所以错误一路静默到列名 | `read_text` 先认 BOM,无 BOM 时按 NUL 占比猜 utf-16 并在 `encoding` 里写明是猜的;再用"高位字节成对=GBK、孤立单字节=cp1252"决定候选顺序(`high_byte_runs`) |
268
+ | 装饰过的数值格子 | `<0.001`、`2.5±0.1`、`25 mV`、`1,234.5`、`2.5*`、`(500)`、`1e400` 这些形态此前让整列被判"全为空"直接丢弃——用户看到的是少一列数据,不是报错 | `cell_number` 逐形态解开并回一条 `warnings` 说明代入了什么;溢出/Infinity 一律按缺值,绝不把 inf 送出桥 |
269
+ | 分隔符嗅探 | 欧陆 `Zeit;Signal / 0,0;1,5` 会被逗号拆开,而且错的拆法因为"列更多"在评分里胜出 | 评分改成:数值列数 → 行宽一致性 → 列数更少者优先;表头行不参与数值比 |
270
+ | 非标准 JSON | `json.dumps` 默认把 inf/nan 写成裸 `Infinity`/`NaN`,那不是合法 JSON——dsh 侧整条响应解析失败,看起来像"桥挂了"而不是"一个数不对" | `sanitize()` 把 ±inf/nan 归 None;`server.py` 写出走 `allow_nan=False`,兜不住就降级成带原 id 的 JSON-RPC 错误;`smoke_test` 客户端用 `strict_loads` 拒绝,测试不会替产品掩盖它(`test_json_safety.py` 18 项) |
271
+
272
+ ## 已知边界
273
+
274
+ - `originpro` 要求 Origin **2021+**。实测 OriginPro 2018C:能连、能存空工程,但导入返回 0 行、`add_plot` 直接打死连接器。
275
+ - 非线性拟合需要 **OriginPro** 授权;标准版调 `origin_fit(kind='gauss')` 会明确报 `fit_function_unavailable`。
276
+ - `.bin` 只覆盖 `80 F2 1B 00` 头的工作站导出;**EIS 的频率表在另一套参数里,所以 `.bin` 的 EIS 只有测量列**。Gamry / Autolab / Pine 等其它厂商二进制没做,也不猜。
277
+ - 图例位置是"按数据坐标摆放",对数轴上分数位置会偏;没有"避让数据点"的智能摆放。
278
+ - 交付路径:中文目录、中文文件名、含空格、200+ 字符长路径、自动建多级目录、`tiff→tif`/`jpeg→jpg` 改名——2026-10-12 在真机上逐条实测可用(PNG/TIF/PDF/SVG/EMF 和 `.opju` 都试过)。**不行的是网络路径**:UNC 与未映射盘返回 `bad_path`,不去猜认证。另有两条 Origin 自己的怪脾气已经兜住:文件名里的 `#` 会被导出滤镜换成编号(桥发现后会改名回你要的名字并在 `warnings` 里说明),路径组件尾部的空格/点会被 Windows 悄悄截掉(桥先截一遍再比较,不会报"文件不存在")。
279
+ - 千分位和逗号分隔符同形的文件(不加引号的 `0,1,234.5`)没法机械还原:桥按最宽行补齐并报"行长度不一致",不会假装列数对得上。欧陆小数点(`0,5`)只在分隔符是 `;` 或 tab 时才认。
280
+ - Origin 以管理员运行而 Python 不是(或反之)时 COM 连不上,两侧权限要一致。
281
+ - 长会话攒太多页面会让 COM 桥报"无效指针"。`origin_close_pages` 能清,根治是 `origin_exit` 后重连。
282
+ - **Origin 实例不会跟着客户端退出**:originpro 需要时自己拉一个 Origin,Python 进程结束那个实例还在。攒够就会报"实例超过上限"。现在插件在进程退出时回收**自己起的、且没有窗口的**实例(`atexit`),历史残留用 `origin_reclaim` 清;有窗口的实例永远不动,因为那可能是你正开着的项目。
283
+ - 统计批处理(t 检验 / ANOVA / PCA / 生存分析)没有,originpro 本身也不提供。
284
+ - **无并发保护**:两个会话同时驱动 Origin 会互相踩。
285
+ - 40 个图表预设只取元数据;预设自带的 `render_template()` 会先 `op.new()`——那会把用户正在编辑的工程整个丢掉,所以这里换成自己的渲染路径。
286
+
287
+ ## 排障
288
+
289
+ | 现象 | 先查 |
290
+ |---|---|
291
+ | dsh 里看不到工具 | 配置是不是漏了 `- insert:`;`name:` 有没有写成 `@deepseek-ai/dsh-mcp-client`;有没有彻底退出重启 dsh;`python server.py --tools` 能否列出 27 个 |
292
+ | `connection_error` | Origin 是否 ≥2021;`pip show originpro`;任务管理器里 `Origin64.exe` 是否只有一个 |
293
+ | 报"实例超过上限" | `origin_instances` 看有几个、哪些没窗口;`origin_reclaim` 关残留的后台实例;有窗口的那些要用户自己关(可能有未保存的东西) |
294
+ | `import_empty` | 先 `origin_read_file` 看结构;空格/多行表头已由自研解析器处理,走到这个报错说明文件本身没有数值行 |
295
+ | `com_timeout` | Origin 窗口是否卡在对话框/许可证弹窗 |
296
+ | `无效指针` | 页面攒太多:`origin_close_pages`,或 `origin_exit` 让它重开 |
297
+ | 图片没出来 | `origin_inspect` 看 `plots` 是否为 0;中文/空格/长路径实测可用,不用刻意改 ASCII |
298
+ | 图是空的但没报错 | 看返回里的 `proof_level`;`plot_axis_degenerate` 会主动抛出 |
299
+ | `bad_path` | 路径本身不行:把目录当文件名、盘符没映射、UNC 主机不可达、或路径里没有真正的文件名。换可写的绝对路径,别原样重试 |
300
+ | `target_locked` | 目标文件已存在且不可写(只读属性或正被 Origin/图片查看器占用)。去掉只读或换名,桥不会覆盖它 |
301
+ | 导出报了 `export_failed` 但目录里有张图 | 看 `warnings`:文件名里的 `#` 会被 Origin 换成编号,桥认出后会改名回你要的名字;认出多个候选时不会猜,直接报错让你换名 |
302
+ | `ERR_PNPM_NO_MATCHING_VERSION`(装不上) | 镜像滞后,不是没发布:`npm config get registry` 看一眼,加 `--registry=https://registry.npmjs.org/` 或改用 `file:` 离线包 |
303
+
304
+ ## 给维护者
305
+
306
+ ```
307
+ dsh-origin-bridge/
308
+ ├── server.py # 同步 stdio JSON-RPC,27 个工具的分发与 schema
309
+ ├── originlab.py # 引擎:COM 专用线程、句柄表、每个操作及其读回验证
310
+ ├── asciiio.py # 健壮的 ASCII 读取(分隔符嗅探 / GBK / 多行表头)
311
+ ├── binio.py # 工作站 .bin:单测量通道 + 方法参数重建电势轴
312
+ ├── templates_catalog.py # 40 个科研图表预设(纯元数据)
313
+ ├── cordis.patch.yml # dsh bundle 补丁(serverName: origin_bridge)
314
+ ├── install_dsh.ps1 # 一键装入 dsh(备份 + 写 profile 层 + 自检)
315
+ ├── icon.svg # 卡片图标(scripts/make_icon.py 生成)
316
+ ├── locale/*.json # dsh 卡片显示名与描述
317
+ ├── scripts/make_preview.py # 重新生成 docs/preview.png(关掉图例后才导出)
318
+ └── test_*.py / smoke_test.py / edge_checks.py # 352 项离线与单元检查(随包发布)
319
+ ```
320
+
321
+ 开发侧另有 `cases/` + `run_cases.py` 的 1118 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
322
+
323
+ 重新生成图标:`python scripts/make_icon.py`(只需要 numpy,PNG 由 stdlib 手写)。
324
+
325
+ ## 商标声明与许可
326
+
327
+ 本项目是独立的第三方工具,与 OriginLab Corporation **无关联、无合作、未获认可**。Origin、OriginPro 为 OriginLab Corporation 的商标;本工具通过 OriginLab 官方公开发布的 `originpro` Python 包以正常方式使用其产品,未分发其任何二进制、图标或文档。
328
+
329
+ MIT © Origin Bridge Contributors