bookworm-smart-assistant/hooks/session-start-restore.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

126 lines
4.7 KiB
JavaScript
Raw 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
/**
* 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) {
// 新会话: 仅重置当前 session 的 heartbeat (X03 keyed 结构) // [PATCH-X05-KEYED-RESET]
if (fs.existsSync(HEARTBEAT_FILE)) {
try {
const hbAll = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf8'));
hbAll[sessionId] = { count: 0, lastActivity: Date.now(), notified: [] };
const tmp = HEARTBEAT_FILE + '.tmp.' + process.pid;
fs.writeFileSync(tmp, JSON.stringify(hbAll), 'utf8');
fs.renameSync(tmp, HEARTBEAT_FILE);
} catch {
// 损坏: 初始化为新 keyed 结构
const init = {}; init[sessionId] = { count: 0, lastActivity: Date.now(), notified: [] };
fs.writeFileSync(HEARTBEAT_FILE, JSON.stringify(init), '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));
// [PATCH-X10-ARCHIVE-CLEANUP]
try {
const archives = fs.readdirSync(SESSION_STATE_DIR)
.filter(f => /^handoff-(\d+|expired-\d+)\.json$/.test(f))
.sort();
if (archives.length > 20) {
for (const old of archives.slice(0, archives.length - 20)) {
try { fs.unlinkSync(path.join(SESSION_STATE_DIR, old)); } catch {}
}
}
} catch {}
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);
})();