按用户令提交:把此前未纳管的 9 个技能目录一并入库
用户令逐字:「E:/ProgramData/.workbuddy/skills 提交仓库是指的这里」—— 即本目录就是仓库(2026-10-07 已在本目录建仓,见当日日志 §22),本轮把余下未纳管的 9 个技能一并提交。 本次入库(9 个技能,46 个文件): 1、`AI HOT` 2、`draw-ui` 3、`dsh-diagnose` 4、`dsh-knowledge` 5、`dsh-local-env` 6、`dsh-opensource-release` 7、`dsh-workflow` 8、`oil-motion` 9、`skills-security-check` 提交前核对: · **凭据类扫描**(`*.env` / `*token*` / `*.key` / `*secret*` / `*.pem`)⇒ **零命中** ✓; · 体积合计约 20 MB(`draw-ui` 12M + `oil-motion` 6.5M 是大头,形态为配图/素材 —— 仓库 `.gitignore` 里明写「`assets/*.png` 是内容不是产物」⇒ 属刻意入库); · 运行产物仍按既定规则排除(`__pycache__` / `logs/` / `tmp/` / `.venv/` / `*.egg-info` / `uv.lock` / `.workbuddy/`)。
This commit is contained in:
1 parent
d26c844f64
commit
e03465c398
46 files changed
+7558
No files matched your search
@@ -0,0 +1,363 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""同名重复会话分组 · 全库只读体检
|
||||
================================================
|
||||
|
||||
用途:一条命令扫出「同一工作区在磁盘上有几份」,以及由此派生的四类隐患:
|
||||
① 会话分组重复(projects\\<cwd-slug> 与 DB 的 sessions.cwd 不再一一对应)
|
||||
② 锁命名空间分裂(dsh 锁按 --ws 算键 ⇒ 两处各一把锁、互不排斥)
|
||||
③ 入口文件双份 / 已分叉(接续棒会读到过期的「本轮动作」)
|
||||
④ automation 的 cwds 指向非权威侧
|
||||
|
||||
只读。绝不写 workbuddy.db、不动任何工作区文件。
|
||||
🔴 2026-09-28 现状:本脚本默认比对的「镜像侧」`E:\ProgramDSH\AIProject` **已整树删除**
|
||||
⇒ 脚本**功能已失效**(两侧恒为空对 ⇒ 只走"跳过"分支)。代码保留备查;若日后重建镜像树,可原样复用。
|
||||
判据与成因分类见 SKILL.md §0.6(甲~己)与「实测事故·四」。
|
||||
|
||||
用法:
|
||||
python audit-mirror.py # 全量(默认)
|
||||
python audit-mirror.py --roots E:\\ProgramData\\AIProject E:\\ProgramDSH\\AIProject
|
||||
python audit-mirror.py --only ai1net_ui # 只看名字含该串的工作区
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
BS = chr(92) # 反斜杠;用 chr() 免得被各种 shell 的转义吃掉
|
||||
DSH_HOME_DEFAULT = r"E:\ProgramDSH\.dsh"
|
||||
WB_HOME_DEFAULT = r"E:\ProgramData\.workbuddy"
|
||||
|
||||
DEFAULT_ROOTS = [
|
||||
r"E:\ProgramData\AIProject",
|
||||
r"E:\ProgramDSH\AIProject",
|
||||
r"E:\ProgramData\WorkBuddy",
|
||||
r"C:\Users\Administrator\WorkBuddy",
|
||||
]
|
||||
|
||||
ENTRY_KEYS = ("接续入口", "交接单", "接续包")
|
||||
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "_venv", "dist", "build", ".next"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 工具
|
||||
|
||||
def norm(p: str) -> str:
|
||||
return p.replace("/", BS).rstrip(BS)
|
||||
|
||||
|
||||
def slug_of(path: str) -> str:
|
||||
"""复现宿主的 cwd -> 分组目录名 规则:分隔符统一成 '-'、合并连续、去首尾、盘符转小写。
|
||||
|
||||
实测(2026-09-27 · 7/7 命中):
|
||||
'E:/ProgramData/AIProject/ai1net_ui' -> 'e-ProgramData-AIProject-ai1net_ui'
|
||||
注意:**只小写盘符**,中间路径段保留原大小写。
|
||||
"""
|
||||
s = path.strip()
|
||||
for ch in (BS, ":", "/", " "):
|
||||
s = s.replace(ch, "-")
|
||||
s = re.sub("-+", "-", s).strip("-")
|
||||
return (s[0].lower() + s[1:]) if len(s) > 1 and s[1] == "-" else s
|
||||
|
||||
|
||||
def ts(v) -> str:
|
||||
try:
|
||||
return datetime.datetime.fromtimestamp(v / 1000).strftime("%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return str(v)
|
||||
|
||||
|
||||
def manifest(root: str, cap: int = 4000) -> dict:
|
||||
out = {}
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
||||
for f in filenames:
|
||||
full = os.path.join(dirpath, f)
|
||||
try:
|
||||
st = os.stat(full)
|
||||
except OSError:
|
||||
continue
|
||||
out[os.path.relpath(full, root)] = (st.st_size, int(st.st_mtime))
|
||||
if len(out) >= cap:
|
||||
out["__TRUNCATED__"] = (1, 0)
|
||||
return out
|
||||
return out
|
||||
|
||||
|
||||
def compare(a: str, b: str) -> dict:
|
||||
ma, mb = manifest(a), manifest(b)
|
||||
ka, kb = set(ma) - {"__TRUNCATED__"}, set(mb) - {"__TRUNCATED__"}
|
||||
only_a, only_b = sorted(ka - kb), sorted(kb - ka)
|
||||
size_diff = sorted(k for k in ka & kb if ma[k][0] != mb[k][0])
|
||||
return {
|
||||
"files_a": len(ka), "files_b": len(kb),
|
||||
"only_a": only_a[:6], "n_only_a": len(only_a),
|
||||
"only_b": only_b[:6], "n_only_b": len(only_b),
|
||||
"size_diff": size_diff[:6], "n_size_diff": len(size_diff),
|
||||
"trunc": ("__TRUNCATED__" in ma or "__TRUNCATED__" in mb),
|
||||
}
|
||||
|
||||
|
||||
def md5(p: str) -> str:
|
||||
with open(p, "rb") as fh:
|
||||
return hashlib.md5(fh.read()).hexdigest()[:8]
|
||||
|
||||
|
||||
def hr(title: str) -> None:
|
||||
print()
|
||||
print("=" * 78)
|
||||
print(title)
|
||||
print("=" * 78)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 各节
|
||||
|
||||
def read_db(wb_home: str):
|
||||
db = os.path.join(wb_home, "workbuddy.db")
|
||||
if not os.path.isfile(db):
|
||||
print("⛔ 找不到 %s" % db)
|
||||
sys.exit(2)
|
||||
con = sqlite3.connect("file:%s?mode=ro" % db.replace(BS, "/"), uri=True)
|
||||
sessions = list(con.execute(
|
||||
"select id,cwd,is_background_automation,deleted_at,created_at,last_activity_at,"
|
||||
"coalesce(custom_title,title) from sessions order by cwd,created_at"))
|
||||
automations = list(con.execute(
|
||||
"select id,name,status,schedule_type,scheduled_at,cwds,created_at from automations order by created_at"))
|
||||
return con, sessions, automations
|
||||
|
||||
|
||||
def sec1_sessions(sessions, roots, only):
|
||||
hr("§1 会话 cwd 存在性 + 同名镜像检测")
|
||||
bycw = {}
|
||||
for r in sessions:
|
||||
bycw.setdefault(norm(r[1]) if r[1] else "", []).append(r)
|
||||
pairs = []
|
||||
for c in sorted(bycw):
|
||||
if not c:
|
||||
continue
|
||||
base = os.path.basename(c)
|
||||
if only and only not in base:
|
||||
continue
|
||||
sibs = [os.path.join(rt, base) for rt in roots
|
||||
if os.path.isdir(os.path.join(rt, base))
|
||||
and os.path.normcase(os.path.join(rt, base)) != os.path.normcase(c)]
|
||||
print("\n--- cwd = %s" % c)
|
||||
print(" 目录存在: %-5s 会话数: %d" % (os.path.isdir(c), len(bycw[c])))
|
||||
for r in bycw[c]:
|
||||
print(" %s auto=%s del=%s created=%s last=%s %r" % (
|
||||
r[0][:8], r[2], r[3], ts(r[4]), ts(r[5]), r[6][:36]))
|
||||
if sibs:
|
||||
for s in sibs:
|
||||
pairs.append((c, s))
|
||||
print(" [戊] 同名镜像: %s" % s)
|
||||
else:
|
||||
print(" [ok] 无同名镜像")
|
||||
return pairs
|
||||
|
||||
|
||||
def sec2_pairs(pairs):
|
||||
hr("§2 镜像对内容比对(真副本 / 空骨架 / 谁更全)")
|
||||
if not pairs:
|
||||
print("(无镜像对)")
|
||||
for a, b in pairs:
|
||||
print("\n--- %s\n vs %s" % (a, b))
|
||||
if not (os.path.isdir(a) and os.path.isdir(b)):
|
||||
print(" ⚠️ 有一侧不存在 —— 属**己类**(入口声明与磁盘不符),不是戊类")
|
||||
continue
|
||||
r = compare(a, b)
|
||||
print(" 文件数 A=%d B=%d%s" % (r["files_a"], r["files_b"],
|
||||
" (已截断)" if r["trunc"] else ""))
|
||||
print(" 仅 A 有 %d;仅 B 有 %d;同名但大小不同 %d" % (
|
||||
r["n_only_a"], r["n_only_b"], r["n_size_diff"]))
|
||||
for k, lbl in (("only_a", "A 独有"), ("only_b", "B 独有"), ("size_diff", "大小不同")):
|
||||
if r[k]:
|
||||
print(" %s: %s" % (lbl, r[k]))
|
||||
if not r["trunc"] and r["n_only_a"] == r["n_only_b"] == r["n_size_diff"] == 0:
|
||||
print(" => 逐字节同构(真副本,最危险)")
|
||||
elif r["n_only_a"] > r["n_only_b"]:
|
||||
print(" => A 更全 ⇒ **A 极可能是权威侧**")
|
||||
elif r["n_only_b"] > r["n_only_a"]:
|
||||
print(" => B 更全 ⇒ **B 极可能是权威侧**")
|
||||
|
||||
|
||||
def sec3_entries(sessions, roots, only):
|
||||
hr("§3 入口 / 交接类文件:双份与分叉")
|
||||
seen_base = set()
|
||||
for r in sessions:
|
||||
if not r[1]:
|
||||
continue
|
||||
c = norm(r[1])
|
||||
base = os.path.basename(c)
|
||||
if base in seen_base or (only and only not in base):
|
||||
continue
|
||||
seen_base.add(base)
|
||||
dirs, seen = [c], {os.path.normcase(c)}
|
||||
for rt in roots:
|
||||
p = os.path.join(rt, base)
|
||||
if os.path.isdir(p) and os.path.normcase(p) not in seen:
|
||||
dirs.append(p)
|
||||
seen.add(os.path.normcase(p))
|
||||
print("\n--- 工作区 %s" % base)
|
||||
found = []
|
||||
for d in dirs:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for f in sorted(os.listdir(d)):
|
||||
if f.lower().endswith(".md") and any(k in f for k in ENTRY_KEYS):
|
||||
p = os.path.join(d, f)
|
||||
found.append((p, os.path.getsize(p),
|
||||
datetime.datetime.fromtimestamp(os.path.getmtime(p)).strftime("%m-%d %H:%M"),
|
||||
md5(p)))
|
||||
if not found:
|
||||
print(" (无入口类文件)")
|
||||
continue
|
||||
for p, sz, mt, h in found:
|
||||
print(" %-62s %7dB %s md5=%s" % (p, sz, mt, h))
|
||||
names = {}
|
||||
for p, sz, mt, h in found:
|
||||
names.setdefault(os.path.basename(p), []).append((p, sz, h))
|
||||
for nm, lst in names.items():
|
||||
if len(lst) > 1 and len({x[2] for x in lst}) > 1:
|
||||
print(" 🔴 已分叉: %s 字节 %s" % (nm, [x[1] for x in lst]))
|
||||
|
||||
|
||||
def sec4_automations(automations, sessions, roots):
|
||||
hr("§4 automation 的 cwds 归属")
|
||||
import json as _j
|
||||
live = {norm(r[1]) for r in sessions if r[1]}
|
||||
for aid, name, status, stype, at, cwds, created in automations:
|
||||
try:
|
||||
arr = _j.loads(cwds) if cwds else []
|
||||
except Exception:
|
||||
arr = [cwds]
|
||||
print("\n--- %s %r" % (aid[:8], (name or "")[:44]))
|
||||
print(" status=%s type=%s at=%s created=%s" % (status, stype, at, ts(created)))
|
||||
for c in arr:
|
||||
c2 = norm(c)
|
||||
others = [os.path.join(rt, os.path.basename(c2)) for rt in roots
|
||||
if os.path.isdir(os.path.join(rt, os.path.basename(c2)))
|
||||
and os.path.normcase(os.path.join(rt, os.path.basename(c2))) != os.path.normcase(c2)]
|
||||
ok_exist = os.path.isdir(c2)
|
||||
authoritative = c2 in live or any(norm(o) in live for o in others)
|
||||
marks = []
|
||||
marks.append("存在" if ok_exist else "🔴不存在(己类:会建幽灵分组)")
|
||||
if others:
|
||||
marks.append("有同名镜像%d个" % len(others))
|
||||
if not authoritative:
|
||||
marks.append("🔴会话不在该路径(疑似写到非权威侧)")
|
||||
print(" cwds=%s %s" % (c2, " · ".join(marks)))
|
||||
for o in others:
|
||||
print(" 镜像侧: %s" % o)
|
||||
|
||||
|
||||
def sec5_locks(dsh_home):
|
||||
hr("§5 dsh 锁命名空间残留(按改动时间倒序)")
|
||||
lk = os.path.join(dsh_home, ".locks")
|
||||
if not os.path.isdir(lk):
|
||||
print(" (无 %s)" % lk)
|
||||
return
|
||||
rows = []
|
||||
for d in sorted(os.listdir(lk)):
|
||||
p = os.path.join(lk, d)
|
||||
if not os.path.isdir(p):
|
||||
continue
|
||||
files = []
|
||||
for rt, _, fs in os.walk(p):
|
||||
files += [os.path.relpath(os.path.join(rt, f), p) for f in fs]
|
||||
rows.append((os.path.getmtime(p), d, len(files)))
|
||||
for mt, d, n in sorted(rows, reverse=True):
|
||||
print(" %s %-52s 文件=%d" % (
|
||||
datetime.datetime.fromtimestamp(mt).strftime("%Y-%m-%d %H:%M:%S"), d, n))
|
||||
# 同一工作区两套键
|
||||
print("\n == 同一工作区出现两套键(锁不互斥的判据) ==")
|
||||
fams = {}
|
||||
for _, d, _n in rows:
|
||||
m = re.match(r"^(e|d|c|f)-(programdata|programdsh|github|tmp)?-?(.*?)-[0-9a-f]{8}$", d)
|
||||
if m:
|
||||
fams.setdefault(m.group(3), set()).add(d)
|
||||
hit = {k: v for k, v in fams.items() if len(v) > 1}
|
||||
if not hit:
|
||||
print(" 无")
|
||||
for k, v in sorted(hit.items()):
|
||||
print(" 🔴 %s -> %s" % (k, sorted(v)))
|
||||
|
||||
|
||||
def sec6_groups(wb_home, sessions):
|
||||
hr("§6 projects 分组目录 <-> DB cwd 是否一一对应")
|
||||
proj = os.path.join(wb_home, "projects")
|
||||
if not os.path.isdir(proj):
|
||||
print(" (无 %s)" % proj)
|
||||
return
|
||||
cwds = sorted({norm(r[1]) for r in sessions if r[1]})
|
||||
groups = sorted(d for d in os.listdir(proj) if os.path.isdir(os.path.join(proj, d)))
|
||||
idx = {slug_of(c).lower(): c for c in cwds}
|
||||
print(" 分组数=%d 不同 cwd 数=%d" % (len(groups), len(cwds)))
|
||||
orphan = []
|
||||
for d in groups:
|
||||
n = len([f for f in os.listdir(os.path.join(proj, d)) if f.endswith(".jsonl")])
|
||||
hit = idx.get(d.lower())
|
||||
print(" %-52s jsonl=%-3d %s" % (d, n, ("<= " + hit) if hit else "🔴 无对应 cwd(孤儿分组)"))
|
||||
if not hit:
|
||||
orphan.append(d)
|
||||
miss = [c for c in cwds if slug_of(c).lower() not in [g.lower() for g in groups]]
|
||||
print("\n 孤儿分组 = %d %s" % (len(orphan), orphan))
|
||||
print(" 未建组的 cwd = %d %s" % (len(miss), miss))
|
||||
if not orphan and not miss:
|
||||
print(" ✅ 一一对应,无重复分组")
|
||||
|
||||
|
||||
def sec7_mirror_tree(roots):
|
||||
hr("§7 根目录整树比对(找「镜像树」,甲/戊/己的共同温床)")
|
||||
ai = [os.path.join(r, "AIProject") for r in roots]
|
||||
a, b = r"E:\ProgramData\AIProject", r"E:\ProgramDSH\AIProject"
|
||||
if not (os.path.isdir(a) and os.path.isdir(b)):
|
||||
print(" (无 ProgramData\\AIProject / ProgramDSH\\AIProject 对,跳过)")
|
||||
return
|
||||
la, lb = set(os.listdir(a)), set(os.listdir(b))
|
||||
both = sorted(la & lb)
|
||||
print(" A = %s 目录数=%d" % (a, len(la)))
|
||||
print(" B = %s 目录数=%d" % (b, len(lb)))
|
||||
print(" 同名(B 是 A 的镜像候选)= %d 个: %s" % (len(both), both))
|
||||
print(" 仅 A 有 = %d 个: %s" % (len(la - lb), sorted(la - lb)))
|
||||
print(" 仅 B 有 = %d 个: %s" % (len(lb - la), sorted(lb - la)))
|
||||
if both:
|
||||
print(" ⇒ ⚠️ 上面这些同名目录每多一个,就多一份「工作区归属判错」的机会")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--wb-home", default=WB_HOME_DEFAULT)
|
||||
ap.add_argument("--dsh-home", default=DSH_HOME_DEFAULT)
|
||||
ap.add_argument("--roots", nargs="*", default=None)
|
||||
ap.add_argument("--only", default=None, help="只看名字含该串的工作区")
|
||||
ns = ap.parse_args()
|
||||
|
||||
roots = ns.roots if ns.roots else DEFAULT_ROOTS
|
||||
import json as _j
|
||||
con, sessions, automations = read_db(ns.wb_home)
|
||||
print("workbuddy home = %s" % ns.wb_home)
|
||||
print("dsh home = %s" % ns.dsh_home)
|
||||
print("候选根 = %s" % roots)
|
||||
print("会话 %d 条 / automation %d 条" % (len(sessions), len(automations)))
|
||||
|
||||
pairs = sec1_sessions(sessions, roots, ns.only)
|
||||
sec2_pairs(pairs)
|
||||
sec3_entries(sessions, roots, ns.only)
|
||||
sec4_automations(automations, sessions, roots)
|
||||
sec5_locks(ns.dsh_home)
|
||||
sec6_groups(ns.wb_home, sessions)
|
||||
sec7_mirror_tree(roots)
|
||||
|
||||
hr("总判读口径")
|
||||
print(" ① 同一工作区在磁盘上有几个目录?>1 ⇒ 戊类(先定权威再开工)")
|
||||
print(" ② 入口声明的路径 Test-Path 是否成立?不成立 ⇒ 己类(回填入口头部,⛔ 别新建目录)")
|
||||
print(" ③ automation 的 cwds 是否落在「会话实际所在的那一侧」?否 ⇒ 立刻改,否则多一个分组")
|
||||
print(" ④ 旧侧入口是否已改 stub?只写「停维护」不算 ⇒ 两侧必分叉")
|
||||
print(" ⑤ 锁有几套键?一个工作区两套 = 锁不互斥(比 UI 观感危险)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user