71 lines
2.6 KiB
Plaintext
71 lines
2.6 KiB
Plaintext
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* token-saver-read-guard.js · TSE Layer 3 · 2026-04-27
|
||
|
|
* PreToolUse (Read) · 大文件读取拦截, 引导 offset/limit 分段
|
||
|
|
* 行为: fail-open, 不阻断读取, 仅注入 additionalContext 建议
|
||
|
|
*/
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
const CLAUDE_ROOT = require('./lib/root.js');
|
||
|
|
const readStdin = require('./lib/read-stdin.js');
|
||
|
|
|
||
|
|
const STATE_DIR = path.join(CLAUDE_ROOT, 'session-state');
|
||
|
|
const STATE_PATH = path.join(STATE_DIR, 'tse-read-guard.json');
|
||
|
|
const WARN_BYTES = 10000;
|
||
|
|
const CRIT_BYTES = 35000;
|
||
|
|
const THROTTLE_MS = 3 * 60 * 1000;
|
||
|
|
|
||
|
|
function loadState() {
|
||
|
|
try { return fs.existsSync(STATE_PATH) ? JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')) : {}; } catch { return {}; }
|
||
|
|
}
|
||
|
|
function saveState(s) {
|
||
|
|
try {
|
||
|
|
if (!fs.existsSync(STATE_DIR)) fs.mkdirSync(STATE_DIR, { recursive: true });
|
||
|
|
const tmp = STATE_PATH + '.tmp.' + process.pid;
|
||
|
|
fs.writeFileSync(tmp, JSON.stringify(s, null, 2), 'utf8');
|
||
|
|
fs.renameSync(tmp, STATE_PATH);
|
||
|
|
} catch {}
|
||
|
|
}
|
||
|
|
|
||
|
|
(async () => {
|
||
|
|
try {
|
||
|
|
const hookData = await readStdin();
|
||
|
|
if (hookData.tool_name !== 'Read') process.exit(0);
|
||
|
|
const input = hookData.tool_input || {};
|
||
|
|
if (!input.file_path) process.exit(0);
|
||
|
|
if (input.offset !== undefined || input.limit !== undefined || input.pages !== undefined) process.exit(0);
|
||
|
|
|
||
|
|
let fileSize = 0;
|
||
|
|
try { fileSize = fs.statSync(input.file_path).size; } catch { process.exit(0); }
|
||
|
|
if (fileSize < WARN_BYTES) process.exit(0);
|
||
|
|
|
||
|
|
const state = loadState();
|
||
|
|
const now = Date.now();
|
||
|
|
const key = input.file_path.replace(/[\\\/\:]/g, '_');
|
||
|
|
if (state[key] && (now - state[key]) < THROTTLE_MS) process.exit(0);
|
||
|
|
state[key] = now;
|
||
|
|
for (const k of Object.keys(state)) { if (state[k] < now - 600000) delete state[k]; }
|
||
|
|
saveState(state);
|
||
|
|
|
||
|
|
const estLines = Math.round(fileSize * 30 / 1000);
|
||
|
|
const estTokens = Math.round(fileSize / 3.5);
|
||
|
|
const bn = path.basename(input.file_path);
|
||
|
|
|
||
|
|
let msg;
|
||
|
|
if (fileSize >= CRIT_BYTES) {
|
||
|
|
msg = '[TSE·READ_GUARD] ⚠️ 大文件: ' + bn + ' ≈' + estLines + '行 (' + estTokens + ' tokens)\n' +
|
||
|
|
'你必须使用 offset+limit 分段读取。如需全文分析, 委托 Agent 子进程。';
|
||
|
|
} else {
|
||
|
|
msg = '[TSE·READ_GUARD] 提示: ' + bn + ' ≈' + estLines + '行 (' + estTokens + ' tokens). 建议用 offset+limit 分段。';
|
||
|
|
}
|
||
|
|
|
||
|
|
process.stdout.write(JSON.stringify({
|
||
|
|
continue: true,
|
||
|
|
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: msg }
|
||
|
|
}));
|
||
|
|
process.exit(0);
|
||
|
|
} catch { process.exit(0); }
|
||
|
|
})();
|