fix(node): auto-sync discovered capabilities
This commit is contained in:
parent
f0afc9e0ca
commit
365b049f1b
|
|
@ -460,11 +460,11 @@ scheduled_jobs(§8.5) channel_bindings(§8.7,判别列+JSONB)
|
|||
|
||||
第一阶段以 `docs/windows-node-mvp-intranet.md` 为实现契约:Windows Node 只作为受控执行节点,通过出站 HTTP/WS 主动连接 zcbot;首批能力固定为 Origin 绘图。长期方案中的 mTLS、Service/DesktopRunner 双进程、完整租约与多节点调度暂不进入 MVP,但 URL path、Node ID、Bearer Header 和任务协议保留原位升级空间。
|
||||
|
||||
云端控制面使用独立的 `software_node_enrollments` 与 `software_nodes`,不复用用户外部系统连接。管理员创建的一次性注册码具有 128 bit 随机熵,数据库只保存 SHA-256 摘要;节点注册在行锁事务中校验有效期、预期名称和允许能力,成功后原子消费。每个节点获得独立高熵 Token,数据库只保存 bcrypt 强哈希,明文仅在注册响应出现一次。
|
||||
云端控制面使用独立的 `software_node_enrollments` 与 `software_nodes`,不复用用户外部系统连接。管理员创建的一次性注册码具有 128 bit 随机熵,数据库只保存 SHA-256 摘要;节点注册在行锁事务中校验有效期、预期名称和服务端支持的能力,成功后原子消费。注册码只建立节点身份,不要求管理员逐项授权专业软件能力。每个节点获得独立高熵 Token,数据库只保存 bcrypt 强哈希,明文仅在注册响应出现一次。
|
||||
|
||||
专业软件采用“共享能力契约 + 本机 adapter”边界。仓库根目录 `software-contracts/*.json` 是语言无关的声明式事实源,描述 capability、请求 JSON Schema、输入额度、输出 manifest、feature 与最低 adapter 版本;Core 按文件签名热加载契约,只负责身份、账本、调度、传输、摘要与最终发布,不包含 Origin 或其他软件的操作分支。热加载先完整校验新快照再原子切换,写入中或非法版本继续使用上一份有效快照。Windows Node 是稳定的可信宿主,只负责持久化 job 目录、下载/上传、恢复、取消,以及从程序目录 `adapters/*/adapter.json` 发现 adapter;Host 对请求只做通用 JSON Schema 校验,具体软件探测和二次语义校验属于 Worker。manifest 声明 capability、实际 adapter 版本、运行类型、入口和契约文件,并可声明 1–1440 分钟的单任务超时;旧 manifest 缺省时保持 30 分钟,版本与时限不编译进 EXE。完整 Node 包按目录自动收集 adapter,并把根目录契约复制到只读 `software-contracts/`;单 adapter 更新包仍可把同一契约副本放在 adapter 目录,加载时本地副本优先,两者都不形成源码第二事实源。Worker 可以是受管 Python 脚本或独立 EXE,进程只接收 job 目录,并以 `--probe` 返回运行状态,通过 `state.json`、`terminal.json` 与固定输出目录交接;Python、COM 或某个 SDK 都不属于通用协议。当前 Origin 和 ANSYS adapter 使用 Python 只是实现选择。
|
||||
|
||||
扩展现有 capability 的 feature 时修改共享契约、对应 Worker 和测试,不修改 Core 调度、Job 生命周期或 Node EXE;增加新专业软件时新增契约与一个 adapter 目录。Cloud 会在下一次契约查询时获得新校验和工具 schema;Node 在启动时从本地目录获得能力、版本和入口。当前选择明确的运维边界:zcbot 契约可热更新,不要求重启;本机 adapter 更新时先退出托盘 Node,整体替换 adapter 目录后重新启动,不重装、不替换 Node EXE,也不建设自动更新平台。当前 Node 仍按整机单执行槽保守串行,未来只有真实并行软件需求出现时,才把 slot 账本升级为 per-capability 租约,而不改变 Job 协议。
|
||||
扩展现有 capability 的 feature 时修改共享契约、对应 Worker 和测试,不修改 Core 调度、Job 生命周期或 Node EXE;增加新专业软件时新增契约与一个 adapter 目录。Cloud 会在下一次契约查询时获得新校验和工具 schema;Node 在启动时从本地目录获得能力、版本和入口,并在首次连接及每次心跳中上报全部已发现 adapter。服务端用本次上报中自己具有共享契约的 capability 原子替换节点能力列表,未知能力不进入调度;管理端只读展示该结果,不保存人工能力选择。现有节点因此不需要清身份或重新注册,升级 Node 后重启一次即可在下一次心跳同步。当前选择明确的运维边界:zcbot 契约可热更新,不要求重启;本机 adapter 更新时先退出托盘 Node,整体替换 adapter 目录后重新启动,不重装、不替换 Node EXE,也不建设自动更新平台。当前 Node 仍按整机单执行槽保守串行,未来只有真实并行软件需求出现时,才把 slot 账本升级为 per-capability 租约,而不改变 Job 协议。
|
||||
|
||||
Node 通过 `Authorization: Bearer` 与 `X-Node-Id` 建立 `/v1/software-nodes/connect` WebSocket。进程内 Connection Manager 保证同一节点单活,新连接关闭旧连接;`hello`/`heartbeat` 更新版本、容量、软件健康与最后在线时间。管理员禁用节点时先持久化禁用态,再关闭现有连接;断线收尾不得覆盖禁用态。当前单活只覆盖单 Web 进程,生产启用多实例前必须增加 Redis/PG fencing 或将 Node API 固定路由到单一控制面实例。
|
||||
|
||||
|
|
@ -486,7 +486,7 @@ Recipe 是专业软件的声明式目标状态,不是任意脚本或逐次鼠
|
|||
|
||||
Job 完成后的对话闭环复用 `software_jobs` 账本,不增加通用事件表。提交时用 `completion_action=report|analyze` 区分“只报告产物”和“报告并分析”,未明确分析时默认 `report`;成功终态将 `followup_status` 置为 `pending`。分发器只在原 task 空闲时领取:`report` 直接写入带 artifact refs 的固定 assistant 消息,不产生模型费用;`analyze` 写入前端隐藏的内部完成事件,并复用 task 单活锁、持久消息和 SSE run 自动续跑 Agent。`pending/running/completed/failed` 状态使服务重启、对话繁忙和重复完成回执均不会造成重复回复;Job 中心的手动深入分析通过同一入口重新排入分析,而不是模拟点击或只预填输入框。
|
||||
|
||||
ANSYS 第二能力第一阶段新增 `ansys.mechanical.static_structural@v1`,目标固定为 Windows 上的 Mechanical 2024 R2(revision 242)。契约只接受一份已登记 STEP/Parasolid/IGES 几何、显式单位、内置 Structural Steel、全局网格尺寸、Named Selection 固定支撑、力/压力和三类结果,不接受 Mechanical 工程、Python、APDL、Journal、URL 或本机路径;输出命名固定为 MECHDAT、汇总/结果表、应力/变形图、solver log 和 `.meta` 溯源。adapter 使用独立 `runtimes/ansys` Python 3.12 环境与 240 分钟 Worker 超时,不与 Origin 依赖混用。2024 R2 没有新版 PyMechanical 的进程内许可证管理,周期 probe 只做 v242 安装和绑定探测,真实许可证通过首次求解验收;在专用节点完成连续算例、取消、残留进程和许可证释放检查前,由机器级验收门保持 `health=unavailable`,且 capability 不默认加入注册码。
|
||||
ANSYS 第二能力第一阶段新增 `ansys.mechanical.static_structural@v1`,目标固定为 Windows 上的 Mechanical 2024 R2(revision 242)。契约只接受一份已登记 STEP/Parasolid/IGES 几何、显式单位、内置 Structural Steel、全局网格尺寸、Named Selection 固定支撑、力/压力和三类结果,不接受 Mechanical 工程、Python、APDL、Journal、URL 或本机路径;输出命名固定为 MECHDAT、汇总/结果表、应力/变形图、solver log 和 `.meta` 溯源。adapter 使用独立 `runtimes/ansys` Python 3.12 环境与 240 分钟 Worker 超时,不与 Origin 依赖混用。2024 R2 没有新版 PyMechanical 的进程内许可证管理,周期 probe 只做 v242 安装和绑定探测,真实许可证通过首次求解验收;adapter 会被节点自动发现和上报,但在专用节点完成连续算例、取消、残留进程和许可证释放检查前,由机器级验收门保持 `health=unavailable`,不会进入实际调度。
|
||||
|
||||
后续仍需实现 Token 轮换;不得以任意命令或脚本接口临时代替。当前 Job 中心采用轮询而非用户事件推送,单活与 offer 选择仍只覆盖单 Web 进程;生产启用多实例前必须增加 Redis/PG fencing 或固定路由到单一控制面实例。
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
---
|
||||
## 已完成关键能力
|
||||
|
||||
- **08-18 / Unreleased / Windows Node 能力自动同步**:注册码只建立节点身份,不再要求管理员选择 Origin/ANSYS 能力;Node 从 adapter registry 自动发现全部本机能力,并在首次连接及每次心跳上报,服务端以双方已有共享契约的能力覆盖节点调度列表,管理后台改为只读展示。现有节点无需清身份或重新注册,替换新版 Node 并重启一次即可同步。
|
||||
|
||||
- **08-18 / Unreleased / Windows Node 多 runtime 安装修复**:修复统一 BAT 安装器在连续处理 Origin 与 ANSYS 时依赖跨子程序 `PYTHON_EXE/PYTHON_ARGS` 状态、第二个 venv 可能执行成空命令的问题;每个缺失 runtime 现在都直接用已验证的显式 Python 3.12 命令创建,不再共享可变命令状态。
|
||||
|
||||
- **08-18 / Unreleased / ANSYS Mechanical 2024 R2 Node 第一阶段**:新增非默认注册的 `ansys.mechanical.static_structural@v1`,以单几何、显式单位、Structural Steel、Named Selection 支撑/力/压力和受控结果建立声明式静力契约,固定 Worker 已完成 v242/PyMechanical probe、二次语义校验及真机验收门,尚未开放真实求解。Windows Node 打包改为自动收集多个 adapter 和共享契约,manifest 可声明每软件 1–1440 分钟超时且兼容旧版 30 分钟默认值,统一安装器为 Origin/ANSYS 创建隔离 Python 3.12 runtime;68 项专项 unittest、Ruff 致命规则与 .NET build 通过,全量 659 项仅 3 个既有数据库集成模块因测试库缺少 `users` 表未通过(另跳过 4 项),未连接 ANSYS 或数据库。
|
||||
|
|
|
|||
2
RUN.md
2
RUN.md
|
|
@ -1100,7 +1100,7 @@ install-windows-node.bat
|
|||
|
||||
若 Python 未加入 PATH,可把绝对路径作为第一个参数,例如 `install-windows-node.bat "C:\Python312\python.exe"`。默认解释器分别为 `%ProgramData%\Zcbot\WindowsNode\runtimes\origin\Scripts\python.exe` 与 `%ProgramData%\Zcbot\WindowsNode\runtimes\ansys\Scripts\python.exe`。如需使用其他受管解释器,使用机器级 `ZCBOT_ADAPTER_ORIGIN_PYTHON` 或 `ZCBOT_ADAPTER_ANSYS_PYTHON`;旧名 `ZCBOT_ORIGIN_PYTHON` 暂时兼容。`node.json`、可恢复任务和 runtime 集中保存在 `%ProgramData%\Zcbot\WindowsNode\`,不会因替换程序目录而丢失。运行时固定依赖见各 adapter 的 `requirements.txt`;任务请求无权选择解释器、脚本或路径。Origin Worker 支持 CSV/XLSX/JSON 输入及 OPJU/PNG/SVG/PDF 输出,成功产物发布到 `origin/<job_id>/`,plot spec 与 provenance 位于其 `.meta/`;上传中断会在重连时幂等续传。
|
||||
|
||||
ANSYS 第一阶段能力为 `ansys.mechanical.static_structural@v1`,只面向 Mechanical 2024 R2(revision 242),且注册码必须由管理员显式允许该 capability。当前交付先完成契约、安装探测、请求语义校验和执行安全门,默认 probe 返回不可用;在目标机器完成真实许可证与基准算例验收前,不得设置 `ZCBOT_ANSYS_242_VALIDATED=1`。验收至少包括:一个带 `fixed_face/load_face` Named Selection 的 STEP/Parasolid 几何能够完成线性静力求解并导出 MECHDAT、汇总、结果表和应力/变形图;取消能终止完整进程树;连续 20 次执行无残留 Mechanical/solver 进程且许可证均释放。验收通过后才以机器级环境变量开启执行门并重启托盘 Node。当前 Worker 的真实求解实现仍待该专用节点联调,开启安全门不会绕过 `ANSYS_242_SOLVER_IMPLEMENTATION_PENDING`。
|
||||
ANSYS 第一阶段能力为 `ansys.mechanical.static_structural@v1`,只面向 Mechanical 2024 R2(revision 242)。Node 会从 `adapters/` 自动发现并在每次心跳上报全部能力,服务端管理页只读展示双方已有共享契约的能力,不需要管理员勾选,也不需要现有节点清身份或重新注册;更新 Node 后重启一次即可同步。当前交付先完成契约、安装探测、请求语义校验和执行安全门,默认 probe 返回不可用;在目标机器完成真实许可证与基准算例验收前,不得设置 `ZCBOT_ANSYS_242_VALIDATED=1`。验收至少包括:一个带 `fixed_face/load_face` Named Selection 的 STEP/Parasolid 几何能够完成线性静力求解并导出 MECHDAT、汇总、结果表和应力/变形图;取消能终止完整进程树;连续 20 次执行无残留 Mechanical/solver 进程且许可证均释放。验收通过后才以机器级环境变量开启执行门并重启托盘 Node。当前 Worker 的真实求解实现仍待该专用节点联调,开启安全门不会绕过 `ANSYS_242_SOLVER_IMPLEMENTATION_PENDING`。
|
||||
|
||||
Web 用户登录后,文件栏 Job 中心会聚合本人最近任务。活动任务或完成后的自动分析约 4 秒刷新一次,空闲时降为约 30 秒;卡片优先显示状态、所属对话、输入输出、完成策略和耗时,Job ID、输出目录、执行节点及软件版本收在“任务详情”中。成功任务可查看结果目录、复制完整 Job ID、深入分析或重新分析。停止已派发任务是协作取消,状态先显示“正在停止”,Node 在线时立即接收,断线后在下次连接或心跳时重放。Agent 可调用 `software_capability_list`、`register_artifact`、`software_job_submit`、`software_job_status`、`software_job_revise` 和 `software_job_cancel`。Origin 输入必须是 artifact:已有 UUID 可直接提交,普通 task 文件先逐个用相对路径登记;登记不会发布聊天交付卡片。提交工具接收 `inputs`、`operation`、`outputs` 和可选的 `completion_action=report|analyze`:只要求生成或保存时用默认 `report`,任务成功后平台向原对话写入固定产物报告;明确要求解释、总结或结论时用 `analyze`,平台在原 task 空闲后自动续跑 Agent。两者均不阻塞提交轮次。请求支持 1–16 个输入、跨输入系列和多个显式输出;`plot.canvas` 可指定毫米画布,轴可指定范围、步长、尺度、刻度角度/字号、标题字号和网格,`legend` 可控制显隐、位置和字号,`series[].style` 可控制颜色、线宽/线型、点型/点大小和透明度。所有排版字段可选,旧请求保持默认样式。成功状态提供 `output_dir`,正式输出的 artifact 带 `software_job_id`,供结果卡和产物详情展示来源。Node 输出上传的逐任务诊断日志位于 `%ProgramData%\Zcbot\WindowsNode\jobs\<job-id>\logs\node-output-upload.log`;日志包含上传阶段、产物文件名、重试次数和 Windows `HRESULT`,单文件达到 1 MiB 后轮转一份 `.1`,不记录 Node Token 或认证请求头。
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from uuid import UUID, uuid4
|
|||
import bcrypt
|
||||
from sqlalchemy import select
|
||||
|
||||
from core.software_contracts import default_capabilities, supported_capabilities
|
||||
from core.software_contracts import supported_capabilities
|
||||
from core.storage.engine import session_scope
|
||||
from core.storage.models import SoftwareNode, SoftwareNodeEnrollment
|
||||
|
||||
|
|
@ -44,7 +44,9 @@ def create_enrollment(
|
|||
capabilities: list[str] | None = None,
|
||||
ttl_seconds: int = 600,
|
||||
) -> dict:
|
||||
allowed = list(dict.fromkeys(capabilities or default_capabilities()))
|
||||
allowed = list(dict.fromkeys(
|
||||
capabilities if capabilities is not None else sorted(supported_capabilities())
|
||||
))
|
||||
if not allowed or any(item not in supported_capabilities() for item in allowed):
|
||||
raise SoftwareNodeError("unsupported capability")
|
||||
if not 60 <= ttl_seconds <= 3600:
|
||||
|
|
@ -157,6 +159,13 @@ def update_node_runtime(node_id: UUID, *, status: str, runtime: dict) -> None:
|
|||
raise SoftwareNodeError("node is disabled or missing")
|
||||
node.status = status
|
||||
node.runtime = runtime
|
||||
reported = runtime.get("installed_capabilities", runtime.get("capabilities", []))
|
||||
if isinstance(reported, list):
|
||||
supported = supported_capabilities()
|
||||
node.capabilities = list(dict.fromkeys(
|
||||
item for item in reported
|
||||
if isinstance(item, str) and item in supported
|
||||
))
|
||||
node.last_seen_at = datetime.now(timezone.utc)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ from core.software_nodes import (
|
|||
_enrollment_digest,
|
||||
_hash_secret,
|
||||
_verify_secret,
|
||||
create_enrollment,
|
||||
delete_node,
|
||||
update_node_runtime,
|
||||
)
|
||||
from web.routers.software_nodes import NodeConnectionManager, _bearer
|
||||
|
||||
|
|
@ -68,6 +70,47 @@ class SoftwareNodeSecurityTests(unittest.TestCase):
|
|||
)
|
||||
self.assertIn('code=4003, reason="invalid node credentials"', rejection)
|
||||
|
||||
@patch("core.software_nodes.session_scope")
|
||||
def test_enrollment_allows_every_server_supported_capability_by_default(
|
||||
self, session_scope
|
||||
) -> None:
|
||||
session = session_scope.return_value.__enter__.return_value
|
||||
|
||||
result = create_enrollment(uuid4())
|
||||
|
||||
enrollment = session.add.call_args.args[0]
|
||||
self.assertEqual(
|
||||
set(enrollment.allowed_capabilities),
|
||||
{"origin.plot@v2", "ansys.mechanical.static_structural@v1"},
|
||||
)
|
||||
self.assertEqual(set(result["capabilities"]), set(enrollment.allowed_capabilities))
|
||||
|
||||
@patch("core.software_nodes.session_scope")
|
||||
def test_heartbeat_replaces_capabilities_with_auto_discovered_adapters(
|
||||
self, session_scope
|
||||
) -> None:
|
||||
session = session_scope.return_value.__enter__.return_value
|
||||
node = type("Node", (), {"status": "offline", "capabilities": ["origin.plot@v2"]})()
|
||||
session.get.return_value = node
|
||||
runtime = {
|
||||
"installed_capabilities": [
|
||||
"origin.plot@v2",
|
||||
"ansys.mechanical.static_structural@v1",
|
||||
"future.unsupported@v1",
|
||||
"origin.plot@v2",
|
||||
]
|
||||
}
|
||||
|
||||
update_node_runtime(uuid4(), status="online", runtime=runtime)
|
||||
|
||||
self.assertEqual(node.status, "online")
|
||||
self.assertIs(node.runtime, runtime)
|
||||
self.assertEqual(
|
||||
node.capabilities,
|
||||
["origin.plot@v2", "ansys.mechanical.static_structural@v1"],
|
||||
)
|
||||
self.assertIsNotNone(node.last_seen_at)
|
||||
|
||||
|
||||
class SoftwareNodeConnectionTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_new_connection_replaces_old_without_removing_new(self) -> None:
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ class StaticVendorTests(unittest.TestCase):
|
|||
self.assertIn('id="node-enrollment-modal" class="modal"', html)
|
||||
self.assertIn("生成 Windows Node 注册码", html)
|
||||
self.assertIn('"/v1/admin/software-node-enrollments"', admin_js)
|
||||
self.assertIn('capabilities: ["origin.plot@v2"]', admin_js)
|
||||
self.assertNotIn('capabilities: ["origin.plot@v2"]', admin_js)
|
||||
self.assertIn("节点能力由客户端自动发现并上报", html)
|
||||
self.assertIn("runtime.capability_runtime", admin_js)
|
||||
self.assertIn("item.adapter_version", admin_js)
|
||||
self.assertIn("item.software_version", admin_js)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,18 @@ class WindowsNodeSourceTests(unittest.TestCase):
|
|||
):
|
||||
self.assertIn(marker, source)
|
||||
|
||||
def test_runtime_auto_reports_all_discovered_adapter_capabilities(self) -> None:
|
||||
connection = (PROJECT / "NodeConnectionLoop.cs").read_text(encoding="utf-8")
|
||||
config_store = (PROJECT / "NodeConfigStore.cs").read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
"var installedCapabilities = adapters.All.Select(item => item.Capability).ToArray()",
|
||||
connection,
|
||||
)
|
||||
self.assertIn("capabilities = installedCapabilities", connection)
|
||||
self.assertIn("installed_capabilities = installedCapabilities", connection)
|
||||
self.assertNotIn("config.Capabilities.Contains", connection)
|
||||
self.assertNotIn("InstalledCapabilities.Contains", config_store)
|
||||
|
||||
def test_node_does_not_expose_arbitrary_execution_primitives(self) -> None:
|
||||
source = "\n".join(
|
||||
path.read_text(encoding="utf-8")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from uuid import UUID
|
|||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from core.software_contracts import default_capabilities
|
||||
from core.software_contracts import default_capabilities, supported_capabilities
|
||||
|
||||
|
||||
class TaskCreateRequest(BaseModel):
|
||||
|
|
@ -120,7 +120,7 @@ class ExternalSystemCredentialsRequest(BaseModel):
|
|||
|
||||
class SoftwareEnrollmentCreateRequest(BaseModel):
|
||||
expected_name: str = ""
|
||||
capabilities: list[str] = Field(default_factory=lambda: list(default_capabilities()))
|
||||
capabilities: list[str] = Field(default_factory=lambda: sorted(supported_capabilities()))
|
||||
ttl_seconds: int = 600
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@
|
|||
<div class="node-modal-body" id="node-enrollment-inputs">
|
||||
<label for="node-expected-name">限定节点名称(可选)</label>
|
||||
<input id="node-expected-name" maxlength="120" autocomplete="off" placeholder="例如:实验室绘图节点 01" />
|
||||
<p class="node-help">填写后,客户端注册时必须使用完全相同的节点名称。注册码有效期为 10 分钟。</p>
|
||||
<p class="node-help">填写后,客户端注册时必须使用完全相同的节点名称。注册码有效期为 10 分钟;节点能力由客户端自动发现并上报。</p>
|
||||
</div>
|
||||
<div class="node-modal-body" id="node-enrollment-result" hidden>
|
||||
<p class="node-warning">该注册码只能成功使用一次,关闭窗口后无法再次查看。请立即复制并发送到目标电脑。</p>
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ function renderWindowsNodes() {
|
|||
+ `<div><h2>Windows Node(${softwareNodes.length})</h2><div class="node-help">查看节点状态;禁用会立即断开节点并拒绝后续连接。</div></div>`
|
||||
+ `<button id="node-enrollment-open" class="primary" type="button">生成 Windows Node 注册码</button>`
|
||||
+ `</div><div class="scroll-x"><table><thead><tr><th>节点</th><th>状态</th><th>运行环境</th>`
|
||||
+ `<th>版本</th><th>能力</th><th>最近心跳</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table></div></div>`;
|
||||
+ `<th>版本</th><th>自动发现能力</th><th>最近心跳</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table></div></div>`;
|
||||
$("node-enrollment-open").onclick = openNodeEnrollmentModal;
|
||||
$("s-windows-node").querySelectorAll("[data-node-toggle]").forEach(button => {
|
||||
button.onclick = () => toggleSoftwareNode(button);
|
||||
|
|
@ -335,7 +335,6 @@ async function createNodeEnrollment(e) {
|
|||
try {
|
||||
const result = await apiSend("POST", "/v1/admin/software-node-enrollments", {
|
||||
expected_name: $("node-expected-name").value.trim(),
|
||||
capabilities: ["origin.plot@v2"],
|
||||
ttl_seconds: 600,
|
||||
});
|
||||
$("node-enrollment-code").textContent = result.enrollment_code || "";
|
||||
|
|
|
|||
|
|
@ -55,13 +55,9 @@ internal sealed class NodeConfigStore(NodePaths paths)
|
|||
if (stored.NodeId == Guid.Empty
|
||||
|| stored.InstallId == Guid.Empty
|
||||
|| string.IsNullOrWhiteSpace(stored.NodeName)
|
||||
|| string.IsNullOrWhiteSpace(token)
|
||||
|| stored.Capabilities.Count == 0
|
||||
|| stored.Capabilities.Any(
|
||||
item => !NodeAdapterRegistry.InstalledCapabilities.Contains(
|
||||
item, StringComparer.Ordinal)))
|
||||
|| string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
throw new NodeConfigurationException("Node configuration contains an invalid identity or capability.");
|
||||
throw new NodeConfigurationException("Node configuration contains an invalid identity.");
|
||||
}
|
||||
return new NodeConfig(
|
||||
NodeUri.NormalizeServerUrl(stored.ServerUrl),
|
||||
|
|
|
|||
|
|
@ -484,7 +484,6 @@ internal sealed class NodeConnectionLoop(NodeConfig config, Action<NodeStatus>?
|
|||
{
|
||||
var root = Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\";
|
||||
var capabilityRuntime = adapters.All
|
||||
.Where(item => config.Capabilities.Contains(item.Capability, StringComparer.Ordinal))
|
||||
.ToDictionary(
|
||||
item => item.Capability,
|
||||
item =>
|
||||
|
|
@ -504,6 +503,7 @@ internal sealed class NodeConnectionLoop(NodeConfig config, Action<NodeStatus>?
|
|||
detail = runtime.Detail,
|
||||
};
|
||||
});
|
||||
var installedCapabilities = adapters.All.Select(item => item.Capability).ToArray();
|
||||
// Keep the legacy top-level Origin payload during its deprecation window.
|
||||
var originAdapter = adapters.Find("origin.plot@v2");
|
||||
var origin = originAdapter?.DetectRuntime()
|
||||
|
|
@ -515,7 +515,8 @@ internal sealed class NodeConnectionLoop(NodeConfig config, Action<NodeStatus>?
|
|||
node_name = config.NodeName,
|
||||
node_version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.1.0",
|
||||
os_version = RuntimeInformation.OSDescription,
|
||||
capabilities = config.Capabilities,
|
||||
capabilities = installedCapabilities,
|
||||
installed_capabilities = installedCapabilities,
|
||||
available_slots = origin.Health == "ready"
|
||||
&& !jobInbox.HasPendingJobs
|
||||
&& !(originAdapter?.HasActiveJobs ?? false) ? 1 : 0,
|
||||
|
|
|
|||
Loading…
Reference in New Issue