Files

288 lines
11 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""dsh —— 本项目工作流「统一入口」(第 1 版,2026-10-01)
存在理由(实测):规则写成「多步祈使句」⇒ 每步 = 1 次工具调用;
每棒固定开销 12~15 次。本入口把多步合进 1 次调用。
用法:
dsh open <会话名> [目标文件...] 开工:状态 → 判锁范围 → 抢锁(原 3 步)
dsh close <会话名> [单号...] 收尾:反序释放 + 清单提醒(原 2~5 步)
dsh stat [--full] 中途查:锁 + git + 本会话日志水位(原 3~4 步)
dsh log [会话id] 看本会话诊断日志字节数 + 档位判断(原 1~2 步)
dsh probe <探针名> [参数...] 跑已存探针(替代「写→跑→清」3 步)
dsh collect 收尾四件套(原 4 步)
⛔ 只读生产日志(仅 stat 取大小);⛔ 不常驻;⛔ 不写任何日志文件。
"""
import os
import shutil
import subprocess
import sys
import time
PY = r"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe"
WS = r"E:/ProgramData/AIProject/ai1net-dsh-server"
DOC = r"D:/github/dsh_shenxian/dsh-server-docs"
CFG = r"E:/ProgramData/.workbuddy"
GUARD = DOC + "/07-scripts/handoff-guard.sh"
PREFLIGHT = DOC + "/07-scripts/preflight-lock.sh"
DOMAIN = "ai1net-dsh-server/"
SOFT_LOG = 5 * 1024 * 1024
HARD_LOG = 8 * 1024 * 1024
COLLECT = [
("docs-audit", PY, DOC + "/07-scripts/docs-audit.py"),
("docs-index-stats", PY, DOC + "/07-scripts/docs-index-stats.py", "--write"),
("docs-manifest", PY, DOC + "/07-scripts/docs-manifest.py"),
("docs-sync-check", "bash", DOC + "/07-scripts/docs-sync-check.sh"),
]
# ⛔ 2026-10-01 实测:这些底层脚本**不认 `--help`**(会当成路径/参数照跑;`docs-manifest.py --help`
# 甚至去写 `--help\docs-manifest.json`)⇒ 入口自己拦,否则 `dsh collect --help` 会**静默跑四件套**
USAGE = {
"open": 'dsh open <会话名> [目标文件...] # ⚠️ 必须带目标文件,否则跳过 preflight',
"close": "dsh close <会话名> [单号...] # ⛔ 单号可省:省了只释放 OWNER==本会话名的",
"stat": "dsh stat [--full] # --full 再跑一遍 state.py(默认不跑,只给锁/git/日志三项)",
"log": "dsh log [会话id]",
"probe": "dsh probe <探针名> [参数...]",
"collect": "dsh collect # 收尾四件套(⚠️ 会写 INDEX.md,需先持文档库锁)",
}
def say(text):
sys.stdout.buffer.write((text + "\n").encode("utf-8", "replace"))
sys.stdout.buffer.flush()
def run(cmd, cwd=WS):
say("$ " + " ".join(str(c) for c in cmd))
try:
p = subprocess.run(cmd, cwd=cwd, capture_output=True, timeout=600)
except Exception as exc:
say("!! 执行失败: %r" % (exc,))
return 127, ""
out = (p.stdout or b"") + (p.stderr or b"")
txt = out.decode("utf-8", "replace")
lines = txt.splitlines()
if len(lines) > 40:
lines = lines[:40] + ["…(共 %d 行,已截断)" % len(txt.splitlines())]
say("\n".join(lines).rstrip())
say("rc=%d" % p.returncode)
return p.returncode, txt
def bash_path():
return shutil.which("bash") or r"E:/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin/bash.exe"
def newest_log():
d = os.path.join(CFG, "logs", time.strftime("%Y-%m-%d"), "sdk", "conversations")
if not os.path.isdir(d):
return None
best, mt = None, -1
for n in os.listdir(d):
if not n.endswith(".log"):
continue
f = os.path.join(d, n)
st = os.stat(f).st_mtime
if st > mt:
best, mt = f, st
return best
def find_session_log(sid=None):
"""按**会话 id** 精确定位本会话日志;命中不了才退回 `newest_log()`。
⚠️ 2026-10-01 实测隐患:`newest_log()` 只按 mtime 取 ⇒ **可能取到别的会话的日志**,
于是软/硬档位算到别人头上(虚高虚低都可能)。本函数优先精确匹配,
⛔ 且**不改变** `newest_log()` 原语义(它仍是兜底)。
返回 `(精确命中的路径 or None, 实际采用的路径 or None)`。
"""
sid = (sid or os.environ.get("CODEBUDDY_SESSION_ID") or "").strip()
if not sid:
return None, newest_log()
d = os.path.join(CFG, "logs", time.strftime("%Y-%m-%d"), "sdk", "conversations")
if os.path.isdir(d):
for n in sorted(os.listdir(d)):
if n.endswith(".log") and sid in n:
p = os.path.join(d, n)
return p, p
return None, newest_log()
def log_size(f):
"""字节数 + 档位(软 5 MiB / 硬 8 MiB,与 §G 一致)。"""
n = os.path.getsize(f)
tier = "🟢 低于软档"
if n >= HARD_LOG:
tier = "🔴 硬档:立刻停手 + 建接续会话"
elif n >= SOFT_LOG:
tier = "🟡 软档:开始写交接材料"
return n, tier
def cmd_stat(a):
"""一次取齐「锁 / git / 本会话日志水位」= 原 3~4 次工具调用。
⛔ 只读:不抢锁、不碰文件、不写日志。`--full` 才追加跑 state.py。
"""
say("== 锁(只读查询,⛔ 不抢)==")
run([bash_path(), GUARD, "--status"])
say("== git ==")
run(["git", "-C", WS, "log", "-1", "--format=%h %ad %s", "--date=format:%m-%d %H:%M"])
rc, txt = run(["git", "-C", WS, "status", "--porcelain=v1", "--branch"])
if rc == 0:
dirty = [x for x in txt.splitlines() if x.strip() and not x.startswith("##")]
say("(本工作区改动 %d 项)" % len(dirty))
say("== 本会话日志水位 ==")
sid = os.environ.get("CODEBUDDY_SESSION_ID") or ""
exact, f = find_session_log(sid)
if not f or not os.path.exists(f):
say("找不到会话日志(logs/<今天>/sdk/conversations/ 下无 .log)")
else:
n, tier = log_size(f)
say("会话 id=%s 精确命中=%s" % (sid or "(环境里没有 id)",
"是" if exact else "否(回退今日最新 mtime)"))
say("%s\n %d B (%.2f MiB) Δ到硬档 %d KB\n %s" % (
f, n, n / 1048576.0, max(HARD_LOG - n, 0) // 1024, tier))
if "--full" in a:
say("== state.py(--full)==")
run([PY, os.path.join(WS, "state.py")])
return 0
def cmd_open(a):
if not a:
say("用法: dsh open <会话名> [目标文件...]")
return 2
name, targets = a[0], a[1:]
say("== 1/3 跑状态 ==")
run([PY, os.path.join(WS, "state.py")])
say("== 2/3 判可锁定范围 ==")
if targets:
rc, _ = run([bash_path(), PREFLIGHT, name] + list(targets))
if rc != 0:
say("⛔ preflight rc=%d ⇒ 拒绝开工(未归类或机制层非空)" % rc)
return rc
else:
say("(未给目标文件 ⇒ 跳过 preflight)")
say("== 3/3 抢锁 ==")
rc, _ = run([bash_path(), GUARD, "--claim-exec", name, "--domains", DOMAIN])
if rc != 0:
say("⛔ 抢不到锁 ⇒ 停手报告(R9)")
return rc
say("✅ 开工就绪。收尾务必跑: dsh close %s" % name)
return 0
def my_doing_locks(name):
"""扫 05-交接单/.doing-*,只挑 OWNER 第 1 行 == 本会话名的单号锁。
⚠️ 2026-10-01 实测:底层 `--release <单号>` 是**裸 `rm -rf`,不做任何归属校验**
(`--release-skeleton` / `--release-publish` 2026-09-25 都补了校验,**这个漏了**)
⇒ 归属必须由**本入口自己卡**,否则 `dsh close` 会变成绕过 R9 的后门。
"""
d = os.path.join(DOC, "05-交接单")
out = []
if not os.path.isdir(d):
return out
for n in sorted(os.listdir(d)):
if not n.startswith(".doing-"):
continue
try:
with open(os.path.join(d, n, "OWNER"), "r", encoding="utf-8", errors="replace") as fh:
first = fh.readline().strip()
except OSError:
continue
if first == name: # OWNER 第 1 行 = 会话名(claim 时写入)
out.append(n[len(".doing-"):])
return out
def cmd_close(a):
if not a:
say("用法: dsh close <会话名> [单号...]")
return 2
name, tasks = a[0], a[1:]
say("== 反序释放 ==")
# 【1】单号锁:显式单号优先;没给就只释放「OWNER == 本会话名」的那几个
if not tasks:
tasks = my_doing_locks(name)
if tasks:
for t in tasks:
run([bash_path(), GUARD, "--release", t])
say("(本会话的单号锁 %d 个已释放)" % len(tasks))
else:
say("(无需释放的单号锁 —— 本会话没有 .doing-* 占用)")
# 【2】全局执行锁(含域锁)
rc, _ = run([bash_path(), GUARD, "--release-exec", name])
say("== 收尾清单(逐条自问,本入口不代做)==")
for line in (
"1. 记忆/台账写了吗(.workbuddy/memory/<日期>.md 追加)",
"2. 提交边界:未明确要求 ⇒ 不 commit/push",
"3. 清 tmp/ 本棒残留",
"4. 结束语写明『锁已释放』或点名锁在谁手上",
):
say(" - " + line)
return 0
def cmd_log(a):
"""⚠️ 语义保留:`dsh log <已存在路径>` 仍按路径走;给 id 则精确匹配;不给则回退今日本会话。"""
want = a[0] if a else ""
if want and os.path.exists(want):
f = want
else:
_exact, f = find_session_log(want)
if not f or not os.path.exists(f):
say("找不到会话日志%s" % ("(指定: %s)" % want if want else ""))
return 1
n, tier = log_size(f)
say("%s\n %d B (%.2f MiB) Δ到硬档 %d KB\n %s" % (
f, n, n / 1048576.0, max(HARD_LOG - n, 0) // 1024, tier))
return 0
def cmd_probe(a):
if not a:
d = os.path.join(WS, "tmp")
names = sorted(n for n in os.listdir(d)) if os.path.isdir(d) else []
say("已存探针(tmp/): " + (", ".join(names) or "(空)"))
return 0
p = os.path.join(WS, "tmp", a[0])
if not p.endswith(".py"):
p += ".py"
if not os.path.exists(p):
say("⛔ 探针不存在: %s(复用已有的,不要每次新写)" % p)
return 1
return run([PY, p] + list(a[1:]))[0]
def cmd_collect(_a):
bad = 0
for item in COLLECT:
say("== %s ==" % item[0])
rc, _ = run(list(item[1:]))
bad += (rc != 0)
say("四件套完成:%d 项非零" % bad)
return 1 if bad else 0
def main():
argv = sys.argv[1:]
if not argv or argv[0] in ("-h", "--help", "help"):
say(__doc__)
return 0
table = {"open": cmd_open, "close": cmd_close, "stat": cmd_stat,
"log": cmd_log, "probe": cmd_probe, "collect": cmd_collect}
fn = table.get(argv[0])
if not fn:
say("未知子命令: %s(可用: %s)" % (argv[0], " / ".join(table)))
return 2
if any(x in ("-h", "--help") for x in argv[1:]):
say(USAGE.get(argv[0], ""))
return 0
return fn(argv[1:])
if __name__ == "__main__":
sys.exit(main())