bookworm-smart-assistant/hooks/activity-logger.js

171 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();
}