dsh-origin-bridge 0.4.8 → 0.4.10

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
@@ -11,14 +11,14 @@
11
11
  </p>
12
12
 
13
13
  <p align="center">
14
- <sub>27 个工具 · <b>1126 例全链路测试全部通过</b> · 数据不出本机 · 支持工作站 <code>.txt</code> / <code>.bin</code></sub>
14
+ <sub>27 个工具 · <b>1135 例全链路测试全部通过</b> · 数据不出本机 · 支持工作站 <code>.txt</code> / <code>.bin</code></sub>
15
15
  </p>
16
16
 
17
17
  <p align="center">
18
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-1126%2F1126%20passing-brightgreen">
19
+ <img alt="tests" src="https://img.shields.io/badge/tests-1135%2F1135%20passing-brightgreen">
20
20
  <img alt="license" src="https://img.shields.io/badge/license-MIT-lightgrey">
21
- <a href="https://github.com/caob23/dsh-origin-bridge/actions/workflows/offline-checks.yml"><img alt="offline checks" src="https://github.com/caob23/dsh-origin-bridge/actions/workflows/offline-checks.yml/badge.svg"></a>
21
+ <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>
22
22
  </p>
23
23
 
24
24
  <p align="center">
@@ -36,7 +36,7 @@
36
36
  - **数据不出本机。** 没有 API key、没有云端组件,dsh 拉起一个本地 Python 进程,通过 OriginLab 官方的 [`originpro`](https://pypi.org/project/originpro/) 驱动你机器上的 Origin。
37
37
  - **每一步都有证据。** Origin 的 X-Function 普遍"返回成功但什么都没发生"。这里画图后读回轴范围与数据跨度比对,导出后校验文件存在/体积/文件头魔数,样式逐项读回——读不回的进 `failed` 列表,不会假装成功。
38
38
  - **不是 74 个工具,是 27 个。** 每个都有实测用途,工具描述里写着实测限制,模型不会踩坑。
39
- - **1000+ 次真实测试,不是自称。** **1126 例**全链路用例跑在真实 stdio 服务器 + 本机 OriginPro 上,**1126 通过 / 0 失败**:27 个工具每个至少 15 例、最多 134 例,按副作用分五层(L0 纯解析 301、L1 只读 COM 63、L2 改会话 521、L3 落盘 163、L4 进程级 78)。提示词全部按人话写,跑之前先用在线 `tools/list` schema 校验用例本身,所以失败只会是产品问题,不会是题目写错。另有 **417 项**离线/单元/边界检查(asciiio 84、binio 30、lifecycle 42、smoke 129、edge 26、paths 35、json 26、readfile 12、labtalk 33)**随包发布,你可以直接复跑**;那 1126 例题库含提示词与模拟数据,是开发侧资产,不随包分发。originpro 的十几个文档陷阱全部记录在案并绕开。
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 的十几个文档陷阱全部记录在案并绕开。
40
40
 
41
41
  ## 它能做什么
42
42
 
@@ -73,13 +73,12 @@ dsh plugin --profile desktop add dsh-origin-bridge
73
73
  > dsh plugin --profile desktop add file:C:\path\to\dsh-origin-bridge
74
74
  > ```
75
75
 
76
- 两个包名指向同一个包,装哪个都行:
77
-
78
76
  ```bash
79
- dsh plugin --profile desktop add dsh-origin-bridge # 正式包
80
- dsh plugin --profile desktop add @caob23/dsh-origin-bridge # 转发壳,依赖上面那个
77
+ dsh plugin --profile desktop add dsh-origin-bridge
81
78
  ```
82
79
 
80
+ > 旧账号下的转发壳 `@caob23/dsh-origin-bridge` 已弃用(deprecated),请直接安装上面的正式包。
81
+
83
82
  本包是 bundle 包(`package.json` 的 `dsh.bundle.patch` 指向根目录的 `cordis.patch.yml`),`dsh plugin` 装完会自动把它加进 profile 的 `dsh.profile.bundles`,**重启 dsh 即加载**,不用再手写补丁。`dsh plugin` 转发给 pnpm,所以 pnpm 要在 PATH 上。
84
83
 
85
84
  **npm 只管分发,不管你的 Python 环境**——Origin 依赖必须自己装进 dsh 实际使用的那个解释器:
@@ -133,12 +132,12 @@ pip install -r requirements.txt # 国内网络慢就加 -i https://pypi.tun
133
132
  .\.venv\Scripts\python.exe test_binio.py # 30 项,不需要 Origin;有真实 .bin 时会跟厂商 .txt 逐点对账
134
133
  .\.venv\Scripts\python.exe test_lifecycle.py # 42 项,不需要 Origin:实例分类/回收/掉线重连的安全边界
135
134
  .\.venv\Scripts\python.exe test_paths.py # 35 项,不需要 Origin:导出/保存目标路径的规范化与拒绝
136
- .\.venv\Scripts\python.exe test_json_safety.py # 26 项,不需要 Origin:返回值必须是严格 JSON
135
+ .\.venv\Scripts\python.exe test_json_safety.py # 29 项,不需要 Origin:返回值必须是严格 JSON
137
136
  .\.venv\Scripts\python.exe test_readfile.py # 12 项,不需要 Origin:read_file 的 sample 与二进制表格拒绝
138
137
  .\.venv\Scripts\python.exe test_labtalk_guard.py # 33 项,不需要 Origin:LabTalk 黑名单匹配
139
138
  ```
140
139
 
141
- 上面这 417 项随包发布,你可以自己复跑。另外那 **1126 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
140
+ 上面这 420 项随包发布,你可以自己复跑。另外那 **1135 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
142
141
 
143
142
  ### 不是 dsh,是别的 MCP 客户端
144
143
 
@@ -280,7 +279,7 @@ Origin Bridge 的做法:
280
279
  | LabTalk 黑名单 | 原来按子串匹配,2026-10-20 用 45 条高危语句实测 **37 条能过**:`win -cd` 销毁页面、`doc -n` 新建工程丢当前项目、`del/delete` 删数据集、`run -p`/`run.file` 执行外部程序、`exit`、以及能绕过句柄重置的 `doc -o` | 改成按语句(`;`/换行/`{}` 切分)+ 词边界规则:`doc -`、`win -c`、`del/delete`、`layer -d`、`page -d`、`quit/exit`、`run.`/`run -`、`system/shell`、`rmdir/mkdir`、`type -`、`close`、`saveas` 全拦;`test_labtalk_guard.py` 32 项锁住"该拦的拦、该放的放"。仍是劝告式护栏,不是沙箱 |
281
280
  | dsh 新 profile 的补丁模板 | `cordis.patch.yml` 模板内容就是一行 `[]`(顶层数组占位)。往它后面**追加** `- insert:` 会得到两个 YAML 文档,dsh 直接 `failed to parse overlay` 起不来——`install_dsh.ps1` 2026-10-20 前正是这样,全新环境按 README 用脚本装会把配置写坏 | 安装器现在把"只有注释/只有一个 `[]`"的文件当空文件处理,插块成为数组本身;装完会用 PyYAML 校验一次,解析失败就提示从备份恢复 |
282
281
  | `save_fig` 的 `width` | 传了也不生效:`save_fig(path)` 只有 path 时用页面尺寸,**必须同时给 type** 才认 width (实测 400 和 12000 导出都是 3216×2461、字节数相同)。我们的 `origin_export` 以前恰好只传 path | `save_fig(path, fmt, width=N)` + 缺文件时回退纯 path;PNG 导完比对 `dimensions.0` 与请求值,不一致就回 warning。题库宽度用例从"有 dimensions"改成**断言等于请求宽度** |
283
- | 非标准 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 项) |
282
+ | 非标准 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` 29 项,含幻觉工具名与未知方法的协议层断言) |
284
283
 
285
284
  ## 已知边界
286
285
 
@@ -328,10 +327,10 @@ dsh-origin-bridge/
328
327
  ├── icon.svg # 卡片图标(scripts/make_icon.py 生成)
329
328
  ├── locale/*.json # dsh 卡片显示名与描述
330
329
  ├── scripts/make_preview.py # 重新生成 docs/preview.png(关掉图例后才导出)
331
- └── test_*.py / smoke_test.py / edge_checks.py # 417 项离线与单元检查(随包发布)
330
+ └── test_*.py / smoke_test.py / edge_checks.py # 420 项离线与单元检查(随包发布)
332
331
  ```
