dsh-origin-bridge 0.4.3 → 0.4.5
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 +6 -2
- package/docs/shot-cv.png +0 -0
- package/docs/shot-eis.png +0 -0
- package/docs/shot-heatmap.png +0 -0
- package/docs/shot-workstation.png +0 -0
- package/package.json +82 -76
- package/screenshots.json +7 -0
- package/scripts/make_screenshots.py +187 -0
- package/server.py +612 -612
package/README.md
CHANGED
|
@@ -10,9 +10,13 @@
|
|
|
10
10
|
拿回一张发表级图片和一个<b>能在 Origin 里继续编辑</b>的 <code>.opju</code> 工程。
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
|
+
<p align="center">
|
|
14
|
+
<sub>27 个工具 · <b>1079 例全链路测试全部通过</b> · 数据不出本机 · 支持工作站 <code>.txt</code> / <code>.bin</code></sub>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
13
17
|
<p align="center">
|
|
14
18
|
<a href="#安装"><img alt="platform" src="https://img.shields.io/badge/platform-Windows%20%2B%20Origin%202021%2B-blue"></a>
|
|
15
|
-
<img alt="tests" src="https://img.shields.io/badge/tests-
|
|
19
|
+
<img alt="tests" src="https://img.shields.io/badge/tests-1079%2F1079%20passing-brightgreen">
|
|
16
20
|
<img alt="license" src="https://img.shields.io/badge/license-MIT-lightgrey">
|
|
17
21
|
</p>
|
|
18
22
|
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
- **数据不出本机。** 没有 API key、没有云端组件,dsh 拉起一个本地 Python 进程,通过 OriginLab 官方的 [`originpro`](https://pypi.org/project/originpro/) 驱动你机器上的 Origin。
|
|
32
36
|
- **每一步都有证据。** Origin 的 X-Function 普遍"返回成功但什么都没发生"。这里画图后读回轴范围与数据跨度比对,导出后校验文件存在/体积/文件头魔数,样式逐项读回——读不回的进 `failed` 列表,不会假装成功。
|
|
33
37
|
- **不是 74 个工具,是 27 个。** 每个都有实测用途,工具描述里写着实测限制,模型不会踩坑。
|
|
34
|
-
-
|
|
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 的十几个文档陷阱全部记录在案并绕开。
|
|
35
39
|
|
|
36
40
|
## 它能做什么
|
|
37
41
|
|
package/docs/shot-cv.png
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,76 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-origin-bridge",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/caob23/dsh-origin-bridge.git"
|
|
10
|
-
},
|
|
11
|
-
"bugs": {
|
|
12
|
-
"url": "https://github.com/caob23/dsh-origin-bridge/issues"
|
|
13
|
-
},
|
|
14
|
-
"homepage": "https://github.com/caob23/dsh-origin-bridge#readme",
|
|
15
|
-
"main": "./index.js",
|
|
16
|
-
"icon": "./icon.svg",
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@deepseek-ai/dsh-mcp-client": ">=0.1.0-rc.1"
|
|
19
|
-
},
|
|
20
|
-
"bin": {
|
|
21
|
-
"origin-bridge": "bin/origin-bridge.mjs"
|
|
22
|
-
},
|
|
23
|
-
"exports": {
|
|
24
|
-
".": "./index.js",
|
|
25
|
-
"./package.json": "./package.json",
|
|
26
|
-
"./locale/*.json": "./locale/*.json"
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"index.js",
|
|
30
|
-
"bin/origin-bridge.mjs",
|
|
31
|
-
"install_dsh.ps1",
|
|
32
|
-
"cordis.patch.yml",
|
|
33
|
-
"icon.svg",
|
|
34
|
-
"locale/en.json",
|
|
35
|
-
"locale/zh.json",
|
|
36
|
-
"docs/preview.png",
|
|
37
|
-
"scripts/
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-origin-bridge",
|
|
3
|
+
"version": "0.4.5",
|
|
4
|
+
"description": "在对话里驱动本机 Origin / OriginPro:导入 .dat/.csv/.txt 与电化学工作站 .bin,绘图、拟合、改样式,导出图片并保存可在 Origin 里继续编辑的 .opju 工程。dsh 插件,27 个 MCP 工具。第三方工具,与 OriginLab Corporation 无关联、未获认可。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/caob23/dsh-origin-bridge.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/caob23/dsh-origin-bridge/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/caob23/dsh-origin-bridge#readme",
|
|
15
|
+
"main": "./index.js",
|
|
16
|
+
"icon": "./icon.svg",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@deepseek-ai/dsh-mcp-client": ">=0.1.0-rc.1"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"origin-bridge": "bin/origin-bridge.mjs"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./index.js",
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
"./locale/*.json": "./locale/*.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"bin/origin-bridge.mjs",
|
|
31
|
+
"install_dsh.ps1",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"icon.svg",
|
|
34
|
+
"locale/en.json",
|
|
35
|
+
"locale/zh.json",
|
|
36
|
+
"docs/preview.png",
|
|
37
|
+
"scripts/make_screenshots.py",
|
|
38
|
+
"docs/shot-workstation.png",
|
|
39
|
+
"docs/shot-heatmap.png",
|
|
40
|
+
"docs/shot-eis.png",
|
|
41
|
+
"docs/shot-cv.png",
|
|
42
|
+
"screenshots.json",
|
|
43
|
+
"scripts/make_icon.py",
|
|
44
|
+
"scripts/make_preview.py",
|
|
45
|
+
"server.py",
|
|
46
|
+
"originlab.py",
|
|
47
|
+
"asciiio.py",
|
|
48
|
+
"binio.py",
|
|
49
|
+
"templates_catalog.py",
|
|
50
|
+
"fixtures/make_fixtures.py",
|
|
51
|
+
"fixtures/*.dat",
|
|
52
|
+
"fixtures/*.csv",
|
|
53
|
+
"fixtures/*.txt",
|
|
54
|
+
"smoke_test.py",
|
|
55
|
+
"edge_checks.py",
|
|
56
|
+
"test_asciiio.py",
|
|
57
|
+
"test_binio.py",
|
|
58
|
+
"test_lifecycle.py",
|
|
59
|
+
"client_check.py",
|
|
60
|
+
"requirements.txt",
|
|
61
|
+
"sample.dat",
|
|
62
|
+
"README.md",
|
|
63
|
+
"LICENSE"
|
|
64
|
+
],
|
|
65
|
+
"dsh": {
|
|
66
|
+
"bundle": {
|
|
67
|
+
"patch": "./cordis.patch.yml"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"keywords": [
|
|
71
|
+
"dsh",
|
|
72
|
+
"dsh-plugin",
|
|
73
|
+
"dsh-bundle",
|
|
74
|
+
"deepseek-harness",
|
|
75
|
+
"mcp",
|
|
76
|
+
"origin",
|
|
77
|
+
"originpro",
|
|
78
|
+
"opju",
|
|
79
|
+
"scientific-plotting",
|
|
80
|
+
"electrochemistry"
|
|
81
|
+
]
|
|
82
|
+
}
|
package/screenshots.json
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Regenerate the storefront screenshots in docs/ through the real engine.
|
|
2
|
+
|
|
3
|
+
Every image here is a genuine Origin export built from closed-form synthetic
|
|
4
|
+
data: no measurement from the user's lab ever appears in a screenshot, and the
|
|
5
|
+
files are reproducible by rerunning this script.
|
|
6
|
+
|
|
7
|
+
python scripts/make_screenshots.py # 全部四张
|
|
8
|
+
python scripts/make_screenshots.py --only cv # 只重做一张
|
|
9
|
+
|
|
10
|
+
The screenshots are declared to the plugin market by the repository-root
|
|
11
|
+
screenshots.json, which points at these paths.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
|
|
19
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
20
|
+
ROOT = os.path.dirname(HERE)
|
|
21
|
+
sys.path.insert(0, ROOT)
|
|
22
|
+
|
|
23
|
+
import originlab # noqa: E402
|
|
24
|
+
|
|
25
|
+
DOCS = os.path.join(ROOT, "docs")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _cv():
|
|
29
|
+
"""A synthetic reversible voltammogram: forward and reverse sweep, mA/cm2."""
|
|
30
|
+
pts = []
|
|
31
|
+
e_up, e_dn, step = 0.8, -0.2, 0.005
|
|
32
|
+
e = e_up
|
|
33
|
+
direction = -1.0
|
|
34
|
+
while True:
|
|
35
|
+
anodic = 0.42 * math.exp(-((e - 0.42) ** 2) / 0.0045)
|
|
36
|
+
cathodic = -0.36 * math.exp(-((e + 0.06) ** 2) / 0.0055)
|
|
37
|
+
base = 0.02 * e + 0.006 * math.sin(e * 9.0)
|
|
38
|
+
pts.append((round(e, 4), round(anodic + cathodic + base, 5)))
|
|
39
|
+
e = round(e + direction * step, 4)
|
|
40
|
+
if direction < 0 and e <= e_dn:
|
|
41
|
+
direction = 1.0
|
|
42
|
+
if direction > 0 and e >= e_up:
|
|
43
|
+
break
|
|
44
|
+
return {"E / V": [p[0] for p in pts], "i / mA cm-2": [p[1] for p in pts]}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _bode(n=41):
|
|
48
|
+
"""Randles-style synthetic impedance: log-frequency sweep."""
|
|
49
|
+
rs, rct, c = 12.0, 180.0, 6e-5
|
|
50
|
+
freq, zre, zim = [], [], []
|
|
51
|
+
for i in range(n):
|
|
52
|
+
f = 1e5 / (10 ** (i * 0.175))
|
|
53
|
+
w = 2 * math.pi * f
|
|
54
|
+
den = 1 + (w * rct * c) ** 2
|
|
55
|
+
freq.append(round(f, 4))
|
|
56
|
+
zre.append(round(rs + rct / den, 4))
|
|
57
|
+
zim.append(round(w * rct ** 2 * c / den, 4))
|
|
58
|
+
return {"Freq / Hz": freq, "Z' / ohm": zre, "Z'' / ohm": zim}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _grid(rows=12, cols=12):
|
|
62
|
+
return [[round(3.0 * math.sin(i / 2.4) + 2.0 * math.cos(j / 3.1)
|
|
63
|
+
+ 0.6 * math.sin((i + j) / 2.0), 4) for j in range(cols)] for i in range(rows)]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _workstation_txt():
|
|
67
|
+
"""A CH Instruments style export, written to a temp file, to show the .txt route."""
|
|
68
|
+
lines = [
|
|
69
|
+
"Jan. 04, 2026 09:12:07", "Linear Sweep Voltammetry",
|
|
70
|
+
"File: d:\\demo\\synthetic_lsv.txt", "Data Source: Experiment",
|
|
71
|
+
"Instrument Model: CHI660E", "Header: ", "Note: ", "",
|
|
72
|
+
"Init E (V) = 0.0", "Final E (V) = 0.6", "Scan Rate (V/s) = 0.05",
|
|
73
|
+
"Sample Interval (V) = 0.002", "Quiet Time (sec) = 2",
|
|
74
|
+
"Sensitivity (A/V) = 0.0001", "", "Results:", "", "Channel 1:",
|
|
75
|
+
"Segment 1:", "", "Potential/V, Current/A", "",
|
|
76
|
+
]
|
|
77
|
+
for i in range(120):
|
|
78
|
+
e = i * 0.005
|
|
79
|
+
lines.append("%.3f, %.3e" % (e, 1.2e-5 + 3.4e-5 / (1 + math.exp(-(e - 0.24) * 26))
|
|
80
|
+
+ 4e-7 * math.sin(i / 3.0)))
|
|
81
|
+
fd, path = tempfile.mkstemp(prefix="synthetic_lsv_", suffix=".txt")
|
|
82
|
+
os.close(fd)
|
|
83
|
+
with open(path, "w", encoding="utf-8", newline="\r\n") as handle:
|
|
84
|
+
handle.write("\r\n".join(lines) + "\r\n")
|
|
85
|
+
return path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _finish(graph, out, label):
|
|
89
|
+
try:
|
|
90
|
+
originlab.legend_entries(graph, show=False)
|
|
91
|
+
except originlab.OriginError:
|
|
92
|
+
pass # a heat map has no legend box to hide
|
|
93
|
+
e = originlab.export(graph, out, fmt="png", width=1600)
|
|
94
|
+
print(" %-14s -> %s (%d 字节, %s)" % (label, os.path.basename(out), e["bytes"],
|
|
95
|
+
e.get("dimensions")))
|
|
96
|
+
return e
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _line(graph, color="blue", width=2.5, series=0, symbol=0, size=0.0):
|
|
100
|
+
"""A 1 px black hairline reads as a broken chart in a screenshot."""
|
|
101
|
+
args = {"graph": graph, "series": series, "color": color, "line_width": width}
|
|
102
|
+
if symbol:
|
|
103
|
+
args["symbol_kind"] = symbol
|
|
104
|
+
if size:
|
|
105
|
+
args["symbol_size"] = size
|
|
106
|
+
originlab.style(**args)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def shot_cv():
|
|
110
|
+
sheet = originlab.write_columns(_cv())
|
|
111
|
+
p = originlab.plot(sheet["worksheet"], x=1, y=2, plot_type="line",
|
|
112
|
+
title="Synthetic cyclic voltammogram")
|
|
113
|
+
originlab.style(p["graph"], x_title="E / V", y_title="i / mA cm-2")
|
|
114
|
+
_line(p["graph"], "blue", 2.5)
|
|
115
|
+
_finish(p["graph"], os.path.join(DOCS, "shot-cv.png"), "CV")
|
|
116
|
+
return p["graph"]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def shot_bode():
|
|
120
|
+
data = _bode(n=25)
|
|
121
|
+
freq = data["Freq / Hz"]
|
|
122
|
+
sheet = originlab.write_columns({"Freq / Hz": freq, "|Z| / ohm":
|
|
123
|
+
[round(math.hypot(zr, zi), 3) for zr, zi
|
|
124
|
+
in zip(data["Z' / ohm"], data["Z'' / ohm"])]})
|
|
125
|
+
p = originlab.plot(sheet["worksheet"], x=1, y=2, plot_type="line_symbol",
|
|
126
|
+
title="Synthetic Bode plot")
|
|
127
|
+
st = originlab.style(p["graph"], x_title="Frequency / Hz", y_title="|Z| / ohm",
|
|
128
|
+
x_scale="log10")
|
|
129
|
+
read = [o for o in st.get("applied", []) if o.get("op") == "x_scale"]
|
|
130
|
+
if not read or read[0].get("readback") != 2:
|
|
131
|
+
raise SystemExit("横轴没能改成对数(读回 %r),不导出" % (read or "没有这项",))
|
|
132
|
+
# A log axis auto-ranges over the whole decade grid; without this the curve
|
|
133
|
+
# sits in a third of the panel and the tick labels collide.
|
|
134
|
+
originlab.style(p["graph"], xlim=[min(freq) * 0.6, max(freq) * 1.6])
|
|
135
|
+
_line(p["graph"], "red", 2.0, symbol=2, size=7.0)
|
|
136
|
+
_finish(p["graph"], os.path.join(DOCS, "shot-eis.png"), "EIS")
|
|
137
|
+
return p["graph"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def shot_heatmap():
|
|
141
|
+
m = originlab.matrix_plot(_grid(), template="heat_map", title="Synthetic response surface")
|
|
142
|
+
graph = m.get("graph") or m.get("graph_page")
|
|
143
|
+
originlab.style(graph, x_title="Applied potential / V", y_title="Cycle",
|
|
144
|
+
layer=0)
|
|
145
|
+
_finish(graph, os.path.join(DOCS, "shot-heatmap.png"), "Heat map")
|
|
146
|
+
return graph
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def shot_workstation():
|
|
150
|
+
path = _workstation_txt()
|
|
151
|
+
try:
|
|
152
|
+
imp = originlab.import_file(path)
|
|
153
|
+
cols = [c["label"] for c in imp["columns"]]
|
|
154
|
+
if cols[:2] != ["Potential", "Current"]:
|
|
155
|
+
raise SystemExit("工作站 txt 的列名没解析成 Potential/Current,而是 %s" % cols)
|
|
156
|
+
p = originlab.plot(imp["worksheet"], x=1, y=2, plot_type="line",
|
|
157
|
+
title="Workstation .txt import")
|
|
158
|
+
originlab.style(p["graph"], x_title="E / V", y_title="I / A")
|
|
159
|
+
_line(p["graph"], "green", 2.5)
|
|
160
|
+
_finish(p["graph"], os.path.join(DOCS, "shot-workstation.png"), "Workstation txt")
|
|
161
|
+
return p["graph"]
|
|
162
|
+
finally:
|
|
163
|
+
os.remove(path)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
SHOTS = {"cv": shot_cv, "eis": shot_bode, "heatmap": shot_heatmap,
|
|
167
|
+
"workstation": shot_workstation}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
only = None
|
|
172
|
+
if "--only" in sys.argv:
|
|
173
|
+
only = sys.argv[sys.argv.index("--only") + 1]
|
|
174
|
+
os.makedirs(DOCS, exist_ok=True)
|
|
175
|
+
for name, fn in SHOTS.items():
|
|
176
|
+
if only and name != only:
|
|
177
|
+
continue
|
|
178
|
+
print("生成 %s ..." % name)
|
|
179
|
+
fn()
|
|
180
|
+
originlab.close_pages("graph")
|
|
181
|
+
originlab.close_pages("workbook")
|
|
182
|
+
originlab.close_pages("matrix")
|
|
183
|
+
print("完成,图片在 %s" % DOCS)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
main()
|