158 lines
4.8 KiB
JavaScript
158 lines
4.8 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* PostToolUse Hook: Pre-Compaction 记忆持久化触发器
|
|||
|
|
* @version 1.0.0
|
|||
|
|
* @file memory-persistence-trigger.js
|
|||
|
|
* @matcher Bash|Edit|Write|Read|Glob|Grep|Skill|Agent
|
|||
|
|
*
|
|||
|
|
* 功能: 追踪工具调用次数,每 30 次提醒 Claude 持久化重要记忆
|
|||
|
|
* 触发条件: count 达到 30 的倍数时(30、60、90...)
|
|||
|
|
* 冷却机制: 触发后 5 分钟内不重复触发
|
|||
|
|
*
|
|||
|
|
* stdin: { "session_id": "...", "tool_name": "...", "tool_input": {...}, "tool_output": "..." }
|
|||
|
|
* stdout: { "hookSpecificOutput": { "additionalContext": "..." } } (仅在触发时)
|
|||
|
|
* 退出码: 0 (始终放行,fail-open)
|
|||
|
|
*
|
|||
|
|
* 性能目标: <5ms (仅读写一个小 JSON 文件 + 计数器判断)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const readStdin = require('./lib/read-stdin.js');
|
|||
|
|
|
|||
|
|
// ─── 路径解析 ────────────────────────────────────────
|
|||
|
|
let debugDir;
|
|||
|
|
try {
|
|||
|
|
const { PATHS } = require('../scripts/paths.config.js');
|
|||
|
|
debugDir = PATHS.debugDir;
|
|||
|
|
} catch {
|
|||
|
|
debugDir = path.resolve(__dirname, '..', 'debug');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const COUNTER_FILE = path.join(debugDir, 'tool-call-counter.json');
|
|||
|
|
const TRIGGER_INTERVAL = 30; // 每 30 次触发
|
|||
|
|
const COOLDOWN_MS = 5 * 60 * 1000; // 5 分钟冷却
|
|||
|
|
|
|||
|
|
// ─── 计数器文件读写 ──────────────────────────────────
|
|||
|
|
/**
|
|||
|
|
* 读取计数器状态
|
|||
|
|
* @returns {{ count: number, lastTrigger: string, sessionId: string }}
|
|||
|
|
*/
|
|||
|
|
function readCounter() {
|
|||
|
|
try {
|
|||
|
|
if (fs.existsSync(COUNTER_FILE)) {
|
|||
|
|
return JSON.parse(fs.readFileSync(COUNTER_FILE, 'utf8'));
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
return { count: 0, lastTrigger: '', sessionId: '' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 写入计数器状态 (原子写入: temp+rename 防止竞态截断)
|
|||
|
|
* @param {{ count: number, lastTrigger: string, sessionId: string }} state
|
|||
|
|
*/
|
|||
|
|
function writeCounter(state) {
|
|||
|
|
try {
|
|||
|
|
if (!fs.existsSync(debugDir)) {
|
|||
|
|
fs.mkdirSync(debugDir, { recursive: true });
|
|||
|
|
}
|
|||
|
|
var tmp = COUNTER_FILE + '.tmp.' + process.pid;
|
|||
|
|
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n');
|
|||
|
|
fs.renameSync(tmp, COUNTER_FILE);
|
|||
|
|
} catch {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否在冷却期内
|
|||
|
|
* @param {string} lastTrigger - ISO 时间字符串
|
|||
|
|
* @returns {boolean}
|
|||
|
|
*/
|
|||
|
|
function isInCooldown(lastTrigger) {
|
|||
|
|
if (!lastTrigger) return false;
|
|||
|
|
try {
|
|||
|
|
const elapsed = Date.now() - new Date(lastTrigger).getTime();
|
|||
|
|
return elapsed < COOLDOWN_MS;
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 构建 additionalContext 提醒文本
|
|||
|
|
* @param {number} count - 当前工具调用次数
|
|||
|
|
* @returns {string}
|
|||
|
|
*/
|
|||
|
|
function buildReminder(count) {
|
|||
|
|
return `[MEMORY_PERSISTENCE_TRIGGER] 你已进行了较多工具调用(${count}次),上下文可能即将被压缩。
|
|||
|
|
如果本次会话有重要决策、关键发现或架构变更,请考虑写入记忆文件。
|
|||
|
|
写入规则:
|
|||
|
|
- 稳定结论写入 MEMORY.md 对应章节
|
|||
|
|
- 项目细节写入 memory/ 下的专题文件
|
|||
|
|
- 章节标题加 #tag 标签便于检索 (如 ## 部署记录 #deploy #服务器)
|
|||
|
|
- 常用标签: #bookworm #deploy #agenttin #wan22 #ai #安全 #架构 #debug #进度
|
|||
|
|
- 禁止写入: API Key、密码、token、SSH 密钥等敏感信息
|
|||
|
|
- 禁止写入: 会话临时状态、未验证的猜测
|
|||
|
|
检索命令: node ~/.claude/scripts/memory-search.js "<关键词>" [--tag <标签>]`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 主流程 ──────────────────────────────────────────
|
|||
|
|
function main() {
|
|||
|
|
readStdin({ maxSize: 128 * 1024 }).then(input => {
|
|||
|
|
const sessionId = input.session_id || 'unknown';
|
|||
|
|
|
|||
|
|
// 读取当前计数器状态
|
|||
|
|
const state = readCounter();
|
|||
|
|
|
|||
|
|
// 新会话检测: sessionId 变化则重置计数器
|
|||
|
|
if (state.sessionId && state.sessionId !== sessionId) {
|
|||
|
|
state.count = 0;
|
|||
|
|
state.lastTrigger = '';
|
|||
|
|
}
|
|||
|
|
state.sessionId = sessionId;
|
|||
|
|
|
|||
|
|
// 递增计数器
|
|||
|
|
state.count++;
|
|||
|
|
|
|||
|
|
// 判断是否需要触发提醒
|
|||
|
|
const shouldTrigger = (state.count % TRIGGER_INTERVAL === 0)
|
|||
|
|
&& !isInCooldown(state.lastTrigger);
|
|||
|
|
|
|||
|
|
if (shouldTrigger) {
|
|||
|
|
// 更新触发时间
|
|||
|
|
state.lastTrigger = new Date().toISOString();
|
|||
|
|
writeCounter(state);
|
|||
|
|
|
|||
|
|
// 输出 additionalContext 提醒
|
|||
|
|
const output = {
|
|||
|
|
hookSpecificOutput: {
|
|||
|
|
additionalContext: buildReminder(state.count)
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
process.stdout.write(JSON.stringify(output));
|
|||
|
|
} else {
|
|||
|
|
// 仅更新计数器,无输出
|
|||
|
|
writeCounter(state);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
process.exit(0);
|
|||
|
|
}).catch(() => process.exit(0));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 模块导出 (供测试使用)
|
|||
|
|
if (typeof module !== 'undefined') {
|
|||
|
|
module.exports = {
|
|||
|
|
readCounter,
|
|||
|
|
writeCounter,
|
|||
|
|
isInCooldown,
|
|||
|
|
buildReminder,
|
|||
|
|
COUNTER_FILE,
|
|||
|
|
TRIGGER_INTERVAL,
|
|||
|
|
COOLDOWN_MS,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (require.main === module) {
|
|||
|
|
main();
|
|||
|
|
}
|