#!/usr/bin/env node /** * PostToolUse Hook: 活动日志记录器 * 匹配器: Edit|Write|Skill|Agent|Bash|mcp__.* * 触发: 关键工具调用完成后记录到 JSONL * 退出码: 始终 0 (纯记录,不阻断工作流) * * 记录的事件类型: * skill — Skill 路由调用 * agent — Agent 子进程 spawn / EnterWorktree * agent — Agent spawn / TaskCreate/TaskUpdate / EnterWorktree (统一为 agent 保持下游兼容) * mcp — MCP 服务器工具调用 * bash — Bash 命令执行 * write — 文件写入/编辑操作 * * 日志文件: debug/activity-YYYY-MM-DD.jsonl */ const fs = require('fs'); const path = require('path'); const { safeAppendJsonl } = require('./lib/safe-append.js'); const readStdin = require('./lib/read-stdin.js'); // 动态检测 Claude 配置根目录(兼容不同用户名) const CLAUDE_ROOT = require('./lib/root.js'); const DEBUG_DIR = path.join(CLAUDE_ROOT, 'debug'); /** * 日志脱敏: 使用共享脱敏模块 (v5.9 统一规则) */ const sanitizeForLog = (() => { try { return require('../scripts/sanitize.js').sanitize; } catch { // 回退: 共享模块缺失时使用内联最小脱敏 return (text) => { if (!text || typeof text !== 'string') return text || ''; return text .replace(/(?:key|token|password|secret)[\s]*[=:]\s*\S{6,}/gi, '[REDACTED]') .replace(/\b(?:sk-|ghp_|Bearer\s+)\S{10,}/g, '[REDACTED_TOKEN]'); }; } })(); // 事件分类规则 function classifyEvent(toolName) { if (toolName === 'Skill') return 'skill'; if (toolName === 'Agent') return 'agent'; if (toolName === 'TaskCreate' || toolName === 'TaskUpdate') return 'agent'; // LV-04: 保留 'agent' 兼容下游消费者 if (toolName.startsWith('mcp__')) return 'mcp'; if (toolName === 'Bash') return 'bash'; if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') return 'write'; if (toolName === 'EnterWorktree') return 'agent'; return null; // 未知工具不记录 } // 提取事件详情 function extractDetail(event, toolName, toolInput) { switch (event) { case 'skill': return (toolInput && toolInput.skill) || 'unknown'; case 'agent': if (toolName === 'Agent') { // Agent 工具: 记录 subagent_type + description const agentType = (toolInput && toolInput.subagent_type) || 'general-purpose'; const desc = (toolInput && toolInput.description) || ''; return `${agentType}${desc ? ':' + desc.slice(0, 80) : ''}`; } if (toolName === 'TaskCreate') { return (toolInput && toolInput.subject) || 'new-task'; } if (toolName === 'TaskUpdate') { return toolInput ? `${toolInput.taskId || '?'}:${toolInput.status || 'update'}` : 'update'; } if (toolName === 'EnterWorktree') { return (toolInput && toolInput.name) || 'worktree'; } return ''; case 'mcp': { // mcp__server__action → server/action const parts = toolName.replace(/^mcp__/, '').split('__'); return parts.join('/'); } case 'bash': { const cmd = (toolInput && toolInput.command) || ''; return cmd.slice(0, 120); } case 'write': return (toolInput && (toolInput.file_path || toolInput.filePath || toolInput.notebook_path)) || ''; default: return ''; } } function main() { readStdin({ maxSize: 512 * 1024 }).then(input => { const toolName = input.tool_name; if (!toolName) { process.exit(0); return; } const event = classifyEvent(toolName); if (!event) { process.exit(0); return; } const toolInput = input.tool_input || {}; const toolResult = input.tool_result || ''; const rawDetail = extractDetail(event, toolName, toolInput); const detail = sanitizeForLog(rawDetail); // 推断执行结果: 检查 tool_result 是否包含错误信号 const resultStr = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult); const hasError = /\b(error|Error|ERROR|failed|FAILED|exception|Exception)\b/.test( resultStr.slice(0, 500) ); // v5.9: 从 route-state-current.json 读取 traceId + mustInvoke,实现端到端追踪 let traceId = null; let mustInvoke = false; try { const stateFile = path.join(DEBUG_DIR, 'route-state-current.json'); if (fs.existsSync(stateFile)) { const st = JSON.parse(fs.readFileSync(stateFile, 'utf8')); traceId = st.traceId || null; mustInvoke = st.mustInvoke || false; // Phase 4.4: 关联 MUST_INVOKE 标记 } } catch {} const logEntry = { ts: new Date().toISOString(), event, tool: toolName, detail, phase: 'post', success: !hasError, sessionId: input.session_id || null, traceId, mustInvoke, }; // 按日期分文件,安全追加 (高频写入启用文件锁) const dateStr = new Date().toISOString().slice(0, 10); const logFile = path.join(DEBUG_DIR, `activity-${dateStr}.jsonl`); safeAppendJsonl(logFile, logEntry, { useLock: true }); // v5.9: Skill 执行结果观测 — 写入直接反馈信号 if (event === 'skill' && traceId && detail) { try { const signalFile = path.join(DEBUG_DIR, 'skill-outcome.jsonl'); const signal = { ts: logEntry.ts, traceId, skill: detail, success: logEntry.success, type: 'observed', }; safeAppendJsonl(signalFile, signal); } catch {} } process.exit(0); }).catch(() => process.exit(0)); } // 模块导出 (供测试使用) if (typeof module !== 'undefined') { module.exports = { CLAUDE_ROOT, classifyEvent, extractDetail, sanitizeForLog }; } if (require.main === module) { main(); }