- 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)
348 lines
16 KiB
JavaScript
348 lines
16 KiB
JavaScript
#!/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);
|
||
}
|