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}` + `