diff --git a/CHANGELOG.md b/CHANGELOG.md
index d10e291..478d403 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@
## Unreleased
+- Windows Node 不再错误显示 Origin 运行状态中的中文;本机窗口与 zcbot 管理页会同时显示 Node、Adapter 及从本机程序读取的 Origin 软件版本。
+
- Windows Node 的专业软件适配器现在可作为独立目录更新;后续扩展 Origin 绘图参数时,可只替换适配器并重启本机 Node,无需重装或替换 Node 程序,服务端契约更新也无需重启 zcbot。
- Origin 绘图新增出版排版控制:可指定毫米画布、坐标范围与对数尺度、刻度角度和字号、标题/图例字号、网格,以及每条曲线的颜色、线型、点型和透明度;旧绘图请求继续沿用默认样式。
diff --git a/tests/test_origin_worker.py b/tests/test_origin_worker.py
index bbf1a46..4fc9dd8 100644
--- a/tests/test_origin_worker.py
+++ b/tests/test_origin_worker.py
@@ -5,6 +5,7 @@ import json
import tempfile
import unittest
from pathlib import Path
+from unittest.mock import patch
WORKER_PATH = (
Path(__file__).resolve().parents[1] / "windows-node" / "origin-worker" / "worker.py"
@@ -35,6 +36,42 @@ class OriginWorkerUnitTests(unittest.TestCase):
manifest = json.loads(ADAPTER_MANIFEST_PATH.read_text(encoding="utf-8"))
self.assertEqual(worker.ADAPTER_VERSION, manifest["adapter_version"])
+ def test_registered_origin_version_comes_from_com_server_executable(self) -> None:
+ class FakeKey:
+ def __init__(self, path: str):
+ self.path = path
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *_):
+ return None
+
+ class FakeRegistry:
+ HKEY_CLASSES_ROOT = object()
+ values = {
+ r"Origin.ApplicationSI\CLSID": "{origin-clsid}",
+ r"CLSID\{origin-clsid}\LocalServer32": (
+ r'"C:\Program Files\OriginLab\Origin2025\Origin64.exe" /Automation'
+ ),
+ }
+
+ @classmethod
+ def OpenKey(cls, _root, path):
+ return FakeKey(path)
+
+ @classmethod
+ def QueryValueEx(cls, key, _name):
+ return cls.values[key.path], 1
+
+ with patch.object(worker, "_windows_file_version", return_value="2025") as read:
+ self.assertEqual(worker._registered_origin_version(FakeRegistry), "2025")
+ read.assert_called_once_with(
+ Path(r"C:\Program Files\OriginLab\Origin2025\Origin64.exe")
+ )
+ FakeRegistry.values[r"CLSID\{origin-clsid}\LocalServer32"] = "invalid"
+ self.assertIsNone(worker._registered_origin_version(FakeRegistry))
+
def test_csv_and_json_inputs_are_read_without_origin(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
diff --git a/tests/test_static_vendor.py b/tests/test_static_vendor.py
index 534c6db..fdd1eda 100644
--- a/tests/test_static_vendor.py
+++ b/tests/test_static_vendor.py
@@ -63,7 +63,10 @@ class StaticVendorTests(unittest.TestCase):
self.assertIn("生成 Windows Node 注册码", html)
self.assertIn('"/v1/admin/software-node-enrollments"', admin_js)
self.assertIn('capabilities: ["origin.plot@v2"]', admin_js)
- self.assertIn('origin.health === "ready"', admin_js)
+ self.assertIn("runtime.capability_runtime", admin_js)
+ self.assertIn("item.adapter_version", admin_js)
+ self.assertIn("item.software_version", admin_js)
+ self.assertIn('runtime.origin ? [["origin.plot@v2", runtime.origin]]', admin_js)
self.assertIn("ttl_seconds: 600", admin_js)
self.assertIn("navigator.clipboard.writeText(value)", admin_js)
self.assertIn('apiGet("/v1/admin/software-nodes")', admin_js)
diff --git a/tests/test_windows_node_source.py b/tests/test_windows_node_source.py
index ce24578..58224cf 100644
--- a/tests/test_windows_node_source.py
+++ b/tests/test_windows_node_source.py
@@ -172,6 +172,11 @@ class WindowsNodeSourceTests(unittest.TestCase):
self.assertIn('r"Origin.ApplicationSI\\CLSID"', worker)
self.assertIn('["--probe"]', runner)
self.assertIn('root.GetProperty("adapter_version")', runner)
+ self.assertIn('startInfo.Environment["PYTHONUTF8"] = "1"', runner)
+ self.assertIn('startInfo.Environment["PYTHONIOENCODING"] = "utf-8"', runner)
+ form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
+ self.assertIn('Adapter {adapter.AdapterVersion}', form)
+ self.assertIn('Origin 版本未知', form)
self.assertIn(
"&& !jobInbox.HasPendingJobs",
connection,
diff --git a/web/static/js/admin.js b/web/static/js/admin.js
index 8771ff8..def8a54 100644
--- a/web/static/js/admin.js
+++ b/web/static/js/admin.js
@@ -166,17 +166,40 @@ function nodeStatusHTML(status) {
return `${labels[value]}`;
}
+function nodeCapabilityRuntimes(runtime) {
+ const entries = Object.entries(runtime.capability_runtime || {})
+ .filter(([, value]) => value && typeof value === "object");
+ if (entries.length) return entries;
+ return runtime.origin ? [["origin.plot@v2", runtime.origin]] : [];
+}
+
+function nodeSoftwareName(capability, item) {
+ if (item.software === "OriginPro" || capability.startsWith("origin.")) return "Origin";
+ return item.software || capability;
+}
+
function renderWindowsNodes() {
const rows = softwareNodes.map(node => {
const runtime = node.runtime || {};
- const origin = runtime.origin || {};
- const originState = origin.health === "ready" ? "Origin 可用" : "Origin 不可用";
- const originVersion = origin.software_version ? ` ${origin.software_version}` : "";
+ const capabilityRuntimes = nodeCapabilityRuntimes(runtime);
const runtimeParts = [
node.os_version || "",
runtime.desktop_session === true ? "桌面会话" : "",
runtime.available_slots != null ? `可用槽位 ${runtime.available_slots}` : "",
- runtime.origin ? `${originState}${originVersion}` : "",
+ ...capabilityRuntimes.map(([capability, item]) => {
+ const name = nodeSoftwareName(capability, item);
+ return `${name}${item.health === "ready" ? "可用" : "不可用"}`;
+ }),
+ ].filter(Boolean);
+ const versionParts = [
+ node.node_version ? `Node ${node.node_version}` : "",
+ ...capabilityRuntimes.flatMap(([capability, item]) => {
+ const name = nodeSoftwareName(capability, item);
+ return [
+ item.adapter_version ? `${name} Adapter ${item.adapter_version}` : "",
+ item.software_version ? `${name} ${item.software_version}` : "",
+ ];
+ }),
].filter(Boolean);
const lastSeen = node.last_seen_at
? `${escapeHtml(fmtTimeAgo(node.last_seen_at))}`
@@ -187,7 +210,7 @@ function renderWindowsNodes() {
+ `
${escapeHtml(node.node_id)}
`
+ `${nodeStatusHTML(node.status)} | `
+ `${escapeHtml(runtimeParts.join(" · ") || "—")} | `
- + `${escapeHtml(node.node_version || "—")} | `
+ + `${escapeHtml(versionParts.join(" · ") || "—")} | `
+ `${(node.capabilities || []).map(x => `${escapeHtml(x)}`).join(" ") || "—"} | `
+ `${lastSeen} | `
+ ` |