Created
April 14, 2026 09:25
-
-
Save lixiangwuxian/70af6eb0a7f4855d58333e39914b8bd5 to your computer and use it in GitHub Desktop.
Unlock claude code 1h cache ttl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Claude Code 1h Cache Patch Tool | |
| 跨平台:macOS / Linux / Windows | |
| 安装类型:native binary / npm cli.js 自动识别 | |
| 用法: | |
| python3 claude-1h-cache.py # 打补丁 + 安装自动监听 | |
| python3 claude-1h-cache.py patch # 仅打补丁 | |
| python3 claude-1h-cache.py watch # 仅安装自动监听 | |
| python3 claude-1h-cache.py status # 查看状态 | |
| python3 claude-1h-cache.py restore # 还原备份 | |
| python3 claude-1h-cache.py unwatch # 卸载自动监听 | |
| python3 claude-1h-cache.py daemon # 内部用:守护进程模式(Windows/Linux 后台监听) | |
| """ | |
| import sys | |
| import os | |
| import shutil | |
| import subprocess | |
| import platform | |
| import time | |
| import json | |
| # ─── 版本 ───────────────────────────────────────────────────────────────────── | |
| VERSION = "2.0.0" | |
| # ─── 补丁锚点(版本无关,依赖语义字符串)──────────────────────────────────── | |
| # ─── 语义锚点(不依赖混淆变量名,跨版本稳定)───────────────────────────────── | |
| # 这两个字符串是 env var 名和配置 key,不会被混淆器重命名 | |
| ANCHOR_ENV = b'ENABLE_PROMPT_CACHING_1H_BEDROCK' | |
| ANCHOR_KEY = b'tengu_prompt_cache_1h_config' | |
| PATCH_MARKER = b'/*__1h_patched__*/' | |
| def _find_func_bounds(data: bytes, start: int = 0): | |
| """ | |
| 从 start 开始,通过语义锚点定位下一个 1h 缓存判断函数的 [func_start, func_end)。 | |
| 向锚点前最多 400 字节内找最近的 'function ',再用括号计数找闭合 '}'。 | |
| 验证第二个锚点也在函数体内才返回结果,防止误命中。 | |
| 找不到返回 None。 | |
| """ | |
| pos0 = start | |
| while True: | |
| idx = data.find(ANCHOR_ENV, pos0) | |
| if idx == -1: | |
| return None | |
| search_start = max(0, idx - 400) | |
| rel = data[search_start:idx].rfind(b'function ') | |
| if rel == -1: | |
| pos0 = idx + 1 | |
| continue | |
| func_start = search_start + rel | |
| try: | |
| brace_open = data.index(b'{', func_start) | |
| except ValueError: | |
| pos0 = idx + 1 | |
| continue | |
| depth, p = 0, brace_open | |
| func_end = None | |
| while p < len(data): | |
| b = data[p] | |
| if b == ord('{'): | |
| depth += 1 | |
| elif b == ord('}'): | |
| depth -= 1 | |
| if depth == 0: | |
| func_end = p + 1 | |
| break | |
| p += 1 | |
| if func_end is None: | |
| pos0 = idx + 1 | |
| continue | |
| if ANCHOR_KEY in data[func_start:func_end]: | |
| return func_start, func_end | |
| # 锚点不在函数体内,从该锚点之后继续找下一个 | |
| pos0 = idx + 1 | |
| def _find_all_func_bounds(data: bytes): | |
| """定位所有未 patch 的目标函数。patched 函数体里没有锚点字符串,自动被跳过。""" | |
| bounds = [] | |
| start = 0 | |
| while True: | |
| b = _find_func_bounds(data, start) | |
| if b is None: | |
| break | |
| bounds.append(b) | |
| start = b[1] | |
| return bounds | |
| def _apply_patch_bytes(data: bytes): | |
| """ | |
| 返回 (patched_data, count) 或 (None, 0) 表示没有可 patch 的函数。 | |
| 所有未 patch 的目标函数都会被等长替换。 | |
| """ | |
| all_bounds = _find_all_func_bounds(data) | |
| if not all_bounds: | |
| return None, 0 | |
| # 等长替换,偏移不会变化,但按倒序写更安全 | |
| out = data | |
| for func_start, func_end in sorted(all_bounds, reverse=True): | |
| orig = out[func_start:func_end] | |
| repl = _make_replacement(orig) | |
| out = out[:func_start] + repl + out[func_end:] | |
| return out, len(all_bounds) | |
| def _make_replacement(orig: bytes) -> bytes: | |
| """构造等长替换:保留函数名,函数体改为 return!0,尾部空格填充。""" | |
| import re | |
| m = re.match(rb'function (\w+)\(', orig) | |
| if not m: | |
| raise ValueError("无法从原函数提取函数名") | |
| func_name = m.group(1) | |
| core = PATCH_MARKER + b'function ' + func_name + b'(H){return!0}' | |
| if len(core) > len(orig): | |
| raise ValueError(f"补丁长度超出原函数:{len(core)} > {len(orig)}") | |
| return core + b' ' * (len(orig) - len(core)) | |
| # ─── 平台 ───────────────────────────────────────────────────────────────────── | |
| SYSTEM = platform.system() # Darwin / Linux / Windows | |
| IS_MAC = SYSTEM == "Darwin" | |
| IS_WIN = SYSTEM == "Windows" | |
| IS_LINUX = SYSTEM == "Linux" | |
| HOME = os.path.expanduser("~") | |
| SCRIPT = os.path.abspath(sys.argv[0]) | |
| # ─── 颜色(Windows CMD 不一定支持,做了兼容)────────────────────────────────── | |
| _use_color = sys.stdout.isatty() and not IS_WIN or (IS_WIN and os.environ.get("WT_SESSION")) | |
| def _c(code, s): return f"\033[{code}m{s}\033[0m" if _use_color else s | |
| def green(s): return _c("32", s) | |
| def yellow(s): return _c("33", s) | |
| def red(s): return _c("31", s) | |
| def cyan(s): return _c("36", s) | |
| def bold(s): return _c("1", s) | |
| def ok(msg): print(f" {green('v')} {msg}") | |
| def warn(msg): print(f" {yellow('!')} {msg}") | |
| def err(msg): print(f" {red('x')} {msg}", file=sys.stderr) | |
| def info(msg): print(f" {cyan('i')} {msg}") | |
| def section(msg): print(f"\n{bold(cyan(msg))}") | |
| # ─── 定位安装 ───────────────────────────────────────────────────────────────── | |
| def find_target(): | |
| """ | |
| 返回 (path, mode, versions_dir_or_None) | |
| mode: "binary" | "npm" | |
| versions_dir: native binary 模式下的版本目录(用于监听) | |
| """ | |
| # ── native binary:~/.local/share/claude/versions/(macOS / Linux) | |
| versions_dir = os.path.join(HOME, ".local", "share", "claude", "versions") | |
| if os.path.isdir(versions_dir): | |
| entries = [ | |
| v for v in os.listdir(versions_dir) | |
| if not v.endswith(".bak") and not v.endswith(".1h-cache-bak") | |
| and os.path.isfile(os.path.join(versions_dir, v)) | |
| ] | |
| if entries: | |
| def ver_key(v): | |
| parts = v.split(".") | |
| return [int(x) if x.isdigit() else 0 for x in parts] | |
| latest = sorted(entries, key=ver_key, reverse=True)[0] | |
| return os.path.join(versions_dir, latest), "binary", versions_dir | |
| # ── native binary:~\.local\bin\claude.exe(Unix 风格安装在 Windows) | |
| if IS_WIN: | |
| local_bin_exe = os.path.join(HOME, ".local", "bin", "claude.exe") | |
| if os.path.isfile(local_bin_exe): | |
| return local_bin_exe, "binary", os.path.join(HOME, ".local", "bin") | |
| # ── native binary:Windows %LOCALAPPDATA%\Programs\claude\ | |
| if IS_WIN: | |
| local_app = os.environ.get("LOCALAPPDATA", "") | |
| for base in [ | |
| os.path.join(local_app, "Programs", "claude"), | |
| os.path.join(local_app, "claude"), | |
| ]: | |
| if os.path.isdir(base): | |
| # versions 子目录 | |
| vdir = os.path.join(base, "versions") | |
| if os.path.isdir(vdir): | |
| entries = [ | |
| v for v in os.listdir(vdir) | |
| if os.path.isfile(os.path.join(vdir, v)) | |
| and not v.endswith(".bak") | |
| ] | |
| if entries: | |
| def ver_key2(v): | |
| stem = os.path.splitext(v)[0] | |
| return [int(x) if x.isdigit() else 0 for x in stem.split(".")] | |
| latest = sorted(entries, key=ver_key2, reverse=True)[0] | |
| return os.path.join(vdir, latest), "binary", vdir | |
| # 直接的 .exe | |
| exe = os.path.join(base, "claude.exe") | |
| if os.path.isfile(exe): | |
| return exe, "binary", base | |
| # ── npm 全局安装 | |
| npm_candidates = _find_npm_cli() | |
| if npm_candidates: | |
| return npm_candidates, "npm", None | |
| return None, None, None | |
| def _same_binary(a: str, b: str, head_bytes: int = 8192) -> bool: | |
| """两个文件是否是"同一份二进制":大小一致且文件头若干字节一致。""" | |
| try: | |
| if os.path.getsize(a) != os.path.getsize(b): | |
| return False | |
| with open(a, "rb") as fa, open(b, "rb") as fb: | |
| return fa.read(head_bytes) == fb.read(head_bytes) | |
| except Exception: | |
| return False | |
| def find_secondary_copies(primary: str) -> list: | |
| """ | |
| 找到与 primary 内容一致但独立存在的副本(非硬链接)。 | |
| Windows 场景:Unix 风格安装时,`~/.local/bin/claude.exe` 和 | |
| `~/.local/share/claude/versions/<latest>` 是两份独立文件。 | |
| find_target() 只返回 versions 下那份,导致 .local/bin/claude.exe | |
| 残留未 patch 的原始副本,而它恰好是 PATH 上被优先执行的那个。 | |
| """ | |
| copies = [] | |
| if not primary: | |
| return copies | |
| candidates = [] | |
| if IS_WIN: | |
| candidates.append(os.path.join(HOME, ".local", "bin", "claude.exe")) | |
| for c in candidates: | |
| if not os.path.isfile(c): | |
| continue | |
| try: | |
| if os.path.abspath(c) == os.path.abspath(primary): | |
| continue | |
| if os.path.samefile(c, primary): | |
| continue # 硬链接指向同一 inode | |
| except Exception: | |
| pass | |
| if _same_binary(c, primary): | |
| copies.append(c) | |
| return copies | |
| def _find_npm_cli(): | |
| # npm root -g | |
| try: | |
| npm_root = subprocess.check_output( | |
| ["npm", "root", "-g"], text=True, stderr=subprocess.DEVNULL, shell=IS_WIN | |
| ).strip().splitlines()[0].strip() | |
| p = os.path.join(npm_root, "@anthropic-ai", "claude-code", "cli.js") | |
| if os.path.isfile(p): return p | |
| except Exception: | |
| pass | |
| # 固定路径 | |
| candidates = [] | |
| if IS_WIN: | |
| appdata = os.environ.get("APPDATA", "") | |
| candidates += [ | |
| os.path.join(appdata, "npm", "node_modules", "@anthropic-ai", "claude-code", "cli.js"), | |
| ] | |
| nvm_home = os.environ.get("NVM_HOME", "") | |
| if nvm_home and os.path.isdir(nvm_home): | |
| for v in os.listdir(nvm_home): | |
| candidates.append(os.path.join(nvm_home, v, "node_modules", "@anthropic-ai", "claude-code", "cli.js")) | |
| else: | |
| candidates += [ | |
| "/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js", | |
| "/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js", | |
| os.path.join(HOME, ".npm-global", "lib", "node_modules", "@anthropic-ai", "claude-code", "cli.js"), | |
| ] | |
| # nvm | |
| nvm_dir = os.environ.get("NVM_DIR", os.path.join(HOME, ".nvm")) | |
| if os.path.isdir(nvm_dir): | |
| vdir = os.path.join(nvm_dir, "versions", "node") | |
| if os.path.isdir(vdir): | |
| for v in os.listdir(vdir): | |
| candidates.append(os.path.join(vdir, v, "lib", "node_modules", "@anthropic-ai", "claude-code", "cli.js")) | |
| # Homebrew | |
| for root in ["/opt/homebrew/lib/node_modules", "/usr/local/lib/node_modules"]: | |
| p = os.path.join(root, "@anthropic-ai", "claude-code", "cli.js") | |
| candidates.append(p) | |
| for p in candidates: | |
| if os.path.isfile(p): return p | |
| return None | |
| # ─── 补丁状态 ───────────────────────────────────────────────────────────────── | |
| def detect_state(data: bytes) -> str: | |
| # 先检查是否还有未 patch 的目标函数(可能存在 marker 的同时另一份副本仍是原始的) | |
| if _find_func_bounds(data): return "original" | |
| if PATCH_MARKER in data: return "patched" | |
| return "unknown" | |
| # ─── macOS 重签名 ───────────────────────────────────────────────────────────── | |
| def resign_macos(path: str) -> bool: | |
| try: | |
| subprocess.run(["codesign", "--remove-signature", path], | |
| check=True, capture_output=True) | |
| subprocess.run(["codesign", "-s", "-", path], | |
| check=True, capture_output=True) | |
| return True | |
| except Exception: | |
| return False | |
| # ─── 核心:打补丁 ───────────────────────────────────────────────────────────── | |
| def do_patch(target: str, mode: str, secondaries=None) -> bool: | |
| try: | |
| data = open(target, "rb").read() | |
| except PermissionError: | |
| err(f"读取失败,权限不足。") | |
| _hint_sudo() | |
| return False | |
| state = detect_state(data) | |
| info(f"当前状态:{green('已打补丁') if state=='patched' else yellow('未打补丁') if state=='original' else red('未知')}") | |
| if state == "patched": | |
| ok("已是补丁状态,无需操作。") | |
| return True | |
| if state == "unknown": | |
| err("未找到目标函数锚点,版本结构可能已变更。") | |
| return False | |
| all_bounds = _find_all_func_bounds(data) | |
| if not all_bounds: | |
| err("定位失败。") | |
| return False | |
| for fs, fe in all_bounds: | |
| info(f"未 patch 函数偏移:{fs},长度:{fe - fs} 字节") | |
| try: | |
| patched, count = _apply_patch_bytes(data) | |
| except ValueError as e: | |
| err(str(e)) | |
| return False | |
| if patched is None: | |
| err("无可 patch 的函数。") | |
| return False | |
| info(f"本次将 patch {count} 处副本") | |
| # 备份 | |
| bak = target + ".1h-cache-bak" | |
| if not os.path.isfile(bak): | |
| try: | |
| shutil.copy2(target, bak) | |
| ok(f"备份已创建:{bak}") | |
| except Exception as e: | |
| err(f"备份失败:{e}") | |
| return False | |
| else: | |
| info("备份已存在,跳过。") | |
| try: | |
| with open(target, "wb") as f: | |
| f.write(patched) | |
| except PermissionError: | |
| err("写入失败,权限不足。") | |
| _hint_sudo() | |
| return False | |
| # macOS:重签名 | |
| if mode == "binary" and IS_MAC: | |
| if resign_macos(target): | |
| ok("代码签名已更新(ad-hoc)。") | |
| else: | |
| warn("codesign 不可用,跳过签名。若程序崩溃请手动运行:") | |
| warn(f" codesign -s - {target}") | |
| # 验证 | |
| verify = open(target, "rb").read() | |
| if PATCH_MARKER not in verify: | |
| err("验证失败,自动还原...") | |
| shutil.copy2(bak, target) | |
| if mode == "binary" and IS_MAC: | |
| resign_macos(target) | |
| return False | |
| ok("补丁写入成功,已验证。") | |
| # 同步到其它独立副本(如 Windows 的 ~/.local/bin/claude.exe) | |
| if secondaries: | |
| for sec in secondaries: | |
| sec_bak = sec + ".1h-cache-bak" | |
| if not os.path.isfile(sec_bak): | |
| try: | |
| shutil.copy2(sec, sec_bak) | |
| ok(f"副本备份已创建:{sec_bak}") | |
| except Exception as e: | |
| warn(f"副本备份失败,跳过同步:{sec}: {e}") | |
| continue | |
| try: | |
| shutil.copy2(target, sec) | |
| if mode == "binary" and IS_MAC: | |
| resign_macos(sec) | |
| ok(f"副本已同步:{sec}") | |
| except PermissionError: | |
| warn(f"副本写入失败(权限不足):{sec}") | |
| except Exception as e: | |
| warn(f"副本同步失败:{sec}: {e}") | |
| return True | |
| def do_restore(target: str, mode: str, secondaries=None) -> bool: | |
| bak = target + ".1h-cache-bak" | |
| if not os.path.isfile(bak): | |
| err(f"未找到备份:{bak}") | |
| return False | |
| try: | |
| shutil.copy2(bak, target) | |
| if mode == "binary" and IS_MAC: | |
| resign_macos(target) | |
| ok("已还原至原始版本。") | |
| except PermissionError: | |
| err("写入失败,权限不足。") | |
| _hint_sudo() | |
| return False | |
| if secondaries: | |
| for sec in secondaries: | |
| sec_bak = sec + ".1h-cache-bak" | |
| if not os.path.isfile(sec_bak): | |
| warn(f"副本无备份,跳过:{sec}") | |
| continue | |
| try: | |
| shutil.copy2(sec_bak, sec) | |
| if mode == "binary" and IS_MAC: | |
| resign_macos(sec) | |
| ok(f"副本已还原:{sec}") | |
| except Exception as e: | |
| warn(f"副本还原失败:{sec}: {e}") | |
| return True | |
| # ─── 自动监听:macOS launchd ────────────────────────────────────────────────── | |
| _LAUNCHD_LABEL = "com.claude.1h-cache-autopatch" | |
| _LAUNCHD_PLIST = os.path.join(HOME, "Library", "LaunchAgents", f"{_LAUNCHD_LABEL}.plist") | |
| def watch_install_macos(versions_dir: str): | |
| plist = f"""<?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" | |
| "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key> | |
| <string>{_LAUNCHD_LABEL}</string> | |
| <key>WatchPaths</key> | |
| <array> | |
| <string>{versions_dir}</string> | |
| </array> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>{sys.executable}</string> | |
| <string>{SCRIPT}</string> | |
| <string>patch</string> | |
| </array> | |
| <key>StandardOutPath</key> | |
| <string>{os.path.join(HOME, ".local", "share", "claude", "1h-cache-patch.log")}</string> | |
| <key>StandardErrorPath</key> | |
| <string>{os.path.join(HOME, ".local", "share", "claude", "1h-cache-patch.log")}</string> | |
| <key>ThrottleInterval</key> | |
| <integer>5</integer> | |
| </dict> | |
| </plist> | |
| """ | |
| os.makedirs(os.path.dirname(_LAUNCHD_PLIST), exist_ok=True) | |
| with open(_LAUNCHD_PLIST, "w") as f: | |
| f.write(plist) | |
| # 卸载旧的再装 | |
| subprocess.run(["launchctl", "unload", _LAUNCHD_PLIST], | |
| capture_output=True) | |
| result = subprocess.run(["launchctl", "load", _LAUNCHD_PLIST], | |
| capture_output=True) | |
| if result.returncode == 0: | |
| ok(f"launchd 监听已安装(WatchPaths: {versions_dir})") | |
| return True | |
| else: | |
| warn(f"launchctl load 失败:{result.stderr.decode().strip()}") | |
| return False | |
| def watch_uninstall_macos(): | |
| if os.path.isfile(_LAUNCHD_PLIST): | |
| subprocess.run(["launchctl", "unload", _LAUNCHD_PLIST], capture_output=True) | |
| os.remove(_LAUNCHD_PLIST) | |
| ok("launchd 监听已卸载。") | |
| else: | |
| info("launchd 监听未安装。") | |
| # ─── 自动监听:Linux systemd ────────────────────────────────────────────────── | |
| _SYSTEMD_DIR = os.path.join(HOME, ".config", "systemd", "user") | |
| _SYSTEMD_SERVICE = "claude-1h-cache-autopatch.service" | |
| _SYSTEMD_PATH = "claude-1h-cache-autopatch.path" | |
| def watch_install_linux(versions_dir: str): | |
| os.makedirs(_SYSTEMD_DIR, exist_ok=True) | |
| service = f"""[Unit] | |
| Description=Claude Code 1h Cache Auto Patch | |
| [Service] | |
| Type=oneshot | |
| ExecStart={sys.executable} {SCRIPT} patch | |
| StandardOutput=append:{HOME}/.local/share/claude/1h-cache-patch.log | |
| StandardError=append:{HOME}/.local/share/claude/1h-cache-patch.log | |
| """ | |
| path_unit = f"""[Unit] | |
| Description=Watch Claude Code versions directory | |
| [Path] | |
| PathChanged={versions_dir} | |
| Unit={_SYSTEMD_SERVICE} | |
| [Install] | |
| WantedBy=default.target | |
| """ | |
| with open(os.path.join(_SYSTEMD_DIR, _SYSTEMD_SERVICE), "w") as f: | |
| f.write(service) | |
| with open(os.path.join(_SYSTEMD_DIR, _SYSTEMD_PATH), "w") as f: | |
| f.write(path_unit) | |
| try: | |
| subprocess.run(["systemctl", "--user", "daemon-reload"], check=True, capture_output=True) | |
| subprocess.run(["systemctl", "--user", "enable", "--now", _SYSTEMD_PATH], | |
| check=True, capture_output=True) | |
| ok(f"systemd path 监听已安装并启动。") | |
| return True | |
| except subprocess.CalledProcessError as e: | |
| warn(f"systemctl 失败:{e.stderr.decode().strip() if e.stderr else e}") | |
| info("如 systemd 不可用,改用守护进程模式:") | |
| return _watch_install_daemon_linux(versions_dir) | |
| def _watch_install_daemon_linux(versions_dir: str): | |
| """systemd 不可用时,改写 autostart 桌面条目或 .bashrc 启动守护""" | |
| autostart_dir = os.path.join(HOME, ".config", "autostart") | |
| os.makedirs(autostart_dir, exist_ok=True) | |
| desktop = f"""[Desktop Entry] | |
| Type=Application | |
| Name=Claude 1h Cache Autopatch | |
| Exec={sys.executable} {SCRIPT} daemon {versions_dir} | |
| Hidden=false | |
| NoDisplay=false | |
| X-GNOME-Autostart-enabled=true | |
| """ | |
| desktop_file = os.path.join(autostart_dir, "claude-1h-cache-autopatch.desktop") | |
| with open(desktop_file, "w") as f: | |
| f.write(desktop) | |
| ok(f"已写入 autostart 条目:{desktop_file}") | |
| info("登录后自动启动守护进程。立即启动:") | |
| info(f" nohup {sys.executable} {SCRIPT} daemon {versions_dir} &") | |
| return True | |
| def watch_uninstall_linux(): | |
| try: | |
| subprocess.run(["systemctl", "--user", "disable", "--now", _SYSTEMD_PATH], | |
| capture_output=True) | |
| except Exception: | |
| pass | |
| for f in [_SYSTEMD_SERVICE, _SYSTEMD_PATH]: | |
| p = os.path.join(_SYSTEMD_DIR, f) | |
| if os.path.isfile(p): os.remove(p) | |
| try: | |
| subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True) | |
| except Exception: | |
| pass | |
| ok("systemd 监听已卸载。") | |
| # ─── 自动监听:Windows 任务计划 ─────────────────────────────────────────────── | |
| _TASK_NAME = "ClaudeCode1hCacheAutopatch" | |
| def watch_install_windows(versions_dir: str): | |
| """ | |
| 创建一个在用户登录时启动的任务,以守护进程方式持续监听。 | |
| 使用 Windows Task Scheduler + pythonw 静默运行。 | |
| """ | |
| pythonw = sys.executable.replace("python.exe", "pythonw.exe") | |
| if not os.path.isfile(pythonw): | |
| pythonw = sys.executable # 降级使用 python.exe | |
| xml = f"""<?xml version="1.0" encoding="UTF-16"?> | |
| <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> | |
| <Triggers> | |
| <LogonTrigger> | |
| <Enabled>true</Enabled> | |
| </LogonTrigger> | |
| </Triggers> | |
| <Actions Context="Author"> | |
| <Exec> | |
| <Command>{pythonw}</Command> | |
| <Arguments>"{SCRIPT}" daemon "{versions_dir}"</Arguments> | |
| </Exec> | |
| </Actions> | |
| <Settings> | |
| <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> | |
| <ExecutionTimeLimit>PT0S</ExecutionTimeLimit> | |
| <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> | |
| <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> | |
| </Settings> | |
| </Task> | |
| """ | |
| xml_path = os.path.join(os.environ.get("TEMP", ""), "claude_patch_task.xml") | |
| with open(xml_path, "w", encoding="utf-16") as f: | |
| f.write(xml) | |
| result = subprocess.run( | |
| ["schtasks", "/Create", "/F", "/TN", _TASK_NAME, "/XML", xml_path], | |
| capture_output=True, text=True | |
| ) | |
| os.remove(xml_path) | |
| if result.returncode == 0: | |
| ok(f"Windows 任务计划已创建:{_TASK_NAME}") | |
| # 立即启动守护 | |
| subprocess.Popen( | |
| [pythonw, SCRIPT, "daemon", versions_dir], | |
| creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW | |
| ) | |
| ok("守护进程已在后台启动。") | |
| return True | |
| else: | |
| warn(f"任务计划创建失败:{result.stderr.strip()}") | |
| info("请以管理员身份运行本脚本。") | |
| return False | |
| def watch_uninstall_windows(): | |
| result = subprocess.run( | |
| ["schtasks", "/Delete", "/F", "/TN", _TASK_NAME], | |
| capture_output=True, text=True | |
| ) | |
| if result.returncode == 0: | |
| ok("Windows 任务计划已删除。") | |
| else: | |
| info("任务计划不存在或删除失败。") | |
| # ─── 守护进程:跨平台文件监听(用于 Windows / Linux 无 systemd 场景)────────── | |
| def daemon_watch(versions_dir: str): | |
| """ | |
| 轮询 versions_dir,发现新文件时自动打补丁。 | |
| 日志写入同目录的 1h-cache-patch.log。 | |
| """ | |
| log_path = os.path.join(HOME, ".local", "share", "claude", "1h-cache-patch.log") | |
| os.makedirs(os.path.dirname(log_path), exist_ok=True) | |
| def log(msg): | |
| ts = time.strftime("%Y-%m-%d %H:%M:%S") | |
| line = f"[{ts}] {msg}\n" | |
| with open(log_path, "a", encoding="utf-8") as f: | |
| f.write(line) | |
| log(f"守护进程启动,监听:{versions_dir}") | |
| def get_files(): | |
| if not os.path.isdir(versions_dir): return set() | |
| return { | |
| f for f in os.listdir(versions_dir) | |
| if not f.endswith(".bak") and not f.endswith(".1h-cache-bak") | |
| and os.path.isfile(os.path.join(versions_dir, f)) | |
| } | |
| known = get_files() | |
| while True: | |
| time.sleep(10) | |
| try: | |
| current = get_files() | |
| new_files = current - known | |
| if new_files: | |
| log(f"发现新文件:{new_files},触发补丁...") | |
| target, mode, _ = find_target() | |
| if target: | |
| data = open(target, "rb").read() | |
| state = detect_state(data) | |
| if state == "original": | |
| bak = target + ".1h-cache-bak" | |
| if not os.path.isfile(bak): | |
| shutil.copy2(target, bak) | |
| patched, pcount = _apply_patch_bytes(data) | |
| if patched is None: | |
| log("锚点定位失败,跳过。") | |
| known = current | |
| continue | |
| log(f"patch {pcount} 处副本") | |
| with open(target, "wb") as f: | |
| f.write(patched) | |
| if mode == "binary" and IS_MAC: | |
| resign_macos(target) | |
| log(f"补丁成功:{target}") | |
| for sec in find_secondary_copies(target): | |
| try: | |
| sec_bak = sec + ".1h-cache-bak" | |
| if not os.path.isfile(sec_bak): | |
| shutil.copy2(sec, sec_bak) | |
| shutil.copy2(target, sec) | |
| log(f"副本已同步:{sec}") | |
| except Exception as e: | |
| log(f"副本同步失败 {sec}: {e}") | |
| elif state == "patched": | |
| log("已是补丁状态,跳过。") | |
| else: | |
| log("锚点未找到,版本可能已变更。") | |
| known = current | |
| except Exception as e: | |
| log(f"错误:{e}") | |
| # ─── 监听安装分发 ───────────────────────────────────────────────────────────── | |
| def do_watch_install(versions_dir: str) -> bool: | |
| if versions_dir is None: | |
| warn("npm 安装模式不支持目录监听(npm 更新不会产生新文件)。") | |
| info("建议更新后手动运行:python3 claude-1h-cache.py patch") | |
| return False | |
| if IS_MAC: | |
| return watch_install_macos(versions_dir) | |
| elif IS_LINUX: | |
| return watch_install_linux(versions_dir) | |
| elif IS_WIN: | |
| return watch_install_windows(versions_dir) | |
| else: | |
| warn(f"未知平台 {SYSTEM},跳过监听安装。") | |
| return False | |
| def do_watch_uninstall(): | |
| if IS_MAC: | |
| watch_uninstall_macos() | |
| elif IS_LINUX: | |
| watch_uninstall_linux() | |
| elif IS_WIN: | |
| watch_uninstall_windows() | |
| else: | |
| warn(f"未知平台 {SYSTEM}。") | |
| # ─── 权限提示 ───────────────────────────────────────────────────────────────── | |
| def _hint_sudo(): | |
| if IS_WIN: | |
| info("请以管理员身份运行 PowerShell / CMD,再重试。") | |
| else: | |
| info(f"请用:sudo python3 {SCRIPT}") | |
| # ─── 主入口 ─────────────────────────────────────────────────────────────────── | |
| def main(): | |
| cmd = sys.argv[1] if len(sys.argv) > 1 else "all" | |
| # 守护进程模式(内部调用,不打印 banner) | |
| if cmd == "daemon": | |
| watch_dir = sys.argv[2] if len(sys.argv) > 2 else None | |
| if not watch_dir: | |
| _, _, watch_dir = find_target() | |
| if not watch_dir: | |
| sys.exit(1) | |
| daemon_watch(watch_dir) | |
| return | |
| print() | |
| print(bold(cyan(" ╔═══════════════════════════════════════════════════╗"))) | |
| print(bold(cyan(f" ║ Claude Code 1h Cache Patch Tool v{VERSION} ║"))) | |
| print(bold(cyan(f" ║ 平台:{SYSTEM:<10} Python {sys.version.split()[0]:<10} ║"))) | |
| print(bold(cyan(" ╚═══════════════════════════════════════════════════╝"))) | |
| print() | |
| # ── 定位 | |
| section("[ 1 ] 定位 Claude Code 安装...") | |
| target, mode, versions_dir = find_target() | |
| if not target: | |
| err("未找到 Claude Code。请确认已安装(native 或 npm)。") | |
| sys.exit(1) | |
| secondaries = find_secondary_copies(target) | |
| ok(f"安装类型:{bold(mode)}") | |
| ok(f"目标文件:{cyan(target)}") | |
| if versions_dir: | |
| info(f"版本目录:{cyan(versions_dir)}") | |
| for sec in secondaries: | |
| ok(f"独立副本:{cyan(sec)}") | |
| # ── status | |
| if cmd == "status": | |
| section("[ 状态 ]") | |
| data = open(target, "rb").read() | |
| state = detect_state(data) | |
| if state == "patched": | |
| ok("已打补丁,1h 缓存已启用。") | |
| elif state == "original": | |
| warn("未打补丁。") | |
| else: | |
| warn("状态未知,锚点字符串未找到。") | |
| bak = target + ".1h-cache-bak" | |
| if os.path.isfile(bak): | |
| info(f"备份存在:{cyan(bak)}") | |
| for sec in secondaries: | |
| try: | |
| sdata = open(sec, "rb").read() | |
| except Exception as e: | |
| warn(f"副本读取失败 {sec}: {e}") | |
| continue | |
| sstate = detect_state(sdata) | |
| label = green("已打补丁") if sstate == "patched" else yellow("未打补丁") if sstate == "original" else red("未知") | |
| info(f"副本 {cyan(sec)}: {label}") | |
| sec_bak = sec + ".1h-cache-bak" | |
| if os.path.isfile(sec_bak): | |
| info(f" 副本备份:{cyan(sec_bak)}") | |
| # 监听状态 | |
| if IS_MAC and os.path.isfile(_LAUNCHD_PLIST): | |
| ok("launchd 自动监听已安装。") | |
| elif IS_LINUX: | |
| sp = os.path.join(_SYSTEMD_DIR, _SYSTEMD_PATH) | |
| if os.path.isfile(sp): | |
| ok("systemd 自动监听已安装。") | |
| sys.exit(0) | |
| # ── restore | |
| if cmd == "restore": | |
| section("[ 还原 ]") | |
| do_restore(target, mode, secondaries) | |
| sys.exit(0) | |
| # ── unwatch | |
| if cmd == "unwatch": | |
| section("[ 卸载监听 ]") | |
| do_watch_uninstall() | |
| sys.exit(0) | |
| # ── patch only | |
| if cmd == "patch": | |
| section("[ 打补丁 ]") | |
| ok_patch = do_patch(target, mode, secondaries) | |
| sys.exit(0 if ok_patch else 1) | |
| # ── watch only | |
| if cmd == "watch": | |
| section("[ 安装自动监听 ]") | |
| do_watch_install(versions_dir) | |
| sys.exit(0) | |
| # ── all(默认):patch + watch | |
| section("[ 2 ] 打补丁...") | |
| ok_patch = do_patch(target, mode, secondaries) | |
| section("[ 3 ] 安装自动监听...") | |
| ok_watch = do_watch_install(versions_dir) | |
| print() | |
| print(bold(green(" ┌────────────────────────────────────────────────┐"))) | |
| if ok_patch: | |
| print(bold(green(" │ v 补丁已生效 │"))) | |
| if ok_watch: | |
| print(bold(green(" │ v 自动监听已安装(更新后自动重打) │"))) | |
| print(bold(green(" └────────────────────────────────────────────────┘"))) | |
| print() | |
| info("重启 Claude Code 后生效。") | |
| info(f"查看日志:{cyan(os.path.join(HOME, '.local', 'share', 'claude', '1h-cache-patch.log'))}") | |
| info(f"还原命令:python3 {os.path.basename(SCRIPT)} restore") | |
| info(f"卸载监听:python3 {os.path.basename(SCRIPT)} unwatch") | |
| print() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment