# -*- 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)