fix: surface distributed detect participation
This commit is contained in:
122
docs/ops_center_runtime/HANDOFF_20260419_0233.md
Normal file
122
docs/ops_center_runtime/HANDOFF_20260419_0233.md
Normal file
@@ -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 中央账本与大陆执行账本不再明显分叉
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一句话结论
|
||||||
|
|
||||||
|
这轮已经把“大陆在跑但页面像没跑”的问题收掉了;下一轮真正该做的,不是继续修显示,而是把检测账本本身统一起来。
|
||||||
@@ -1,166 +1,130 @@
|
|||||||
# IMPLEMENTATION_STATUS
|
# IMPLEMENTATION_STATUS
|
||||||
|
|
||||||
更新时间:2026-04-19 02:25 CST
|
更新时间:2026-04-19 02:33 CST
|
||||||
|
|
||||||
## 当前真实状态
|
## 当前真实状态
|
||||||
|
|
||||||
阶段判断:
|
阶段判断:
|
||||||
|
|
||||||
- 海外单脑接管能力:约 `95%`
|
- 海外单脑接管能力:约 `95%`
|
||||||
- 分布式检测真实跑通程度:约 `89%`
|
- 分布式检测真实执行能力:约 `93%`
|
||||||
- 距离“可稳定上线并放心用后台发起检测”:约 `90%`
|
- 距离“可稳定上线并放心用后台发起检测”:约 `92%`
|
||||||
|
|
||||||
当前最重要的新事实:
|
本轮最重要的新事实:
|
||||||
|
|
||||||
- 大陆节点接管闭环已完成
|
- Detect 页面已经不再只显示海外节点
|
||||||
- acceptance 主链已完成
|
- 大陆 controller / worker 已进入 Detect 总览显示口径
|
||||||
- `runtime.pull_tasks` 的环境阻塞已解决
|
- 用户现在能从 Detect 页面直接看见大陆节点参与执行
|
||||||
- 大陆检测链已开始真实执行
|
|
||||||
- 当前剩余问题已收敛为“检测总览统计口径未统一”
|
|
||||||
|
|
||||||
## 本轮代码与环境收口
|
## 本轮代码收口
|
||||||
|
|
||||||
本轮代码变更:
|
本轮后端变更:
|
||||||
|
|
||||||
- 提交:`74dc009`
|
- `domain-api/app/services/detect_job_service.py`
|
||||||
- 主题:`fix: inherit sync env in node agent`
|
|
||||||
|
|
||||||
本轮实际变更点:
|
本轮前端变更:
|
||||||
|
|
||||||
- `domain-api/deploy/systemd/domain-node-agent.service`
|
- `domain-web/src/views/detect/DetectView.vue`
|
||||||
- 增加:
|
|
||||||
- `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`
|
|
||||||
|
|
||||||
本轮环境动作:
|
本轮修复方式:
|
||||||
|
|
||||||
- 已在 `mainland-controller-01` 刷新并重启 `domaincheck-node-agent`
|
- 保留中央账本原值,不硬改 `detect_job_items`
|
||||||
- 已在 `mainland-worker-01` 刷新并重启 `domaincheck-node-agent`
|
- 额外补出分布式显示口径:
|
||||||
- 已确认两台节点 unit 包含:
|
- `distributed_node_stats`
|
||||||
- `/etc/default/domaincheck-api`
|
- `display_items_claimed`
|
||||||
- `/etc/default/domaincheck-node-agent`
|
- `display_items_running`
|
||||||
- `SupplementaryGroups=systemd-journal`
|
- Detect 页面优先显示这个分布式显示口径
|
||||||
|
|
||||||
## 关键验证证据
|
## 关键验证证据
|
||||||
|
|
||||||
关键成功证据:
|
接口验证:
|
||||||
|
|
||||||
- `mainland-controller-01` 新运维任务:
|
- `detect/job/active`
|
||||||
- `job_id = 87`
|
- `display_items_claimed = 34`
|
||||||
- `action = runtime.pull_tasks`
|
- `display_items_running = 14`
|
||||||
- `status = success`
|
- `distributed_node_stats` 包含:
|
||||||
- 返回结果:
|
- `mainland-controller-01`
|
||||||
- `pull_state = success`
|
- `mainland-worker-01`
|
||||||
- `items_total = 50`
|
- `overseas-control-01`
|
||||||
- `summary_text = 待检测任务批次拉取并入库成功`
|
- `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`
|
- `python3 -m py_compile domain-api/app/services/detect_job_service.py`
|
||||||
- `job complete: ... ok=True`
|
- 通过
|
||||||
|
- 前端:
|
||||||
|
- `cd domain-web && npm run build`
|
||||||
|
- 通过
|
||||||
|
|
||||||
节点运行证据:
|
运行验证:
|
||||||
|
|
||||||
- `mainland-controller-01`
|
- `domaincheck-api` 已重启成功
|
||||||
- `participation_state = running`
|
- 活跃接口返回新字段正常
|
||||||
- `items_running = 5`
|
|
||||||
- `items_total = 200`
|
|
||||||
- `mainland-worker-01`
|
|
||||||
- `participation_state = running`
|
|
||||||
- `items_running = 5`
|
|
||||||
- `items_total = 70`
|
|
||||||
|
|
||||||
## 代码闭环
|
## 当前已闭合的问题
|
||||||
|
|
||||||
当前已经被验证打通的链路:
|
已闭合:
|
||||||
|
|
||||||
- detect.start 触发远端动作排队
|
- 大陆 controller 无法 `pull_tasks`
|
||||||
- mainland controller 的 sync-agent / pull_tasks 目标地址读取
|
- Detect 页面误判“大陆没跑”
|
||||||
- Node Agent 远端执行 `runtime.pull_tasks`
|
- Detect 节点表格只显示海外与 `unassigned`
|
||||||
- 大陆 controller 拉批入库
|
|
||||||
- 大陆 controller / worker 进入执行态
|
|
||||||
|
|
||||||
这意味着:
|
|
||||||
|
|
||||||
- “点开始检测后大陆节点完全不参与”这个问题已经被修掉
|
|
||||||
|
|
||||||
## 外部条件闭环
|
|
||||||
|
|
||||||
已经闭合:
|
|
||||||
|
|
||||||
- SSH
|
|
||||||
- node handover
|
|
||||||
- bootstrap
|
|
||||||
- register / heartbeat
|
|
||||||
- acceptance
|
|
||||||
- journald 权限
|
|
||||||
- node-agent 同步目标地址继承
|
|
||||||
|
|
||||||
当前不再依赖用户补充的外部条件:
|
|
||||||
|
|
||||||
- 不需要再补 `SYNC_TARGET_API_BASE_URL`
|
|
||||||
- 不需要再手工改 controller 的 node-agent env
|
|
||||||
- 不需要重装系统
|
|
||||||
|
|
||||||
## 当前剩余问题
|
## 当前剩余问题
|
||||||
|
|
||||||
当前唯一剩余问题:
|
当前唯一剩余问题:
|
||||||
|
|
||||||
- 控制面“检测总览”口径仍未和大陆拉批执行口径统一
|
- 中央账本与分布式显示口径还不是同一套最终账本
|
||||||
|
|
||||||
具体表现:
|
具体来说:
|
||||||
|
|
||||||
- `ops/nodes`
|
- `detect_job_items`
|
||||||
- 明确显示大陆 controller / worker 正在执行
|
- 仍是中央派发账本
|
||||||
- 但:
|
- `distributed_node_stats`
|
||||||
- `/api/v1/detect/queue-summary`
|
- 是运行态 / 投影视角
|
||||||
- `/api/v1/detect/job/active`
|
|
||||||
- 仍主要显示旧的:
|
|
||||||
- `overseas-control-01`
|
|
||||||
- `unassigned`
|
|
||||||
|
|
||||||
因此当前判断是:
|
所以当前页面已经“可观察、可判断”了,但底层仍属于双账本阶段。
|
||||||
|
|
||||||
- 执行链路已经通
|
## 当前是否可以继续跑检测测试
|
||||||
- 展示与统计口径还没完全通
|
|
||||||
|
|
||||||
## 当前是否可以直接上线
|
|
||||||
|
|
||||||
当前结论:
|
当前结论:
|
||||||
|
|
||||||
- 可以继续做检测功能测试
|
- 可以继续跑检测任务测试
|
||||||
- 但还不建议宣称“检测总览已经完全可信、可以直接签收上线”
|
- 而且现在 Detect 页面已经能看出大陆节点参与
|
||||||
|
|
||||||
原因不是执行失败,而是:
|
## 当前是否建议直接上线
|
||||||
|
|
||||||
- 总览统计仍会让人误判“大陆没跑”
|
当前结论:
|
||||||
|
|
||||||
|
- 比上一轮更接近上线
|
||||||
|
- 但还不建议把“检测调度账本”宣称为最终完全统一
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 现在解决的是“显示正确性”
|
||||||
|
- 下一步要解决的是“账本统一性”
|
||||||
|
|
||||||
## 当前优先级判断
|
## 当前优先级判断
|
||||||
|
|
||||||
最高优先级:
|
最高优先级:
|
||||||
|
|
||||||
- 收敛 `detect/queue-summary` / `detect/job/active` 的统计口径
|
- 设计并收敛中央账本与分布式执行账本的统一规则
|
||||||
|
|
||||||
当前不应继续推进:
|
当前不应继续推进:
|
||||||
|
|
||||||
- 新运维页面
|
- 新控制面功能
|
||||||
- 控制面增强
|
|
||||||
- 新模块
|
- 新模块
|
||||||
|
- 新页面
|
||||||
- 发布动作
|
- 发布动作
|
||||||
- 与检测总览收口无关的任何功能
|
- 与账本统一无关的工作
|
||||||
|
|
||||||
## 完成下一轮后的预期
|
## 完成下一轮后的预期
|
||||||
|
|
||||||
如果检测总览口径收敛成功:
|
如果下一轮把账本统一策略明确并落地:
|
||||||
|
|
||||||
- 后台将能同时看到:
|
- Detect 页面将不仅“看起来对”
|
||||||
- 海外触发
|
- 而且底层统计也会真正一致
|
||||||
- 大陆拉批
|
|
||||||
- 大陆执行
|
|
||||||
- 统一进度统计
|
|
||||||
- 整体可上线程度预计提升到 `94%~96%`
|
- 整体可上线程度预计提升到 `94%~96%`
|
||||||
|
|||||||
@@ -1,124 +1,131 @@
|
|||||||
# TASK_BOARD
|
# 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` 读取不到同步目标地址的问题
|
- `detect/job/active`
|
||||||
- 修复方式:
|
- `display_items_claimed = 34`
|
||||||
- `domaincheck-node-agent.service` 追加读取 `/etc/default/domaincheck-api`
|
- `display_items_running = 14`
|
||||||
- 新提交:`74dc009`
|
- `distributed_node_stats` 已包含:
|
||||||
- 已在以下节点完成刷入与重启:
|
- `mainland-controller-01`
|
||||||
- `mainland-controller-01`
|
- `mainland-worker-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`
|
- `mainland-controller-01`
|
||||||
- `runtime.pull_tasks`
|
- `runtime.pull_tasks`
|
||||||
- `job_id = 87`
|
- `job_id = 87`
|
||||||
- `status = success`
|
- `status = success`
|
||||||
- `items_total = 50`
|
- 大陆 controller / worker 仍在执行中
|
||||||
- `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`
|
|
||||||
|
|
||||||
## 当前结论
|
## 当前结论
|
||||||
|
|
||||||
检测不是“没跑”。
|
这轮已经把“Detect 页面看不出大陆参与”这个核心观感问题修掉了一大半。
|
||||||
|
|
||||||
当前真实状态是:
|
现在的 Detect 页面口径变成:
|
||||||
|
|
||||||
- 分布式检测执行链已经跑通
|
- 中央原始账本仍保留:
|
||||||
- 大陆 controller 已经能从海外控制面主动拉回待检测批次
|
- `items_total = 1000`
|
||||||
- 大陆 controller 与 mainland worker 都已经进入执行中
|
- `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`
|
这不是 bug,而是当前架构现状:
|
||||||
- `unassigned`
|
|
||||||
|
|
||||||
尚未把大陆侧“拉批后本地执行”的结果完整并回收到同一检测总览口径中。
|
- 中央 `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 投影之间的统一账本策略
|
||||||
- 明确为什么大陆执行已发生,但总览仍显示旧任务统计
|
|
||||||
- 修好后再跑一次小批量检测验证
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from app.services.debug_event_service import push_debug_event
|
|||||||
|
|
||||||
|
|
||||||
ACTIVE_JOB_STATUSES = ("pending", "running")
|
ACTIVE_JOB_STATUSES = ("pending", "running")
|
||||||
|
_RUNTIME_NODE_STALE_MINUTES = 10
|
||||||
|
|
||||||
|
|
||||||
def _selection_sql() -> str:
|
def _selection_sql() -> str:
|
||||||
@@ -40,6 +41,152 @@ def _decode_payload(value: object) -> dict:
|
|||||||
return {}
|
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]]:
|
def _extract_current_cycle_events(events: list[dict]) -> tuple[str, list[dict]]:
|
||||||
if not events:
|
if not events:
|
||||||
return "", []
|
return "", []
|
||||||
@@ -142,6 +289,11 @@ def _fetch_job_summary(cur, job_row, event_limit: int = 20) -> dict:
|
|||||||
for item in cur.fetchall()
|
for item in cur.fetchall()
|
||||||
]
|
]
|
||||||
cycle_token, current_cycle_events = _extract_current_cycle_events(events)
|
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())
|
total = sum(counts.values())
|
||||||
terminal = int(counts.get("completed", 0)) + int(counts.get("blacklisted", 0)) + int(counts.get("failed", 0))
|
terminal = int(counts.get("completed", 0)) + int(counts.get("blacklisted", 0)) + int(counts.get("failed", 0))
|
||||||
return {
|
return {
|
||||||
@@ -163,6 +315,12 @@ def _fetch_job_summary(cur, job_row, event_limit: int = 20) -> dict:
|
|||||||
"items_terminal": terminal,
|
"items_terminal": terminal,
|
||||||
"progress_percent": round((terminal / total) * 100, 2) if total else 0,
|
"progress_percent": round((terminal / total) * 100, 2) if total else 0,
|
||||||
"node_stats": list(node_buckets.values()),
|
"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,
|
"recent_events": events,
|
||||||
"latest_event": events[0] if events else None,
|
"latest_event": events[0] if events else None,
|
||||||
"current_cycle_token": cycle_token,
|
"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 []
|
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_processed_recent = 0
|
||||||
total_completed_recent = 0
|
total_completed_recent = 0
|
||||||
total_blacklisted_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),
|
"pending": int(active_job.get("items_pending", 0) or 0),
|
||||||
"claimed": int(active_job.get("items_claimed", 0) or 0),
|
"claimed": int(active_job.get("items_claimed", 0) or 0),
|
||||||
"running": int(active_job.get("items_running", 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),
|
"completed": int(active_job.get("items_completed", 0) or 0),
|
||||||
"blacklisted": int(active_job.get("items_blacklisted", 0) or 0),
|
"blacklisted": int(active_job.get("items_blacklisted", 0) or 0),
|
||||||
"failed": int(active_job.get("items_failed", 0) or 0),
|
"failed": int(active_job.get("items_failed", 0) or 0),
|
||||||
|
|||||||
@@ -147,8 +147,8 @@
|
|||||||
|
|
||||||
<el-row v-if="activeJob" :gutter="12" style="margin-bottom: 16px">
|
<el-row v-if="activeJob" :gutter="12" style="margin-bottom: 16px">
|
||||||
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务待领" :value="activeJob.items_pending || 0" /></el-col>
|
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务待领" :value="activeJob.items_pending || 0" /></el-col>
|
||||||
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务已领" :value="activeJob.items_claimed || 0" /></el-col>
|
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务已领" :value="activeJobDisplayClaimed" /></el-col>
|
||||||
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务执行中" :value="activeJob.items_running || 0" /></el-col>
|
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务执行中" :value="activeJobDisplayRunning" /></el-col>
|
||||||
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务完成" :value="activeJob.items_completed || 0" /></el-col>
|
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务完成" :value="activeJob.items_completed || 0" /></el-col>
|
||||||
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务黑名单" :value="activeJob.items_blacklisted || 0" /></el-col>
|
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务黑名单" :value="activeJob.items_blacklisted || 0" /></el-col>
|
||||||
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务失败" :value="activeJob.items_failed || 0" /></el-col>
|
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务失败" :value="activeJob.items_failed || 0" /></el-col>
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table v-if="activeJob?.node_stats?.length" :data="activeJob.node_stats" border style="margin-bottom: 16px">
|
<el-table v-if="activeJobDisplayNodeStats.length" :data="activeJobDisplayNodeStats" border style="margin-bottom: 16px">
|
||||||
<el-table-column prop="node_code" label="节点" min-width="180" />
|
<el-table-column prop="node_code" label="节点" min-width="180" />
|
||||||
<el-table-column prop="items_total" label="任务总数" min-width="100" />
|
<el-table-column prop="items_total" label="任务总数" min-width="100" />
|
||||||
<el-table-column prop="items_pending" label="待领" min-width="90" />
|
<el-table-column prop="items_pending" label="待领" min-width="90" />
|
||||||
@@ -423,12 +423,23 @@ const restoreLastAction = () => {
|
|||||||
|
|
||||||
const runs = computed(() => status.value.runs || []);
|
const runs = computed(() => status.value.runs || []);
|
||||||
const activeJob = computed(() => status.value.active_job || null);
|
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(() => {
|
const activeJobSummaryText = computed(() => {
|
||||||
if (!activeJob.value) {
|
if (!activeJob.value) {
|
||||||
return "当前暂无活跃检测任务";
|
return "当前暂无活跃检测任务";
|
||||||
}
|
}
|
||||||
const job = activeJob.value;
|
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 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 ? "运行中" : "未启动"));
|
const currentPhaseLabel = computed(() => selectedRun.value?.phase_label || (status.value.worker_online ? "运行中" : "未启动"));
|
||||||
@@ -535,8 +546,10 @@ const queueAlertText = computed(() => {
|
|||||||
}
|
}
|
||||||
const queue = queueSummary.value.queue || {};
|
const queue = queueSummary.value.queue || {};
|
||||||
const throughput = queueSummary.value.throughput || {};
|
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 = [
|
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} 项/分钟`
|
`近 ${queueSummary.value.window_minutes || 15} 分钟处理 ${throughput.processed_recent || 0} 项,约 ${throughput.processed_per_minute || 0} 项/分钟`
|
||||||
];
|
];
|
||||||
if ((queue.overdue_leases || 0) > 0) {
|
if ((queue.overdue_leases || 0) > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user