Files
admin c1b5e4d966 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/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

384 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""WorkBuddy 会话日志洪水 —— 一行补丁(就地 · 等长 · 可回滚)
【问题】
会话诊断日志里 97.7% 的行是 `event-machine:dispatch`,其中 95.8% 是同一条
`input:"tool_call_update"`;平均每次工具调用产生约 200 帧、约 48 KB。
病根:抑制"流式帧"的集合只列了 agent_message_chunk / agent_thought_chunk,
漏掉 tool_call / tool_call_update,于是逐帧噪声全部落盘。
【本补丁怎么修】
把集合 STREAM_CHUNK_UPDATES 换成"任何 kind 都命中"的对象:
new Set(["agent_message_chunk", "agent_thought_chunk"])
-> {has:()=>true} (补空格到等长 55 字节)
于是只在 turn 收口帧返回 true,逐帧噪声不再落盘(体积降约 96%)。
【为什么是"等长"替换】
app.asar 头部记录每个文件的 size/offset,并带 per-file 完整性块。
改动长度会让其后所有文件偏移错位,必须重打包。
本补丁把 55 字节原样替换成 55 字节,头部 size/offset 全部不变;
只把被改文件自己的完整性校验(1 个总哈希 + N 个分块哈希)原地等长重算。
【用法】
python logpatch.py --check 只看,不改(含完整性核对)
python logpatch.py 打补丁(自动备份 .bak-<日期>)
python logpatch.py --revert 从备份还原(自动找最新的 .bak-*)
python logpatch.py --verify 只核对完整性
python logpatch.py --asar <路径> 指定其它 app.asar
【重要】
· 打补丁后必须重启 WorkBuddy 才生效。
· 完整性记录必须与被改文件一致,否则有加载失败风险;本脚本会改完立即复核。
"""
from __future__ import annotations
import argparse
import datetime as _dt
import glob
import hashlib
import json
import os
import re
import shutil
import sys
OLD = b'new Set(["agent_message_chunk", "agent_thought_chunk"])'
NEW = b'{has:()=>true}' + b" " * (len(OLD) - len(b'{has:()=>true}'))
assert len(NEW) == len(OLD), (len(OLD), len(NEW))
BLOCK_SIZE = 4194304
HEX64 = re.compile(rb'"[0-9a-fA-F]{64}"')
DEFAULT_ASAR = os.path.join(
os.environ.get("LOCALAPPDATA", r"C:\Users\Administrator\AppData\Local"),
"Programs", "WorkBuddy", "resources", "app.asar")
_SYM = {"✅": "[OK]", "⛔": "[!!]", "⚠️": "[!]", "⚠": "[!]", "🔴": "[!]",
"·": "-", "⇒": "=>", "—": "-", "-": "-"}
def log(msg=""):
"""控制台走 PEP528(Unicode 正确);重定向走 UTF-8;符号降级为 ASCII 以免控制台显示方框。"""
s = str(msg)
for k, v in _SYM.items():
s = s.replace(k, v)
try:
if sys.stdout.isatty():
sys.stdout.write(s + "\n")
else:
sys.stdout.buffer.write((s + "\n").encode("utf-8", "replace"))
sys.stdout.flush()
except Exception:
try:
sys.stdout.write(s.encode("ascii", "replace").decode() + "\n")
sys.stdout.flush()
except Exception:
pass
# ---------------------------------------------------------------- 头部
def find_header(raw: bytes):
"""返回 (header_json_start, header_len, data_base)"""
start = raw.find(b'{"files"')
if start < 0:
raise RuntimeError('认不出 asar 头部(未找到 {"files")')
_, consumed = json.JSONDecoder().raw_decode(raw[start:start + 40_000_000].decode("utf-8"))
base = start + consumed
while base % 4:
base += 1
return start, consumed, base
def build_index(meta):
files = []
def walk(node, prefix):
for name, ent in (node.get("files") or {}).items():
p = prefix + "/" + name if prefix else name
if "files" in ent:
walk(ent, p)
else:
try:
off = int(ent.get("offset", "0"))
except Exception:
off = -1
files.append({"path": p, "offset": off,
"size": int(ent.get("size", 0) or 0),
"unpacked": bool(ent.get("unpacked")),
"integrity": ent.get("integrity")})
walk(meta, "")
return files
def owner_packed(files, rel_off):
"""返回**真正**拥有该偏移的打包文件(排除 unpacked:它们 offset=0 会覆盖全区间)。
补丁要点:旧版本只用 "off <= rel < off+size" 取第一个命中,
结果被 unpacked 大文件(offset=0)抢走归属 -> 完整性记录漏改。
"""
cands = [f for f in files
if (not f["unpacked"]) and f["offset"] >= 0
and f["offset"] <= rel_off < f["offset"] + f["size"]]
if not cands:
return None
return min(cands, key=lambda f: f["size"]) # 最具体者
# ---------------------------------------------------------------- 完整性
def calc_integrity(blob: bytes):
return hashlib.sha256(blob).hexdigest(), [
hashlib.sha256(blob[i:i + BLOCK_SIZE]).hexdigest()
for i in range(0, len(blob), BLOCK_SIZE)]
def read_content(raw, base, f):
return raw[base + f["offset"]: base + f["offset"] + f["size"]]
def verify_entry(blob: bytes, integ: dict):
"""返回 (ok|None, 说明)"""
if not integ:
return None, "无 integrity 字段"
if str(integ.get("algorithm", "SHA256")).upper() != "SHA256":
return None, "算法非 SHA256"
gh, gb = calc_integrity(blob)
ok_h = (integ.get("hash") == gh)
stored = list(integ.get("blocks") or [])
if len(blob) == 0:
# 空文件:只认总哈希(分块表示法各家不一,避免假阳性)
return ok_h, "空文件 hash=%s" % ("OK" if ok_h else "MISMATCH")
n_ok = sum(1 for a, b in zip(stored, gb) if a == b)
return (ok_h and n_ok == len(gb) and len(gb) == len(stored)), \
"hash=%s blocks=%d/%d" % ("OK" if ok_h else "MISMATCH", n_ok, len(gb))
def audit_packed(raw, base, files, only=None):
"""对全部打包文件(或指定集合)做完整性核对;返回 (通过数, 失败列表, 跳过数)"""
ok_n, skipped, bad = 0, 0, []
for f in files:
if only is not None and f["path"] not in only:
continue
if not f["integrity"] or f["unpacked"]:
continue
if f["offset"] < 0 or base + f["offset"] + f["size"] > len(raw):
skipped += 1
continue
ok, detail = verify_entry(read_content(raw, base, f), f["integrity"])
if ok is None:
skipped += 1
elif ok:
ok_n += 1
else:
bad.append("%s -> %s" % (f["path"], detail))
return ok_n, bad, skipped
def set_integrity_inplace(buf: bytearray, raw: bytes, start, hlen, base, f):
"""按文件条目(用其唯一 offset 做锚点)就地等长重写 hash / blocks。"""
"""就地重写该文件的 hash / blocks(等长)。成功返回 (True, 说明)。"""
blob = bytes(buf[base + f["offset"]: base + f["offset"] + f["size"]])
new_hash, new_blocks = calc_integrity(blob)
anchor = b'"%d"' % f["offset"]
if raw.count(anchor) != 1:
return False, "锚点 %s 不唯一,拒绝改动" % anchor.decode()
apos = raw.find(anchor)
ipos = raw.find(b'"integrity":{', apos, start + hlen)
if ipos < 0 or ipos - apos > 400:
return False, "定位该文件 integrity 条目失败"
hp = raw.find(b'"hash":"', ipos, start + hlen)
if hp < 0:
return False, "找不到 hash 字段"
hs = hp + len(b'"hash":"')
if not all(c in b"0123456789abcdefABCDEF" for c in raw[hs:hs + 64]):
return False, "hash 字段非 64 位十六进制"
buf[hs:hs + 64] = new_hash.encode()
bp = raw.find(b'"blocks":[', ipos, start + hlen)
if bp < 0:
return False, "找不到 blocks 数组"
be = raw.find(b"]", bp, start + hlen)
if be < 0:
return False, "blocks 数组未闭合"
found = list(HEX64.finditer(bytes(raw[bp:be])))
if len(found) != len(new_blocks):
return False, "分块数不符(头部 %d / 实算 %d)" % (len(found), len(new_blocks))
for m, nb in zip(found, new_blocks):
s = bp + m.start() + 1
buf[s:s + 64] = nb.encode()
return True, "hash 已更新 + %d/%d 个分块" % (len(new_blocks), len(new_blocks))
def find_backup(asar):
d, b = os.path.dirname(asar), os.path.basename(asar)
cands = glob.glob(os.path.join(d, b + ".bak-*"))
if not cands:
return None
return max(cands, key=os.path.getmtime)
# ---------------------------------------------------------------- main
def main() -> int:
ap = argparse.ArgumentParser(add_help=True)
ap.add_argument("--asar", default=DEFAULT_ASAR)
ap.add_argument("--check", action="store_true", help="只看不改")
ap.add_argument("--verify", action="store_true", help="只核对完整性")
ap.add_argument("--revert", action="store_true", help="从备份还原")
a = ap.parse_args()
asar = a.asar
log("=" * 62)
log("WorkBuddy 会话日志补丁 · %s" % _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
log("=" * 62)
if not os.path.isfile(asar):
log("⛔ 找不到 app.asar:%s" % asar)
return 1
log("目标 = %s" % asar)
# ---- 还原 ----
if a.revert:
bak = find_backup(asar)
if not bak:
log("⛔ 没有找到备份(app.asar.bak-*),无法还原")
return 1
log("备份 = %s(%d 字节)" % (bak, os.path.getsize(bak)))
shutil.copy2(bak, asar)
ok = os.path.getsize(asar) == os.path.getsize(bak)
log("✅ 已还原(大小一致:%s)" % ok)
log("")
log("⇒ 请重启 WorkBuddy 生效。")
return 0
raw = open(asar, "rb").read()
size0 = len(raw)
start, hlen, base = find_header(raw)
meta = json.loads(raw[start:start + hlen].decode("utf-8"))
files = build_index(meta)
log("大小 = %d 字节;条目 %d 个;数据区起点 %d" % (size0, len(files), base))
# ---- 只核对 ----
if a.verify:
ok_n, bad, skipped = audit_packed(raw, base, files)
log("")
log("---- 完整性核对(仅打包文件)----")
log("✅ 通过 = %d | ⛔ 不通过 = %d | 跳过 = %d" % (ok_n, len(bad), skipped))
for b in bad:
log(" ⛔ %s" % b)
return 1 if bad else 0
# ---- 找补丁点 ----
hits = []
i = base
while True:
i = raw.find(OLD, i)
if i < 0:
break
if raw[i + len(OLD): i + len(OLD) + 1] == b";": # 只认"定义",跳过 .has(...) 用法
hits.append(i)
i += 1
log("命中「集合定义」 %d 处(等长替换 %d -> %d 字节)" % (len(hits), len(OLD), len(NEW)))
owners, unresolved = [], []
for h in hits:
ow = owner_packed(files, h - base)
if ow is None:
unresolved.append(h)
log(" @%d 归属 = ⛔ 未找到打包文件(将被跳过并告警)" % h)
else:
if ow["path"] not in [o["path"] for o in owners]:
owners.append(ow)
log(" @%d 归属 = %s (unpacked=False)" % (h, ow["path"]))
if a.check:
log("")
log("---- 当前完整性核对(仅打包文件)----")
ok_n, bad, skipped = audit_packed(raw, base, files)
log("✅ 通过 = %d | ⛔ 不通过 = %d | 跳过 = %d" % (ok_n, len(bad), skipped))
for b in bad[:20]:
log(" ⛔ %s" % b)
log("")
log("⇒ --check 模式,未做任何修改。")
return 1 if bad else 0
if not hits:
log("")
log("⇒ 未发现可替换的定义:已经是打过补丁的状态(或版本不同)。什么都不做。")
return 0
if unresolved:
log("")
log("⛔ 有 %d 处命中找不到归属文件,为避免改坏别的文件,**中止,未写盘**。" % len(unresolved))
return 1
# ---- 备份 ----
stamp = _dt.date.today().strftime("%Y%m%d")
bak = "%s.bak-%s" % (asar, stamp)
if not os.path.isfile(bak):
shutil.copy2(asar, bak)
log("✅ 已备份 → %s" % bak)
else:
log("· 备份已存在,复用 → %s" % bak)
# ---- 改字节 ----
buf = bytearray(raw)
for h in hits:
buf[h:h + len(OLD)] = NEW
# ---- 改完整性(逐个受影响文件)----
for ow in owners:
ok, msg = set_integrity_inplace(buf, raw, start, hlen, base, ow)
log(" · %s → %s" % (ow["path"], ("✅ " + msg) if ok else ("⛔ " + msg)))
if not ok:
log("⛔ 完整性重算失败,**中止,未写盘**。")
return 1
if len(buf) != size0:
log("⛔ 长度变化(%d -> %d),中止,未写盘" % (size0, len(buf)))
return 1
# ---- 内存自检 ----
b2 = bytes(buf)
s2, h2, base2 = find_header(b2)
json.loads(b2[s2:s2 + h2].decode("utf-8"))
ok_n, bad, skipped = audit_packed(b2, base2, build_index(json.loads(
b2[s2:s2 + h2].decode("utf-8"))), only={o["path"] for o in owners})
log("")
log("---- 落盘前自检 ----")
log("长度未变:True(%d)| 头部可解析:True" % len(b2))
log("被改文件完整性:%s" % ("✅ 全部通过" if not bad else "⛔ " + "; ".join(bad)))
if bad:
log("⛔ 自检未通过,**中止,未写盘**。")
return 1
with open(asar, "wb") as fp:
fp.write(b2)
chk = open(asar, "rb").read()
log("")
log("---- 落盘后复核 ----")
log("大小未变:%s(%d)" % (len(chk) == size0, len(chk)))
log("旧串残留:%d 处(另一处 .has(...) 用法不在本补丁范围)" % chk.count(OLD))
log("新串出现:%d 处" % chk.count(NEW))
try:
s3, h3, _ = find_header(chk)
json.loads(chk[s3:s3 + h3].decode("utf-8"))
log("头部仍可解析:True")
except Exception as e:
log("⛔ 头部解析失败:%r" % e)
return 1
log("")
log("✅ 完成。请重启 WorkBuddy 生效。")
log(" 还原:python %s --revert" % os.path.basename(__file__))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except SystemExit:
raise
except Exception as exc:
log("⛔ 异常:%r" % (exc,))
raise SystemExit(1)