chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)

- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
This commit is contained in:
admin committed 2026-10-10 23:13:22 +08:00
1 parent 30b46dbd0c
commit c1b5e4d966
735 files changed
+153192 -2415

No files matched your search

@@ -0,0 +1,553 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
派活监管 · 三线执行状态巡检(**只读**)
==================================================================
用途:派活之后**不是结束** —— 本脚本把「各会话到底动没动、动了到哪、有没有卡住、
该不该跟进」变成一次可复跑的读数。⛔ 不起服务、⛔ 不抢锁、⛔ 不改任何文件。
用法:
"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" \
"E:/ProgramData/AIProject/ai1net-dsh-server/交付物/派活监管-三线巡检-20260928.py"
加 `--json` 出机读结果。
退出码:0 = 全部已收官 · 1 = 有棒在跑/待跑(正常等) · 2 = **有需跟进的**(未产出/异常/锁未释放)
判据源(全部只读):
① `workbuddy.db` → `automations`(有没有排上、排的时刻)
② `workbuddy.db` → `automation_runs`(**跑没跑 + 收官结论原文** + result_success)
③ `workbuddy.db` → `automation_runtime_state`(此刻**是不是在跑**)
④ `workbuddy.db` → `sessions`(该工作区的会话活动)
⑤ 全局执行锁 `dsh-server-docs/05-交接单/.exec-lock`(**三线共用一把** ⇒ 谁占着、放没放)
⑥ 各线「收件目录」里**在派活时刻之后**新增/改动的文件(=有没有产出)
⚠️ 本机实测两条硬事实:
- `workbuddy.db` 是 **WAL 三件套**、app 正在运行 ⇒ 一律以 `mode=ro` 打开,⛔ 不写、⛔ 不 cp。
- 在受限沙箱里 **Node 不能 spawn 系统命令**;Python 可以 ⇒ 本脚本用 Python。
"""
import json
import os
import sqlite3
import sys
import time
from datetime import datetime
DB = r"E:\ProgramData\.workbuddy\workbuddy.db"
LOCKROOT = r"D:\github\dsh_shenxian\dsh-server-docs\05-交接单"
EXECLOCK = os.path.join(LOCKROOT, ".exec-lock")
# ---------------------------------------------------------------------------
# 派活登记(**派一条就加一条**;这是本脚本唯一的"配置")
#
# 🔴 2026-09-28 实测教训(排期):**三条棒挤在 5 分钟内会互相挡死**。
# 实测:21:19:12 客户端线抢到**全局独占锁**(未声明 `--domains`)⇒
# 21:20:02 手机接入线 **抢不到 ⇒ 棒 3 未开工**(守规矩停手,但白跑一趟)。
# ⇒ 排期两条铁律:① **错开 ≥ 一次跑的时长**(实测单棒约 5 分钟 ⇒ 建议 ≥6 分钟);
# ② 并行棒一律让各线 `--claim-exec … --domains <域键>`(域不重叠才真并行)。
# ⚠️ 另注意:若别人持的是**旧式全局锁**,域锁也照样抢不到(guard 会明确报这点)。
# ---------------------------------------------------------------------------
DISPATCHES = [
{
"line": "客户端线",
"ws": "ai1net-dsh-desktop",
"auto_id": "b5c82852-04af-4213-936d-60958643029e",
"scheduled_at": "2026-09-28T21:19",
"task": "垫片改指 WorkBuddy 前置结算棒(只出 D-1 定论 + 执行交接单)",
"expect": "docs/ 下新增执行交接单(含 D-1 唯一结论)",
"watch": ["docs"],
"precondition": None,
},
{
"line": "手机接入线",
"ws": "ai1net-dsh-anywhere",
# 21:20 那次(7d50229a)**抢不到全局锁 ⇒ 未开工**(已消耗);
# 22:49 重排(da9a0b9e)—— 同一件、加了 --domains 提示。
"auto_id": "da9a0b9e-5171-47db-b198-1940edb4be2f",
"scheduled_at": "2026-09-28T22:49",
"task": "棒 3 开工(Android MVP · 按 sessions 一套实现)· **重排**",
"expect": "docs/交接单/ 回执 +(可能)dsh-client 壳内改动 / APK",
"watch": ["docs/交接单", "docs"],
"precondition": "首次(21:20)被全局锁挡下 ⇒ 只要锁空闲即可开工",
},
{
"line": "ai1net_ui",
"ws": "ai1net_ui",
"auto_id": "e5faa1ec-6d12-48d2-94dc-1efd829a2c2d",
"scheduled_at": "2026-09-28T21:24",
"task": "新接手外来件(ai1net 并入 M6 设备接入 + 规格修订)",
"expect": "规格修订成稿 + 归并方案(docs/)",
"watch": ["docs/交接单", "docs"],
"precondition": "该线此前未参与本功能 ⇒ 件里已备补课段 §1–§4",
},
]
WSROOT = r"E:\ProgramData\AIProject"
NOW_MS = int(time.time() * 1000)
# ---------------------------------------------------------------------------
# 🔴 2026-09-28 实测教训(本脚本最重要的一个判据)
#
# 事故:手机接入线棒 3 在 21:20 **抢不到全局锁 ⇒ 未开工**(合规停手),
# 但它的运行记录 `result_success = 1`(它确实"跑完了")⇒
# 旧版判据把它归到「收官·疑未产出」,而旧版**处置规则对这一类只报告、不重排**
# ⇒ 两条巡检棒(21:45 / 22:18)**连续 2 轮都发现了、都只报告** ⇒
# **那条活一直躺着死**,直到人手动发现。
#
# ⇒ 两条铁律(改这里之前先读):
# ① **「跑完了」≠「干了活」** —— 结论原文里自述"未开工/未做事"的,判**异常**,不是"疑未产出"。
# ② **只报告的监管 = 没监管**(甚至更糟:制造"已经在管了"的假象)
# ⇒ 本脚本对每条线给出**显式 `action`**;巡检棒照 action 办事,
# ⛔ 不再由巡检棒自己解释状态分类(那正是这次误判的入口)。
#
# 🔴 2026-09-28 第二次教训:**第一版判据有假阳性,已收窄**。
# 原判据含 `未完成` / `阻塞` / `中止` / `放弃` / `被挡` —— 这些词**同时是报告里的字段名**
# (例:「## ⑥ 未完成项」「- 未完成:…」「阻塞项」)⇒ 会把**正常收官**的线误判成
# "未实际执行" ⇒ **凭空重排已完成的活**(=让它们白跑一遍)。
# ⇒ 只保留"**明确没干活/干不下去**"的强信号;⛔ 凡可能是**字段名**的词一律不收。
# ⚠️ 同一份判据在后台监控 `wb-supervisor-watch.py` 里也有一份,**改一处必须改另一处**。
PENDING_MARKERS = (
"未开工", "未做事", "没开工",
"抢不到执行锁", "抢不到锁", "抢不到",
"无法继续", "未能继续",
"停手并报告", "停手并回报", "已停手",
)
# action 四类(巡检棒只认这四个,⛔ 不自创)
ACTION_RESCHEDULE = "reschedule" # 🔴 活没干成 ⇒ **必须当场新建自动化重排**(⛔ 不许只报告)
ACTION_REPORT_LOCK = "report-lock" # 锁未释放 ⇒ **只报告用户**(R9:⛔ 不代删、不接管)
ACTION_REPORT = "report" # 需人判读(如"跑完但产出缺失")⇒ 报告 + 列期望路径
ACTION_WAIT = "wait" # 正常等待,无事不打扰
# ---------------------------------------------------------------------------
def out(text):
"""⛔ 不走 print:文案含 ⛔ 等字符时,裸 print 在非 UTF-8 控制台会 UnicodeEncodeError
⇒ stdout 空 ⇒ **静默丢结论**(本项目已踩过)。一律写 bytes。"""
sys.stdout.buffer.write(text.encode("utf-8", "replace"))
sys.stdout.buffer.write(b"\n")
def ro_conn():
con = sqlite3.connect("file:%s?mode=ro" % DB, uri=True, timeout=5)
con.execute("PRAGMA busy_timeout=5000")
return con
def parse_iso_ms(s):
try:
return int(datetime.strptime(s, "%Y-%m-%dT%H:%M").timestamp() * 1000)
except Exception:
return None
def fmt_ms(ms):
if not ms:
return "-"
return datetime.fromtimestamp(ms / 1000).strftime("%H:%M:%S")
def lock_state():
"""全局执行锁:三线共用一把 ⇒ 监管上这是最强的互斥信号。"""
if not os.path.isdir(EXECLOCK):
# 兼容:也有可能是文件式
if os.path.exists(EXECLOCK):
try:
with open(EXECLOCK, "r", encoding="utf-8", errors="replace") as f:
return {"held": True, "owner": f.read().strip()[:120]}
except Exception:
return {"held": True, "owner": "(文件式锁,读不出)"}
return {"held": False, "owner": None}
owner_file = os.path.join(EXECLOCK, "OWNER")
owner = None
if os.path.isfile(owner_file):
try:
with open(owner_file, "r", encoding="utf-8", errors="replace") as f:
txt = f.read()
lines = [ln for ln in txt.splitlines() if ln.strip()]
owner = lines[0][:120] if lines else "(OWNER 存在但为空)"
except Exception:
owner = "(OWNER 读不出)"
else:
try:
owner = "(目录式锁,无 OWNER;内容=%s)" % ",".join(os.listdir(EXECLOCK))[:80]
except Exception:
owner = "(目录式锁)"
return {"held": True, "owner": owner or "(未署名)"}
def domain_locks():
root = os.path.join(LOCKROOT, ".locks")
if not os.path.isdir(root):
return []
got = []
for name in os.listdir(root):
if name.startswith("."):
continue
got.append(name)
return got
def new_files(ws, dirs, since_ms, limit=8):
"""列出收件目录里 mtime > since_ms 的文件(=派活之后新产出的东西)。"""
hits = []
for d in dirs:
base = os.path.join(WSROOT, ws, d.replace("/", os.sep))
if not os.path.isdir(base):
continue
for dirpath, _dirnames, filenames in os.walk(base):
for fn in filenames:
fp = os.path.join(dirpath, fn)
try:
st = os.stat(fp)
except Exception:
continue
if st.st_mtime * 1000 > since_ms:
hits.append({
"path": os.path.relpath(fp, os.path.join(WSROOT, ws)).replace("\\", "/"),
"mtime": fmt_ms(st.st_mtime * 1000),
"size": st.st_size,
})
hits.sort(key=lambda x: x["mtime"], reverse=True)
return hits[:limit]
def session_stats(con, ws, since_ms):
cwd = "%s/%s" % (WSROOT.replace("\\", "/"), ws)
rows = con.execute(
"select id,title,status,created_at,last_activity_at,is_background_automation "
"from sessions where lower(cwd)=lower(?) and coalesce(created_at,0)>? "
"order by created_at desc", (cwd, since_ms)).fetchall()
out_rows = []
for r in rows:
out_rows.append({
"id": (r[0] or "")[:8],
"title": (r[1] or "")[:44],
"status": r[2],
"created": fmt_ms(r[3]),
"last_activity": fmt_ms(r[4]),
"bg": bool(r[5]),
})
return out_rows
# ---------------------------------------------------------------------------
# 读「结果钩子」台账 —— 这条链的另一半
#
# 设计(用户 2026-09-28 提出):hook 把执行结果交到本地台账 ⇒ 后台任务(本脚本)
# 读台账 ⇒ 结合登记表**决定下一步**。
#
# 🔴 这里必须有的一个自检:**钩子本身可能没挂上/没生效(静默失败)**。
# ⇒ 台账为空**不等于**"没有会话结束过",只等于"钩子没工作"。
# ⇒ 所以台账为空时**报出来**(action=report),⛔ 不要当成"一切正常"。
# ---------------------------------------------------------------------------
LEDGER = os.path.join(WSROOT, "ai1net-dsh-server", "tmp", "supervise-inbox", "ledger.jsonl")
HOOK_SCRIPT = os.path.join(WSROOT, "ai1net-dsh-server", ".workbuddy", "tools", "wb-result-hook.py")
HOOK_LOG = os.path.join(WSROOT, "ai1net-dsh-server", "tmp", "supervise-inbox", "hook.log")
# 钩子判定的"没问题"值;其余都值得看一眼
HOOK_OK = ("ok", "short")
def read_ledger(limit=40):
"""读台账尾部。⛔ 只读,坏行跳过。"""
recs = []
try:
with open(LEDGER, encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
except Exception:
return recs
for ln in lines[-limit:]:
ln = ln.strip()
if not ln:
continue
try:
recs.append(json.loads(ln))
except Exception:
continue
return recs
def hook_health(ledger):
"""钩子自身是否在工作 —— 这条必须是显式的,否则会静默假绿。"""
h = {"installed_script": os.path.isfile(HOOK_SCRIPT),
"ledger_exists": os.path.isfile(LEDGER),
"records": len(ledger),
"last_ts": ledger[-1].get("ts") if ledger else None,
"log_exists": os.path.isfile(HOOK_LOG)}
h["ok"] = bool(h["installed_script"] and h["records"])
if not h["installed_script"]:
h["problem"] = "钩子脚本不在位 ⇒ 台账永远不会写"
elif not h["ledger_exists"] or not h["records"]:
h["problem"] = ("台账为空 ⇒ 钩子**从未触发**(可能:还没等到下一个会话结束 / 钩子没挂上 / "
"Windows 钩子在会话启动时快照 ⇒ 装完后**必须先重启客户端**)")
else:
h["problem"] = None
return h
def ledger_for(ledger, ws, since_ms):
"""挑出属于某条线的台账记录(按 cwd 尾段匹配)+ 非 ok 的判定。"""
tail = "/" + ws.lower()
mine = [r for r in ledger if str(r.get("cwd", "")).lower().endswith(tail)]
bad = [r for r in mine if r.get("verdict") not in HOOK_OK]
return mine, bad
# ---------------------------------------------------------------------------
def check_one(con, d, ledger=None):
sched_ms = parse_iso_ms(d["scheduled_at"]) or 0
rec = {"line": d["line"], "ws": d["ws"], "task": d["task"],
"at": d["scheduled_at"], "expect": d["expect"]}
# ① 自动化本体
# 🔴 幂等待办:`auto_id` 为空 = **这条活还没排上**(不是数据缺失)。
# 用途(2026-09-28 实测教训):一轮里若"读到一半被打断"(如工具瞬时不可用),
# 那把「该排的活」**提前写进登记表**(auto_id 留空)⇒ 中断也只丢最后一步,
# 任何后来的巡检都会看到它并 `reschedule` ⇒ 那条活**不会静默消失**。
if not d.get("auto_id"):
rec.update(state="待排(未登记)",
why="登记表里这条还没排上自动化号 ⇒ 必须新建(幂等待办)",
need_follow=True, action=ACTION_RESCHEDULE)
rec["new_files"] = new_files(d["ws"], d["watch"], parse_iso_ms(d["scheduled_at"]) or 0)
return rec
arow = con.execute(
"select status,scheduled_at,next_run_at,last_run_at from automations where id=?",
(d["auto_id"],)).fetchone()
if not arow:
rec.update(state="异常",
why="登记里这条自动化**查不到**(被删了?)⇒ 必须新建",
need_follow=True, action=ACTION_RESCHEDULE)
return rec
rec["auto_status"] = arow[0]
rec["auto_next"] = fmt_ms(arow[2])
rec["auto_last"] = fmt_ms(arow[3])
# ③ 此刻在不在跑
rrow = con.execute(
"select running,running_started_at,last_error,running_conversation_id "
"from automation_runtime_state where automation_id=?", (d["auto_id"],)).fetchone()
running = bool(rrow[0]) if rrow else False
running_since = fmt_ms(rrow[1]) if rrow and rrow[1] else None
last_err = (rrow[2] if rrow else None)
conv = (rrow[3] if rrow else None)
rec["running"] = running
rec["running_since"] = running_since
if last_err:
rec["last_error"] = str(last_err)[:120]
# ② 跑没跑 + 结论原文
runs = con.execute(
"select created_at,status,result_success,thread_title,failure_code,reason_code "
"from automation_runs where automation_id=? and coalesce(created_at,0)>? "
"order by created_at desc", (d["auto_id"], sched_ms)).fetchall()
rec["run_count"] = len(runs)
if runs:
r0 = runs[0]
rec["run_at"] = fmt_ms(r0[0])
rec["run_status"] = r0[1]
rec["success"] = (r0[2] == 1)
rec["conclusion"] = (r0[3] or "")[:300]
if r0[4] or r0[5]:
rec["failure"] = "%s/%s" % (r0[4], r0[5])
if conv:
rec["conversation"] = str(conv)[:12]
# ④ 会话
rec["sessions"] = session_stats(con, d["ws"], sched_ms)
# ⑥ 产出
rec["new_files"] = new_files(d["ws"], d["watch"], sched_ms)
# ⑦ 结果钩子的台账(这条链的"接收"端)
mine, bad = ledger_for(ledger or [], d["ws"], sched_ms)
rec["hook"] = {"records": len(mine), "bad": [r.get("verdict") for r in bad],
"last": (mine[-1].get("ts") if mine else None)}
# ---- 判定 ----
if not runs and not running:
if NOW_MS < sched_ms:
rec.update(state="待跑", why="还没到点", need_follow=False, action=ACTION_WAIT)
else:
overdue = (NOW_MS - sched_ms) / 60000.0
if overdue < 3:
rec.update(state="待跑", why="到点 %.1f 分钟,尚未见运行记录" % overdue,
need_follow=False, action=ACTION_WAIT)
else:
rec.update(state="异常",
why="已过点 %.0f 分钟仍**无任何运行记录** ⇒ 自动化没触发" % overdue,
need_follow=True, action=ACTION_RESCHEDULE)
return rec
if running:
stuck = ""
if running_since:
try:
start = int(datetime.strptime(
"%s %s" % (datetime.now().strftime("%Y-%m-%d"), running_since),
"%Y-%m-%d %H:%M:%S").timestamp() * 1000)
mins = (NOW_MS - start) / 60000.0
if mins > 60:
stuck = "(已跑 %.0f 分钟 ⇒ **疑似卡住**)" % mins
except Exception:
pass
rec.update(state="进行中", why="正在跑" + stuck,
need_follow=bool(stuck),
action=(ACTION_REPORT if stuck else ACTION_WAIT))
return rec
if runs:
if rec.get("success"):
# 🔴 先判「跑完了但**没干活**」—— 这一条必须排在"有没有产出"之前。
# 判据 = 结论原文自述未执行(机器可读)。判错这里 = 那条活静默死掉(2026-09-28 实测)。
con_text = rec.get("conclusion") or ""
hit = [m for m in PENDING_MARKERS if m in con_text]
if hit:
rec.update(state="跑完·未实际执行",
why="运行成功,但结论自述未执行(命中:%s)⇒ **等同异常**,必须重排" % "、".join(hit[:4]),
need_follow=True, action=ACTION_RESCHEDULE)
elif rec["new_files"]:
# 🔴 交叉校验:**自动化说成功 + 有产出,但钩子台账里这条线有"非正常收尾"**
# ⇒ 两个信号打架 ⇒ 以**证据**为准,交给下一棒看一眼(⛔ 不自动重排,
# 因为产出文件确实在,重排可能重复做同一件事)。
if rec["hook"]["bad"]:
rec.update(state="已收官·但钩子有异常收尾",
why="有产出,但结果钩子记录到非正常收尾:%s ⇒ 需人判读"
% "、".join(rec["hook"]["bad"][:4]),
need_follow=True, action=ACTION_REPORT)
else:
rec.update(state="已收官", why="有结论 + 有产出",
need_follow=False, action=ACTION_WAIT)
else:
rec.update(state="收官·疑未产出",
why="跑完了但收件目录里**没有新文件** ⇒ 核实是否只是「只在对话里答了」",
need_follow=True, action=ACTION_REPORT)
else:
rec.update(state="失败",
why="运行记录 result_success != 1" + ("(%s)" % rec.get("failure", "") if rec.get("failure") else ""),
need_follow=True, action=ACTION_RESCHEDULE)
return rec
rec.update(state="未知", why="信号不足", need_follow=True, action=ACTION_REPORT)
return rec
def main():
as_json = "--json" in sys.argv
con = ro_conn()
lk = lock_state()
dl = domain_locks()
ledger = read_ledger()
hh = hook_health(ledger)
results = [check_one(con, d, ledger) for d in DISPATCHES]
# 未收官且有在跑的 ⇒ 锁被占属正常;全部停跑但锁还占着 ⇒ 异常(本项目红线:锁只能持有者释放)
lock_orphan = bool(lk["held"]) and all(
(not r.get("running")) and r.get("state") in ("已收官", "失败", "异常") for r in results)
follow = [r for r in results if r.get("need_follow")]
rc = 0
if follow or lock_orphan:
rc = 2
elif any(r["state"] in ("待跑", "进行中") for r in results):
rc = 1
if as_json:
out(json.dumps({"now": fmt_ms(NOW_MS), "lock": lk, "domain_locks": dl,
"hook_health": hh, "lines": results,
"lock_orphan": lock_orphan, "rc": rc},
ensure_ascii=False, indent=2))
return rc
out("═══ 派活监管 · 三线执行巡检 @ %s ═══" % fmt_ms(NOW_MS))
out("")
out("【全局执行锁】%s%s" % (
("🔴 被占:%s" % lk["owner"]) if lk["held"] else "✅ 空闲",
" + 域锁:%s" % ", ".join(dl) if dl else ""))
if lock_orphan:
out(" ⛔ **锁被占但三线都已停跑** ⇒ 疑似未释放(红线:锁只能持有者自己释放,⛔ 不许代为删锁)")
out("")
out("【结果钩子】%s 台账记录=%d 最近=%s" % (
"🟢 在工作" if hh["ok"] else "🔴 **未工作**", hh["records"], hh["last_ts"] or "-"))
if hh["problem"]:
out(" ⚠️ %s" % hh["problem"])
out("")
icon = {"已收官": "🟢", "进行中": "🔵", "待跑": "⚪", "失败": "🔴",
"异常": "🔴", "收官·疑未产出": "🟡", "跑完·未实际执行": "🔴",
"待排(未登记)": "🔴", "未知": "🟡"}
for r in results:
out("%s 【%s】%s (排在 %s)" % (icon.get(r["state"], "·"), r["line"], r["state"], r["at"]))
out(" 任务:%s" % r["task"])
if r.get("running"):
out(" 此刻:**正在跑**" + ("(起于 %s)" % r["running_since"] if r.get("running_since") else ""))
if r.get("run_count"):
out(" 运行:%s 次 · 最近 %s · 成功=%s" % (
r["run_count"], r.get("run_at", "-"), "是" if r.get("success") else "否/未定"))
con_txt = (r.get("conclusion") or "").replace("\n", " ").strip()
if con_txt:
out(" 结论:%s" % con_txt[:160])
if r.get("last_error") or r.get("failure"):
out(" 🔴 错误:%s %s" % (r.get("last_error", ""), r.get("failure", "")))
for s in r["sessions"][:3]:
out(" 会话:%s status=%s 创建=%s 最后活动=%s" % (
s["id"], s["status"], s["created"], s["last_activity"]))
if r["new_files"]:
out(" 产出(新):")
for f in r["new_files"][:4]:
out(" · %s (%s, %s B)" % (f["path"], f["mtime"], f["size"]))
else:
out(" 产出(新):**无**")
hk = r.get("hook") or {}
if hk.get("records"):
out(" 钩子台账:%d 条 · 最近 %s%s" % (
hk["records"], hk.get("last") or "-",
(" · 🔴 非正常收尾:" + "、".join(hk["bad"])) if hk.get("bad") else ""))
out(" 判读:%s" % r["why"])
out(" ➡️ **action = %s**%s" % (r.get("action", "?"),
ACTIONS_HELP.get(r.get("action"), "")))
out("")
# ---- 汇总:按 action 分组(巡检棒照 action 办事,⛔ 别自己解释状态)----
by_action = {}
for r in results:
by_action.setdefault(r.get("action", "?"), []).append(r)
if lock_orphan:
by_action.setdefault(ACTION_REPORT_LOCK, []).append({"line": "(全局)", "why": "锁被占但三线都已停跑 ⇒ 疑似未释放"})
if not hh["ok"]:
# 钩子没工作 ⇒ 必须报出来(⛔ 否则"台账为空"会被读成"一切正常")
by_action.setdefault(ACTION_REPORT, []).append(
{"line": "(结果钩子)", "why": hh["problem"] or "钩子未工作"})
if not by_action or set(by_action) == {ACTION_WAIT}:
out("⇒ ✅ 无需动作(wait):%s" % (
"全部收官" if all(r["state"] == "已收官" for r in results) else "正常等待"))
return rc
out("⇒ 本轮动作清单(**照 action 办事**):")
for act in (ACTION_RESCHEDULE, ACTION_REPORT_LOCK, ACTION_REPORT, ACTION_WAIT):
items = by_action.get(act)
if not items:
continue
out(" 【%s】%s" % (act, ACTIONS_HELP.get(act, "")))
for r in items:
out(" · 【%s】%s" % (r["line"], r["why"]))
return rc
ACTIONS_HELP = {
ACTION_RESCHEDULE: "⇒ 🔴 **必须当场新建一条一次性自动化**给该线(⛔ 改时间不触发;⛔ 不许只报告),并把新 id 写回脚本顶部 DISPATCHES",
ACTION_REPORT_LOCK: "⇒ ⛔ **只报告用户**:不代删锁、不接管(红线)",
ACTION_REPORT: "⇒ 报告 + 列期望产出路径(须人判读,别自建自动化)",
ACTION_WAIT: "⇒ 不做任何事(正常等待,无事不打扰)",
}
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e:
out("⛔ 巡检脚本自身出错:%s: %s" % (type(e).__name__, e))
sys.exit(3)