2026-04-21 17:57:05 +08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
/**
|
|
|
|
|
* pre-agent-gate.js — Agent 派遣前置门 (P1, 2026-04-19)
|
|
|
|
|
* PRE_AGENT_GATE_P1
|
|
|
|
|
*
|
|
|
|
|
* PreToolUse(Task) 钩子 — general-purpose + 轻量查询 → 建议改用 explore agent。
|
|
|
|
|
* fail-open: exit 0 始终, 不阻断派遣。
|
|
|
|
|
*/
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
const LIGHTWEIGHT_KEYWORDS = [
|
2026-04-27 17:59:44 +08:00
|
|
|
/\bfind\b/i, /\blocate\b/i, /\bsearch\b/i, /\blist\b/i,
|
|
|
|
|
/\bwhich\b/i, /\bwhere\s+is\b/i, /\blook\s+up\b/i, /\bgrep\b/i,
|
2026-04-21 17:57:05 +08:00
|
|
|
/查找/, /搜索/, /定位/, /列出/, /哪里/, /哪个文件/,
|
|
|
|
|
];
|
|
|
|
|
const SHORT_PROMPT_THRESHOLD = 200;
|
|
|
|
|
|
|
|
|
|
function getLogPath() {
|
|
|
|
|
try {
|
|
|
|
|
const root = require('./lib/root.js');
|
|
|
|
|
return path.join(root, 'debug', 'pre-agent-gate.jsonl');
|
|
|
|
|
} catch {
|
|
|
|
|
return path.join(__dirname, '..', 'debug', 'pre-agent-gate.jsonl');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readStdinSync() {
|
|
|
|
|
try {
|
|
|
|
|
const raw = fs.readFileSync(0, 'utf8');
|
|
|
|
|
return JSON.parse(raw);
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isLightweight(prompt) {
|
|
|
|
|
if (!prompt || typeof prompt !== 'string') return false;
|
|
|
|
|
if (prompt.length < SHORT_PROMPT_THRESHOLD) return true;
|
|
|
|
|
return LIGHTWEIGHT_KEYWORDS.some(re => re.test(prompt));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function logEvent(record) {
|
|
|
|
|
try {
|
|
|
|
|
fs.appendFileSync(getLogPath(), JSON.stringify(record) + '\n');
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function main() {
|
|
|
|
|
const input = readStdinSync();
|
|
|
|
|
if (!input || !input.tool_input) {
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ti = input.tool_input || {};
|
|
|
|
|
const subagentType = ti.subagent_type || '';
|
|
|
|
|
const prompt = ti.prompt || '';
|
|
|
|
|
const description = ti.description || '';
|
|
|
|
|
|
|
|
|
|
const record = {
|
|
|
|
|
ts: new Date().toISOString(),
|
|
|
|
|
sessionId: input.session_id || '',
|
|
|
|
|
subagent_type: subagentType,
|
|
|
|
|
promptLen: prompt.length,
|
|
|
|
|
description,
|
|
|
|
|
hint: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (subagentType !== 'general-purpose') {
|
|
|
|
|
logEvent(record);
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isLightweight(prompt)) {
|
|
|
|
|
logEvent(record);
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
record.hint = true;
|
|
|
|
|
logEvent(record);
|
|
|
|
|
|
|
|
|
|
const hint = '[pre-agent-gate] 检测到轻量查询使用 general-purpose agent。' +
|
|
|
|
|
'建议改用 explore agent (haiku 模型, 成本约 1/5)。' +
|
|
|
|
|
'若确需 general-purpose 请忽略此提示。';
|
|
|
|
|
|
|
|
|
|
const out = {
|
|
|
|
|
hookSpecificOutput: {
|
|
|
|
|
hookEventName: 'PreToolUse',
|
|
|
|
|
additionalContext: hint,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
process.stdout.write(JSON.stringify(out));
|
|
|
|
|
process.exit(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main();
|