- SignalDesk1小时前
Original Summary
让Claude 帮你创建一个mods,自动续期缓存 请帮我在 Claude Code 里创建并启用一个 Claude Mod,名字叫 cache-keepalive。它的作用是:主线程空闲或一轮里长时间阻塞等工具时,自动续上 1 小时的 prompt cache。请按下面的步骤做,每一步做完把结果告诉我。 ## 原理(先读懂再动手) - Claude Code 2.1.287 起支持 Claude Mods,也就是函数钩子插件。
$.model.fork({ prompt })会把主线程上一次的请求(同样的模型、系统提示、工具和消息)后面加一句 prompt 再发一次。这样会命中并读取缓存的前缀,而缓存每被读一次 TTL 就重新计时。fork 不写进对话记录,也不出现在界面和 stdout 里。 - 主线程每发一次模型请求(turn.step,跳过子代理的请求)就记一个时间戳。session.start里启动一个每分钟检查一次的计时器:距离上次请求或上次续期满 54 分钟,就 fork 一次,让模型只回复 "ok"。 - 以下情况停止续期,等下一次真实请求再自动恢复: - 连续续期 6 次仍没有真实请求; - 续期失败; - 已超过 58 分钟(比如电脑睡眠过),缓存多半已过期,再发只会按全价重新写缓存。 - mod 每次加载写一行start日志,每次续期写一行ping日志(只记 token 数,不记内容)。 ## 第 0 步:前置检查(不满足就停下来告诉我,不要继续) 1.claude --version必须 ≥ 2.1.287。 2. 确认 prompt cache 的 TTL 是 1 小时:settings.json 的 env 里有CLAUDE_CODE_PROMPT_CACHE_TTL=1h或ENABLE_PROMPT_CACHING_1H=1,或者用的是 Claude 订阅登录(主对话默认 1h)。如果是 5 分钟 TTL,这个 mod 没有意义:先问我要不要开 1h TTL,注意 1h 缓存的写入价是基础输入价的 2 倍。 3. 确定配置目录 CFG:环境变量CLAUDE_CONFIG_DIR有值就用它,否则用~/.claude。下面都写成绝对路径。 4. settings.json 的 env 里如果已经有CLAUDE_CODE_PLUGIN_DIRS,记下原值,后面追加,不要覆盖。macOS/Linux 用:分隔。 ## 第 1 步:备份 把<CFG>/settings.json复制为<CFG>/settings.json.backup.<YYYYMMDD-HHMM>-add-cache-keepalive-mod,用command cp -p,避免 cp 被别名成交互模式。 ## 第 2 步:创建插件文件(内容必须一字不差) 目录:<CFG>/mods/cache-keepalive/.claude-plugin/plugin.json: ``json {"name": "cache-keepalive", "description": "Keeps the main thread's prompt cache warm while it waits or sits idle.", "version": "0.1.0"}`hooks/hooks.json:`json {"modules": ["./register.js"]}`hooks/register.js:`js // Keeps the main thread's 1h prompt cache warm while the session waits on a // long tool call or sits idle. Every main-thread model request is timestamped; // a once-a-minute check started at session.start pings when INTERVAL has // passed since the last request or ping. A ping is $.model.fork: the main // thread's last request re-sent with a one-line prompt, which reads the cached // prefix and so renews its TTL. Forks stay out of the transcript and the // host's output stream. Each load (event "start") and every ping is logged // to cache-keepalive.log beside the plugin folder. // // Env (settings.jsonenvor the process environment): // CACHE_KEEPALIVE_INTERVAL_SEC wait after the last request or ping (default 3240 = 54 min) // CACHE_KEEPALIVE_MAX_PINGS pings in a row without a real request before giving up (default 6) const LOG_KEEP_LINES = 500 const TICK_MS = 60 * 1000 const TTL_MS = 60 * 60 * 1000 const PROMPT = 'Automated prompt-cache keep-alive ping, not a user request. Reply with exactly: ok' const state = { intervalMs: 3240 * 1000, maxPings: 6, lastMainAt: undefined, // last main-thread model request lastWarmAt: undefined, // last main request or successful ping pings: 0, stopped: true, // until the first main request busy: false, } async function configure($) { const sec = Number(await $.env.get('CACHE_KEEPALIVE_INTERVAL_SEC')) const max = Number(await $.env.get('CACHE_KEEPALIVE_MAX_PINGS')) if (Number.isFinite(sec) && sec >= 30) state.intervalMs = sec * 1000 if (Number.isInteger(max) && max >= 0) state.maxPings = max } function logPath($) { return $.plugin.root.replace(/\/+$/, '').replace(/\/[^/]+$/, '') + '/cache-keepalive.log' } async function log($, entry) { try { const LOG = logPath($) const line = JSON.stringify({ at: new Date().toISOString(), session: await $.session.id(), ...entry }) const old = (await $.fs.exists(LOG)) ? await $.fs.read(LOG) : '' const lines = old.split('\n').filter(Boolean) lines.push(line) await $.fs.write(LOG, lines.slice(-LOG_KEEP_LINES).join('\n') + '\n') } catch {} } async function tick($) { if (state.stopped || state.busy || state.lastWarmAt === undefined) return const now = Date.now() if (now - state.lastWarmAt < state.intervalMs) return state.busy = true try { if (now - state.lastWarmAt >= TTL_MS - 2 * TICK_MS) { // Too late (the machine slept, say): the entry has lapsed and a ping // would only pay for a fresh cache write. state.stopped = true await log($, { event: 'stop', reason: 'lapsed', idleMin: Math.round((now - state.lastWarmAt) / 60000) }) return } if (state.pings >= state.maxPings) { state.stopped = true await log($, { event: 'stop', reason: 'max-pings', pings: state.pings }) return } state.pings += 1 const mainAt = state.lastMainAt const r = await $.model.fork({ prompt: PROMPT }) const u = r.usage ?? {} await log($, { event: 'ping', pings: state.pings, ms: Date.now() - now, ok: r.isAnswered, reason: r.reason, status: r.status, cache_read: u.cache_read_input_tokens, cache_write: u.cache_creation_input_tokens, input: u.input_tokens, output: u.output_tokens, }) if (state.lastMainAt !== mainAt) return // a real request came in meanwhile if (r.isAnswered || r.reason === 'empty-reply') state.lastWarmAt = now else state.stopped = true // api-error, nothing-to-fork, aborted: wait for the next real request } finally { state.busy = false } } export function register(on) { on('session.start', async ($, e, next) => { try { await configure($) $.clock.every(TICK_MS, () => { tick($).catch(() => {}) }) await log($, { event: 'start', intervalSec: state.intervalMs / 1000, maxPings: state.maxPings }) } catch {} return next(e) }) on('turn.step', async function* ($, e, next) { if (e.agentId === undefined) { state.lastMainAt = state.lastWarmAt = Date.now() state.pings = 0 state.stopped = false } return yield* next(e) }) }`注意:Mod 的静态校验要求$只能直接写成$.xxx.yyy(...)调用,或者作为参数传给顶层函数,不能存到变量里留着以后用。所以代码才写成现在这样,不要"优化"这个结构。 ## 第 3 步:校验 运行claude plugin validate <CFG>/mods/cache-keepalive。期望: - 只有一条缺少 author 的警告,结果是Validation passed; - 输出里列出 hooks 为session.start, turn.step; - calls 里有$.model.fork、$.clock.every、$.fs.*。 ## 第 4 步:启用 在<CFG>/settings.json的env里加上"CLAUDE_CODE_PLUGIN_DIRS": "<CFG 的绝对路径>/mods/cache-keepalive"。如果已经有这个键,就用:` 追加。只改这一个键,其他内容保持不变。改完用 JSON 解析确认文件仍然合法,再把改动前后的差异给我看。 ## 第 5 步:实测(会产生少量 API 调用) 在一个临时目录里起一个常驻的 stream-jso- 情报分类:技术学习与提效
- 分类依据:内容涉及技术、AI、软件工具或工程实践
- 信息来源:服务器 / LINUX DO - 最新话题
- 发布时间:2026/10/9 15:27:32
- 暂无回复