333
332
 
334
- 开发侧另有 `cases/` + `run_cases.py` 的 1126 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
333
+ 开发侧另有 `cases/` + `run_cases.py` 的 1135 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
335
334
 
336
335
  重新生成图标:`python scripts/make_icon.py`(只需要 numpy,PNG 由 stdlib 手写)。
337
336
 
@@ -35,7 +35,13 @@ const child = spawn(
35
35
  );
36
36
 
37
37
  // A child killed by a signal is a crash, not a clean exit: reporting 0 here would
38
- // make an MCP client treat a dead server as "finished normally".
38
+ // make an MCP client treat a dead server as "finished normally". The 'error'
39
+ // event (interpreter not found, no execute permission) is otherwise an unhandled
40
+ // Node stack trace instead of a message the MCP client can show.
41
+ child.on('error', (err) => {
42
+ console.error(`[origin-bridge] cannot start python (${candidates.join(', ')}): ${err.message}`);
43
+ process.exit(1);
44
+ });
39
45
  child.on('exit', (code, signal) => {
40
46
  if (signal) {
41
47
  console.error(`[origin-bridge] python exited on ${signal}`);
package/install_dsh.ps1 CHANGED
@@ -99,16 +99,6 @@ if (Test-Path $manifest) {
99
99
  }
100
100
  if (-not (Test-Path (Join-Path $pkgDir 'index.js'))) {
101
101
  New-Item -ItemType Directory -Force -Path $pkgDir | Out-Null
102
- @"
103
- {
104
- "name": "dsh-origin-bridge",
105
- "version": "$pkgVersion",
106
- "description": "Origin Bridge: natural language" -> local Origin/OriginPro -> editable .opju + publication images",
107
- "main": "./index.js",
108
- "type": "module",
109
- "license": "MIT"
110
- }
111
- "@ | Set-Content -Path (Join-Path $pkgDir 'package.json') -Encoding ASCII
112
102
  @'
113
103
  // Registration-only bundle. The Origin tools come from server.py in this
114
104
  // package's own folder, registered by the mcp-origin-bridge insert in the
@@ -122,6 +112,19 @@ export default { name, activate, apply };
122
112
  '@ | Set-Content -Path (Join-Path $pkgDir 'index.js') -Encoding ASCII
123
113
  Write-Host "staged registration-only package -> $pkgDir"
124
114
  }
115
+ # (Re)write package.json on every run: the 0.4.9-and-earlier text embedded raw
116
+ # quotes in the description (unparseable JSON, so dsh could not read the card),
117
+ # and a one-time write also left a stale version behind on re-install.
118
+ @"
119
+ {
120
+ "name": "dsh-origin-bridge",
121
+ "version": "$pkgVersion",
122
+ "description": "Origin Bridge: natural language -> local Origin/OriginPro -> editable .opju + publication images",
123
+ "main": "./index.js",
124
+ "type": "module",
125
+ "license": "MIT"
126
+ }
127
+ "@ | Set-Content -Path (Join-Path $pkgDir 'package.json') -Encoding ASCII
125
128
 
126
129
  $block += "`r`n# self-mount: makes $pkgName appear as its own card in dsh's plugin UI.`r`n"
127
130
  $block += "# apply() is a no-op; the tools come from the $entryId insert above.`r`n"
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "dsh-origin-bridge",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "在对话里驱动本机 Origin / OriginPro:导入 .dat/.csv/.txt 与电化学工作站 .bin,绘图、拟合、改样式,导出图片并保存可在 Origin 里继续编辑的 .opju 工程。dsh 插件,27 个 MCP 工具。第三方工具,与 OriginLab Corporation 无关联、未获认可。",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/caob23/dsh-origin-bridge.git"
9
+ "url": "git+https://github.com/angelky7/dsh-origin-bridge.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/caob23/dsh-origin-bridge/issues"
12
+ "url": "https://github.com/angelky7/dsh-origin-bridge/issues"
13
13
  },
14
- "homepage": "https://github.com/caob23/dsh-origin-bridge#readme",
14
+ "homepage": "https://github.com/angelky7/dsh-origin-bridge#readme",
15
15
  "main": "./index.js",
16
16
  "icon": "./icon.svg",
17
17
  "bin": {
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.8"
23
+ SERVER_VERSION = "0.4.10"
24
24
  PROTOCOL = "2024-11-05"
25
25
 
26
26
 
@@ -145,6 +145,40 @@ def main():
145
145
  ok("extra keys are tolerated",
146
146
  code_for("origin_import", {"path": "x.csv", "bogus": 1}) == "no-raise")
147
147
 
148
+ # ------------------------------------------------ unknown tool / method
149
+ # A model hallucinating a tool name gets a named, actionable error — not a
150
+ # crash, not a silent null. Driven through the real stdio loop.
151
+ child3 = (
152
+ "import io, json, os, sys\n"
153
+ "sys.path.insert(0, os.environ['BRIDGE_DIR'])\n"
154
+ "import server\n"
155
+ "a = {'jsonrpc': '2.0', 'id': 1, 'method': 'tools/call',\n"
156
+ " 'params': {'name': 'origin_frobnicate', 'arguments': {}}}\n"
157
+ "b = {'jsonrpc': '2.0', 'id': 2, 'method': 'no/such/method'}\n"
158
+ "reqs = json.dumps(a) + chr(10) + json.dumps(b) + chr(10)\n"
159
+ "sys.stdin = io.TextIOWrapper(io.BytesIO(reqs.encode('utf-8')), encoding='utf-8')\n"
160
+ "sys.exit(server.main() or 0)\n"
161
+ )
162
+ proc3 = subprocess.run([sys.executable, "-c", child3], capture_output=True, env=env, timeout=120)
163
+ lines3 = proc3.stdout.decode("utf-8", "replace").strip().splitlines()
164
+ ok("unknown tool + unknown method answered", len(lines3) == 2, len(lines3))
165
+ r1, r2 = {}, {}
166
+ try:
167
+ r1, r2 = strict(lines3[0]), strict(lines3[1])
168
+ except Exception as exc: # noqa: BLE001
169
+ ok("both replies parseable", False, exc)
170
+ res1 = (r1.get("result") or {})
171
+ body1 = {}
172
+ try:
173
+ body1 = json.loads(res1.get("content", [{}])[0].get("text", "{}"))
174
+ except ValueError:
175
+ pass
176
+ ok("unknown tool -> isError with code unknown_tool",
177
+ res1.get("isError") is True and body1.get("code") == "unknown_tool",
178
+ (res1, body1))
179
+ ok("unknown method -> JSON-RPC -32601",
180
+ (r2.get("error") or {}).get("code") == -32601, r2)
181
+
148
182
  print("\n%d/%d json-safety checks passed" % (PASSED, PASSED + len(FAILED)))
149
183
  if FAILED:
150
184
  for f in FAILED: