diff --git a/docs/ops_center_runtime/HANDOFF_20260419_0233.md b/docs/ops_center_runtime/HANDOFF_20260419_0233.md new file mode 100644 index 0000000..61427c3 --- /dev/null +++ b/docs/ops_center_runtime/HANDOFF_20260419_0233.md @@ -0,0 +1,122 @@ +# HANDOFF 2026-04-19 02:33 + +## 本轮完成了什么 + +本轮只处理了一个问题: + +- Detect 页面没有把大陆节点参与展示出来 + +实际完成: + +1. 后端补出分布式显示口径 +2. 前端 Detect 页面切到优先显示分布式口径 +3. 本地重启 `domaincheck-api` +4. 重新验证 Detect 接口 + +## 本轮关键结果 + +`detect/job/active` 现在新增: + +- `distributed_node_stats` +- `display_items_claimed` +- `display_items_running` + +当前接口结果: + +- `display_items_claimed = 34` +- `display_items_running = 14` + +当前已显示节点: + +- `mainland-controller-01` +- `mainland-worker-01` +- `overseas-control-01` +- `unassigned` + +`detect/queue-summary` 现在新增: + +- `queue.display_claimed = 34` +- `queue.display_running = 14` + +并且 `nodes` 已包含大陆 controller / worker。 + +## 当前最高优先级 + +唯一最高优先级: + +- 检测账本统一 + +原因: + +- 现在页面显示已经基本正常 +- 剩下的不是“看不见”,而是“中央账本和分布式执行账本还没真正合并” + +## 当前状态结论 + +当前可以明确说: + +- 检测能跑 +- 大陆节点确实参与了 +- Detect 页面现在已经能直接看见大陆参与 + +## 当前唯一剩余问题 + +唯一剩余问题: + +- 目前仍是双账本: + - 中央 `detect_job_items` + - 分布式 runtime / projection 显示口径 + +所以: + +- 页面观感已收口 +- 账本一致性还没完全收口 + +## 下一轮如果继续,只做什么 + +下一轮只做这一件事: + +- 明确并实现“中央派发账本”与“大陆执行账本”的统一规则 + +优先原则: + +- 不发散 +- 不扩新页面 +- 不碰发布动作 + +## 推荐模型与推理等级 + +继续推荐: + +- `GPT-5.4` +- `high` + +原因: + +- 下一轮是账本统一策略与最小实现 +- 仍不需要切超高 + +## 给下一轮的任务卡 + +```text +任务名:H1-检测账本统一设计与最小落地批 + +目标: +1. 查清 detect_job_items 与 runtime/projection 的最终一致关系 +2. 决定 pull / ack / result sync 后中央账本如何更新 +3. 只做最小一致性修复,不扩功能 + +范围限制: +1. 不新增页面 +2. 不扩控制面功能 +3. 不进入发布动作 +4. 不并行做别的方向 + +完成判定: +1. Detect 页面显示正确 +2. Detect 中央账本与大陆执行账本不再明显分叉 +``` + +## 一句话结论 + +这轮已经把“大陆在跑但页面像没跑”的问题收掉了;下一轮真正该做的,不是继续修显示,而是把检测账本本身统一起来。 diff --git a/docs/ops_center_runtime/IMPLEMENTATION_STATUS.md b/docs/ops_center_runtime/IMPLEMENTATION_STATUS.md index 55f03ac..86e4f3e 100644 --- a/docs/ops_center_runtime/IMPLEMENTATION_STATUS.md +++ b/docs/ops_center_runtime/IMPLEMENTATION_STATUS.md @@ -1,166 +1,130 @@ # IMPLEMENTATION_STATUS -更新时间:2026-04-19 02:25 CST +更新时间:2026-04-19 02:33 CST ## 当前真实状态 阶段判断: - 海外单脑接管能力:约 `95%` -- 分布式检测真实跑通程度:约 `89%` -- 距离“可稳定上线并放心用后台发起检测”:约 `90%` +- 分布式检测真实执行能力:约 `93%` +- 距离“可稳定上线并放心用后台发起检测”:约 `92%` -当前最重要的新事实: +本轮最重要的新事实: -- 大陆节点接管闭环已完成 -- acceptance 主链已完成 -- `runtime.pull_tasks` 的环境阻塞已解决 -- 大陆检测链已开始真实执行 -- 当前剩余问题已收敛为“检测总览统计口径未统一” +- Detect 页面已经不再只显示海外节点 +- 大陆 controller / worker 已进入 Detect 总览显示口径 +- 用户现在能从 Detect 页面直接看见大陆节点参与执行 -## 本轮代码与环境收口 +## 本轮代码收口 -本轮代码变更: +本轮后端变更: -- 提交:`74dc009` -- 主题:`fix: inherit sync env in node agent` +- `domain-api/app/services/detect_job_service.py` -本轮实际变更点: +本轮前端变更: -- `domain-api/deploy/systemd/domain-node-agent.service` - - 增加: - - `EnvironmentFile=-/etc/default/domaincheck-api` -- `domain-api/deploy/multi-region/templates/domaincheck-node-agent.env.example` - - 补齐: - - `runtime.start_detection` - - `runtime.stop_detection` - - `runtime.pull_tasks` +- `domain-web/src/views/detect/DetectView.vue` -本轮环境动作: +本轮修复方式: -- 已在 `mainland-controller-01` 刷新并重启 `domaincheck-node-agent` -- 已在 `mainland-worker-01` 刷新并重启 `domaincheck-node-agent` -- 已确认两台节点 unit 包含: - - `/etc/default/domaincheck-api` - - `/etc/default/domaincheck-node-agent` - - `SupplementaryGroups=systemd-journal` +- 保留中央账本原值,不硬改 `detect_job_items` +- 额外补出分布式显示口径: + - `distributed_node_stats` + - `display_items_claimed` + - `display_items_running` +- Detect 页面优先显示这个分布式显示口径 ## 关键验证证据 -关键成功证据: +接口验证: -- `mainland-controller-01` 新运维任务: - - `job_id = 87` - - `action = runtime.pull_tasks` - - `status = success` -- 返回结果: - - `pull_state = success` - - `items_total = 50` - - `summary_text = 待检测任务批次拉取并入库成功` +- `detect/job/active` + - `display_items_claimed = 34` + - `display_items_running = 14` + - `distributed_node_stats` 包含: + - `mainland-controller-01` + - `mainland-worker-01` + - `overseas-control-01` +- `detect/queue-summary` + - `queue.display_claimed = 34` + - `queue.display_running = 14` + - `nodes` 已含大陆 controller / worker -远端日志证据: +编译验证: -- `mainland-controller-01` - - `job start: id=87 ... action=runtime.pull_tasks` - - `job complete: ... ok=True` +- 后端: + - `python3 -m py_compile domain-api/app/services/detect_job_service.py` + - 通过 +- 前端: + - `cd domain-web && npm run build` + - 通过 -节点运行证据: +运行验证: -- `mainland-controller-01` - - `participation_state = running` - - `items_running = 5` - - `items_total = 200` -- `mainland-worker-01` - - `participation_state = running` - - `items_running = 5` - - `items_total = 70` +- `domaincheck-api` 已重启成功 +- 活跃接口返回新字段正常 -## 代码闭环 +## 当前已闭合的问题 -当前已经被验证打通的链路: +已闭合: -- detect.start 触发远端动作排队 -- mainland controller 的 sync-agent / pull_tasks 目标地址读取 -- Node Agent 远端执行 `runtime.pull_tasks` -- 大陆 controller 拉批入库 -- 大陆 controller / worker 进入执行态 - -这意味着: - -- “点开始检测后大陆节点完全不参与”这个问题已经被修掉 - -## 外部条件闭环 - -已经闭合: - -- SSH -- node handover -- bootstrap -- register / heartbeat -- acceptance -- journald 权限 -- node-agent 同步目标地址继承 - -当前不再依赖用户补充的外部条件: - -- 不需要再补 `SYNC_TARGET_API_BASE_URL` -- 不需要再手工改 controller 的 node-agent env -- 不需要重装系统 +- 大陆 controller 无法 `pull_tasks` +- Detect 页面误判“大陆没跑” +- Detect 节点表格只显示海外与 `unassigned` ## 当前剩余问题 当前唯一剩余问题: -- 控制面“检测总览”口径仍未和大陆拉批执行口径统一 +- 中央账本与分布式显示口径还不是同一套最终账本 -具体表现: +具体来说: -- `ops/nodes` - - 明确显示大陆 controller / worker 正在执行 -- 但: - - `/api/v1/detect/queue-summary` - - `/api/v1/detect/job/active` - - 仍主要显示旧的: - - `overseas-control-01` - - `unassigned` +- `detect_job_items` + - 仍是中央派发账本 +- `distributed_node_stats` + - 是运行态 / 投影视角 -因此当前判断是: +所以当前页面已经“可观察、可判断”了,但底层仍属于双账本阶段。 -- 执行链路已经通 -- 展示与统计口径还没完全通 - -## 当前是否可以直接上线 +## 当前是否可以继续跑检测测试 当前结论: -- 可以继续做检测功能测试 -- 但还不建议宣称“检测总览已经完全可信、可以直接签收上线” +- 可以继续跑检测任务测试 +- 而且现在 Detect 页面已经能看出大陆节点参与 -原因不是执行失败,而是: +## 当前是否建议直接上线 -- 总览统计仍会让人误判“大陆没跑” +当前结论: + +- 比上一轮更接近上线 +- 但还不建议把“检测调度账本”宣称为最终完全统一 + +原因: + +- 现在解决的是“显示正确性” +- 下一步要解决的是“账本统一性” ## 当前优先级判断 最高优先级: -- 收敛 `detect/queue-summary` / `detect/job/active` 的统计口径 +- 设计并收敛中央账本与分布式执行账本的统一规则 当前不应继续推进: -- 新运维页面 -- 控制面增强 +- 新控制面功能 - 新模块 +- 新页面 - 发布动作 -- 与检测总览收口无关的任何功能 +- 与账本统一无关的工作 ## 完成下一轮后的预期 -如果检测总览口径收敛成功: +如果下一轮把账本统一策略明确并落地: -- 后台将能同时看到: - - 海外触发 - - 大陆拉批 - - 大陆执行 - - 统一进度统计 +- Detect 页面将不仅“看起来对” +- 而且底层统计也会真正一致 - 整体可上线程度预计提升到 `94%~96%` diff --git a/docs/ops_center_runtime/TASK_BOARD.md b/docs/ops_center_runtime/TASK_BOARD.md index e71ec3d..5f315da 100644 --- a/docs/ops_center_runtime/TASK_BOARD.md +++ b/docs/ops_center_runtime/TASK_BOARD.md @@ -1,124 +1,131 @@ # TASK_BOARD -更新时间:2026-04-19 02:25 CST +更新时间:2026-04-19 02:33 CST ## 当前主批次 -唯一主批次:`F1-分布式检测实跑收口批` +唯一主批次:`G1-检测总览口径统一批` 目标: - 不进入新功能实现 -- 不扩展控制面页面 -- 只确认“海外发起检测 -> 大陆 controller 拉任务 -> 大陆节点执行”已经真实跑通 -- 收敛剩余唯一问题:控制面检测总览口径仍停留在旧活跃任务视图 +- 不扩展控制面 +- 只收口 Detect 页面“大陆明明在跑,但页面像没跑”的口径问题 + +## 本轮已完成 + +本轮完成了最小可见修复: + +- `detect/job/active` + - 新增: + - `distributed_node_stats` + - `display_items_claimed` + - `display_items_running` + - `display_items_completed` + - `display_items_failed` + - `display_active_node_codes` +- `detect/queue-summary` + - 新增: + - `queue.display_claimed` + - `queue.display_running` + - `nodes` 已切到分布式显示口径 +- `DetectView.vue` + - 已优先显示分布式口径 + - 节点表格已优先显示 `distributed_node_stats` ## 当前结果快照 -本轮新增完成: +接口验证结果: -- 已修复 `domaincheck-node-agent` 读取不到同步目标地址的问题 -- 修复方式: - - `domaincheck-node-agent.service` 追加读取 `/etc/default/domaincheck-api` - - 新提交:`74dc009` -- 已在以下节点完成刷入与重启: - - `mainland-controller-01` - - `mainland-worker-01` +- `detect/job/active` + - `display_items_claimed = 34` + - `display_items_running = 14` + - `distributed_node_stats` 已包含: + - `mainland-controller-01` + - `mainland-worker-01` + - `overseas-control-01` + - `unassigned` +- `detect/queue-summary` + - `queue.display_claimed = 34` + - `queue.display_running = 14` + - `nodes` 已包含: + - `mainland-controller-01` + - `mainland-worker-01` + - `overseas-control-01` + - `unassigned` -本轮关键验证: +运行侧证据: - `mainland-controller-01` - `runtime.pull_tasks` - `job_id = 87` - `status = success` - - `items_total = 50` - - `pull_state = success` -- controller 远端日志已确认: - - `job start: runtime.pull_tasks` - - `job complete ... ok=True` - -当前节点执行状态: - -- `mainland-controller-01` - - `agent_state = online_busy` - - `participation_state = running` - - `items_total = 200` - - `items_running = 5` -- `mainland-worker-01` - - `agent_state = online_busy` - - `participation_state = running` - - `items_total = 70` - - `items_running = 5` -- `overseas-control-01` - - `agent_state = online_busy` +- 大陆 controller / worker 仍在执行中 ## 当前结论 -检测不是“没跑”。 +这轮已经把“Detect 页面看不出大陆参与”这个核心观感问题修掉了一大半。 -当前真实状态是: +现在的 Detect 页面口径变成: -- 分布式检测执行链已经跑通 -- 大陆 controller 已经能从海外控制面主动拉回待检测批次 -- 大陆 controller 与 mainland worker 都已经进入执行中 +- 中央原始账本仍保留: + - `items_total = 1000` + - `pending = 950` + - `claimed = 25` + - `running = 4` +- 页面展示口径新增分布式视角: + - `display_claimed = 34` + - `display_running = 14` + - 节点列表能直接看到大陆 controller / worker -当前未完全收敛的不是执行,而是统计口径: +## 当前唯一剩余问题 -- `/api/v1/detect/queue-summary` -- `/api/v1/detect/job/active` +唯一剩余问题: -仍主要显示旧的海外活跃任务统计: +- 中央账本与分布式显示口径仍是“双轨” -- `overseas-control-01` -- `unassigned` +这不是 bug,而是当前架构现状: -尚未把大陆侧“拉批后本地执行”的结果完整并回收到同一检测总览口径中。 +- 中央 `detect_job_items` 统计的是海外控制面原始派发账本 +- 大陆拉批后的本地执行账本在 runtime / projection 侧 -## 当前唯一主问题 +当前已经做到: -唯一主问题:`检测执行已跑通,但控制面检测总览仍是旧口径` +- 页面不再误判“大陆没跑” -表现: +但还没做到: -- `ops/nodes` 能看到大陆节点正在跑 -- `runtime.pull_tasks` 已成功 -- 但 `detect/queue-summary` 与 `detect/job/active` 仍没有把大陆执行量体现在同一总览里 - -问题分类: - -- 不是接管问题 -- 不是环境问题 -- 不是同步目标地址问题 -- 是“检测总览聚合口径 / 运行态映射”问题 +- 把两套账本真正收敛成一个最终一致的统一任务账本 ## 候选批次 -### Candidate G1 +### Candidate H1 -名称:检测总览口径统一批 +名称:检测账本统一设计批 进入条件: -- 用户确认下一轮继续只收敛检测总览,不扩其他功能 +- 用户确认下一轮继续深入“中央账本与分布式执行账本统一” 范围: -- 复核 `detect job` 与 `ops/nodes` 的数据来源差异 -- 明确大陆拉批执行应如何回写到统一检测总览 -- 修复后再做一次检测实跑验证 +- 先设计统一账本规则 +- 不直接大改执行链 +- 明确 pull / ack / result sync 后中央账本应如何回写 -### Candidate G2 +### Candidate H2 -名称:节点能力口径清理批 +名称:Detect 页面文案微收口批 进入条件: -- 需要把节点展示中的 `capabilities` 与现网真实支持动作同步 +- 如果用户只想先把页面提示说清楚 范围: -- 只处理 `OPS_AGENT_CAPABILITIES` 的现网漂移 -- 不碰检测链路主体 +- 在 Detect 页面明确标注: + - “中央账本视角” + - “分布式执行视角” ## 暂停项 @@ -128,12 +135,10 @@ - 新模块 - 运维中枢增强 - 发布动作 -- 与“检测实跑收口”无关的任何工作 +- 与检测账本统一无关的工作 ## 下一轮唯一动作 -下一轮唯一应该做的事情: +下一轮唯一应该继续做的事情: -- 对齐 `detect/queue-summary`、`detect/job/active` 与 `ops/nodes` 三者的数据口径 -- 明确为什么大陆执行已发生,但总览仍显示旧任务统计 -- 修好后再跑一次小批量检测验证 +- 明确中央 `detect_job_items` 与大陆 runtime 投影之间的统一账本策略 diff --git a/domain-api/app/services/detect_job_service.py b/domain-api/app/services/detect_job_service.py index 9dd45fe..779fde1 100644 --- a/domain-api/app/services/detect_job_service.py +++ b/domain-api/app/services/detect_job_service.py @@ -11,6 +11,7 @@ from app.services.debug_event_service import push_debug_event ACTIVE_JOB_STATUSES = ("pending", "running") +_RUNTIME_NODE_STALE_MINUTES = 10 def _selection_sql() -> str: @@ -40,6 +41,152 @@ def _decode_payload(value: object) -> dict: return {} +def _int_value(value: object) -> int: + try: + return int(value or 0) + except Exception: + return 0 + + +def _build_runtime_display_bucket(row: tuple) -> dict | None: + node_code = str(row[0] or "").strip() + if not node_code: + return None + metadata = _decode_payload(row[5]) + items_total = _int_value(metadata.get("job_items_total")) + items_claimed = _int_value(metadata.get("job_items_claimed")) + items_running = _int_value(metadata.get("job_items_running")) + items_completed = _int_value(metadata.get("job_items_completed")) + items_failed = _int_value(metadata.get("job_items_failed")) + current_load = _int_value(row[4]) + detect_participating = bool(metadata.get("detect_participating", False)) + if items_total <= 0 and items_claimed <= 0 and items_running <= 0 and items_completed <= 0 and items_failed <= 0 and current_load <= 0 and not detect_participating: + return None + items_pending = max(items_total - items_claimed - items_running - items_completed - items_failed, 0) + return { + "node_code": node_code, + "items_total": items_total, + "items_pending": items_pending, + "items_claimed": items_claimed, + "items_running": items_running, + "items_completed": items_completed, + "items_blacklisted": 0, + "items_failed": items_failed, + "metrics_source": str(metadata.get("service") or "runtime").strip() or "runtime", + "region": str(row[1] or "").strip(), + "role": str(row[2] or "").strip(), + "status": str(row[3] or "").strip(), + "current_load": current_load, + "last_heartbeat_at": _format_time(row[6]) if row[6] else "", + } + + +def _merge_display_node_stats(*, local_node_stats: list[dict], runtime_node_rows: list[tuple]) -> list[dict]: + merged: dict[str, dict] = {} + + def _ensure_bucket(node_code: str) -> dict: + return merged.setdefault( + node_code, + { + "node_code": node_code, + "items_total": 0, + "items_pending": 0, + "items_claimed": 0, + "items_running": 0, + "items_completed": 0, + "items_blacklisted": 0, + "items_failed": 0, + "metrics_source": "", + "region": "", + "role": "", + "status": "", + "current_load": 0, + "last_heartbeat_at": "", + }, + ) + + for item in list(local_node_stats or []): + node_code = str(item.get("node_code") or "").strip() + if not node_code: + continue + bucket = _ensure_bucket(node_code) + for key in ( + "items_total", + "items_pending", + "items_claimed", + "items_running", + "items_completed", + "items_blacklisted", + "items_failed", + ): + bucket[key] = max(_int_value(bucket.get(key)), _int_value(item.get(key))) + if node_code == "unassigned": + bucket["metrics_source"] = "central_queue" + + for row in list(runtime_node_rows or []): + runtime_bucket = _build_runtime_display_bucket(row) + if not runtime_bucket: + continue + node_code = str(runtime_bucket.get("node_code") or "").strip() + bucket = _ensure_bucket(node_code) + for key in ( + "items_total", + "items_pending", + "items_claimed", + "items_running", + "items_completed", + "items_blacklisted", + "items_failed", + "current_load", + ): + bucket[key] = max(_int_value(bucket.get(key)), _int_value(runtime_bucket.get(key))) + for key in ("metrics_source", "region", "role", "status", "last_heartbeat_at"): + if str(runtime_bucket.get(key) or "").strip(): + bucket[key] = runtime_bucket.get(key) + + return sorted( + merged.values(), + key=lambda item: ( + str(item.get("node_code") or "") == "unassigned", + -_int_value(item.get("items_running")), + -_int_value(item.get("items_claimed")), + -_int_value(item.get("items_total")), + str(item.get("node_code") or ""), + ), + ) + + +def _load_runtime_display_rows(cur) -> list[tuple]: + cur.execute( + f""" + SELECT node_code, region, role, status, current_load, metadata_json, last_heartbeat_at + FROM detect_worker_nodes + WHERE last_heartbeat_at >= CURRENT_TIMESTAMP - interval '{_RUNTIME_NODE_STALE_MINUTES} minutes' + ORDER BY last_heartbeat_at DESC, node_code ASC + """ + ) + return list(cur.fetchall()) + + +def _build_display_summary(node_stats: list[dict]) -> dict: + effective_nodes = [ + item + for item in list(node_stats or []) + if str(item.get("node_code") or "").strip() and str(item.get("node_code") or "").strip() != "unassigned" + ] + return { + "items_claimed": sum(_int_value(item.get("items_claimed")) for item in effective_nodes), + "items_running": sum(_int_value(item.get("items_running")) for item in effective_nodes), + "items_completed": sum(_int_value(item.get("items_completed")) for item in effective_nodes), + "items_failed": sum(_int_value(item.get("items_failed")) for item in effective_nodes), + "active_nodes": [ + str(item.get("node_code") or "").strip() + for item in effective_nodes + if _int_value(item.get("items_claimed")) > 0 or _int_value(item.get("items_running")) > 0 + ], + } + + def _extract_current_cycle_events(events: list[dict]) -> tuple[str, list[dict]]: if not events: return "", [] @@ -142,6 +289,11 @@ def _fetch_job_summary(cur, job_row, event_limit: int = 20) -> dict: for item in cur.fetchall() ] cycle_token, current_cycle_events = _extract_current_cycle_events(events) + distributed_node_stats = _merge_display_node_stats( + local_node_stats=list(node_buckets.values()), + runtime_node_rows=_load_runtime_display_rows(cur), + ) + display_summary = _build_display_summary(distributed_node_stats) total = sum(counts.values()) terminal = int(counts.get("completed", 0)) + int(counts.get("blacklisted", 0)) + int(counts.get("failed", 0)) return { @@ -163,6 +315,12 @@ def _fetch_job_summary(cur, job_row, event_limit: int = 20) -> dict: "items_terminal": terminal, "progress_percent": round((terminal / total) * 100, 2) if total else 0, "node_stats": list(node_buckets.values()), + "distributed_node_stats": distributed_node_stats, + "display_items_claimed": int(display_summary.get("items_claimed", 0) or 0), + "display_items_running": int(display_summary.get("items_running", 0) or 0), + "display_items_completed": int(display_summary.get("items_completed", 0) or 0), + "display_items_failed": int(display_summary.get("items_failed", 0) or 0), + "display_active_node_codes": list(display_summary.get("active_nodes") or []), "recent_events": events, "latest_event": events[0] if events else None, "current_cycle_token": cycle_token, @@ -350,6 +508,27 @@ def get_detect_queue_health(window_minutes: int = 15) -> dict: } for item in active_job.get("node_stats") or [] } + distributed_nodes = list(active_job.get("distributed_node_stats") or []) + if distributed_nodes: + node_map = { + str(item.get("node_code") or "unknown"): { + "node_code": str(item.get("node_code") or "unknown"), + "items_total": _int_value(item.get("items_total")), + "items_pending": _int_value(item.get("items_pending")), + "items_claimed": _int_value(item.get("items_claimed")), + "items_running": _int_value(item.get("items_running")), + "items_completed": _int_value(item.get("items_completed")), + "items_blacklisted": _int_value(item.get("items_blacklisted")), + "items_failed": _int_value(item.get("items_failed")), + "processed_recent": 0, + "processed_per_minute": 0, + "completed_recent": 0, + "blacklisted_recent": 0, + "failed_recent": 0, + "metrics_source": str(item.get("metrics_source") or "").strip(), + } + for item in distributed_nodes + } total_processed_recent = 0 total_completed_recent = 0 total_blacklisted_recent = 0 @@ -412,6 +591,8 @@ def get_detect_queue_health(window_minutes: int = 15) -> dict: "pending": int(active_job.get("items_pending", 0) or 0), "claimed": int(active_job.get("items_claimed", 0) or 0), "running": int(active_job.get("items_running", 0) or 0), + "display_claimed": int(active_job.get("display_items_claimed", active_job.get("items_claimed", 0)) or 0), + "display_running": int(active_job.get("display_items_running", active_job.get("items_running", 0)) or 0), "completed": int(active_job.get("items_completed", 0) or 0), "blacklisted": int(active_job.get("items_blacklisted", 0) or 0), "failed": int(active_job.get("items_failed", 0) or 0), diff --git a/domain-web/src/views/detect/DetectView.vue b/domain-web/src/views/detect/DetectView.vue index e821ee6..07a2593 100644 --- a/domain-web/src/views/detect/DetectView.vue +++ b/domain-web/src/views/detect/DetectView.vue @@ -147,8 +147,8 @@ - - + + @@ -215,7 +215,7 @@ - + @@ -423,12 +423,23 @@ const restoreLastAction = () => { const runs = computed(() => status.value.runs || []); const activeJob = computed(() => status.value.active_job || null); +const activeJobDisplayClaimed = computed(() => Number(activeJob.value?.display_items_claimed ?? activeJob.value?.items_claimed ?? 0)); +const activeJobDisplayRunning = computed(() => Number(activeJob.value?.display_items_running ?? activeJob.value?.items_running ?? 0)); +const activeJobDisplayNodeStats = computed(() => { + const distributed = activeJob.value?.distributed_node_stats; + if (Array.isArray(distributed) && distributed.length) { + return distributed; + } + return Array.isArray(activeJob.value?.node_stats) ? activeJob.value.node_stats : []; +}); const activeJobSummaryText = computed(() => { if (!activeJob.value) { return "当前暂无活跃检测任务"; } const job = activeJob.value; - return `状态:${jobStatusText(job.status)} / 总数 ${job.items_total || 0} / 待领 ${job.items_pending || 0} / 执行中 ${job.items_running || 0}`; + const displayClaimed = Number(job.display_items_claimed ?? job.items_claimed ?? 0); + const displayRunning = Number(job.display_items_running ?? job.items_running ?? 0); + return `状态:${jobStatusText(job.status)} / 总数 ${job.items_total || 0} / 待领 ${job.items_pending || 0} / 已领 ${displayClaimed} / 执行中 ${displayRunning}`; }); const selectedRun = computed(() => runs.value.find((item) => item.run_id === selectedRunId.value) || runs.value[0] || null); const currentPhaseLabel = computed(() => selectedRun.value?.phase_label || (status.value.worker_online ? "运行中" : "未启动")); @@ -535,8 +546,10 @@ const queueAlertText = computed(() => { } const queue = queueSummary.value.queue || {}; const throughput = queueSummary.value.throughput || {}; + const displayClaimed = Number(queue.display_claimed ?? queue.claimed ?? 0); + const displayRunning = Number(queue.display_running ?? queue.running ?? 0); const parts = [ - `当前队列待领 ${queue.pending || 0} / 已领 ${queue.claimed || 0} / 执行中 ${queue.running || 0}`, + `当前队列待领 ${queue.pending || 0} / 已领 ${displayClaimed} / 执行中 ${displayRunning}`, `近 ${queueSummary.value.window_minutes || 15} 分钟处理 ${throughput.processed_recent || 0} 项,约 ${throughput.processed_per_minute || 0} 项/分钟` ]; if ((queue.overdue_leases || 0) > 0) {