Files
2026-07-06 17:16:05 +08:00

106 lines
4.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
// PreToolUse 钩子:编辑 client/** 或 server/** 时,把「必须严格遵守的完整开发规范」注入上下文。
//
// 策略(对应用户要求:三套文档全部规范时刻遵守、新会话也一样):
// - 每个文档目录,每会话「首次」编辑相关区域时全量注入其所有 .md 全文;
// - 同会话之后再改同区域,只给简短「已注入、继续遵守」提醒,避免重复刷屏;
// - 会话级去重用 .claude/.spec-injected/ 下的哨兵文件;SessionStart 钩子会在
// 每个新会话开始时清空该目录 → 新会话必然重新注入。
// - 改 docs/ 本身不打扰。
//
// 本脚本是「工具代码」,只在 Node(Claude Code 宿主)里跑,不受项目 ES5 红线约束。
var fs = require('fs');
var path = require('path');
var HOOK_DIR = __dirname; // .../.claude/hooks
var PROJECT_DIR = path.resolve(HOOK_DIR, '..', '..'); // 项目根
var SENTINEL_DIR = path.join(PROJECT_DIR, '.claude', '.spec-injected');
function slug(relDir) { return relDir.replace(/[^A-Za-z0-9]+/g, '_'); }
function alreadyInjected(relDir) {
try { return fs.existsSync(path.join(SENTINEL_DIR, slug(relDir))); } catch (e) { return false; }
}
function markInjected(relDir) {
try {
fs.mkdirSync(SENTINEL_DIR, { recursive: true });
fs.writeFileSync(path.join(SENTINEL_DIR, slug(relDir)), '1');
} catch (e) {}
}
// 读取某目录下的编号正文 .md(按文件名排序),拼成一段带分隔标题的全文。
// 排除 README.md:它已被 CLAUDE.md 用 @import 常驻每次会话上下文,注入里再带就是重复。
function readDirDocs(relDir) {
var abs = path.join(PROJECT_DIR, relDir);
var files;
try { files = fs.readdirSync(abs); } catch (e) { return ''; }
var out = [];
files.filter(function (f) { return /\.md$/i.test(f) && !/^README\.md$/i.test(f); })
.sort()
.forEach(function (f) {
try {
out.push('===== ' + relDir + '/' + f + ' =====\n' +
fs.readFileSync(path.join(abs, f), 'utf8'));
} catch (e) {}
});
return out.join('\n\n');
}
var chunks = [];
process.stdin.on('data', function (c) { chunks.push(c); });
process.stdin.on('end', function () {
var context = '';
try {
var input = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
var p = String((input.tool_input && input.tool_input.file_path) || '').replace(/\\/g, '/');
var dirs = null, area = '';
if (/\/docs\//.test(p)) {
dirs = null; // 改文档本身不提醒
} else if (/\/server\//.test(p)) {
area = '服务端';
dirs = ['docs/server/development-guide', 'docs/games/engineering'];
} else if (/\/client\/js\//.test(p) || /\/codes\/shared\//.test(p)) {
area = '客户端';
dirs = ['docs/client/development-guide', 'docs/games/engineering'];
}
if (dirs) {
var pending = [];
for (var i = 0; i < dirs.length; i++) {
if (!alreadyInjected(dirs[i])) { pending.push(dirs[i]); }
}
if (pending.length) {
var full = [];
pending.forEach(function (d) {
var txt = readDirDocs(d);
if (txt) { full.push(txt); }
markInjected(d);
});
context =
'【必须严格遵守 · ' + area + '开发规范(本会话首次注入编号正文全文)】\n' +
'你正在修改' + area + '代码。下面是本次及后续所有改动都必须时刻遵守的完整规范正文' +
'(' + dirs.join('、') + ';各目录 README 的红线速查/一页纸总则已随 CLAUDE.md 常驻上下文,此处不再重复)。' +
'逐条落实,勿凭记忆越线;dev-guide 与 engineering 冲突时以 dev-guide 硬红线为准。\n\n' +
full.join('\n\n');
} else {
context =
'【必须严格遵守 · ' + area + '开发规范】本会话已注入完整规范(' + dirs.join('、') +
')。继续严格遵守;细节记不清就回看已注入全文或对应编号文档,不要绕过任何红线与规范。';
}
}
} catch (e) {
context = '';
}
if (context) {
process.stdout.write(JSON.stringify({
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: context }
}));
}
process.exit(0);
});