106 lines
3.8 KiB
JavaScript
106 lines
3.8 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* UserPromptSubmit Hook - 会话启动恢复器
|
|||
|
|
* 1. 检测 handoff.json 存在时注入恢复上下文并归档
|
|||
|
|
* 2. 每次会话首次 prompt 时重置 heartbeat 计数器(解决 /clear 后计数不归零问题)
|
|||
|
|
*/
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const CLAUDE_ROOT = require('./lib/root.js');
|
|||
|
|
const readStdin = require('./lib/read-stdin.js');
|
|||
|
|
|
|||
|
|
const SESSION_STATE_DIR = path.join(CLAUDE_ROOT, 'session-state');
|
|||
|
|
const HANDOFF_PATH = path.join(SESSION_STATE_DIR, 'handoff.json');
|
|||
|
|
const RESTORE_MARKER = path.join(SESSION_STATE_DIR, '.session-restored');
|
|||
|
|
const HEARTBEAT_FILE = path.join(CLAUDE_ROOT, 'debug', 'session-heartbeat.json');
|
|||
|
|
|
|||
|
|
(async () => {
|
|||
|
|
try {
|
|||
|
|
let hookData = {};
|
|||
|
|
try { hookData = await readStdin(); } catch (_) {}
|
|||
|
|
|
|||
|
|
const sessionId = hookData.session_id || '';
|
|||
|
|
let messages = [];
|
|||
|
|
|
|||
|
|
// === 1. 会话 ID 变化检测 → 重置 heartbeat ===
|
|||
|
|
let lastSessionId = '';
|
|||
|
|
try {
|
|||
|
|
if (fs.existsSync(RESTORE_MARKER)) {
|
|||
|
|
lastSessionId = fs.readFileSync(RESTORE_MARKER, 'utf8').trim();
|
|||
|
|
}
|
|||
|
|
} catch (_) {}
|
|||
|
|
|
|||
|
|
if (sessionId && sessionId !== lastSessionId) {
|
|||
|
|
// 新会话(/clear 或新窗口),重置 heartbeat 计数器
|
|||
|
|
if (fs.existsSync(HEARTBEAT_FILE)) {
|
|||
|
|
fs.writeFileSync(HEARTBEAT_FILE, JSON.stringify({
|
|||
|
|
count: 0, lastActivity: Date.now(), notified: []
|
|||
|
|
}), 'utf8');
|
|||
|
|
}
|
|||
|
|
// 记录当前 session_id
|
|||
|
|
if (!fs.existsSync(SESSION_STATE_DIR)) {
|
|||
|
|
fs.mkdirSync(SESSION_STATE_DIR, { recursive: true });
|
|||
|
|
}
|
|||
|
|
fs.writeFileSync(RESTORE_MARKER, sessionId, 'utf8');
|
|||
|
|
|
|||
|
|
// === 1b. Auto Git Pull(新会话拉取最新代码)===
|
|||
|
|
try {
|
|||
|
|
const sync = require('../scripts/auto-git-sync.js');
|
|||
|
|
const pullMsgs = sync.pullLatest();
|
|||
|
|
if (pullMsgs && pullMsgs.length > 0) {
|
|||
|
|
messages.push(pullMsgs.join('\n'));
|
|||
|
|
}
|
|||
|
|
} catch (_) {} // fail-open
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// === 2. Handoff 恢复 ===
|
|||
|
|
if (fs.existsSync(HANDOFF_PATH)) {
|
|||
|
|
let handoff = {};
|
|||
|
|
try {
|
|||
|
|
handoff = JSON.parse(fs.readFileSync(HANDOFF_PATH, 'utf8'));
|
|||
|
|
} catch (_) {
|
|||
|
|
fs.unlinkSync(HANDOFF_PATH);
|
|||
|
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|||
|
|
return process.exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否过期(24 小时)
|
|||
|
|
const age = Date.now() - new Date(handoff.timestamp || 0).getTime();
|
|||
|
|
if (age > 24 * 60 * 60 * 1000) {
|
|||
|
|
const archiveName = `handoff-expired-${Date.now()}.json`;
|
|||
|
|
fs.renameSync(HANDOFF_PATH, path.join(SESSION_STATE_DIR, archiveName));
|
|||
|
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|||
|
|
return process.exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 归档并注入恢复上下文
|
|||
|
|
const archiveName = `handoff-${Date.now()}.json`;
|
|||
|
|
fs.renameSync(HANDOFF_PATH, path.join(SESSION_STATE_DIR, archiveName));
|
|||
|
|
|
|||
|
|
messages.push([
|
|||
|
|
'[SESSION_RESTORE] 检测到上次会话的 handoff 记录:',
|
|||
|
|
`- 时间: ${handoff.timestamp}`,
|
|||
|
|
`- 工作目录: ${handoff.working_directory || 'unknown'}`,
|
|||
|
|
`- 工具调用数: ${handoff.tool_call_count || 'unknown'}`,
|
|||
|
|
`- 摘要: ${handoff.conversation_summary || '无'}`,
|
|||
|
|
'',
|
|||
|
|
'请检查 memory 和 task 列表以恢复上下文。'
|
|||
|
|
].join('\n'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (messages.length > 0) {
|
|||
|
|
console.log(JSON.stringify({
|
|||
|
|
continue: true,
|
|||
|
|
suppressOutput: false,
|
|||
|
|
systemMessage: messages.join('\n')
|
|||
|
|
}));
|
|||
|
|
} else {
|
|||
|
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|||
|
|
}
|
|||
|
|
process.exit(0);
|
|||
|
|
})();
|