bookworm-smart-assistant/scripts/patches/patch-r2-precompact-tier-output.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

154 lines
6.3 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
/**
* patch-r2-precompact-tier-output.js · 2026-04-26
*
* R2: pre-compact-handoff.js 增加工具输出分级摘要
* 在 compact 前扫描 transcript_path JSONL, 识别 TOP-N 大工具结果,
* 按工具类型差异化保留, 写入 handoff.json.tool_output_tiers
*
* 分级规则:
* - Bash 输出 >2000B: 保留头 1500B + 尾 500B + 截断行数
* - Read 结果 >3000B: 保留路径+行范围+前 200B 摘要
* - Write/Edit 结果 >500B: 仅保留路径+行数确认
* - Agent/Task 结果 >2000B: 保留头 1000B + "(Agent 完整结果已 dump)"
* - 其他工具 >5000B: 头 2000B + 尾 500B
*
* 幂等: sentinel "TOOL_OUTPUT_TIER_V1"
*/
'use strict';
const fs = require('fs');
const path = require('path');
const TARGET = path.join(__dirname, '..', '..', 'hooks', 'pre-compact-handoff.js');
const SENTINEL = 'TOOL_OUTPUT_TIER_V1';
const OLD_BLOCK = ` // 构造 handoff 数据
const handoff = {
timestamp: new Date().toISOString(),
session_id: hookData.session_id || \`session-\${Date.now()}\`,
context_hint: '会话因上下文压缩中断,以下是压缩前的状态摘要',
conversation_summary: hookData.transcript_summary || '(由 PreCompact hook 自动捕获)',
tool_call_count: hookData.tool_call_count || 'unknown',
working_directory: process.cwd(),
note: '此文件由 pre-compact-handoff.js 自动生成SessionStart 时自动读取并注入恢复上下文'
};`;
const NEW_BLOCK = ` // TOOL_OUTPUT_TIER_V1 - 扫描 transcript 提取大工具输出分级摘要
const toolOutputTiers = scanToolOutputTiers(hookData.transcript_path);
// 构造 handoff 数据
const handoff = {
timestamp: new Date().toISOString(),
session_id: hookData.session_id || \`session-\${Date.now()}\`,
context_hint: '会话因上下文压缩中断,以下是压缩前的状态摘要',
conversation_summary: hookData.transcript_summary || '(由 PreCompact hook 自动捕获)',
tool_call_count: hookData.tool_call_count || 'unknown',
working_directory: process.cwd(),
tool_output_tiers: toolOutputTiers,
note: '此文件由 pre-compact-handoff.js 自动生成SessionStart 时自动读取并注入恢复上下文'
};`;
const HELPER_FN = `
// === TOOL_OUTPUT_TIER_V1 ===
// 扫描 transcript JSONL, 按工具类型分级保留大输出, 输出 TOP-10 摘要
function scanToolOutputTiers(transcriptPath) {
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
return { applied: false, reason: 'no transcript_path' };
}
try {
const raw = fs.readFileSync(transcriptPath, 'utf8');
const lines = raw.split('\\n').filter(Boolean);
const items = [];
for (const line of lines) {
let obj;
try { obj = JSON.parse(line); } catch { continue; }
// 只关注 tool_result 类型 (含工具调用响应)
const content = obj?.message?.content || obj?.content;
if (!Array.isArray(content)) continue;
for (const part of content) {
if (part?.type !== 'tool_result') continue;
const text = typeof part.content === 'string'
? part.content
: Array.isArray(part.content) ? part.content.map(c => c?.text || '').join('') : '';
const size = Buffer.byteLength(text, 'utf8');
if (size < 500) continue;
items.push({ size, text, tool_use_id: part.tool_use_id });
}
}
items.sort((a, b) => b.size - a.size);
const top = items.slice(0, 10).map(it => tierize(it));
const totalBytes = items.reduce((s, it) => s + it.size, 0);
return {
applied: true,
total_tool_results_scanned: items.length,
total_bytes: totalBytes,
top_offenders: top
};
} catch (e) {
return { applied: false, reason: 'scan_error: ' + (e.message || e) };
}
}
function tierize(item) {
const { size, text, tool_use_id } = item;
// 启发式工具类型判定 (transcript 不直接含工具名, 用文本特征)
let kind = 'other';
if (/^(File created successfully|Wrote \\d+ lines|The file .* has been (created|updated))/m.test(text)) kind = 'write';
else if (/^\\s*\\d+→/m.test(text) || text.startsWith(' 1\\t')) kind = 'read';
else if (/<tool_use_error>|^bash:|stderr:/m.test(text) || /\\$ /m.test(text.slice(0, 100))) kind = 'bash';
else if (/^(Found \\d+ files?|^[a-zA-Z]:\\\\.*\\.(ts|js|md|json))/m.test(text)) kind = 'glob_grep';
else if (size > 3000 && text.includes('agent')) kind = 'agent';
let summary;
switch (kind) {
case 'write':
summary = text.split('\\n').slice(0, 2).join(' | ').slice(0, 200);
break;
case 'read':
summary = '[Read] ' + text.slice(0, 200) + ' ... [+' + (size - 200) + ' bytes]';
break;
case 'bash':
summary = text.slice(0, 1500) + '\\n... [truncated ' + Math.max(0, size - 2000) + ' bytes] ...\\n' + text.slice(-500);
break;
case 'agent':
summary = text.slice(0, 1000) + '\\n... [Agent 完整结果已截断 ' + (size - 1000) + ' bytes]';
break;
case 'glob_grep':
summary = '[Glob/Grep] ' + text.split('\\n').slice(0, 8).join(' | ').slice(0, 400);
break;
default:
summary = text.slice(0, 2000) + '\\n... [+' + Math.max(0, size - 2500) + ' bytes] ...\\n' + text.slice(-500);
}
return { tool_use_id, kind, original_bytes: size, summary };
}
`;
function main() {
const srcRaw = fs.readFileSync(TARGET, 'utf8');
if (srcRaw.includes(SENTINEL)) {
console.log('[r2] already applied, skip');
return;
}
// CRLF 容忍: 检测原文行尾, 把 OLD_BLOCK/NEW_BLOCK 转换成同制
const eol = srcRaw.includes('\r\n') ? '\r\n' : '\n';
const oldNorm = OLD_BLOCK.replace(/\r?\n/g, eol);
const newNorm = NEW_BLOCK.replace(/\r?\n/g, eol);
if (!srcRaw.includes(oldNorm)) {
console.error('[r2] anchor block not found, manual review needed (eol=' + JSON.stringify(eol) + ')');
process.exit(1);
}
let next = srcRaw.replace(oldNorm, newNorm);
// 在 IIFE 结尾 })(); 之后追加 helper 函数
const helperNorm = HELPER_FN.replace(/\r?\n/g, eol);
next = next.replace(/(\}\)\(\);\s*)$/, `$1${eol}${helperNorm}`);
const bak = TARGET + '.bak.r2.' + Date.now();
fs.copyFileSync(TARGET, bak);
const tmp = TARGET + '.tmp.' + process.pid;
fs.writeFileSync(tmp, next, 'utf8');
fs.renameSync(tmp, TARGET);
console.log('[r2] OK, bak:', path.basename(bak), 'eol=', JSON.stringify(eol));
}
main();