dsh-origin-bridge 0.4.9 → 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 +8 -8
- package/bin/origin-bridge.mjs +7 -1
- package/install_dsh.ps1 +13 -10
- package/package.json +1 -1
- package/server.py +1 -1
- package/test_json_safety.py +34 -0
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
<sub>27 个工具 · <b>
|
|
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-
|
|
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
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>
|
|
@@ -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+ 次真实测试,不是自称。** **
|
|
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
|
|
|
@@ -132,12 +132,12 @@ pip install -r requirements.txt # 国内网络慢就加 -i https://pypi.tun
|
|
|
132
132
|
.\.venv\Scripts\python.exe test_binio.py # 30 项,不需要 Origin;有真实 .bin 时会跟厂商 .txt 逐点对账
|
|
133
133
|
.\.venv\Scripts\python.exe test_lifecycle.py # 42 项,不需要 Origin:实例分类/回收/掉线重连的安全边界
|
|
134
134
|
.\.venv\Scripts\python.exe test_paths.py # 35 项,不需要 Origin:导出/保存目标路径的规范化与拒绝
|
|
135
|
-
.\.venv\Scripts\python.exe test_json_safety.py #
|
|
135
|
+
.\.venv\Scripts\python.exe test_json_safety.py # 29 项,不需要 Origin:返回值必须是严格 JSON
|
|
136
136
|
.\.venv\Scripts\python.exe test_readfile.py # 12 项,不需要 Origin:read_file 的 sample 与二进制表格拒绝
|
|
137
137
|
.\.venv\Scripts\python.exe test_labtalk_guard.py # 33 项,不需要 Origin:LabTalk 黑名单匹配
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
上面这
|
|
140
|
+
上面这 420 项随包发布,你可以自己复跑。另外那 **1135 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
|
|
141
141
|
|
|
142
142
|
### 不是 dsh,是别的 MCP 客户端
|
|
143
143
|
|
|
@@ -279,7 +279,7 @@ Origin Bridge 的做法:
|
|
|
279
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 项锁住"该拦的拦、该放的放"。仍是劝告式护栏,不是沙箱 |
|
|
280
280
|
| dsh 新 profile 的补丁模板 | `cordis.patch.yml` 模板内容就是一行 `[]`(顶层数组占位)。往它后面**追加** `- insert:` 会得到两个 YAML 文档,dsh 直接 `failed to parse overlay` 起不来——`install_dsh.ps1` 2026-10-20 前正是这样,全新环境按 README 用脚本装会把配置写坏 | 安装器现在把"只有注释/只有一个 `[]`"的文件当空文件处理,插块成为数组本身;装完会用 PyYAML 校验一次,解析失败就提示从备份恢复 |
|
|
281
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"改成**断言等于请求宽度** |
|
|
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`
|
|
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 项,含幻觉工具名与未知方法的协议层断言) |
|
|
283
283
|
|
|
284
284
|
## 已知边界
|
|
285
285
|
|
|
@@ -327,10 +327,10 @@ dsh-origin-bridge/
|
|
|
327
327
|
├── icon.svg # 卡片图标(scripts/make_icon.py 生成)
|
|
328
328
|
├── locale/*.json # dsh 卡片显示名与描述
|
|
329
329
|
├── scripts/make_preview.py # 重新生成 docs/preview.png(关掉图例后才导出)
|
|
330
|
-
└── test_*.py / smoke_test.py / edge_checks.py #
|
|
330
|
+
└── test_*.py / smoke_test.py / edge_checks.py # 420 项离线与单元检查(随包发布)
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
-
开发侧另有 `cases/` + `run_cases.py` 的
|
|
333
|
+
开发侧另有 `cases/` + `run_cases.py` 的 1135 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
|
|
334
334
|
|
|
335
335
|
重新生成图标:`python scripts/make_icon.py`(只需要 numpy,PNG 由 stdlib 手写)。
|
|
336
336
|
|
package/bin/origin-bridge.mjs
CHANGED
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-origin-bridge",
|
|
3
|
-
"version": "0.4.
|
|
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",
|
package/server.py
CHANGED
package/test_json_safety.py
CHANGED
|
@@ -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:
|