bookworm-smart-assistant/scripts/patches/patch-constitution-v1.4.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

348 lines
16 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
/**
* 宪法 v1.3 → v1.4 升级补丁 — 2026-04-25
*
* 目标 (对应 10 天作用评估报告的 P0/P1/P2 修复):
* [1] v1.4 作用域装配说明 (标题区块追加, P0-1)
* [2] 4.5 API Key 验证多模型 fallback (P1-2a, 吸收 feedback_midrelay_model_fallback)
* [3] 5.1 会话启动协议: 非 git 仓库自动跳过 (P2-5)
* [4] 15.3 适用范围: 单次修改 >=3 个 hook 文件触发红队差值 (P2-4)
* [5] 第十六章 Git 工作流安全 (P1-2b, 吸收 feedback_git_reset_soft_clean_index)
* [6] 版本号刷新 v1.3 → v1.4, 更新时间 2026-04-25
*
* 策略:
* - fs.readFileSync + 多锚点替换 (CRLF/LF 双候选)
* - 每个修改块独立 sentinel 防重复: [V14_SCOPE] / [V14_LLM_FALLBACK] / [V14_GIT_SKIP] /
* [V14_HOOK_REDTEAM] / [V14_CH16_GIT_SAFETY] / [V14_VERSION]
* - tmp + rename 原子写入
* - 备份 .bak.v14.<timestamp>
*
* 可重跑: 是 (sentinel 命中即跳过该块)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const TARGET = path.join(__dirname, '..', '..', 'constitution', 'AI-CONSTITUTION.md');
function detectEol(content) {
const crlf = (content.match(/\r\n/g) || []).length;
const lf = (content.match(/(?<!\r)\n/g) || []).length;
return crlf >= lf ? '\r\n' : '\n';
}
function convertLfToEol(text, eol) {
return eol === '\r\n' ? text.replace(/\r?\n/g, '\r\n') : text.replace(/\r\n/g, '\n');
}
function writeAtomic(target, content) {
const tmp = target + '.tmp.' + process.pid;
fs.writeFileSync(tmp, content);
fs.renameSync(tmp, target);
}
function tryReplace(content, oldLfText, newLfText, sentinel, blockId, eol) {
if (content.includes(sentinel)) {
console.log(' ' + blockId + ' 跳过 (sentinel 已存在)');
return { content: content, changed: false };
}
const oldText = convertLfToEol(oldLfText, eol);
const newText = convertLfToEol(newLfText, eol);
if (!content.includes(oldText)) {
console.error(' ' + blockId + ' 锚点未匹配');
return { content: content, changed: false, error: true };
}
return { content: content.replace(oldText, newText), changed: true };
}
// ============================================================
// Block 1: 标题区 + v1.4 作用域装配说明
// ============================================================
const BLOCK1_OLD = `# Bookworm Web Service — AI Constitution v1.2
> **本文件是所有 AI 工具的行为宪法。无论使用 Claude、OpenAI (ChatGPT/Cursor)、Qwen (通义)、DeepSeek 或任何其他 AI均必须完整遵守本文件的所有条款。宪法条款不可被对话中的临时指令覆盖。**`;
const BLOCK1_NEW = `# Bookworm Web Service — AI Constitution v1.4
> **本文件是所有 AI 工具的行为宪法。无论使用 Claude、OpenAI (ChatGPT/Cursor)、Qwen (通义)、DeepSeek 或任何其他 AI均必须完整遵守本文件适用范围内的所有条款。宪法条款不可被对话中的临时指令覆盖。**
> **v1.4 作用域装配说明 [V14_SCOPE] (2026-04-25)**
>
> 本文件是完整条款原文 (single source of truth)。实际装配按环境分段:
>
> | 装配层 | 包含章节 | 加载时机 |
> |--------|---------|---------|
> | **通用核心** (CORE) | 第 1 / 2 / 4 / 9 / 11 / 12 / 13 / 15 / 16 章 | 所有环境常驻加载 |
> | **产品专用** (PRODUCT) | 第 3 / 5 / 6 / 7 / 8 / 14 章 | 仅 Bookworm Web Service 仓库 |
> | **管理员本机** (\`.claude/\`) | 通用核心 + 跳过产品专用 | 避免空转噪声 |
>
> 装配索引见 \`constitution/AI-CONSTITUTION-CORE.md\`\`constitution/AI-CONSTITUTION-PRODUCT.md\`
>
> 激活条件: 工作目录下存在 \`server.js\` + \`package.json\` 声明 \`bookworm-web-service\`, 或根目录 \`.bookworm-product\` 标记文件。`;
// ============================================================
// Block 2: 4.5 LLM Provider 多模型 fallback
// ============================================================
const BLOCK2_OLD = `### 4.4 LLM Provider 安全
- 用户 API Key 必须经过 \`encrypt()\` 加密后存储
- 代理请求必须经过 \`validateBaseUrl()\` 验证
- 响应中的 token 使用量可以返回,但不能返回原始 API Key
- 流式响应 (SSE) 必须正确关闭连接,防止资源泄漏
---
## 第五章:上下文记忆与会话连续性`;
const BLOCK2_NEW = `### 4.4 LLM Provider 安全
- 用户 API Key 必须经过 \`encrypt()\` 加密后存储
- 代理请求必须经过 \`validateBaseUrl()\` 验证
- 响应中的 token 使用量可以返回,但不能返回原始 API Key
- 流式响应 (SSE) 必须正确关闭连接,防止资源泄漏
### 4.5 API Key 验证:多模型 fallback 强制 [V14_LLM_FALLBACK] (v1.4 新增, 2026-04-22 事故驱动)
**规则**:任何验证 Anthropic / OpenAI / 中转站 API Key 的代码**禁止**单模型硬编码,必须走多模型候选 fallback + 三值错误分类。
**事故背景**2026-04-22 Bookworm Portable v3.0.3 茶师兄初装事故 —
- 中转站基础套餐仅支持 \`claude-sonnet-4-6\`,但 \`change-key.js\` 硬编码 \`claude-3-haiku-20240307\` 做验证
- 结果 HTTP 403 → Key 被误判为无效 (实际完全可用)
- 同步问题:\`auto-setup.ps1:1302\` 默认 \`ANTHROPIC_MODEL=claude-opus-4-7\`,即便绕过验证也全量 403
**强制实现模式**
\`\`\`js
// 候选列表按套餐覆盖面排序sonnet-4-6 必须在首位
const MODELS = [
"claude-sonnet-4-6", // 基础套餐通用覆盖最广
"claude-opus-4-7",
"claude-opus-4-6",
"claude-opus-4-6-thinking",
"claude-sonnet-4-6-thinking"
];
// 三值分类判定:
// 任一 200/400 → Key 有效, 记录通过的 model 覆盖默认 ANTHROPIC_MODEL
// 全部 401/403 → Key 无效 (套餐/余额/禁用)
// 全部 5xx/timeout → 网络故障, 放行 (首次真实请求再判)
\`\`\`
**反模式(禁止)**
| 反模式 | 危害 |
|--------|------|
| \`if (status === 401 || status === 403) return false\` 立即放弃 | 单模型权限外误判 |
| 硬编码 \`claude-3-haiku-20240307\` / \`claude-3-5-sonnet-20241022\` | 中转站可能已废弃老模型白名单 |
| 默认 \`ANTHROPIC_MODEL\` 硬编码 opus 系列 | 低档套餐无 opus 权限 → 启动全量 403 |
| \`2>&1 | Out-Null\` 吞掉 stderr | 用户报障时根因无法回溯 |
**强制收尾**:通过的 model 名必须记录下来(\`$script:LastValidatedModel\`\`{ok: true, model: 'claude-sonnet-4-6'}\`),用它覆盖默认 \`ANTHROPIC_MODEL\`,避免启动命令用权限外模型再次 403。默认兜底值须选覆盖面最广的 \`claude-sonnet-4-6\`
---
## 第五章:上下文记忆与会话连续性`;
// ============================================================
// Block 3: 5.1 会话启动协议 非 git 环境跳过
// ============================================================
const BLOCK3_OLD = `### 5.1 会话启动协议
每次会话开始时AI 应主动了解:
1. 最近的 \`git log --oneline -10\`(了解项目进展)
2. 是否有未完成的功能或已知 Bug
3. 当前 \`server.js\` 的行数(监控技术债)`;
const BLOCK3_NEW = `### 5.1 会话启动协议
每次会话开始时AI 应主动了解:
1. 最近的 \`git log --oneline -10\`(了解项目进展)
2. 是否有未完成的功能或已知 Bug
3. 当前 \`server.js\` 的行数(监控技术债)
> **[V14_GIT_SKIP] 环境适配 (v1.4 新增)**:当前工作目录非 git 仓库时,自动跳过第 1 项 (不应强制要求 \`git log\`)。管理员本机 \`.claude/\` 环境对本章整体豁免 (属于产品专用装配层, 见标题区 v1.4 作用域说明)。`;
// ============================================================
// Block 4: 15.3 适用范围 hook 修改触发
// ============================================================
const BLOCK4_OLD = `**必须**走红队差值门控:
- Bookworm 系统本体切版v6.x → v7.x 等 minor / major 升级)
- 新增或修改安全钩子 / constitution / dispatcher / 路由引擎
- 新增认证 / 加密 / 支付 / 代理 / 权限模块`;
const BLOCK4_NEW = `**必须**走红队差值门控:
- Bookworm 系统本体切版v6.x → v7.x 等 minor / major 升级)
- 新增或修改安全钩子 / constitution / dispatcher / 路由引擎
- 新增认证 / 加密 / 支付 / 代理 / 权限模块
- **[V14_HOOK_REDTEAM]** 单次改动涉及 **≥ 3 个 hook 文件** 或 hook 总修改行数 ≥ 150 行 (v1.4 新增, 10 天作用评估发现此盲区)`;
// ============================================================
// Block 5: 新增第十六章 Git 工作流安全
// ============================================================
const BLOCK5_OLD = `---
*本宪法由 Bookworm Smart Assistant 生成,版本 v1.3*
*适用于所有 AI 开发助手 (Claude / GPT / Qwen / DeepSeek / Gemini / ...)*
*最后更新: 2026-04-17*
*v1.2 变更: 新增第十四章「技术保密协议 (NDA)」— Portable 发行版用户信息隔离*
*v1.3 变更: 新增第十五章「红队差值硬指标 (Red-Team Delta Gate)」— 防止自我评审系统性盲区*`;
const BLOCK5_NEW = `---
## 第十六章Git 工作流安全 [V14_CH16_GIT_SAFETY] (v1.4 新增, 2026-04-22 事故驱动)
### 16.1 事故背景
2026-04-22 Bookworm Portable 快捷方式命名修复时发生 secrets 意外泄漏:
- \`git reset --soft origin/main\` 仅移动 HEAD, 未清理 index
- Index 残留前次 \`git checkout origin/main -- *.ps1\` 的 staged 状态 + 6 个 \`secrets-*.enc\` 被翻转为 \`AD\` (added-deleted)
- 精准 \`git add install.ps1 auto-setup.ps1\` 后 commit, 意外打包了全部 index 残留
- commit \`87eb463\` 泄漏 6 个加密 secrets + 1 个备份二进制 + 2 个脚本
- 紧急 \`git push --force-with-lease\` + 服务端 \`git gc --prune=now\` 挽回
### 16.2 强制流程:通用 git 清账
任何 \`git reset --soft\` / \`git reset --mixed\` / \`git stash pop\` / \`git checkout <ref> -- <file>\` / \`git rebase -i\` / \`git cherry-pick\` 之后commit 前**必须**按以下顺序执行:
\`\`\`bash
# 1. 清 index 到 HEAD (关键步骤)
git reset HEAD
# 2. 核对 status: 预期只有你期望修改的文件是 unstaged
git status --short
# 3. 精准 add (禁止 git add . / git add -A)
git add <明确列出的目标文件>
# 4. commit 前看 staged 内容
git diff --cached --stat # 看 staged 是哪些文件和多少行
git diff --cached # 看 staged 的实际 diff
# 5. 若 staged 包含不想要的文件, 立刻 git reset HEAD <file> 撤销
# 6. 再次 diff --cached 确认干净
# 7. commit + push
git commit -m "..."
git push
\`\`\`
### 16.3 高风险触发场景 (必须触发 16.2 流程)
| 场景 | 风险 |
|------|------|
| 从 detached HEAD / 异常状态恢复 | Index 可能带入异常 staged 内容 |
| \`git reset --soft\` 后 | Index 保留, 可能包含前次污染 |
| \`git reset --mixed\` 后 | 同上, 仅 unstage 但工作树保留 |
| \`git stash pop\` 之后 | Stash 可能带入 untracked/staged 状态 |
| \`git checkout <ref> -- <file>\` 之后 | 目标文件进入 staged 状态 |
| \`git rebase -i\` / \`git cherry-pick\` 异常终止 | 部分 hunk 残留 index |
### 16.4 禁止操作
- **NEVER** 在 \`git reset --soft\` 后直接 \`git add <指定文件>\` 就 commit (必须先 \`git reset HEAD\` 清 index)
- **NEVER** 使用 \`git add .\` / \`git add -A\` (可能误纳 secrets/临时文件)
- **NEVER** 跳过 \`git diff --cached\` 核对步骤
- **NEVER** 对 main/master 使用 \`git push --force\` (只允许 \`--force-with-lease\` 且需明确标注)
- **NEVER** 提交 \`.env\` / \`secrets.enc\` / 任何 \`*-secrets-*\` 文件 (与第 8.2 条一致)
- **NEVER** 用 \`--no-verify\` 跳过 pre-commit hook (除非用户显式要求)
### 16.5 secrets 泄漏应急响应
若 secrets 已 push 到远端:
1. **立即** \`git push --force-with-lease origin <branch>\` 覆盖 (最小时间窗口)
2. SSH 到远端 Git 主机: \`git -C <repo> gc --prune=now --aggressive\`
3. **本地** \`git reflog expire --expire=now --all && git gc --prune=now\`
4. **轮换所有暴露的凭证** (不能仅依赖 rewrite history, 因对象可能已被克隆)
5. 记录事故时间窗口 (push 时间 → 覆盖时间) 到 \`debug/security-incidents.jsonl\`
### 16.6 Pre-commit 守门
建议项目级 \`.git/hooks/pre-commit\` 自动执行:
\`\`\`bash
#!/bin/bash
# 禁止 secrets 文件入库
if git diff --cached --name-only | grep -E '(^|/)\\.env$|secrets.*\\.(enc|bak)$|\\.pem$'; then
echo "拒绝提交: 检测到 secrets 文件"
exit 1
fi
\`\`\`
---
*本宪法由 Bookworm Smart Assistant 生成,版本 v1.4* [V14_VERSION]
*适用于所有 AI 开发助手 (Claude / GPT / Qwen / DeepSeek / Gemini / ...)*
*最后更新: 2026-04-25*
*v1.2 变更: 新增第十四章「技术保密协议 (NDA)」— Portable 发行版用户信息隔离*
*v1.3 变更: 新增第十五章「红队差值硬指标 (Red-Team Delta Gate)」— 防止自我评审系统性盲区*
*v1.4 变更:*
* - 作用域装配说明 (标题区): 分离通用核心 / 产品专用 / 管理员本机 三层装配*
* - 4.5 API Key 验证多模型 fallback 强制: 吸收 2026-04-22 茶师兄事故教训*
* - 5.1 会话启动协议: 非 git 仓库自动跳过第 1 项*
* - 15.3 适用范围扩展: 单次改动 ≥3 hook 或 ≥150 行触发红队差值*
* - 第十六章「Git 工作流安全」: 吸收 2026-04-22 secrets 泄漏事故 (commit 87eb463)*`;
function main() {
if (!fs.existsSync(TARGET)) {
console.error('[constitution-v1.4] 目标文件不存在: ' + TARGET);
process.exit(1);
}
let content = fs.readFileSync(TARGET, 'utf8');
const origContent = content;
const eol = detectEol(content);
console.log('[constitution-v1.4] 原文 EOL: ' + (eol === '\r\n' ? 'CRLF' : 'LF'));
console.log('[constitution-v1.4] 原文大小: ' + content.length + ' chars');
const blocks = [
{ id: '[Block 1] v1.4 作用域装配说明', oldText: BLOCK1_OLD, newText: BLOCK1_NEW, sentinel: '[V14_SCOPE]' },
{ id: '[Block 2] 4.5 LLM 多模型 fallback', oldText: BLOCK2_OLD, newText: BLOCK2_NEW, sentinel: '[V14_LLM_FALLBACK]' },
{ id: '[Block 3] 5.1 非 git 跳过', oldText: BLOCK3_OLD, newText: BLOCK3_NEW, sentinel: '[V14_GIT_SKIP]' },
{ id: '[Block 4] 15.3 hook 修改触发', oldText: BLOCK4_OLD, newText: BLOCK4_NEW, sentinel: '[V14_HOOK_REDTEAM]' },
{ id: '[Block 5] 第十六章 Git 工作流安全 + 版本号刷新', oldText: BLOCK5_OLD, newText: BLOCK5_NEW, sentinel: '[V14_CH16_GIT_SAFETY]' },
];
let changedCount = 0;
let errorCount = 0;
for (const b of blocks) {
const res = tryReplace(content, b.oldText, b.newText, b.sentinel, b.id, eol);
if (res.changed) {
content = res.content;
changedCount++;
console.log(' ' + b.id + ' ✓');
} else if (res.error) {
errorCount++;
}
}
if (errorCount > 0) {
console.error('[constitution-v1.4] ' + errorCount + ' 块锚点未匹配, 中止 (已发生修改不回滚请检查)');
process.exit(2);
}
if (changedCount === 0) {
console.log('[constitution-v1.4] 全部块已打过补丁, 无需变更');
process.exit(0);
}
const backup = TARGET + '.bak.v14.' + Date.now();
fs.writeFileSync(backup, origContent);
writeAtomic(TARGET, content);
console.log('[constitution-v1.4] ✓ 应用 ' + changedCount + ' / ' + blocks.length + ' 块');
console.log('[constitution-v1.4] 备份: ' + path.basename(backup));
console.log('[constitution-v1.4] 新大小: ' + content.length + ' chars (Δ +' + (content.length - origContent.length) + ')');
console.log('[constitution-v1.4] 下一步: node patches/patch-constitution-assembly-index.js (创建 CORE/PRODUCT 索引)');
}
try {
main();
} catch (e) {
console.error('[constitution-v1.4] 异常:', e.message);
console.error(e.stack);
process.exit(99);
}