Compare commits

..

76 Commits
v1.5.0 ... main

Author SHA1 Message Date
bookworm
6ac69b93ac security(v3.2.0): W4 凭证不嵌 URL + W5 OTA Copy 替代 Move
W4: clone URL 不再含明文凭证, 改为先 Cache-GitCredentials
    (credential manager) 再用原始 URL clone, git 自动取凭证
W5: OTA 原子替换 Move-Item → Copy-Item + Remove
    (04-27 Move 数据丢失教训, Copy 中途失败可完整回滚)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 00:37:35 +08:00
bookworm
df3a105f92 fix(v3.2.0): 审查修复 3 Blocker
1. $TOTAL_PHASES 7→8 (Phase 8 OTA 后 GUI 进度条溢出)
2. OTA config.json 只在不存在时写入 (保留 lastCheck 冷却)
3. remote URL 比对从硬编码改为动态 $expectedBase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 00:28:55 +08:00
bookworm
fa287ae27d fix(v3.2.0): profile 注入 null 崩溃 (全新机器 profile.ps1 不存在)
Get-Content -Raw 对空文件返回 $null 而非空字符串,
导致 [regex]::Replace($null, ...) 抛 ArgumentNull。
加 null-coalesce 兜底为空字符串。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 23:15:01 +08:00
bookworm
5ca772ebbe fix(v3.2.0): git proxy bypass + clone 完整性验证加固
1. git http.proxy="" 强制 code.letcareme.com 直连 (VPN 环境 clone 失败根因)
2. clone 验证从只检查 CLAUDE.md 升级为 CLAUDE.md + skills/ + hooks/ 三项
3. remote URL 自动修正 (旧仓库 → bookworm-smart-assistant)

确保 VPN 用户一键安装体验完整

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:57:47 +08:00
bookworm
1f90b933c2 fix(v3.2.0): auto-detect and fix stale git remote in .claude/
When .claude/ was previously cloned from bookworm-portable-config (deleted),
re-running the installer would git pull against the wrong remote.
Now auto-detects stale remote and switches to bookworm-smart-assistant,
then fetches + resets to get the correct content (including skills/).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:51:03 +08:00
bookworm
57ca443386 fix: 更新Bookworm.bat git pull 前 stash 本地修改
settings.json 由 template 渲染后与仓库版本不同,
导致 git pull --rebase 报 unstaged changes 失败。
pull 前 stash、pull 后 pop 恢复。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:19:32 +08:00
bookworm
47079cb8b1 fix(v3.2.0): 移除 Bun crash 降级逻辑 (根因: 安装后 claude 被降到 v2.0.1)
- 删除 claude --version 自检 → 自动降级到 2.0.1 的逻辑
- Claude Code 2.1.x+ 已不使用 Bun, 降级完全不必要
- 此逻辑导致对方机器 claude 被降到 v2.0.1 (远古版本)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 21:56:56 +08:00
bookworm
697b58ef15 fix(v3.2.0): 取消安装后自动弹终端 + wt.exe 参数修复
- 安装完毕不再自动启动终端窗口 (避免 wt.exe 参数拼接错误)
- 用户通过桌面快捷方式启动 Bookworm
- 受限模式也改为提示而非自动启动

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 21:07:27 +08:00
bookworm
7a9a5b8dfa fix(v3.2.0): GitUrl 指向已删仓库 → bookworm-smart-assistant
- $GitUrl bookworm-portable-config.git → bookworm-smart-assistant.git
- 旧仓已删除导致用户安装 [3/7] clone 失败
- EXE 重新打包含此修复

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 20:46:27 +08:00
bookworm
aa662d8744 feat(v3.2.0): Phase 8 OTA 自动更新基础设施
- auto-setup.ps1 v3.2.0: Phase 8 pubkey/DPAPI凭证/bw-ota.ps1 部署
- bw-ota.ps1: OTA 升级脚本 (Ed25519签名验证+SHA256+原子替换+DryRun)
- 启动Bookworm.bat: 启动时 fail-open 调用 OTA 检查
- Bookworm-Setup.exe: 重建含以上变更

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 18:22:37 +08:00
bookworm
0b4402d417 hotfix(v3.1.3): 卸载精准删除 + 体检凭证链路修正
W1: 卸载脚本改为只删 Bookworm 注入的 7 目录+3 文件,
    保留用户自有 CLAUDE.md/memory/projects (防误杀)
W2: bw-doctor [9] 改检 DPAPI→profile 注入链路,
    不再检查 User 持久环境变量 (逻辑反转修正)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 17:06:02 +08:00
leesu
2d36862f38 chore: sync Bookworm-Setup.exe v3.1.3 2026-04-26 17:04:59 +08:00
bookworm
ec490c3f35 feat(v3.1.2): 完整生命周期 (闭合 L9-L12)
新增:
- 卸载Bookworm.bat + 卸载Bookworm-impl.ps1: 7 步清理
- bw-doctor.ps1: 13 维度健康体检工具
- 桌面快捷方式 2→4 (新增体检+卸载)

增强:
- bw-launch.ps1: nvm/fnm/volta 探测 + log rotation
- auto-setup.ps1: npm/uvx 国内镜像双 fallback

12/12 局限全部闭合, v3.1.x 生命周期完整.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 15:12:15 +08:00
leesu
3beb79af7a chore: sync Bookworm-Setup.exe v3.1.2 2026-04-26 15:09:01 +08:00
bookworm
7c8540b542 feat(v3.1.1): E2E 护栏 + UX 4 项闭合 + 并发安全
闭合 v3.1.0 后续 4 个 HIGH 局限:

[L8] smoke 覆盖不到 .lnk 行为 → 引入 tools/test-launcher-e2e.ps1
  4 测试套: PARSE / wrapper 6 特性 / .lnk Args 6 契约 / profile 双注入契约
  集成到 build.ps1 后置, 失败 exit 1 拒绝发布
  闭合 v3.0.10 -or 类 PSParser 漏网 bug 风险

[L5] Phase 1 总结弹窗仅有动作时弹 → 始终弹
  老用户全就绪重跑 EXE 跳过总结不知是否完成
  v3.1.1 移除条件, 永远显示 (零新装也弹 OK 态)

[L6] 更新.bat 完成无反馈 → GUI YesNo 询问立即启动
  PS MessageBox: '同步完成. 是否立即启动 Claude?' Yes 触发 Start-Process 桌面 .lnk

[L7] profile 注入并发损坏 → FileShare.None 排他锁 + 5 次重试
  WriteAllText 隐式 FileShare.Read 改为显式 FileStream
  IOException catch sleep 50ms 重试, 5 次都失败 throw 触发 v3.1.0 显式弹窗

8/12 局限闭合 (剩余 L9-L12 计划 v3.1.2)
2026-04-25 23:16:08 +08:00
bookworm
e225a5c758 feat(v3.1.0): wrapper 模式 + 双 profile + 凭证注入失败弹窗
闭合 v3.0.11 已知 CRITICAL 局限 (3/4):

[L4] claude.ps1 路径迁移破坏 .lnk → 引入 wrapper bw-launch.ps1
  桌面 .lnk Args 不再 bake claude.ps1 路径, 改指向 $BootDir\bw-launch.ps1
  wrapper 启动时动态查 claude.ps1 (PATH 重载 + npm prefix + 硬编码兜底)
  失败时写 bw-launch.log + GUI MessageBox 弹清晰诊断
  好处: claude.ps1 路径变 → wrapper 自动重新查, .lnk 永不 stale

[L2] PS5.1-only 启动 profile BW_CRED 不加载 → 双 profile 注入
  Inject-CredentialLoaderProfile 同时写:
    Documents\PowerShell\profile.ps1 (PS7 主用)
    Documents\WindowsPowerShell\profile.ps1 (PS5.1 兜底)
  任一成功即 OK, 双方都失败才 throw

[L3] profile 注入失败 fail soft 不可见 → catch 弹窗
  Show-MsgBox Warning 含影响 + 异常原文 + 3 种手动修复方案
  不阻断主安装, Phase 5+ 继续

[L1] DPAPI 跨用户 (设计内禀, 文档化) — 未闭合, 用户换电脑/换 Win 用户重跑 EXE 重激活

新文件: bw-launch.ps1 (4472 字节, 启动 wrapper)
EXE 228352 → 231424 bytes (+3072)
向后兼容: v3.0.11 老 .lnk 直调 claude.ps1 仍能跑, 重装升级到 wrapper
2026-04-25 22:42:14 +08:00
bookworm
4ad048040e feat(macos): Bookworm-Setup.sh 同步主版本号 → v3.0.11
macOS 不受 Windows wt+Base64 启动链路问题影响 (Mac shell 无 wt,
claude 启动经标准 PATH 即可), 此版本仅同步主版本号注释,
核心逻辑保持 v3.0.3 流程稳定.

发布到 R2 + 鑫霖 nginx + Worker /static/Bookworm-Setup.sh.
四端字节 26533 / MD5 32cc7bbdbc453d674f22d334ad25a7fa.
2026-04-25 21:54:48 +08:00
bookworm
95422376ce audit(v3.0.11): P0 -ExecutionPolicy Bypass + P1 4-项自验证
落地前红队审查发现 2 个阻断性 bug + 6 个非阻断:

[P0] Bug 2: .lnk Args 缺 -ExecutionPolicy Bypass
  根因: npm 全局生成的 claude.ps1 shim 是未签名脚本.
       Phase 1 设的 RemoteSigned 在 LTSC / Group Policy AllSigned 机器
       仍会拒绝执行. 用户启动时报'脚本未数字签名'.
  修: -NoLogo -NoExit -ExecutionPolicy Bypass -File ...

[P1] Bug 8: 自验证只查 claude.ps1 路径, 不查 perms / Bypass
  根因: 验证太松, 关键参数丢失也会通过.
  修: 扩展为 4 项检查 (Target / claude.ps1 路径 / --skip-perms / Bypass),
      任一失败立即删除 .lnk + 弹错, 不交付半成品.

P1 已知限制 (PS5.1 profile / Inject fail soft) 不阻断本版.

EXE 227840 → 228352 bytes (+512)
2026-04-25 21:38:28 +08:00
bookworm
1421338da3 feat(v3.0.11): 架构重构 — 启动链路 7 跳 → 1 跳直链
用户报障驱动 (v3.0.3-3.0.10 共 9 轮迭代后接受架构原罪).

旧架构: .lnk → bat → wt → pwsh → Base64 → DPAPI → claude (7 跳)
新架构: .lnk → pwsh → claude.ps1 (1 跳, 凭证由 profile 异步加载)

ADR-001 桌面 .lnk 直调 pwsh + claude.ps1 绝对路径
  - 安装时 Get-Command claude / npm config get prefix 定位 claude.ps1
  - bake 进 .lnk Args, 运行时不依赖任何 PATH 重载
  - 自验证: 创建后回读 .lnk 确认 Target/Args 完整, 失败立即删除拒绝交付

ADR-002 DPAPI 凭证加载迁移到 PowerShell profile sentinel
  - BW_CRED_START..END 块写入 ~/Documents/PowerShell/profile.ps1
  - pwsh 启动自动 source, 与启动链路彻底解耦
  - profile 编辑用 String.Replace 字面替换 (避开 -replace $ backreference)

ADR-003 启动 / 更新功能拆分
  - 启动Bookworm.lnk → 仅启动 (1 跳)
  - 更新Bookworm.lnk → 仅 git pull
  - 更新并启动Bookworm.bat → 重命名为 更新Bookworm.bat (仅同步)
  - tools/gen-launcher-bats.ps1 归档 (新架构不需要 Base64 生成器)

8 个失败模式中 7 个被架构性消除:
   F1 wt ; 切 tab (无 wt)
   F2 cmd 转义 (无 cmd 启动路径)
   F3 wt -d 引号 (无 wt -d)
   F4 Base64 解码语义错 (无 Base64)
   F5 PATH 时序 (绝对路径不依赖 PATH)
   F6 claude shim (直调 .ps1)
   F8 wt 自身 issue (无 wt)
  ⚠️ F7 DPAPI 跨用户 (设计限制, 跨用户需重激活)

EXE 220672 → 227840 bytes (+7168, ADR-002 profile 注入函数)
2026-04-25 18:19:07 +08:00
bookworm
609e82bac0 hotfix(v3.0.10): Base64 '-or' 括号 bug + dry-run 实跑验证护栏
v3.0.9 Base64 脚本启动即报:
  Test-Path: A parameter cannot be found that matches parameter name 'or'.
  claude.exe not found

根因: 'Test-Path (Join-Path $p claude.ps1) -or (Test-Path ...)' 中 -or
被当成 Test-Path 的命名参数. PSParser 静态检查看合法, 运行时炸.

修复:
  F1: 括号修正 — $hasClaude 抽为独立变量, 三元 -or 每项带外括号
  F2: gen-launcher-bats.ps1 强制加 dry-run 实跑验证护栏
      解码后的 Base64 脚本必须被 pwsh -File 实跑到底部 __BW_DRYRUN_OK__
      才算通过. 检查 ErrorRecord / ParameterBindingException / 未知命令.
      任何未来 Base64 改动都被此验证拦截.

验证层级教训:
  PSParser = 抓语法   / 抓不到参数绑定错
  dry-run  = 抓运行时 / 抓不到业务逻辑
  smoke    = 抓业务   / 需要前两层通过

EXE 220160 → 220672 bytes (+512)
2026-04-24 22:48:08 +08:00
bookworm
a3b4ff3a78 fix(v3.0.9): Claude 装完 npm prefix 固化 + 三层 PATH + 快捷方式自验证
用户反馈 (核心): '不要客户端修修补补, EXE 必须一次对'

诊断: v3.0.8 Phase 1 全 OK, 但桌面快捷方式 claude.exe not found
  user npm config get prefix = C:\Users\Administrator\AppData\Roaming\npm
  但 Get-Command claude 在 PS 7 为空 → User PATH 没含这个目录
  Node.js MSI 写 User PATH 时机/行为被企业镜像/防病毒/组策略干扰

三层根治:

F1: auto-setup.ps1 装完 Claude Code 后立即 `npm config get prefix` 查真实路径,
    强制写入 User PATH (永久生效). 不依赖 npm 自己的 PATH 注入时机.
F2: gen-launcher-bats Base64 脚本加 3 层 PATH 修复:
    层 1 Machine+User env PATH / 层 2 npm config get prefix 动态查询 /
    层 3 硬编码候选 (%APPDATA%\npm / Program Files\nodejs / LOCALAPPDATA\npm)
F3: auto-setup.ps1:2256 创建桌面快捷方式前 Test-Cmd claude 自验证,
    失败则拒绝创建 + 弹明确错误, 不再静默产出坏快捷方式

EXE 217088 → 220160 bytes (+3072)
Base64 3544 → 5880 chars
产品原则: 点快捷方式要么成功要么给清晰诊断, 不再给 'claude not found' 无头案
2026-04-24 22:39:17 +08:00
bookworm
9fc7f1c9fa feat(v3.0.8): Phase 1 引导性大升级 (PS7 硬核+per-dep 弹窗+总结+Claude 诊断)
用户反馈: v3.0.7 EXE 安装没有检测和引导 PS7/Claude Code. 根因:

B1: PS7 标记为'可选', 装失败只 Log-Info, 用户滚动错过.
    但启动器 bat 强依赖 pwsh, 降级 5.1 体验差 (粘贴多行命令被拆/Base64 截断).
B2: Core 依赖装失败只 Log-Fail 到 GUI, 末尾才一次 manualGuide.
B3: Claude Code 失败没具体诊断 ('npm 安装失败' 模糊无法报障).

修复:
  F1: PS7 升硬核 (最终检查含 pwsh, 缺失 exit 1 + manualGuide 含 winget+MSI)
  F2: foreach 内 Core 失败立即弹 Warning 专属窗 (winget/MSI/EXE/npm 全方案)
  F3: Phase 1 末尾总结 MessageBox (就绪/新装/失败分组, 只在有动作时弹)
  F4: Claude 失败写诊断到 bw-crash.log (node -v/npm -v/npm prefix/PATH)

EXE 212480 → 217088 bytes (+4608)
UX 对比: v3.0.7 用户滚动错过关键信息; v3.0.8 每步都有专属弹窗或总结
2026-04-24 22:13:02 +08:00
bookworm
5d16b7cfcf hotfix(v3.0.7): Phase 4 取 Key 后静默闪退根治
v3.0.6 用户报障: 取 Key 成功后窗口消失无提示. 三重叠加根因:

B1: $ErrorActionPreference=Stop + 无顶层 trap → PS2EXE 遇异常直接 exit
B2: Phase 5.5 profile 注入 -replace 把 $script:/$HOME/$IntervalMs 当
    .NET Regex backreference 吃掉 → profile.ps1 损坏, Regex.Replace
    3参传 array 抛 ArgumentException
B3: Phase 5.5 整段无 try-catch → B2 异常直接终止主流程

修复:
  F1: 顶层 trap 写 $env:TEMP\bw-crash.log (含 message+type+line+stack) + GUI 弹窗
  F2: Phase 5.5 regex 替换改用 String.Replace 字面替换 (绕过 $ backreference)
  F3: Phase 5.5 整段 try-catch 降级, 截图助手失败不阻断 Phase 6+

EXE 198144 → 212480 bytes (+14336 含 trap + try-catch + String.Replace)
向后兼容: 老 profile.ps1 sentinel 块会被自动覆盖修复.
2026-04-24 21:06:43 +08:00
bookworm
8f6c53ec99 fix(v3.0.6): 启动器四连击 bug 根治 + 分发版阈值调整
真实用户报障驱动, 4 个 P0 闭环:

B1: wt.exe ';' 切 tab 误报 → -EncodedCommand Base64-UTF-16LE
B2: -d '%~dp0' 尾 \ 配 " 转义引号 → -d '%CD%' 无尾反斜杠 (cd /d 已切入)
B3: 子 pwsh 继承老 PATH 致 claude.exe not found → Base64 首行强制重载
    $env:Path = Machine + ';' + User, 找不到 claude 还附带 PATH 诊断
B4: install.ps1 [8/9] Skills>50 Hooks>10 阈值误报脱敏分发版残缺
    → Skills>=10 Hooks>=3 匹配 bookworm-portable-config.git 实际规模

新增工具 tools/gen-launcher-bats.ps1: 单一明文源 → Base64 → 两 bat 自动同步,
含字符集 + 长度 + round-trip PARSE 三重 health check, 以后改启动逻辑
只需改 $plainScript 一处.

向后兼容: DPAPI 凭证语义 round-trip 不变, 老快捷方式继续可用,
Installer banner v1.5 → v1.6, BWVersion 3.0.5 → 3.0.6.
2026-04-24 20:47:16 +08:00
bookworm
55ac953cd8 feat(v3.0.5): winget 三层 fallback + 截图助手集成 + 启动器自检
4 组改动闭环用户报障:

[B1] Node.js + Git MSI/EXE 直链兜底 (修复: 只有 PS7 有 MSI 兜底, Node/Git
     winget 失败后无后路, 新机装 Bookworm 必挂). 新增统一分派:
     MsiUrl → msiexec /quiet /qn, ExeUrl → Inno Setup /VERYSILENT.

[B2] install.ps1 -StartOnly 依赖缺失改 GUI 弹窗 + auto-setup.ps1 Phase 1
     失败时清除桌面僵尸快捷方式 (启动Bookworm.lnk / Bookworm.lnk /
     更新Bookworm.lnk). 根治 'claude.exe not found' 闪退.

[F1] winget 检测醒目提示: 不可用时弹 Information 列出 Store / GitHub /
     系统要求 / 备用方案, 但不阻断安装流程.

[F2] Phase 1 手动安装清单重构: 每依赖列 ManualUrl + 步骤 + 重启 EXE 指引 +
     排查清单.

[F3+F4] 新增 Phase 5.5 截图粘贴助手部署:
     - profile sentinel 追加式注入 (# BW_CLIP_START/END, 重装精准替换)
     - 截图 Toast 关闭 + 原值备份到 HKCU:\Software\Bookworm\ToastBackup
       (卸载可还原, Q2=B 方案)

$BWVersion = 3.0.5, EXE 198144 → 209920 bytes (+11776).
向后兼容: 老用户已装依赖全跳过, profile 无 BW_CLIP 块首次追加.
2026-04-24 18:59:16 +08:00
bookworm
85f2bcd52f fix(auto-setup): v3.0.4 Key 验证 + 默认模型 + 指纹 Win11 兼容
茶师兄 Win11 机器真实报障驱动 4 Bug 闭环:

B1: 移除 change-key.js 优先验证分支 (硬编码 haiku → sonnet-only 套餐必 403)
    统一走 Test-ApiKey (多模型 fallback + 三值错误分类).

B2: 默认 ANTHROPIC_MODEL 兜底 opus-4-7 → sonnet-4-6 (基础套餐覆盖面最广).
    Test-ApiKey 成功时记录 $script:LastValidatedModel → Phase 4 末尾覆盖为真·可用模型.

B4: stderr 不再 2>&1 | Out-Null. PASS/AUTH_FAIL/NETWORK_ACCEPT 三态记录到
    $env:TEMP\bw-phase4-validate.log 并附失败模型列表便于报障.

B3 在 bookworm-portable-config.git c46f0b6 解决 (fingerprint.js wmic→CIM).

Test-ApiKey 候选顺序: sonnet-4-6 > opus-4-7 > opus-4-6 > thinking 变体.
401/403 不再立即 return false, 改累积到 $authFailModels, 全候选失败才判 false.
2026-04-22 20:05:56 +08:00
bookworm
235839a880 feat: 桌面快捷方式命名统一「启动Bookworm」+「更新Bookworm」
- install.ps1 / auto-setup.ps1 的「Bookworm.lnk」改名为「启动Bookworm.lnk」
- 顺序反转: 先建新 lnk -> 验证存在 -> 再删老 lnk (防空窗)
- install.ps1 的 -StartOnly 分支也调用 New-DesktopShortcuts,
  让只从老 Bookworm.lnk 启动的老用户也能自动完成命名迁移
- 所有分支幂等, Test-Path 守门, 多次运行不报错

用户端生效路径:
- 新用户装 Bookworm-Setup.exe (195584 B, 已含新 auto-setup) -> 直接建新 lnk
- 老用户下次启动 -> bat 的 git pull 拉此 commit 的 install.ps1 -> 自动迁移

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 01:02:43 +08:00
bookworm-admin
656460e13e Setup.sh v3.0.3: onboarding pre-fill + header bump 2026-04-21 19:05:01 +08:00
Bookworm Admin
ddcb53fe95 cleanup: remove rotation probe 2026-04-21 16:33:25 +08:00
Bookworm Admin
78237d4e83 test: credential rotation probe (2026-04-21) 2026-04-21 16:33:23 +08:00
Bookworm Admin
13337134bc v3.0.3: Win10 PS7 MSI fallback (when winget absent/outdated)
Phase 1 依赖循环增加 MSI 直链兜底:
- winget 失败 or 缺席 -> Invoke-WebRequest 拉 PS 7.4.6 MSI
- 强制 TLS 1.2 (Win10 默认 TLS 1.0 访问 GitHub 会断)
- msiexec /quiet /qn ADD_PATH=1 静默装
- 装后补 PATH (PS7 装完默认不在 PATH 直到重登)

同时将 PowerShell 7 改为 Core=true, winget/MSI 都失败会明确提示用户.

背景: 第二/第三台 Win10 开机一直用 PS 5.1, 原因 pwsh 未装 (winget 静默失败),
bat 路径 3/4 fallback 到 5.1, Unicode/TUI 体验差.
2026-04-21 14:31:32 +08:00
Bookworm Admin
18210ac497 v3.0.2: fix 3-machine login loop (ExecutionPolicy + WM_SETTINGCHANGE broadcast + .claude.json onboarding pre-fill)
3 台机实测发现 5 个串联坑, 源头修 3 处:

1. auto-setup.ps1 Phase 1: Set-ExecutionPolicy CurrentUser RemoteSigned
   修复: 管理员 PS 下 claude.ps1 因 Restricted 被拦

2. auto-setup.ps1 Phase 4: SendMessageTimeout WM_SETTINGCHANGE 广播
   修复: User scope env 不自动同步到已打开 explorer/shell

3. auto-setup.ps1 + Setup.sh Phase 4: 预填 .claude.json
   {hasCompletedOnboarding:true, customApiKeyResponses.approved:[<sk-前20字符>]}
   修复: Claude Code v2.0.1 登录选择页两个选项都走 anthropic.com OAuth,
         国内连不上. 预填标记后首次 claude 直接进对话提示符.

配套 Gitea 'bookworm-boot/启动Bookworm.bat' (a6f9cf7) 已清理 &$env:ANTHROPIC_MODEL call-operator 误解析.
2026-04-21 14:22:48 +08:00
Bookworm Admin
a6f9cf7157 fix(launcher): strip buggy & $env:ANTHROPIC_MODEL cmd — caused 'term not recognized' on pwsh call-operator parse
ANTHROPIC_MODEL 已由 line 6 'set ANTHROPIC_MODEL=' 传入子 pwsh 作为 process env,
无需在 -Command 串内再 set。原写法 '& $env:ANTHROPIC_MODEL=...' 被 pwsh 当
call 运算符去执行 'claude-opus-4-7' 导致启动失败。

嵌套双引号也一并清理。
2026-04-21 12:28:45 +08:00
Bookworm
080ff71653 sync: v3.0.1 auto-setup.ps1 + Setup.sh (P0 fixes) 2026-04-21 01:54:42 +08:00
Bookworm
9668a58480 fix(launch): use claude.ps1 direct (bypass cmd.exe shim) + ANTHROPIC_MODEL env 2026-04-21 01:22:45 +08:00
Bookworm
4dfff1b062 fix(launch): default ANTHROPIC_MODEL=claude-opus-4-7 (bypass Claude Code 2.0.x hardcoded 4-5 → 中转站 503) 2026-04-21 00:43:16 +08:00
bookworm
1aa2c3e6ef feat: v3.0.0-beta - 切换到 portable-config 脱敏仓库 + Phase 4.5 License 激活 2026-04-18 23:04:40 +08:00
bookworm
1b97332239 fix: v2.3.1 Phase 4 统一调用 change-key.js
- Win auto-setup.ps1: 优先管道到 change-key.js, 回退 Test-ApiKey
- Mac Bookworm-Setup.sh: 凭证 printf 管道传 change-key.js, 不再重复 export
- 统一验证逻辑, 避免双实现漂移
2026-04-17 02:25:40 +08:00
bookworm
f3a58e1c6d feat: v2.3.0 - Phase 4 改为用户直接输入中转站凭证
Win: Show-ApiKeyDialog GUI + Test-ApiKey 验证
Mac: 调用 ~/.claude/change-key.js (统一逻辑)
两者都保留旧授权码流程作为向后兼容

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 00:22:17 +08:00
bookworm
58099da6da fix: Mac 注释版本号 1.5→2.2.4 2026-04-13 23:19:28 +08:00
bookworm
9c595fae86 feat: Mac v2.2.4 - MCP inject + git robustness + credential whitelist 2026-04-13 22:32:02 +08:00
bookworm
b2d5e4aa7b fix: v2.2.4 - fallback 补齐 supabase (22/22 对齐) 2026-04-13 18:37:05 +08:00
bookworm
4dc5f8e0f8 feat: v2.2.3 - 启动终端优先 Windows Terminal > pwsh > cmd
检测 wt.exe (Windows Terminal) 可用则优先使用 (现代UI/全屏/标签页),
其次 pwsh (PowerShell 7), 最后降级 cmd.exe。
解决 Win10 用户只能看到老旧蓝色终端的问题。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:40:02 +08:00
Bookworm Bot
9e0d22513f fix(launcher): Windows Terminal优先 + pwsh7优先 + 修复/dev/null→nul
- 终端优先级: wt.exe > conhost (解决渲染问题和全屏闪退)
- Shell优先级: pwsh7 > powershell 5.1 (更好的TUI支持)
- 修复Unix路径错误: /dev/null → nul (Windows批处理兼容)
- 4条启动路径: wt+pwsh7 / wt+ps5.1 / conhost+pwsh7 / fallback

解决问题:
1. 终端窗不好看 → Windows Terminal优先
2. 全屏闪退 → wt.exe原生支持缩放
3. pwsh7优先 → where pwsh检测 + 4级回退
2026-04-12 15:05:22 +09:00
Bookworm Bot
675606b5c4 fix(auto-setup): npm/npx Start-Process 兼容 Windows
Windows 上 Start-Process "npm" 会匹配到无扩展名的 Unix shell 脚本
而非 npm.cmd,导致 "%1 不是有效的 Win32 应用程序" 错误。

在 Run-CmdWithUI 函数入口自动将 npm/npx 映射为 npm.cmd/npx.cmd,
一劳永逸覆盖所有调用点。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 13:59:54 +09:00
bookworm
9d1cef0331 fix: v2.2.2 - Python/pwsh 降为可选依赖 + quick-start 安装前准备
- Python 从核心 deps 移除 (只在 uvx MCP 需要)
- PowerShell 7 降为可选 (系统 PS 5.1 可替代)
- 非核心依赖 winget 不可用时不弹阻断对话框
- quick-start.html 新增"安装前准备"区块 (ExecutionPolicy + 手动装依赖)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:24:15 +08:00
bookworm
edee7b777e fix: v2.2.1 - Top5 鲁棒化修复 (F-09/F-12/F-17/F-19/F-22)
F-22: Phase 4 解密成功后调用 Save-SecretsToCache, 下次免授权码
F-12: 解密结果写入 env 前校验白名单 + 长度 < 512
F-19: 冲突检测改逐行匹配, 修复 Out-String 多行正则失效
F-09: boot 仓库 clone 始终弹凭证对话框, 不依赖 $cred 残留
F-17: uvx tool install 参数顺序修正 (包名在前 --python 在后)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:29:04 +08:00
bookworm
50d3ef0377 fix: v2.2.0 - 彻底修复 git pull 失败 + MCP 注入鲁棒化
4 个根因全修:
1. Phase 3 凭证缓存: clone 后 git credential approve 写入 Credential Manager
2. Phase 3 冲突清理: pull 前 reset --hard 清除运行时文件冲突
3. Phase 3 认证重试: pull 失败时弹窗重输凭证 + 缓存
4. Phase 5 fallback: inject-mcp.js 不存在时用内嵌 22 MCP 脚本

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 22:00:48 +08:00
bookworm
15449c86d8 feat: v2.1.0 - 同步 22 MCP + 简化 Phase 5 注入逻辑
Phase 5 MCP 注入改为直接调用 ~/.claude/inject-mcp.js (从 config 仓库),
移除复杂的 PowerShell JSON 提取+临时脚本生成逻辑。
inject-mcp.js 包含完整 22 个 portable MCP server 配置。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:24:50 +08:00
bookworm
aee3cc708b fix: v2.0.3 - MCP 注入改用临时 JS 文件执行 (避免 -e 参数解析)
v2.0.2 的 node -e + -- 参数传递在 PowerShell 下 argv 偏移导致 mcpServers 为空。
改为: 将合并脚本写入 %TEMP%/bw-inject-mcp.js 再 node 执行, 绕过所有参数解析陷阱。
新增 inject-mcp.js 独立脚本供手动执行。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:15:53 +08:00
bookworm
6168279f78 fix: v2.0.2 - Node.js 安全合并 .claude.json (保留现有字段)
v2.0.1 的 PowerShell 合并会覆盖 .claude.json 现有字段 (installMethod/isLoggedIn),
导致 Claude Code 重新登录后又把 mcpServers 覆盖掉。

改用 Node.js JSON.parse/stringify 做精确字段合并:
- 读取现有 .claude.json 全部字段
- 只注入/更新 mcpServers 键
- 过滤 __comment 键
- 保留 installMethod, isLoggedIn, completedOnboarding 等

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:27:59 +08:00
bookworm
f262f1bd21 fix: v2.0.1 - MCP 写入 ~/.claude.json (Claude Code v2.1+ 正确位置)
settings.json 中的 mcpServers 在 Claude Code v2.1+ 不再被读取。
Phase 5 现在从渲染后的 settings.json 提取 mcpServers 写入 ~/.claude.json,
支持与用户已有配置合并。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:16:43 +08:00
bookworm
7d0e4edd16 fix: v2.0.0 - 修复 MCP 加载 + HOME 路径 JSON 转义
P0: mcpServers 已合并到 settings.template.json (settings.local.json 不被加载)
P2: {{HOME}} 替换改用正斜杠, 避免 \U \l 等非法 JSON 转义
新增: diagnose-tooluse.js 中转站工具调用诊断脚本

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:01:40 +08:00
bookworm
844419b951 style: 桌面图标更换为翡翠量子螺旋 (quantum emerald) 2026-04-10 13:51:39 +08:00
bookworm
123db247a7 feat(v1.9.0): 启动时自动 pull 配置更新 2026-04-10 12:41:17 +08:00
bookworm
722190404a fix: 闪屏后启动带 Bookworm 标题的终端窗口 2026-04-10 12:29:54 +08:00
bookworm
38b2a2d022 feat(v1.8.0): 祝贺闪屏动画替代 MsgBox + cmd 黑窗
安装成功后的体验:
- 移除: MsgBox "安装成功" 弹窗 + cmd.exe /k claude 黑窗口
- 新增: 品牌闪屏 (深色底 + 蓝紫装饰条 + 绿色大勾)
  - 淡入动画 (300ms) → 停留 2.5s → 淡出 (250ms)
  - 显示: 版本号 + Skills/Hooks 数量 + "善读者,必善造"
  - 底部提示 "双击桌面 Bookworm 图标即可启动"
- 不再自动弹出 cmd 窗口 (用户通过桌面快捷方式启动)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:28:51 +08:00
bookworm
23d4577743 fix(v1.7.1): Phase 7 闪退修复 + 7a/7b/7c 防崩 2026-04-10 12:18:22 +08:00
bookworm
7dd72082b7 feat(v1.7.0): 全自动环境加固 — claude alias + OAuth 清理 + 冲突修复 2026-04-10 12:08:16 +08:00
bookworm
c4177c21aa fix(installer): 授权码跳过改为强制确认 + 前置提示
问题: 旧版用户重装时, 缓存过期/格式不兼容 → 弹授权码
→ 用户以为已装过点"取消" → 凭证空 → Claude 不能用

修复:
1. 弹授权码前先 MsgBox 解释"需要授权码才能使用"
2. 点"取消安装"时二次确认"没有授权码将无法使用,确定跳过?"
3. 三级自动检测 (User env → DPAPI cache → 旧明文 cache)
   能自动恢复的不弹窗, 都恢复不了才要求输入

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:20:33 +08:00
bookworm
7114f4e7a8 feat: 版本号 v1.6.0, 标题栏/弹窗/EXE 属性统一显示, build.ps1 自动读取 2026-04-10 11:11:13 +08:00
bookworm
d20d9d91a7 feat(installer): 三级凭证自动检测, 重装免输授权码
Phase 4 凭证加载优先级:
1. User 环境变量 (上次安装已永久写入) → 直接加载, 完全跳过授权码
2. Registry DPAPI 缓存 → 加载 + 写 User 环境变量
3. 授权码输入 → 解密 + 写 User 环境变量

解决: 用户重装/升级时不再被要求重新输入授权码,
旧凭证自动检测并沿用. "全自动检测+修复"闭环.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:07:54 +08:00
bookworm
b58538bbae fix: 旧版明文缓存兼容 + 自动迁移 DPAPI + 写 User 环境变量 2026-04-10 11:05:25 +08:00
bookworm
105a328120 fix(installer): 凭证写入用户级环境变量 (永久生效)
根因: auto-setup.ps1 用 DPAPI 缓存凭证, 但 install.ps1 (bat 调用)
未同步更新, 导致桌面快捷方式启动时凭证无法加载.

修复: Phase 4 解密成功后同时写入 Process + User 级环境变量.
用户在任何终端直接输入 claude 即可启动, 无需通过 bat 启动.
移除 "缓存至 23:59" 选项 (不再需要, User 环境变量本身就是持久的).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:54:10 +08:00
bookworm
6275ce55dc fix(installer): bash PATH 自动修复 — Claude Code 核心工具依赖
Git for Windows 默认只把 cmd\ 加入 PATH (含 git.exe),
但 bash.exe 在 bin\ 目录, 导致 Claude Code 的 Bash 工具不可用.

Phase 1 新增: 安装 Git 后自动检测 bash.exe 位置,
将 Git\bin 永久加入用户 PATH. 搜索 4 个常见安装路径.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:33:38 +08:00
bookworm
d87b92319f chore: 清理过期文件 (-2731 行) + AuthGen 历史记录功能
删除:
- 5 个旧版 .bat 安装器 (已被 auto-setup.ps1 EXE 替代)
- 4 个旧版 HTML 指南 (已被 guide-unified.html 替代)
- encrypt-secrets.ps1 (已被 gen-authcode.js 替代)
- lessons-learned.md (开发备忘, 非功能文件)
- 6 个未提交测试 .enc + 备份文件 (本地清理)

新增:
- AuthGen GUI: 生成成功后自动追加 authcode-history.log
- AuthGen GUI: "推送到 Gitea" 按钮 (git add + commit + push 一键完成)
- AuthGen GUI: $args→$nodeArgs (PowerShell 保留变量冲突)
- AuthGen GUI: UTF-8 Process 编码 (修复 PS2EXE 中文乱码)
- .gitignore: +authcode-history.log +Bookworm-AuthGen.exe

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 03:20:39 +08:00
bookworm
c2e3f0e65f fix(admin): AuthGen $y 类型修复 — [int]$y 防止 PS2EXE Object[] 降级
PS2EXE 编译后 $y 被隐式当作 Object[], 导致 $y - 2 算术运算失败:
"Method invocation failed because [System.Object[]] does not contain
a method named 'op_Subtraction'"

修复: 强类型声明 [int]$y = 72

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:15:10 +08:00
bookworm
dba9d19129 fix(admin): AuthGen EXE 在 dist/ 子目录运行时找不到 gen-authcode.js
ScriptDir 指向 EXE 所在目录 (dist/), 但 gen-authcode.js 和
secrets.txt 在父目录。添加向上一级查找的 fallback 逻辑。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 02:10:15 +08:00
bookworm
353bbffada feat(admin): GUI 授权码生成器 Bookworm-AuthGen.exe
新增管理员可视化工具, 替代 CLI gen-authcode.js:
- WinForms GUI: 用户名/Sub-Key/有效期 + 一键生成 + 一键复制
- 品牌视觉: 白底 + #5865F2 蓝紫 + 深色标题栏 + 书虫学者图标
- PS2EXE -NoConsole -NoOutput (92 KB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:59:27 +08:00
bookworm
385d3de57f security(installer): 9 BLOCKER 修复 — 三路审计验收
基于 code-reviewer + red-team-attacker + red-team-logic 三路并行审计:

B1: 日志脱敏 — Run-CmdWithUI 的 Bw-Log 对 URL 做 ://***@ 替换
B2: DPAPI 加密注册表 — Save/Get-CachedSecrets 用 ProtectedData.Protect/Unprotect
B3: credential.helper store → manager (Windows Credential Manager, DPAPI)
B4: 单实例 Mutex — Global\BookwormPortableSetup, 重复启动弹提示退出
B5: URL-encode git 凭证 — EscapeDataString 处理 @/#/% 等特殊字符
B6: 移除 OpenSSL fallback — BWENC1 格式与 openssl enc 不兼容, Node.js 为硬性要求
B7: validAttempts++ 后移 — 文件不存在不消耗尝试次数, 避免用户误锁
B8: ErrorActionPreference try/finally — 防止 uv 安装异常后全局静默吞错
B9: Registry 加载白名单 — CacheAllowedKeys 防止 PATH/COMSPEC 注入
V-04: Get-Random → GetTempFileName (原子创建+加密随机, 防并发碰撞)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:17:23 +08:00
bookworm
8a6611d96e ux(installer): 双专家审计修复 — P0 取消按钮 + P1 表单优化 + 品牌视觉
基于 UX Auditor + CTO Reviewer 两路并行审计报告的交叉修复:

P0 — 进度窗口逃生通道 (UX F1):
- ControlBox=$true, X 按钮触发 "取消安装?" 确认
- $BWInstallDone 标记: Phase 7 完成后 X 不再弹确认

P1 — 授权码 "跳过" 按钮误导 (UX F6):
- "跳过" → "取消安装", 明确传达后果

P1 — Gitea 空密码静默通过 (UX F10):
- 添加 lblError 红字验证: "用户名和密码不能为空"
- btnOK 改为手动 Click 事件验证, 不再自动 DialogResult

P1 — Gitea 对话框缺少上下文 (UX F11):
- 新增说明: "用于下载 Bookworm 配置文件,由管理员提供"

P2 — 内部术语暴露 (UX F13):
- "crypto-helper.js 缺失" → "Bookworm 启动工具包下载失败" + 3 点排查提示

P2 — 品牌视觉统一 (UX Visual):
- 进度窗口: 白底 + Bookworm 蓝紫(#5865F2) Phase 标签 + 右下角计时器
- TopMost=$false (不遮挡其他窗口)
- ProgressBar Style=Continuous (平滑)
- 全局计时器 $BWStartTime + $BWElapsedLabel "Nm Ns" 实时显示

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:03:36 +08:00
bookworm
7d60911f65 perf(installer): 消除全部 UI 冻结 + Write-Host 清零
## P0: 子进程 UI 阻塞 → 非阻塞轮询
- 新增 Wait-ProcessWithUI: 替代所有 WaitForExit(), 每 200ms
  泵 DoEvents(), 进度窗口保持响应, 显示等待计时
- 新增 Run-CmdWithUI: 替代 & cmd 2>&1 | ForEach-Object 模式,
  所有子进程输出走临时文件→日志, 不阻塞 UI

## P1: Phase 3 git clone/pull 无超时 → 带超时 + UI 泵
- 9 处 git 调用全部改为 Run-CmdWithUI (120s/180s 超时)
- 消除 Push-Location/Pop-Location, 改用 git -C <dir>

## P1: Phase 1 winget/npm 阻塞 → 带超时 + UI 泵
- winget install: 5 分钟超时 + 进度状态显示
- npm i -g: 2-3 分钟超时 + 进度状态显示
- Claude Code npm install: 3 分钟超时

## P2: Write-Host 归零
- 27 处非注释 Write-Host 全部替换为 Bw-Log + Update-Progress-SubStatus
- EXE 在 PS2EXE -NoOutput 下不再有任何静默丢失的输出

验证: 7/7 补丁字符串确认编译; Write-Host 仅剩 1 处 (注释内)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:54:28 +08:00
bookworm
1e8a7250c2 fix(build): EXE 内嵌图标改用 bookworm-desktop.ico (B 圆)
旧 bookworm.ico (galaxy 神经螺旋) 在 Explorer 32x32 显示时
中心亮点变成一个像素, 整体看起来像深色空白方块, 用户体感"没图标".

新 bookworm-desktop.ico (蓝紫渐变 B 圆) 在 16/32/48px 都保持
高对比度和强辨识, 与桌面快捷方式图标统一.

build.ps1 优先用 desktop.ico, 不存在时回退 bookworm.ico.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:52:18 +08:00
bookworm
17c600fe93 feat(installer): GUI 进度窗 + uv 三层 fallback + 桌面专用图标 + 静默化
修复 v1.5.1 用户实测发现的两个体验问题:

1. **uv 安装失败 + RemoteException 弹窗** (auto-setup.ps1)
   - 旧逻辑: python -m pip install uv (网络/权限/$ErrorActionPreference=Stop 易触发)
   - 新逻辑: 三层 fallback (winget → Astral 官方脚本 → pip), 全程 SilentlyContinue
   - 失败仅写入 $TEMP/bookworm-uv-install.log, 不阻断不弹窗 (uv 是可选依赖)

2. **PS2EXE -NoConsole 把 Log-X 弹窗化** (build.ps1)
   - 加 -NoOutput + -NoError, 所有 Write-Host 静默吞掉
   - 用户不再被 70+ 个 [!] 弹窗轰炸

3. **静默后无进度反馈 → GUI 进度窗口** (auto-setup.ps1)
   - 新增 Show-ProgressForm/Update-Progress/Update-Progress-SubStatus/Close-ProgressForm
   - 顶部常驻 Form: 标题 + Phase 标签 + 当前状态 + 进度条 + 日志路径
   - 所有 Log-X 改写日志文件 ($TEMP/bookworm-setup-{ts}.log) + 更新进度窗口

4. **桌面专用图标** (auto-setup.ps1 + bookworm-desktop.ico)
   - 从 og-image.png 自动检测蓝紫渐变 B 圆 → 圆形 alpha mask → 7 尺寸 ICO (86 KB)
   - New-DesktopShortcuts 增加 IconLocation, 桌面快捷方式显示 Bookworm 主图标
   - 主图比 favicon 神经螺旋更突出, 48px 也清晰可辨

5. **Phase 7 安装完成 banner**: Write-Host → Show-MsgBox
6. **Claude Code 启动**: 主进程启动 → Start-Process cmd /k claude (新窗口)

构建验证: 7/7 补丁字符串 (Show-ProgressForm/BWLogFile/winget/astral/
bookworm-desktop.ico/IconLocation 等) 经 EXE 字符串扫描确认编译进 build artifact.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:02:53 +08:00
bookworm
3cb6f1dac1 fix(installer): 修复 EXE 安装器关键缺陷 + 多用户加密文件首发
根因 (auto-setup.ps1):
1. $BootUrl 定义但未使用 — bookworm-boot 仓库从未被克隆
2. $ScriptDir 在 PS2EXE 打包后回退到 $PWD (Downloads 目录)
3. Phase 4 在 EXE 同目录找 crypto-helper.js / secrets-*.enc → 必然失败
4. Phase 6 桌面快捷方式同样指向 $ScriptDir → 路径错误

修复:
- $ScriptDir 改用 Process.MainModule + PSScriptRoot 三级回退 (PS2EXE 兼容)
- 新增 $BootDir = $ScriptDir/bookworm-boot
- Phase 3 增加克隆/更新 bookworm-boot 仓库逻辑 (失败 fail-fast)
- Phase 4 crypto-helper.js / secrets-*.enc 路径统一指向 $BootDir
- Phase 6 快捷方式 .bat 路径同样改用 $BootDir

附加:
- 新增多用户加密文件 secrets-dfff6f13.enc (用户: 茶师兄, 30 天有效期)
- .gitignore 新增 users.txt / .tmp-authcodes.json / auto-setup.ps1.bak-* 防泄露
- 删除旧版 secrets.enc (单用户共享模式废弃)

测试:
- gen-authcode.js → encrypt → crypto-helper.js decrypt 闭环验证通过
- PS2EXE build artifact (215 KB) 经字符串扫描确认 5 处补丁已编译

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:28:12 +08:00
bookworm
a2a7ff1edb feat: EXE 图标支持 (bookworm.ico 16/32/48/256px + rcedit 注入) 2026-04-07 01:08:41 +08:00
36 changed files with 5640 additions and 3831 deletions

7
.gitignore vendored
View File

@ -1,3 +1,10 @@
secrets.txt secrets.txt
users.txt
authcode-history.log
auto-setup.ps1.bak-*
.tmp-authcodes.json
.tmp-release.json
.tmp-*.png
Bookworm-AuthGen.exe
管理员SOP.html 管理员SOP.html
dist/ dist/

View File

@ -1,80 +0,0 @@
@echo off
chcp 65001 > nul
title Bookworm Portable - 全自动安装
echo.
echo ====================================================
echo Bookworm Portable - 全自动安装器
echo 双击即可完成全部配置,无需手动操作
echo ====================================================
echo.
:: 检查 auto-setup.ps1 是否在当前目录
if exist "%~dp0auto-setup.ps1" (
cd /d "%~dp0"
goto :RUN_SETUP
)
:: 检查 bookworm-boot 是否已克隆
if exist "%USERPROFILE%\bookworm-boot\auto-setup.ps1" (
echo [OK] 检测到 bookworm-boot 仓库
cd /d "%USERPROFILE%\bookworm-boot"
goto :RUN_SETUP
)
:: 检查 git
where git >nul 2>nul
if %errorlevel% neq 0 (
echo [!!] Git 未安装,请先安装 Git:
echo https://git-scm.com/download/win
echo.
echo 安装 Git 后重新双击本文件即可。
pause
exit /b 1
)
:: bookworm-boot 目录已存在但不完整 — 先删除再克隆
if exist "%USERPROFILE%\bookworm-boot" (
echo [..] 检测到不完整的 bookworm-boot重新下载...
rmdir /s /q "%USERPROFILE%\bookworm-boot" 2>nul
)
echo [..] 首次运行,下载配置文件 (需输入 Gitea 账号密码)...
echo.
git clone https://code.letcareme.com/bookworm/bookworm-boot.git "%USERPROFILE%\bookworm-boot"
if %errorlevel% neq 0 (
echo.
echo [!!] 克隆失败,请检查:
echo 1. 网络是否正常
echo 2. Gitea 账号密码是否正确
echo 3. 能否访问 https://code.letcareme.com
pause
exit /b 1
)
cd /d "%USERPROFILE%\bookworm-boot"
echo [OK] 引导仓库下载完成
echo.
:RUN_SETUP
:: 更新到最新版本
git pull >nul 2>nul
:: 检测 PowerShell 7 (pwsh): 新窗口启动, 退出 cmd
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
echo [OK] 使用 PowerShell 7
start "Bookworm 全自动安装" pwsh -NoLogo -ExecutionPolicy Bypass -File "%CD%\auto-setup.ps1"
exit
)
:: 回退 PowerShell 5.1 (auto-setup 会安装 pwsh7, Phase 7 再切换)
echo [..] PowerShell 7 未安装, 先用 5.1 执行安装 (将自动安装 pwsh7)
powershell -ExecutionPolicy Bypass -File auto-setup.ps1
if %errorlevel% neq 0 (
echo.
echo [!!] 安装过程中出现错误
echo 请截图上方信息联系管理员
echo.
)
pause

View File

@ -1,37 +0,0 @@
@echo off
setlocal
chcp 65001 > nul 2>&1
title Bookworm Smart Assistant - 全自动安装 v3.0
:: 极简入口: 只负责确保 Node.js 存在, 核心逻辑全在 setup-all.js 中
:: 规则: 不超过 30 行, 不用 if(), 不嵌 PowerShell, 用 %~s 短路径
net session >nul 2>&1
if %errorlevel% equ 0 goto :HAS_ADMIN
echo Set s = CreateObject("Shell.Application") > "%TEMP%\bw_uac.vbs"
echo s.ShellExecute "cmd.exe", "/k cd /d ""%~sdp0"" ^& ""%~snx0""", "", "runas", 1 >> "%TEMP%\bw_uac.vbs"
cscript //nologo "%TEMP%\bw_uac.vbs"
del /f /q "%TEMP%\bw_uac.vbs" 2>nul
exit /b
:HAS_ADMIN
where node >nul 2>nul
if %errorlevel% equ 0 goto :HAS_NODE
echo [..] Node.js 未安装, 正在通过 winget 安装...
winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements --silent 2>nul
for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path 2^>nul') do set "PATH=%%b"
set "PATH=%PATH%;C:\Program Files\nodejs"
where node >nul 2>nul
if %errorlevel% equ 0 goto :HAS_NODE
echo [!!] Node.js 安装失败。请手动下载: https://nodejs.org/
goto :END
:HAS_NODE
echo [OK] Node.js 就绪, 启动安装引擎...
node "%~dp0setup-all.js" %* || echo [!] 安装过程出错, 请查看上方日志
:END
echo.
echo 按任意键关闭...
pause > nul
endlocal

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ============================================================ # ============================================================
# Bookworm Smart Assistant - macOS 全自动安装 v2.0 # Bookworm Smart Assistant - macOS 全自动安装 v2.3.1
# #
# 用法 (任选一种): # 用法 (任选一种):
# 方式1: 下载后运行 # 方式1: 下载后运行
@ -39,7 +39,7 @@ echo " | _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__| '\`_ \` _ \\"
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |" echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|" echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
echo "" echo ""
echo -e " ${BOLD}全自动安装 v2.0 — macOS${NC}" echo -e " ${BOLD}全自动安装 v2.3.1 — macOS${NC}"
echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}" echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}"
echo -e "${NC}" echo -e "${NC}"

View File

@ -1,266 +0,0 @@
@echo off
chcp 65001 > nul 2>&1
title Bookworm Smart Assistant - 全自动安装 (Win10 兼容)
:: ─── 自动提升管理员权限 ──
:: 用 goto 而非 if() 避免文件名含括号(如"(2)")导致解析崩溃
net session >nul 2>&1
if %errorlevel% equ 0 goto :IS_ADMIN
echo 需要管理员权限来安装软件,正在请求...
echo Set objShell = CreateObject("Shell.Application") > "%TEMP%\bw_elevate.vbs"
echo objShell.ShellExecute "cmd.exe", "/k cd /d ""%~dp0"" & ""%~nx0""", "", "runas", 1 >> "%TEMP%\bw_elevate.vbs"
cscript //nologo "%TEMP%\bw_elevate.vbs"
del /f /q "%TEMP%\bw_elevate.vbs" 2>nul
exit /b
:IS_ADMIN
:: ─── 初始化 ─────────────────────────────────────────
setlocal EnableDelayedExpansion
color 1F
set "NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
set "no_proxy=%NO_PROXY%"
set "INSTALL_DIR=%USERPROFILE%\bookworm-boot"
set "GITEA_URL=https://code.letcareme.com/bookworm/bookworm-boot.git"
set "TEMP_DL=%TEMP%\bookworm-setup"
set "ERRORS=0"
set "NEED_PATH_REFRESH=0"
echo.
echo +============================================================+
echo ^| ^|
echo ^| Bookworm Smart Assistant ^|
echo ^| 全自动安装 v2.0 (Windows 10 兼容版) ^|
echo ^| ^|
echo ^| 兼容 Windows 10 1809+ / Windows 11 ^|
echo ^| 无需 winget, 通过直接下载安装包实现全自动 ^|
echo ^| ^|
echo +============================================================+
echo.
if not exist "%TEMP_DL%" mkdir "%TEMP_DL%"
:: ─── 检测安装方式: winget 优先, 回退直接下载 ─────────
set "HAS_WINGET=0"
where winget >nul 2>nul
if %errorlevel% equ 0 set "HAS_WINGET=1"
if "%HAS_WINGET%"=="1" (
echo [OK] 检测到 winget, 使用 winget 安装
) else (
echo [i] 未检测到 winget, 使用直接下载方式安装
)
echo.
:: ─── 步骤 1/7: 安装 Git ────────────────────────────
echo [1/7] 检查 Git...
where git >nul 2>nul
if %errorlevel% neq 0 (
if "%HAS_WINGET%"=="1" (
echo [..] 通过 winget 安装 Git...
winget install Git.Git --accept-source-agreements --accept-package-agreements --silent
) else (
echo [..] 下载 Git 安装包...
powershell -Command "& {[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri 'https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1.2-64-bit.exe' -OutFile '%TEMP_DL%\git-install.exe'}"
if exist "%TEMP_DL%\git-install.exe" (
echo [..] 静默安装 Git...
"%TEMP_DL%\git-install.exe" /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh"
if !errorlevel! neq 0 (
echo [!!] Git 安装失败 ^(exit: !errorlevel!^)
set /a ERRORS+=1
) else (
echo [OK] Git 安装完成
)
) else (
echo [!!] Git 下载失败, 请手动安装: https://git-scm.com
set /a ERRORS+=1
)
)
set "NEED_PATH_REFRESH=1"
) else (
echo [OK] Git 已安装
)
echo.
:: ─── 步骤 2/7: 安装 Node.js ────────────────────────
echo [2/7] 检查 Node.js...
where node >nul 2>nul
if %errorlevel% neq 0 (
if "%HAS_WINGET%"=="1" (
echo [..] 通过 winget 安装 Node.js LTS...
winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements --silent
) else (
echo [..] 下载 Node.js LTS 安装包...
powershell -Command "& {[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri 'https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi' -OutFile '%TEMP_DL%\node-install.msi'}"
if exist "%TEMP_DL%\node-install.msi" (
echo [..] 静默安装 Node.js...
msiexec /i "%TEMP_DL%\node-install.msi" /qn /norestart
echo [OK] Node.js 安装完成
) else (
echo [!!] Node.js 下载失败, 请手动安装: https://nodejs.org
set /a ERRORS+=1
)
)
set "NEED_PATH_REFRESH=1"
) else (
echo [OK] Node.js 已安装
)
echo.
:: ─── 刷新 PATH ──────────────────────────────────────
if "%NEED_PATH_REFRESH%"=="1" (
echo [..] 刷新系统 PATH...
for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path 2^>nul') do set "SYS_PATH=%%b"
for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v Path 2^>nul') do set "USR_PATH=%%b"
set "PATH=!SYS_PATH!;!USR_PATH!"
set "PATH=!PATH!;C:\Program Files\nodejs;C:\Program Files\Git\cmd;C:\Program Files\Git\usr\bin"
set "PATH=!PATH!;%APPDATA%\npm"
echo [OK] PATH 已刷新
echo.
)
:: ─── 二次验证 ────────────────────────────────────────
where git >nul 2>nul
if %errorlevel% neq 0 (
echo [!!] Git 仍不可用 — 可能需要重启电脑后重新运行本程序
pause
exit /b 1
)
where node >nul 2>nul
if %errorlevel% neq 0 (
echo [!!] Node.js 仍不可用 — 可能需要重启电脑后重新运行本程序
pause
exit /b 1
)
:: ─── 步骤 3/7: 安装 Claude Code ─────────────────────
echo [3/7] 检查 Claude Code...
:: 国内 npm 镜像 - 淘宝源, 避免 npmjs.org 超时
call npm config set registry https://registry.npmmirror.com 2>nul
where claude >nul 2>nul
if %errorlevel% neq 0 (
echo [..] 通过 npm 安装 Claude Code - 淘宝镜像加速...
call npm i -g @anthropic-ai/claude-code 2>&1
if !errorlevel! neq 0 (
echo [!!] Claude Code 安装失败
set /a ERRORS+=1
) else (
echo [OK] Claude Code 安装成功
)
) else (
echo [OK] Claude Code 已安装
)
echo.
:: ─── 步骤 4/7: 代理检测 ──────────────────
echo [4/7] 检测网络代理...
:: 纯 batch 实现, 不依赖 PowerShell, 不含括号
set "PROXY_FOUND="
netstat -an 2>nul | findstr "LISTENING" | findstr ":7890 :7893 :7891 :10792 :1080 :8118" >nul 2>nul
if !errorlevel! equ 0 set "PROXY_FOUND=1"
if defined PROXY_FOUND echo [OK] 检测到本地代理端口
if not defined PROXY_FOUND echo [!] 未检测到代理, Claude Code 在国内可能无法启动
if not defined PROXY_FOUND echo 请确保代理软件已启动
echo.
:: ─── 步骤 5/7: 克隆/更新 Bookworm ──────────────────
echo [5/7] 同步 Bookworm 配置...
git config --global credential.helper manager 2>nul
if exist "%INSTALL_DIR%\.git" (
echo 已有安装, 更新到最新版...
pushd "%INSTALL_DIR%"
git pull 2>&1
popd
) else (
if exist "%INSTALL_DIR%" rmdir /s /q "%INSTALL_DIR%" 2>nul
echo 首次下载 - 需输入 Gitea 用户名密码...
git clone "%GITEA_URL%" "%INSTALL_DIR%" 2>&1
if !errorlevel! neq 0 (
echo [!!] 下载失败, 请检查网络和 Gitea 凭证
pause
exit /b 1
)
)
echo [OK] Bookworm 文件已就绪
echo.
:: ─── 步骤 6/7: 执行安装配置 ────────────────────────
echo [6/7] 执行安装配置...
echo.
echo 首次安装需要输入主密码来解密 API 凭证
echo - 主密码由管理员提供, 区分大小写
echo.
where pwsh >nul 2>nul
if !errorlevel! equ 0 (
pwsh -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1"
) else (
powershell -NoLogo -ExecutionPolicy Bypass -File "%INSTALL_DIR%\install.ps1"
)
echo.
:: ─── 步骤 7/7: 桌面快捷方式 + 完成 ─────────────────
echo [7/7] 创建桌面快捷方式...
:: 用 VBScript 创建快捷方式, 避免 PowerShell 花括号被 cmd 截获
echo Set ws = CreateObject("WScript.Shell") > "%TEMP%\bw_shortcut.vbs"
echo Set sc = ws.CreateShortcut(ws.SpecialFolders("Desktop") ^& "\Bookworm.lnk") >> "%TEMP%\bw_shortcut.vbs"
echo sc.TargetPath = "%INSTALL_DIR%\启动Bookworm.bat" >> "%TEMP%\bw_shortcut.vbs"
echo sc.WorkingDirectory = "%INSTALL_DIR%" >> "%TEMP%\bw_shortcut.vbs"
echo sc.Description = "Bookworm Smart Assistant" >> "%TEMP%\bw_shortcut.vbs"
echo sc.Save >> "%TEMP%\bw_shortcut.vbs"
echo Set sc = ws.CreateShortcut(ws.SpecialFolders("Desktop") ^& "\更新Bookworm.lnk") >> "%TEMP%\bw_shortcut.vbs"
echo sc.TargetPath = "%INSTALL_DIR%\更新并启动Bookworm.bat" >> "%TEMP%\bw_shortcut.vbs"
echo sc.WorkingDirectory = "%INSTALL_DIR%" >> "%TEMP%\bw_shortcut.vbs"
echo sc.Save >> "%TEMP%\bw_shortcut.vbs"
cscript //nologo "%TEMP%\bw_shortcut.vbs" 2>nul
if !errorlevel! equ 0 echo [OK] 桌面快捷方式已创建
del /f /q "%TEMP%\bw_shortcut.vbs" 2>nul
if exist "%INSTALL_DIR%\guide.html" start "" "%INSTALL_DIR%\guide.html"
:: 清理临时文件
if exist "%TEMP_DL%" rmdir /s /q "%TEMP_DL%" 2>nul
echo.
echo +============================================================+
echo ^| ^|
echo ^| 安装完成! ^|
echo ^| ^|
echo ^| 已安装: ^|
echo ^| [v] Node.js LTS [v] Git ^|
echo ^| [v] Claude Code [v] Bookworm - 92 Skills ^|
echo ^| ^|
echo ^| 桌面快捷方式: ^|
echo ^| Bookworm — 日常启动 ^|
echo ^| 更新Bookworm — 同步最新版后启动 ^|
echo ^| ^|
echo +============================================================+
echo.
if %ERRORS% gtr 0 (
echo [注意] 安装过程中有 %ERRORS% 个警告, 请查看上方日志
echo.
)
echo 按任意键启动 Bookworm...
pause > nul
cd /d "%INSTALL_DIR%"
if exist "启动Bookworm.bat" (
call "启动Bookworm.bat"
) else (
where pwsh >nul 2>nul
if !errorlevel! equ 0 (
pwsh -NoLogo -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
) else (
powershell -NoLogo -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
)
)
endlocal
:: ─── 兜底: 任何情况下窗口不自动关闭 ──
echo.
echo 如看到此消息说明流程已结束,按任意键关闭窗口...
pause > nul

View File

@ -1,163 +0,0 @@
@echo off
chcp 65001 > nul
title Bookworm Smart Assistant - 一键安装
color 1F
:: 中转站在国内,不走代理
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
echo.
echo +================================================+
echo ^| ^|
echo ^| Bookworm Smart Assistant ^|
echo ^| 一键安装程序 v1.5 ^|
echo ^| ^|
echo ^| 92 Skills / 18 Agents / 34 Hooks ^|
echo ^| ^|
echo +================================================+
echo.
:: ─── 检查依赖 ───────────────────────────────────────
echo [1/4] 检查环境...
echo.
where git >nul 2>nul
if %errorlevel% neq 0 (
echo [!!] Git 未安装
echo.
echo 请先安装 Git:
echo 下载: https://git-scm.com/download/win
echo 安装后重新运行本程序
echo.
pause
exit /b 1
)
echo [OK] Git
where node >nul 2>nul
if %errorlevel% neq 0 (
echo [!!] Node.js 未安装
echo.
echo 请先安装 Node.js:
echo 下载: https://nodejs.org (选 LTS 版本)
echo 安装后重新打开本程序
echo.
pause
exit /b 1
)
echo [OK] Node.js
where claude >nul 2>nul
if %errorlevel% neq 0 (
echo [..] Claude Code 未安装,正在安装...
call npm i -g @anthropic-ai/claude-code
if %errorlevel% neq 0 (
echo [!!] Claude Code 安装失败
echo 手动安装: npm i -g @anthropic-ai/claude-code
pause
exit /b 1
)
)
echo [OK] Claude Code
echo.
:: ─── 选择安装目录 ───────────────────────────────────
set "INSTALL_DIR=%USERPROFILE%\bookworm-boot"
:: ─── 克隆/更新仓库 ──────────────────────────────────
echo [2/4] 下载 Bookworm...
echo.
if exist "%INSTALL_DIR%\.git" (
echo 已安装,更新到最新版...
cd /d "%INSTALL_DIR%"
git pull 2>&1
) else (
if exist "%INSTALL_DIR%" (
echo 目录已存在但非 git 仓库,清理后重新下载...
rmdir /s /q "%INSTALL_DIR%" 2>nul
)
echo 首次下载...
git config --global credential.helper store
git clone https://code.letcareme.com/bookworm/bookworm-boot.git "%INSTALL_DIR%" 2>&1
if %errorlevel% neq 0 (
echo.
echo [!!] 下载失败
echo 请检查:
echo - 网络是否正常
echo - 是否能访问 https://code.letcareme.com
echo - Gitea 用户名密码是否正确
echo.
pause
exit /b 1
)
)
echo.
echo [OK] Bookworm 文件已就绪
echo.
:: ─── 创建桌面快捷方式 ───────────────────────────────
echo [3/4] 创建桌面快捷方式...
echo.
:: 用 PowerShell 创建 .lnk
powershell -ExecutionPolicy Bypass -Command ^
"$s=(New-Object -COM WScript.Shell).CreateShortcut('%USERPROFILE%\Desktop\Bookworm.lnk');^
$s.TargetPath='%INSTALL_DIR%\启动Bookworm.bat';^
$s.WorkingDirectory='%INSTALL_DIR%';^
$s.Description='Bookworm Smart Assistant';^
$s.Save()" 2>nul
if %errorlevel% equ 0 (
echo [OK] 桌面快捷方式: Bookworm
) else (
echo [!] 快捷方式创建失败 (不影响使用)
)
powershell -ExecutionPolicy Bypass -Command ^
"$s=(New-Object -COM WScript.Shell).CreateShortcut('%USERPROFILE%\Desktop\更新Bookworm.lnk');^
$s.TargetPath='%INSTALL_DIR%\更新并启动Bookworm.bat';^
$s.WorkingDirectory='%INSTALL_DIR%';^
$s.Description='Bookworm 更新并启动';^
$s.Save()" 2>nul
echo.
:: ─── 启动 Bookworm ──────────────────────────────────
echo [4/4] 启动 Bookworm...
echo.
echo +================================================+
echo ^| ^|
echo ^| 安装完成! ^|
echo ^| ^|
echo ^| 桌面已创建快捷方式: ^|
echo ^| Bookworm - 日常启动 ^|
echo ^| 更新Bookworm - 同步后启动 ^|
echo ^| ^|
echo ^| 接下来会启动 Bookworm ^|
echo ^| 请输入管理员提供的主密码 ^|
echo ^| ^|
echo +================================================+
echo.
cd /d "%INSTALL_DIR%"
:: 打开使用教程
if exist "%INSTALL_DIR%\guide.html" (
start "" "%INSTALL_DIR%\guide.html"
)
:: 启动安装脚本
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
pwsh -ExecutionPolicy Bypass -File install.ps1 -AutoAccept
) else (
powershell -ExecutionPolicy Bypass -File install.ps1 -AutoAccept
)
if %errorlevel% neq 0 (
echo.
echo 启动失败,请查看上方错误信息
pause
)

BIN
Bookworm-Setup.exe Normal file

Binary file not shown.

View File

@ -1,12 +1,16 @@
#!/bin/bash #!/bin/bash
# ============================================================ # ============================================================
# Bookworm Portable - macOS Setup (从 boot 仓库内运行) # Bookworm Portable - macOS Setup (从 boot 仓库内运行)
# Version: 1.5 # Version: 3.0.11
# #
# 用法: cd ~/bookworm-boot && bash Bookworm-Setup.sh # 用法: cd ~/bookworm-boot && bash Bookworm-Setup.sh
# #
# 前提: 已 git clone bookworm-boot 到本地 # 前提: 已 git clone bookworm-boot 到本地
# 功能: 检查依赖 → 代理检测 → 克隆配置 → 解密凭证 → 配置别名 # 功能: 检查依赖 → 代理检测 → 克隆配置 → 解密凭证 → 配置别名
#
# v3.0.11 注: macOS 不受 Windows wt+Base64 启动链路问题影响
# (Mac shell 无 wt, claude 启动经标准 PATH 即可),
# 此版本仅同步主版本号. 核心逻辑保持 v3.0.3 流程稳定.
# ============================================================ # ============================================================
set -e set -e
@ -35,7 +39,7 @@ banner() {
echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |" echo " | |_) | (_) | (_) | < \\ V V / (_) | | | | | | | |"
echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|" echo " |____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_| |_| |_| |_|"
echo "" echo ""
echo -e " ${BOLD}Portable macOS Setup v1.5${NC}" echo -e " ${BOLD}Portable macOS Setup v2.3.1${NC}"
echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}" echo -e " ${BLUE}92 Skills | 18 Agents | 34 Hooks${NC}"
echo -e "${NC}" echo -e "${NC}"
} }
@ -177,9 +181,18 @@ git config --global credential.helper osxkeychain 2>/dev/null || true
if [ -d "$CLAUDE_DIR/.git" ]; then if [ -d "$CLAUDE_DIR/.git" ]; then
info "配置仓库已存在, 更新..." info "配置仓库已存在, 更新..."
cd "$CLAUDE_DIR" && git pull --ff-only 2>/dev/null || git pull 2>/dev/null || true cd "$CLAUDE_DIR"
cd "$BOOT_DIR" # 设置 git 身份 (auto-resolve 需要)
git config user.email "bookworm@auto.local" 2>/dev/null
git config user.name "Bookworm" 2>/dev/null
# 清除冲突状态 (运行时文件不重要, 后续会重新渲染)
git reset --hard HEAD 2>/dev/null || true
if git pull --rebase --autostash 2>/dev/null; then
success "配置仓库已更新" success "配置仓库已更新"
else
warn "git pull 失败, 使用本地版本"
fi
cd "$BOOT_DIR"
elif [ -f "$CLAUDE_DIR/CLAUDE.md" ]; then elif [ -f "$CLAUDE_DIR/CLAUDE.md" ]; then
warn "~/.claude 已存在但非 git 仓库, 备份后克隆..." warn "~/.claude 已存在但非 git 仓库, 备份后克隆..."
mv "$CLAUDE_DIR" "$CLAUDE_DIR.bak.$(date +%s)" mv "$CLAUDE_DIR" "$CLAUDE_DIR.bak.$(date +%s)"
@ -202,6 +215,25 @@ done
# ============================================================ # ============================================================
step 4 "解密凭证" step 4 "解密凭证"
# ─── v3.0.1: $BW_LICENSE_KEY 静默激活 (零输入路径) ───
# 若 install.sh 通过 env 传入 License Key (BW-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX), 优先走这条
# activate.js 已支持 HTTPS_PROXY 的 HTTP CONNECT 隧道 (Gitea ce354ca)
ACTIVATE_JS="$CLAUDE_DIR/lib/activate.js"
BW_TOKEN_FILE="$HOME/.claude/.bw-token"
if [ -n "$BW_LICENSE_KEY" ] && [[ "$BW_LICENSE_KEY" =~ ^BW-[A-F0-9]{4}(-[A-F0-9]{4}){5}$ ]] && [ -f "$ACTIVATE_JS" ] && command -v node &>/dev/null; then
info "检测到 \$BW_LICENSE_KEY, 静默激活..."
if printf '%s' "$BW_LICENSE_KEY" | node "$ACTIVATE_JS" 2>&1 | tail -3 | grep -q "OK\|激活成功"; then
if [ -f "$BW_TOKEN_FILE" ]; then
success "License 静默激活成功"
else
warn "activate.js 返回 OK 但 .bw-token 未生成, 回退到交互模式"
fi
else
warn "静默激活失败, 回退到交互模式 (中转站 sk-Key 流程)"
fi
unset BW_LICENSE_KEY # 清掉, 不在子进程泄露
fi
# Keychain 缓存相关 # Keychain 缓存相关
KEYCHAIN_SERVICE="bookworm-secrets" KEYCHAIN_SERVICE="bookworm-secrets"
KEYCHAIN_ACCOUNT="$(whoami)" KEYCHAIN_ACCOUNT="$(whoami)"
@ -312,7 +344,122 @@ parse_authcode() {
# 先尝试缓存 # 先尝试缓存
if load_cached_secrets 2>/dev/null; then if load_cached_secrets 2>/dev/null; then
: # 缓存加载成功 : # 缓存加载成功
elif [ -f "$SECRETS_ENC" ] || ls "$BOOT_DIR"/secrets-*.enc 2>/dev/null | head -1 | grep -q .; then else
# 优先级 3: 调用 change-key.js 验证+持久化 (stdin 管道, 无 argv 泄露)
CHANGE_KEY_JS="$CLAUDE_DIR/change-key.js"
if [ -f "$CHANGE_KEY_JS" ] && command -v node &>/dev/null; then
echo ""
info "配置中转站凭证 (https://bww.letcareme.com)"
for attempt in 1 2 3; do
echo ""
read -rs -p " 粘贴凭证 (第 $attempt/3 次, 输入不显示, 留空跳过): " UCRED
echo ""
[ -z "$UCRED" ] && { warn "已跳过"; break; }
if printf '%s' "$UCRED" | node "$CHANGE_KEY_JS"; then
UCRED=""
success "凭证已验证并持久化"
echo ""
info "换凭证方式:"
info " 1. 重跑安装器"
info " 2. bash ~/.claude/change-key.sh"
info " 3. Claude Code 里: /change-key"
break
else
UCRED=""
[ $attempt -lt 3 ] && warn "验证失败, 剩余 $((3-attempt))" || fail "3 次失败"
fi
done
fi
fi
# 优先级 3.5: v3.0.1 新增 — 直接输入 sk- Key (中转站 Key) + 5 模型候选验证
# 适用: fresh install 没 change-key.js, 没 .enc 文件的新用户 (BYOK)
if [ -z "$ANTHROPIC_API_KEY" ]; then
# 测 sk- Key 是否可调通 (5 模型候选, 中转站白名单)
validate_sk_key() {
local key="$1"
local baseurl="${ANTHROPIC_BASE_URL:-https://bww.letcareme.com}"
local models=("claude-opus-4-7" "claude-opus-4-6" "claude-opus-4-6-thinking" "claude-sonnet-4-6" "claude-sonnet-4-6-thinking")
for model in "${models[@]}"; do
local code
code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 15 --noproxy '*' \
-X POST "$baseurl/v1/messages" \
-H "x-api-key: $key" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$model\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" 2>/dev/null)
# 401/403 认证失败, 立即退, 不继续试
[[ "$code" == "401" || "$code" == "403" ]] && { echo "AUTH_FAIL"; return 1; }
# 200 或 400 都说明 Key 通过, 400 只是请求体问题
[[ "$code" == "200" || "$code" == "400" ]] && { echo "OK"; return 0; }
# 503/404 继续试下个模型
done
echo "NO_CHANNEL" # 全部 503 = 中转站无渠道
return 1
}
echo ""
info "配置中转站 API Key (没有的话去 bww.letcareme.com 注册+充值)"
for attempt in 1 2 3; do
echo ""
read -rs -p " 粘贴 sk- Key (第 $attempt/3 次, 输入不显示, 留空跳过): " SK_KEY
echo ""
[ -z "$SK_KEY" ] && { warn "已跳过"; break; }
# 基础格式校验
if [[ ! "$SK_KEY" =~ ^sk- ]] || [ ${#SK_KEY} -lt 20 ]; then
warn "格式错误 (应 sk- 开头, 至少 20 字符), 请重试"
continue
fi
info "验证中 (试 5 个模型候选)..."
result=$(validate_sk_key "$SK_KEY")
case "$result" in
OK)
success "sk- Key 验证成功"
# v3.0.1: chmod 600 防同机其它 uid 读取 + 清 .bak 残留 (red-team-attacker P0)
for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do
[ -f "$rc" ] || touch "$rc"
# BSD sed (macOS 默认): -i '' 无 .bak; GNU sed (Linux): -i 无 .bak
if sed --version 2>/dev/null | grep -q GNU; then
sed -i '/^export ANTHROPIC_API_KEY=/d' "$rc" 2>/dev/null || true
sed -i '/^export ANTHROPIC_BASE_URL=/d' "$rc" 2>/dev/null || true
else
sed -i '' '/^export ANTHROPIC_API_KEY=/d' "$rc" 2>/dev/null || true
sed -i '' '/^export ANTHROPIC_BASE_URL=/d' "$rc" 2>/dev/null || true
fi
echo "export ANTHROPIC_API_KEY=\"$SK_KEY\"" >> "$rc"
echo "export ANTHROPIC_BASE_URL=\"https://bww.letcareme.com\"" >> "$rc"
chmod 600 "$rc" # 只 owner 可读, 防同机 uid 泄露
done
# 扫残留 .bak 副本 (可能含旧 Key)
rm -f "$HOME/.zshrc.bak" "$HOME/.bashrc.bak" 2>/dev/null || true
export ANTHROPIC_API_KEY="$SK_KEY"
export ANTHROPIC_BASE_URL="https://bww.letcareme.com"
# 存 Keychain 本日免密
security add-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w "ANTHROPIC_API_KEY=$SK_KEY
ANTHROPIC_BASE_URL=https://bww.letcareme.com
EXPIRY=$(date -v+1d -u +%FT%TZ 2>/dev/null || date -u -d '+1 day' +%FT%TZ)" -U 2>/dev/null || true
SK_KEY=""
break
;;
AUTH_FAIL)
warn "Key 无效或余额为 0 (中转站返回 401/403)"
SK_KEY=""
[ $attempt -lt 3 ] && continue || { fail "3 次失败, 跳过 sk- 配置"; break; }
;;
NO_CHANNEL)
fail "中转站没有可用 Claude 渠道 (5 模型全返 503). 联系中转站客服"
SK_KEY=""
break
;;
*)
warn "验证异常, 剩余 $((3-attempt))"
SK_KEY=""
;;
esac
done
fi
# 优先级 4: 授权码模式 (向后兼容旧用户)
if [ -z "$ANTHROPIC_API_KEY" ] && { [ -f "$SECRETS_ENC" ] || ls "$BOOT_DIR"/secrets-*.enc 2>/dev/null | head -1 | grep -q .; }; then
DECRYPTED="" DECRYPTED=""
valid_attempts=0 valid_attempts=0
total_attempts=0 total_attempts=0
@ -341,24 +488,27 @@ elif [ -f "$SECRETS_ENC" ] || ls "$BOOT_DIR"/secrets-*.enc 2>/dev/null | head -1
DECRYPTED=$(_decrypt_secrets "$TOKEN" "$ENC_FILE") || true DECRYPTED=$(_decrypt_secrets "$TOKEN" "$ENC_FILE") || true
TOKEN="" TOKEN=""
if [ -n "$DECRYPTED" ]; then if [ -n "$DECRYPTED" ]; then
# 白名单校验 (与 Windows 版对齐)
ALLOWED_KEYS="ANTHROPIC_API_KEY ANTHROPIC_BASE_URL GITHUB_PERSONAL_ACCESS_TOKEN SLACK_BOT_TOKEN ATLASSIAN_API_TOKEN BROWSERBASE_API_KEY FIRECRAWL_API_KEY GEMINI_API_KEY"
while IFS= read -r line; do while IFS= read -r line; do
[ -z "$line" ] && continue [ -z "$line" ] && continue
key="${line%%=*}" key="${line%%=*}"
value="${line#*=}" value="${line#*=}"
key=$(echo "$key" | tr -d ' ') key=$(echo "$key" | tr -d ' ')
if [ -n "$key" ] && [ -n "$value" ]; then if [ -n "$key" ] && [ -n "$value" ]; then
# 白名单 + 长度校验
if echo "$ALLOWED_KEYS" | grep -qw "$key" && [ ${#value} -lt 512 ]; then
export "$key=$value" export "$key=$value"
success "已注入: $key" success "已注入: $key"
else
warn "跳过未知 key: $key"
fi
fi fi
done <<< "$DECRYPTED" done <<< "$DECRYPTED"
DECRYPTED="" DECRYPTED=""
# 询问是否缓存 # 自动缓存 (不再询问, 与 Windows 版对齐)
echo ""
read -p " 今日内免密启动? (y/n): " CACHE_CHOICE
if [ "$CACHE_CHOICE" = "y" ] || [ "$CACHE_CHOICE" = "Y" ]; then
save_secrets_to_cache save_secrets_to_cache
fi
break break
else else
if [ $valid_attempts -lt 3 ]; then if [ $valid_attempts -lt 3 ]; then
@ -376,6 +526,83 @@ else
fi fi
fi fi
# ─── v3.0.2: 预填 ~/.claude.json 跳过 Claude Code 2.0.1 的登录选择页 ───
# Win auto-setup.ps1:1361-1369 等价逻辑: 两选项都走 anthropic.com OAuth, 国内不通
# 预填 hasCompletedOnboarding + customApiKeyResponses.approved 直接进主界面
if [ -n "$ANTHROPIC_API_KEY" ] && command -v node &>/dev/null; then
BW_KEY_PREFIX="${ANTHROPIC_API_KEY:0:20}" node -e '
const fs = require("fs"), p = require("path");
const H = process.env.HOME || process.env.USERPROFILE;
const f = p.join(H, ".claude.json");
const prefix = process.env.BW_KEY_PREFIX || "";
let d = {};
try { d = JSON.parse(fs.readFileSync(f, "utf8")); } catch (e) {}
d.hasCompletedOnboarding = true;
d.hasSeenWelcome = true;
d.bypassPermissionsModeAccepted = true;
d.customApiKeyResponses = d.customApiKeyResponses || { approved: [], rejected: [] };
if (prefix && !d.customApiKeyResponses.approved.includes(prefix)) {
d.customApiKeyResponses.approved.push(prefix);
}
if (d.numStartups === undefined) d.numStartups = 5;
if (!d.projects) d.projects = {};
fs.writeFileSync(f, JSON.stringify(d, null, 2));
' 2>/dev/null && success "Claude Code onboarding 已预填 (跳过 2.0.1 登录选择页)" || warn ".claude.json onboarding 预填失败 (首次启动可能需手工过登录画面)"
fi
# ── MCP 注入到 ~/.claude.json (Claude Code v2.1+ 正确位置) ──
INJECT_SCRIPT="$CLAUDE_DIR/inject-mcp.js"
MCP_INJECTED=false
# 方案 A: 调用 config 仓库里的 inject-mcp.js
if [ -f "$INJECT_SCRIPT" ] && command -v node &>/dev/null; then
MCP_OUT=$(node "$INJECT_SCRIPT" 2>&1) && {
success "$MCP_OUT"
MCP_INJECTED=true
} || warn "inject-mcp.js 执行失败"
fi
# 方案 B: 内嵌 fallback (git pull 失败时)
if [ "$MCP_INJECTED" = false ] && command -v node &>/dev/null; then
info "inject-mcp.js 不可用, 使用内嵌 MCP 注入..."
FALLBACK_JS=$(mktemp /tmp/bw-mcp-XXXXXX.js)
cat > "$FALLBACK_JS" << 'MCPEOF'
var fs=require("fs"),p=require("path");
var H=process.env.HOME||process.env.USERPROFILE;
var f=p.join(H,".claude.json");
var d={};try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch(e){}
var N="npx",Y="--yes",S={};
S.context7={command:N,args:[Y,"@upstash/context7-mcp@2.1.1"],type:"stdio"};
S.playwright={command:N,args:[Y,"@playwright/mcp@0.0.68","--headless"],type:"stdio"};
S["session-continuity"]={command:N,args:[Y,"claude-session-continuity-mcp@1.13.0"],type:"stdio"};
S["browser-mcp"]={command:N,args:[Y,"@browsermcp/mcp@latest"],type:"stdio"};
S["desktop-commander"]={command:N,args:[Y,"@wonderwhy-er/desktop-commander@latest"],type:"stdio"};
S["chrome-devtools"]={command:N,args:[Y,"chrome-devtools-mcp@0.18.1"],type:"stdio"};
S.github={command:N,args:[Y,"@modelcontextprotocol/server-github"],type:"stdio"};
S.slack={command:N,args:[Y,"@modelcontextprotocol/server-slack"],type:"stdio"};
S.firecrawl={command:N,args:[Y,"firecrawl-mcp"],type:"stdio"};
S["mcp-image"]={command:N,args:[Y,"mcp-image"],type:"stdio"};
S["google-drive"]={command:N,args:[Y,"@piotr-agier/google-drive-mcp"],type:"stdio"};
S.browserbase={command:N,args:[Y,"@anthropic-ai/browserbase-mcp"],type:"stdio"};
S.notebooklm={command:N,args:[Y,"notebooklm-mcp@latest"],type:"stdio"};
S.cloudflare={command:N,args:[Y,"mcp-remote","https://docs.mcp.cloudflare.com/sse"],type:"stdio"};
S.mobile={command:N,args:[Y,"@mobilenext/mobile-mcp@0.0.35"],type:"stdio"};
var K="@modelcontextprotocol/server-sequential-thinking";
S["sequential-thinking"]={command:N,args:[Y,K+"@2025.12.18"],type:"stdio"};
S.linear={type:"http",url:"https://mcp.linear.app/mcp"};
S.supabase={type:"http",url:"https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo"};
S.figma={type:"http",url:"https://mcp.figma.com/mcp"};
S["windows-mcp"]={command:"uvx",args:["--python","3.13","windows-mcp"],type:"stdio"};
S.atlassian={command:"uvx",args:["mcp-atlassian"],type:"stdio"};
S["computer-control-mcp"]={command:"uvx",args:["computer-control-mcp@latest"],type:"stdio"};
d.mcpServers=S;
fs.writeFileSync(f,JSON.stringify(d,null,2));
console.log("OK: "+Object.keys(S).length+" MCP servers (fallback)");
MCPEOF
MCP_OUT=$(node "$FALLBACK_JS" 2>&1) && success "$MCP_OUT" || warn "MCP fallback 注入失败"
rm -f "$FALLBACK_JS"
fi
# 渲染 settings.json (替换占位符) # 渲染 settings.json (替换占位符)
TEMPLATE_FILE="$CLAUDE_DIR/settings.template.json" TEMPLATE_FILE="$CLAUDE_DIR/settings.template.json"
SETTINGS_FILE="$CLAUDE_DIR/settings.json" SETTINGS_FILE="$CLAUDE_DIR/settings.json"
@ -401,7 +628,7 @@ if ! grep -q "$ALIAS_MARKER" "$SHELL_RC" 2>/dev/null; then
cat >> "$SHELL_RC" << 'ALIASES' cat >> "$SHELL_RC" << 'ALIASES'
# Bookworm Portable aliases # Bookworm Portable aliases
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude --dangerously-skip-permissions' alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}" claude --dangerously-skip-permissions'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"' alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES ALIASES
success "已添加到 $SHELL_RC:" success "已添加到 $SHELL_RC:"
@ -415,7 +642,7 @@ else
cat >> "$SHELL_RC" << 'ALIASES' cat >> "$SHELL_RC" << 'ALIASES'
# Bookworm Portable aliases # Bookworm Portable aliases
alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude --dangerously-skip-permissions' alias bw='NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}" claude --dangerously-skip-permissions'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"' alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES ALIASES
success "终端别名已更新 (bookworm → bw)" success "终端别名已更新 (bookworm → bw)"
@ -453,5 +680,7 @@ if [ "$START_NOW" = "y" ] || [ "$START_NOW" = "Y" ]; then
info "正在启动 Claude Code..." info "正在启动 Claude Code..."
cd "$HOME" cd "$HOME"
export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1" export NO_PROXY="bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
# v3.0.1: 默认模型 (中转站兼容, 默认 claude-sonnet-4-5 会 503)
export ANTHROPIC_MODEL="${ANTHROPIC_MODEL:-claude-opus-4-7}"
exec claude --dangerously-skip-permissions exec claude --dangerously-skip-permissions
fi fi

482
admin-authcode-gui.ps1 Normal file
View File

@ -0,0 +1,482 @@
<#
.SYNOPSIS
Bookworm 授权码生成器 (管理员 GUI 工具)
.DESCRIPTION
内部调用 node gen-authcode.js, 提供可视化界面生成多用户授权码
打包命令: build.ps1 -Admin
#>
# 全局错误捕获 (PS2EXE -NoOutput 会吞掉所有错误, 这里确保弹窗显示)
try {
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
# ─── 路径 ─────────────────────────────────────────────
$ScriptDir = if ($PSScriptRoot) { $PSScriptRoot }
elseif ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName -match '\.exe$') {
Split-Path -Parent ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName)
} elseif ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else { $PWD.Path }
# gen-authcode.js / secrets.txt 查找: 当前目录 → 父目录 (dist/ 内运行时)
$GenScript = Join-Path $ScriptDir "gen-authcode.js"
if (-not (Test-Path $GenScript)) {
$parentDir = Split-Path $ScriptDir -Parent
$GenScript = Join-Path $parentDir "gen-authcode.js"
if (Test-Path $GenScript) { $ScriptDir = $parentDir } # 切到父目录
}
$SecretsTxt = Join-Path $ScriptDir "secrets.txt"
# ─── 品牌色 ───────────────────────────────────────────
$brandBlue = [System.Drawing.Color]::FromArgb(88, 101, 242)
$brandDark = [System.Drawing.Color]::FromArgb(24, 25, 38)
$brandLight = [System.Drawing.Color]::FromArgb(245, 246, 250)
$cardBg = [System.Drawing.Color]::FromArgb(248, 249, 253)
$successGreen = [System.Drawing.Color]::FromArgb(35, 134, 54)
$warningOrange = [System.Drawing.Color]::FromArgb(227, 137, 29)
$textPrimary = [System.Drawing.Color]::FromArgb(36, 41, 47)
$textSecondary = [System.Drawing.Color]::FromArgb(110, 119, 129)
$inputBorder = [System.Drawing.Color]::FromArgb(208, 215, 222)
# ─── 主窗口 ───────────────────────────────────────────
$form = New-Object System.Windows.Forms.Form
$form.Text = "Bookworm 授权码生成器"
$form.ClientSize = New-Object System.Drawing.Size(540, 594)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
$form.MaximizeBox = $false
$form.BackColor = [System.Drawing.Color]::White
$form.Font = New-Object System.Drawing.Font("Segoe UI", 9)
# ─── 标题栏 (深色渐变 + 副标题) ──────────────────────
$header = New-Object System.Windows.Forms.Panel
$header.Dock = "Top"
$header.Size = New-Object System.Drawing.Size(540, 70)
$header.BackColor = $brandDark
$form.Controls.Add($header)
$titleLabel = New-Object System.Windows.Forms.Label
$titleLabel.Location = New-Object System.Drawing.Point(24, 12)
$titleLabel.Size = New-Object System.Drawing.Size(500, 28)
$titleLabel.Text = "Bookworm 授权码生成器"
$titleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold)
$titleLabel.ForeColor = [System.Drawing.Color]::White
$header.Controls.Add($titleLabel)
$subtitleLabel = New-Object System.Windows.Forms.Label
$subtitleLabel.Location = New-Object System.Drawing.Point(24, 42)
$subtitleLabel.Size = New-Object System.Drawing.Size(500, 18)
$subtitleLabel.Text = "管理员专用 · 为每位用户生成独立授权码与加密凭证"
$subtitleLabel.Font = New-Object System.Drawing.Font("Segoe UI", 8.5)
$subtitleLabel.ForeColor = [System.Drawing.Color]::FromArgb(160, 170, 200)
$header.Controls.Add($subtitleLabel)
# ═══ 输入卡片 (Panel 模拟卡片) ═══════════════════════
$inputCard = New-Object System.Windows.Forms.Panel
$inputCard.Location = New-Object System.Drawing.Point(20, 84)
$inputCard.Size = New-Object System.Drawing.Size(500, 186)
$inputCard.BackColor = $cardBg
$form.Controls.Add($inputCard)
# ── 卡片标题
$inputTitle = New-Object System.Windows.Forms.Label
$inputTitle.Location = New-Object System.Drawing.Point(16, 10)
$inputTitle.Size = New-Object System.Drawing.Size(200, 20)
$inputTitle.Text = "用户信息"
$inputTitle.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
$inputTitle.ForeColor = $brandBlue
$inputCard.Controls.Add($inputTitle)
# ── 用户名
$lblUser = New-Object System.Windows.Forms.Label
$lblUser.Location = New-Object System.Drawing.Point(16, 40)
$lblUser.Size = New-Object System.Drawing.Size(90, 22)
$lblUser.Text = "用户标识"
$lblUser.ForeColor = $textPrimary
$lblUser.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$inputCard.Controls.Add($lblUser)
$txtUser = New-Object System.Windows.Forms.TextBox
$txtUser.Location = New-Object System.Drawing.Point(114, 38)
$txtUser.Size = New-Object System.Drawing.Size(370, 26)
$txtUser.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$txtUser.BorderStyle = "FixedSingle"
$inputCard.Controls.Add($txtUser)
# ── Relay Key
$lblKey = New-Object System.Windows.Forms.Label
$lblKey.Location = New-Object System.Drawing.Point(16, 74)
$lblKey.Size = New-Object System.Drawing.Size(90, 22)
$lblKey.Text = "Relay Key"
$lblKey.ForeColor = $textPrimary
$lblKey.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$inputCard.Controls.Add($lblKey)
$txtKey = New-Object System.Windows.Forms.TextBox
$txtKey.Location = New-Object System.Drawing.Point(114, 72)
$txtKey.Size = New-Object System.Drawing.Size(370, 26)
$txtKey.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$txtKey.PasswordChar = '*'
$txtKey.BorderStyle = "FixedSingle"
$inputCard.Controls.Add($txtKey)
$chkShowKey = New-Object System.Windows.Forms.CheckBox
$chkShowKey.Location = New-Object System.Drawing.Point(114, 102)
$chkShowKey.Size = New-Object System.Drawing.Size(100, 20)
$chkShowKey.Text = "显示 Key"
$chkShowKey.ForeColor = $textSecondary
$chkShowKey.Font = New-Object System.Drawing.Font("Segoe UI", 8)
$chkShowKey.Add_CheckedChanged({ $txtKey.PasswordChar = if ($chkShowKey.Checked) { [char]0 } else { '*' } })
$inputCard.Controls.Add($chkShowKey)
# ── 有效期
$lblDays = New-Object System.Windows.Forms.Label
$lblDays.Location = New-Object System.Drawing.Point(16, 132)
$lblDays.Size = New-Object System.Drawing.Size(90, 22)
$lblDays.Text = "有效期"
$lblDays.ForeColor = $textPrimary
$lblDays.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight
$inputCard.Controls.Add($lblDays)
$cmbDays = New-Object System.Windows.Forms.ComboBox
$cmbDays.Location = New-Object System.Drawing.Point(114, 130)
$cmbDays.Size = New-Object System.Drawing.Size(80, 26)
$cmbDays.DropDownStyle = "DropDownList"
$cmbDays.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$null = $cmbDays.Items.AddRange(@(7, 14, 30, 60, 90, 180, 365))
$cmbDays.SelectedIndex = 2
$inputCard.Controls.Add($cmbDays)
$lblDaysUnit = New-Object System.Windows.Forms.Label
$lblDaysUnit.Location = New-Object System.Drawing.Point(198, 132)
$lblDaysUnit.Size = New-Object System.Drawing.Size(30, 22)
$lblDaysUnit.Text = ""
$lblDaysUnit.ForeColor = $textSecondary
$inputCard.Controls.Add($lblDaysUnit)
$lblDaysHint = New-Object System.Windows.Forms.Label
$lblDaysHint.Location = New-Object System.Drawing.Point(240, 132)
$lblDaysHint.Size = New-Object System.Drawing.Size(240, 22)
$lblDaysHint.Text = ""
$lblDaysHint.ForeColor = $textSecondary
$lblDaysHint.Font = New-Object System.Drawing.Font("Segoe UI", 8.5)
$inputCard.Controls.Add($lblDaysHint)
$cmbDays.Add_SelectedIndexChanged({
$d = [int]$cmbDays.SelectedItem
$lblDaysHint.Text = "-> 到期: " + (Get-Date).AddDays($d).ToString("yyyy-MM-dd")
})
$cmbDays.SelectedIndex = 2
# ── 环境状态行 ──
$nodeOK = [bool](Get-Command node -ErrorAction SilentlyContinue)
$secretsOK = Test-Path $SecretsTxt
$lblStatus1 = New-Object System.Windows.Forms.Label
$lblStatus1.Location = New-Object System.Drawing.Point(16, 160)
$lblStatus1.Size = New-Object System.Drawing.Size(230, 18)
$lblStatus1.Font = New-Object System.Drawing.Font("Segoe UI", 8)
if ($secretsOK) {
$lineCount = (Get-Content $SecretsTxt -ErrorAction SilentlyContinue | Where-Object { $_ -match '=' }).Count
$lblStatus1.Text = "secrets.txt: $lineCount 个凭证"
$lblStatus1.ForeColor = $successGreen
} else {
$lblStatus1.Text = "secrets.txt: 未找到"
$lblStatus1.ForeColor = [System.Drawing.Color]::Red
}
$inputCard.Controls.Add($lblStatus1)
$lblStatus2 = New-Object System.Windows.Forms.Label
$lblStatus2.Location = New-Object System.Drawing.Point(250, 160)
$lblStatus2.Size = New-Object System.Drawing.Size(230, 18)
$lblStatus2.Font = New-Object System.Drawing.Font("Segoe UI", 8)
if ($nodeOK) {
$nodeVer = try { (& node --version 2>$null) } catch { "" }
$lblStatus2.Text = "Node.js: $nodeVer"
$lblStatus2.ForeColor = $successGreen
} else {
$lblStatus2.Text = "Node.js: 未安装"
$lblStatus2.ForeColor = [System.Drawing.Color]::Red
}
$inputCard.Controls.Add($lblStatus2)
# ═══ 操作按钮 ═════════════════════════════════════════
$btnGenerate = New-Object System.Windows.Forms.Button
$btnGenerate.Location = New-Object System.Drawing.Point(138, 282)
$btnGenerate.Size = New-Object System.Drawing.Size(180, 42)
$btnGenerate.Text = " 生成授权码"
$btnGenerate.Font = New-Object System.Drawing.Font("Segoe UI", 11, [System.Drawing.FontStyle]::Bold)
$btnGenerate.FlatStyle = "Flat"
$btnGenerate.BackColor = $brandBlue
$btnGenerate.ForeColor = [System.Drawing.Color]::White
$btnGenerate.FlatAppearance.BorderSize = 0
$btnGenerate.Cursor = [System.Windows.Forms.Cursors]::Hand
$btnGenerate.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$form.Controls.Add($btnGenerate)
$btnClear = New-Object System.Windows.Forms.Button
$btnClear.Location = New-Object System.Drawing.Point(330, 282)
$btnClear.Size = New-Object System.Drawing.Size(72, 42)
$btnClear.Text = "清空"
$btnClear.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$btnClear.FlatStyle = "Flat"
$btnClear.BackColor = [System.Drawing.Color]::White
$btnClear.ForeColor = $textSecondary
$btnClear.FlatAppearance.BorderColor = $inputBorder
$btnClear.FlatAppearance.BorderSize = 1
$form.Controls.Add($btnClear)
# ═══ 结果卡片 ═════════════════════════════════════════
$resultCard = New-Object System.Windows.Forms.Panel
$resultCard.Location = New-Object System.Drawing.Point(20, 338)
$resultCard.Size = New-Object System.Drawing.Size(500, 220)
$resultCard.BackColor = $cardBg
$form.Controls.Add($resultCard)
$lblResultTitle = New-Object System.Windows.Forms.Label
$lblResultTitle.Location = New-Object System.Drawing.Point(16, 10)
$lblResultTitle.Size = New-Object System.Drawing.Size(200, 20)
$lblResultTitle.Text = "生成结果"
$lblResultTitle.Font = New-Object System.Drawing.Font("Segoe UI", 9, [System.Drawing.FontStyle]::Bold)
$lblResultTitle.ForeColor = $brandBlue
$resultCard.Controls.Add($lblResultTitle)
$txtAuthCode = New-Object System.Windows.Forms.TextBox
$txtAuthCode.Location = New-Object System.Drawing.Point(16, 38)
$txtAuthCode.Size = New-Object System.Drawing.Size(380, 34)
$txtAuthCode.Font = New-Object System.Drawing.Font("Consolas", 13, [System.Drawing.FontStyle]::Bold)
$txtAuthCode.ReadOnly = $true
$txtAuthCode.BackColor = [System.Drawing.Color]::White
$txtAuthCode.ForeColor = $brandDark
$txtAuthCode.BorderStyle = "FixedSingle"
$txtAuthCode.Text = ""
$resultCard.Controls.Add($txtAuthCode)
$btnCopy = New-Object System.Windows.Forms.Button
$btnCopy.Location = New-Object System.Drawing.Point(404, 38)
$btnCopy.Size = New-Object System.Drawing.Size(80, 34)
$btnCopy.Text = "复制"
$btnCopy.Font = New-Object System.Drawing.Font("Segoe UI", 9)
$btnCopy.FlatStyle = "Flat"
$btnCopy.BackColor = $brandBlue
$btnCopy.ForeColor = [System.Drawing.Color]::White
$btnCopy.FlatAppearance.BorderSize = 0
$btnCopy.Cursor = [System.Windows.Forms.Cursors]::Hand
$btnCopy.Enabled = $false
$resultCard.Controls.Add($btnCopy)
$lblDetails = New-Object System.Windows.Forms.Label
$lblDetails.Location = New-Object System.Drawing.Point(16, 82)
$lblDetails.Size = New-Object System.Drawing.Size(468, 66)
$lblDetails.Text = "点击「生成授权码」后,结果将显示在此处。"
$lblDetails.ForeColor = $textSecondary
$lblDetails.Font = New-Object System.Drawing.Font("Segoe UI", 8.5)
$resultCard.Controls.Add($lblDetails)
# 推送到 Gitea 按钮
$btnPush = New-Object System.Windows.Forms.Button
$btnPush.Location = New-Object System.Drawing.Point(16, 156)
$btnPush.Size = New-Object System.Drawing.Size(468, 38)
$btnPush.Text = "推送到 Gitea (git add + commit + push)"
$btnPush.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$btnPush.FlatStyle = "Flat"
$btnPush.BackColor = $successGreen
$btnPush.ForeColor = [System.Drawing.Color]::White
$btnPush.FlatAppearance.BorderSize = 0
$btnPush.Cursor = [System.Windows.Forms.Cursors]::Hand
$btnPush.Enabled = $false
$resultCard.Controls.Add($btnPush)
# ═══ 状态栏 ═══════════════════════════════════════════
$statusBar = New-Object System.Windows.Forms.Label
$statusBar.Location = New-Object System.Drawing.Point(0, 568)
$statusBar.Size = New-Object System.Drawing.Size(540, 26)
$statusBar.BackColor = $brandDark
$statusBar.ForeColor = [System.Drawing.Color]::FromArgb(160, 170, 200)
$statusBar.Text = " 就绪 | $ScriptDir"
$statusBar.Font = New-Object System.Drawing.Font("Segoe UI", 8)
$statusBar.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
$form.Controls.Add($statusBar)
# ─── 事件处理 ─────────────────────────────────────────
$btnCopy.Add_Click({
if ($txtAuthCode.Text) {
[System.Windows.Forms.Clipboard]::SetText($txtAuthCode.Text)
$statusBar.Text = " 已复制到剪贴板"
$statusBar.ForeColor = $successGreen
}
})
$btnPush.Add_Click({
$btnPush.Enabled = $false
$btnPush.Text = "推送中..."
$statusBar.Text = " git add + commit + push ..."
$statusBar.ForeColor = $warningOrange
[System.Windows.Forms.Application]::DoEvents()
try {
$gitExe = (Get-Command git -ErrorAction Stop).Source
$userName = if ($global:lastGenUser) { $global:lastGenUser } else { "user" }
# git add secrets-*.enc
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $gitExe
$psi.Arguments = "add secrets-*.enc"
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
$psi.CreateNoWindow = $true
$psi.WorkingDirectory = $ScriptDir
$p = [System.Diagnostics.Process]::Start($psi)
$p.WaitForExit(15000)
# git commit
$psi.Arguments = "commit -m `"add user $userName`""
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardOutput.ReadToEnd() | Out-Null
$p.WaitForExit(15000)
# git push
$statusBar.Text = " git push 中 (可能需要几秒)..."
[System.Windows.Forms.Application]::DoEvents()
$psi.Arguments = "push"
$p = [System.Diagnostics.Process]::Start($psi)
$pushOut = $p.StandardError.ReadToEnd() # git push 输出在 stderr
$p.WaitForExit(30000)
if ($p.ExitCode -eq 0) {
$btnPush.Text = "已推送"
$btnPush.BackColor = [System.Drawing.Color]::FromArgb(200, 220, 200)
$btnPush.ForeColor = $successGreen
$statusBar.Text = " 推送成功 — 现在可以把授权码发给 $userName"
$statusBar.ForeColor = $successGreen
} else {
throw "git push 失败: $pushOut"
}
} catch {
$btnPush.Text = "推送失败 (点击重试)"
$btnPush.BackColor = [System.Drawing.Color]::FromArgb(220, 200, 200)
$btnPush.ForeColor = [System.Drawing.Color]::Red
$btnPush.Enabled = $true
$statusBar.Text = " 推送失败: $_"
$statusBar.ForeColor = [System.Drawing.Color]::Red
[System.Windows.Forms.MessageBox]::Show("推送失败:`n$_`n`n请检查 Git 配置和网络。", "Git 错误", "OK", "Error")
}
})
$btnClear.Add_Click({
$txtUser.Text = ""
$txtKey.Text = ""
$txtAuthCode.Text = ""
$lblDetails.Text = ""
$btnCopy.Enabled = $false
$statusBar.Text = " 已清空"
$statusBar.ForeColor = [System.Drawing.Color]::Gray
})
$btnGenerate.Add_Click({
# 校验
$user = $txtUser.Text.Trim()
$key = $txtKey.Text.Trim()
$days = $cmbDays.SelectedItem.ToString()
if (-not $user) {
[System.Windows.Forms.MessageBox]::Show("请输入用户标识", "缺少用户名", "OK", "Warning")
$txtUser.Focus(); return
}
if (-not $key) {
[System.Windows.Forms.MessageBox]::Show("请输入 Relay Sub-Key`n(从中转站后台为用户创建)", "缺少 Sub-Key", "OK", "Warning")
$txtKey.Focus(); return
}
if (-not (Test-Path $SecretsTxt)) {
[System.Windows.Forms.MessageBox]::Show("secrets.txt 不存在:`n$SecretsTxt`n`n请先创建 secrets.txt (每行 KEY=VALUE)", "缺少凭证文件", "OK", "Error")
return
}
if (-not $nodeOK) {
[System.Windows.Forms.MessageBox]::Show("Node.js 未安装。`n请先安装: https://nodejs.org", "缺少 Node.js", "OK", "Error")
return
}
$statusBar.Text = " 生成中..."
$statusBar.ForeColor = $warningOrange
$btnGenerate.Enabled = $false
[System.Windows.Forms.Application]::DoEvents()
try {
# 用 Process + UTF8 编码读取 (PS2EXE 默认 GBK 会乱码)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = (Get-Command node -ErrorAction Stop).Source
$psi.Arguments = "`"$GenScript`" $days -k `"$key`" -u `"$user`""
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
$psi.CreateNoWindow = $true
$psi.WorkingDirectory = $ScriptDir
$proc = [System.Diagnostics.Process]::Start($psi)
$stdout = $proc.StandardOutput.ReadToEnd()
$stderr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
if ($proc.ExitCode -ne 0) {
throw "gen-authcode.js 退出码 $($proc.ExitCode)`n$stderr"
}
# 解析输出 (编码无关: PS2EXE 下中文可能乱码, 只匹配 ASCII 格式)
$authCode = if ($stdout -match '(BW-\d{8}-[A-F0-9]{24})') { $Matches[1] } else { "" }
$fileId = if ($stdout -match '(secrets-[a-f0-9]{8}\.enc)') { $Matches[1] } else { "" }
$expiry = if ($stdout -match '(\d{4}-\d{2}-\d{2})') { $Matches[1] } else { "" }
if ($authCode) {
$txtAuthCode.Text = $authCode
$lblDetails.Text = "用户: $user`n加密文件: $fileId`n有效期: $days 天 (至 $expiry)`n路径: $ScriptDir\$fileId"
$btnCopy.Enabled = $true
$btnPush.Enabled = $true
$global:lastGenUser = $user
$global:lastGenFile = $fileId
$statusBar.Text = " 生成成功 — 点击「推送到 Gitea」完成部署"
$statusBar.ForeColor = $successGreen
# 追加历史记录 (仅本机, 已在 .gitignore)
$historyFile = Join-Path $ScriptDir "authcode-history.log"
$logLine = "$(Get-Date -Format 'yyyy-MM-dd HH:mm') $($user.PadRight(12)) $fileId $($days)天 至$expiry $authCode"
try { Add-Content -Path $historyFile -Value $logLine -Encoding utf8 } catch {}
} else {
throw "无法解析授权码输出:`n$stdout"
}
} catch {
$txtAuthCode.Text = ""
$lblDetails.Text = "错误: $_"
$btnCopy.Enabled = $false
$statusBar.Text = " 生成失败"
$statusBar.ForeColor = [System.Drawing.Color]::Red
[System.Windows.Forms.MessageBox]::Show("生成失败:`n$_", "错误", "OK", "Error")
} finally {
$btnGenerate.Enabled = $true
}
})
# ─── 启动 ─────────────────────────────────────────────
$form.Add_Shown({ $txtUser.Focus() })
[System.Windows.Forms.Application]::Run($form)
} catch {
# 全局错误弹窗 (PS2EXE 下唯一的错误可见方式)
$errMsg = "Bookworm AuthGen 启动失败:`n`n" +
"错误: $($_.Exception.Message)`n" +
"行号: $($_.InvocationInfo.ScriptLineNumber)`n" +
"代码: $($_.InvocationInfo.Line.Trim())`n`n" +
"ScriptDir: $ScriptDir`n" +
"GenScript: $GenScript`n" +
"SecretsTxt: $SecretsTxt"
try {
[System.Windows.Forms.MessageBox]::Show($errMsg, "AuthGen 错误", "OK", "Error")
} catch {
# 如果连 MsgBox 都失败 (WinForms 未加载), 写文件
$errMsg | Out-File "$env:TEMP\bookworm-authgen-error.txt" -Encoding utf8
}
}

BIN
admin-authcode.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because it is too large Load Diff

BIN
bookworm-desktop.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
bookworm.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

116
build.ps1
View File

@ -65,14 +65,32 @@ if ($buildSetup) {
Write-Host " 输入: $inputPs1" -ForegroundColor Gray Write-Host " 输入: $inputPs1" -ForegroundColor Gray
Write-Host " 输出: $outputExe" -ForegroundColor Gray Write-Host " 输出: $outputExe" -ForegroundColor Gray
Invoke-ps2exe ` # 从 auto-setup.ps1 读取版本号
-InputFile $inputPs1 ` $versionLine = Select-String -Path $inputPs1 -Pattern '^\$BWVersion\s*=\s*"([^"]+)"' | Select-Object -First 1
-OutputFile $outputExe ` $bwVer = if ($versionLine) { $versionLine.Matches[0].Groups[1].Value } else { "0.0.0" }
-Title "Bookworm Portable Setup" ` Write-Host " 版本: $bwVer" -ForegroundColor Gray
-Description "Bookworm Smart Assistant 安装向导" `
-Company "Bookworm" ` # 优先用桌面专用 B 圆图标, 回退到 galaxy
-Version "1.5.0.0" ` $iconFile = Join-Path $ScriptDir "bookworm-desktop.ico"
-NoConsole # 纯 GUI不弹黑色控制台窗口 if (-not (Test-Path $iconFile)) {
$iconFile = Join-Path $ScriptDir "bookworm.ico"
}
$ps2exeArgs = @{
InputFile = $inputPs1
OutputFile = $outputExe
Title = "Bookworm Portable Setup v$bwVer"
Description = "Bookworm Smart Assistant 安装向导 v$bwVer"
Company = "Bookworm"
Version = "$bwVer.0"
NoConsole = $true
NoOutput = $true
NoError = $true
}
if (Test-Path $iconFile) {
$ps2exeArgs.IconFile = $iconFile
Write-Host " 图标: $iconFile" -ForegroundColor Gray
}
Invoke-ps2exe @ps2exeArgs
if (Test-Path $outputExe) { if (Test-Path $outputExe) {
$sizeKB = [math]::Round((Get-Item $outputExe).Length / 1KB) $sizeKB = [math]::Round((Get-Item $outputExe).Length / 1KB)
@ -84,50 +102,48 @@ if ($buildSetup) {
} }
# ════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════
# 2. gen-authcode.exe (gen-authcode.js → pkg) # 2. Bookworm-AuthGen.exe (admin-authcode-gui.ps1 → PS2EXE)
# ════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════
if ($buildAdmin) { if ($buildAdmin) {
Write-Step "打包 gen-authcode.exe (pkg)" Write-Step "打包 Bookworm-AuthGen.exe (PS2EXE GUI)"
# 检查 Node.js $inputPs1 = Join-Path $ScriptDir "admin-authcode-gui.ps1"
if (-not (Get-Command node -ErrorAction SilentlyContinue)) { $outputExe = Join-Path $DistDir "Bookworm-AuthGen.exe"
Write-Fail "需要 Node.js请先安装: https://nodejs.org"
if (-not (Test-Path $inputPs1)) {
Write-Fail "找不到 admin-authcode-gui.ps1"
exit 1 exit 1
} }
# 安装/检查 pkg Write-Host " 输入: $inputPs1" -ForegroundColor Gray
# 优先找 npm global bin 目录下的 pkg不依赖 PATH 是否刷新)
$npmGlobalBin = Join-Path (npm root -g 2>$null) "..\pkg.cmd" | Resolve-Path -ErrorAction SilentlyContinue
$pkgCmd = if ($npmGlobalBin) { $npmGlobalBin.Path } else { (Get-Command pkg -ErrorAction SilentlyContinue)?.Source }
if (-not $pkgCmd) {
Write-Warn "pkg 未安装,正在安装..."
npm install -g pkg
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path","User")
$npmGlobalBin = Join-Path (npm root -g 2>$null) "..\pkg.cmd" | Resolve-Path -ErrorAction SilentlyContinue
$pkgCmd = $npmGlobalBin.Path
}
$inputJs = Join-Path $ScriptDir "gen-authcode.js"
$outputExe = Join-Path $DistDir "gen-authcode.exe"
if (-not (Test-Path $inputJs)) {
Write-Fail "找不到 gen-authcode.js"
exit 1
}
Write-Host " 输入: $inputJs" -ForegroundColor Gray
Write-Host " 输出: $outputExe" -ForegroundColor Gray Write-Host " 输出: $outputExe" -ForegroundColor Gray
Write-Host " (内嵌 Node.js 运行时,首次打包约 1 分钟)" -ForegroundColor Gray
& $pkgCmd $inputJs --targets node18-win-x64 --output $outputExe 2>&1 | # 优先用书虫学者图标, 回退到 B 圆
ForEach-Object { Write-Host " $_" -ForegroundColor Gray } $adminIcon = Join-Path $ScriptDir "admin-authcode.ico"
if (-not (Test-Path $adminIcon)) { $adminIcon = Join-Path $ScriptDir "bookworm-desktop.ico" }
$ps2exeArgs = @{
InputFile = $inputPs1
OutputFile = $outputExe
Title = "Bookworm AuthCode Generator"
Description = "Bookworm 授权码生成器 (管理员工具)"
Company = "Bookworm"
Version = "1.5.1.0"
NoConsole = $true
NoOutput = $true
NoError = $true
}
if (Test-Path $adminIcon) {
$ps2exeArgs.IconFile = $adminIcon
Write-Host " 图标: $adminIcon" -ForegroundColor Gray
}
Invoke-ps2exe @ps2exeArgs
if (Test-Path $outputExe) { if (Test-Path $outputExe) {
$sizeMB = [math]::Round((Get-Item $outputExe).Length / 1MB, 1) $sizeKB = [math]::Round((Get-Item $outputExe).Length / 1KB)
Write-OK "gen-authcode.exe 打包完成 (${sizeMB} MB)" Write-OK "Bookworm-AuthGen.exe 打包完成 (${sizeKB} KB)"
} else { } else {
Write-Fail "gen-authcode.exe 打包失败" Write-Fail "Bookworm-AuthGen.exe 打包失败"
exit 1 exit 1
} }
} }
@ -141,6 +157,20 @@ Write-Host " 打包完成!输出目录: dist\" -ForegroundColor Green
Write-Host " ============================================" -ForegroundColor Green Write-Host " ============================================" -ForegroundColor Green
Write-Host "" Write-Host ""
# v3.1.1: build 后自动跑 E2E 行为测试 (闭合 L8: 防 v3.0.10 -or 类运行时 bug)
$e2eTest = Join-Path $ScriptDir "tools\test-launcher-e2e.ps1"
if (Test-Path $e2eTest) {
Write-Host " ── 运行 E2E 行为测试 (build 后自动护栏)" -ForegroundColor Cyan
& pwsh -NoProfile -File $e2eTest 2>&1 | ForEach-Object { Write-Host " $_" }
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host " [!] E2E 测试失败 (exit $LASTEXITCODE)" -ForegroundColor Red
Write-Host " EXE 已生成但启动器契约/wrapper 有问题, 修复后重打包" -ForegroundColor Yellow
exit 1
}
Write-Host ""
}
Get-ChildItem $DistDir | ForEach-Object { Get-ChildItem $DistDir | ForEach-Object {
$sizeMB = [math]::Round($_.Length / 1MB, 1) $sizeMB = [math]::Round($_.Length / 1MB, 1)
Write-Host " $($_.Name.PadRight(30)) ${sizeMB} MB" -ForegroundColor White Write-Host " $($_.Name.PadRight(30)) ${sizeMB} MB" -ForegroundColor White
@ -148,6 +178,6 @@ Get-ChildItem $DistDir | ForEach-Object {
Write-Host "" Write-Host ""
Write-Host " 分发说明:" -ForegroundColor Gray Write-Host " 分发说明:" -ForegroundColor Gray
Write-Host " Bookworm-Setup.exe → 放入 bookworm-boot 仓库根目录,发给用户" -ForegroundColor Gray Write-Host " Bookworm-Setup.exe → 用户安装器 (公开下载)" -ForegroundColor Gray
Write-Host " gen-authcode.exe → 管理员本机使用,勿对外分发" -ForegroundColor Gray Write-Host " Bookworm-AuthGen.exe → 管理员授权码生成器 (勿对外分发)" -ForegroundColor Gray
Write-Host "" Write-Host ""

457
bw-doctor.ps1 Normal file
View File

@ -0,0 +1,457 @@
<#
.SYNOPSIS
Bookworm Portable 体检工具 (v3.1.3)
.DESCRIPTION
13 维度自检, 覆盖 auto-setup.ps1 7 阶段所有安装产物.
输出彩色 PASS/WARN/FAIL 报告, 不修改任何文件.
维度:
[1] PowerShell 7 (pwsh)
[2] Node.js
[3] Git
[4] Claude Code (claude.ps1 可达)
[5] 桌面 .lnk (启动/更新 + Args 契约)
[6] DPAPI 凭证 (HKCU CachedEnv)
[7] Profile BW_CRED (PS7 + PS5.1)
[8] Profile BW_CLIP (PS7)
[9] 环境变量 (ANTHROPIC_*)
[10] ~/.claude 完整性 (CLAUDE.md / Skills / Hooks / Settings)
[11] API 中转站连通 (bww.letcareme.com)
[12] Worker 连通 (bookworm-router)
[13] Gitea 连通 (code.letcareme.com)
.NOTES
用法: pwsh -NoProfile -ExecutionPolicy Bypass -File bw-doctor.ps1
日志: $env:TEMP\bw-doctor.log
退出码: 0 = PASS / 1 = FAIL
#>
$ErrorActionPreference = "Continue"
$doctorLog = Join-Path $env:TEMP "bw-doctor.log"
$pass = 0; $warn = 0; $fail = 0
$results = @()
function Log-Doctor {
param([string]$Msg)
try { "[$([DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))] $Msg" | Out-File -FilePath $doctorLog -Append -Encoding UTF8 -EA SilentlyContinue } catch {}
}
function Report {
param([string]$Dim, [string]$Status, [string]$Detail)
$color = switch ($Status) { 'PASS' { 'Green' } 'WARN' { 'Yellow' } 'FAIL' { 'Red' } default { 'Gray' } }
$icon = switch ($Status) { 'PASS' { [char]0x2714 } 'WARN' { '!' } 'FAIL' { [char]0x2718 } default { '?' } }
$line = " $icon [$Status] $Dim"
if ($Detail) { $line += "$Detail" }
Write-Host $line -ForegroundColor $color
Log-Doctor "$Status $Dim $Detail"
switch ($Status) {
'PASS' { $script:pass++ }
'WARN' { $script:warn++ }
'FAIL' { $script:fail++ }
}
$script:results += @{ Dim = $Dim; Status = $Status; Detail = $Detail }
}
function Test-Cmd($name) { return [bool](Get-Command $name -EA SilentlyContinue) }
# ── Banner ──
Write-Host ""
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host " | Bookworm Doctor v3.1.3 |" -ForegroundColor Cyan
Write-Host " | 13 维度健康体检 |" -ForegroundColor Cyan
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host ""
Log-Doctor "=== Bookworm Doctor v3.1.3 START ==="
# ══════════════════════════════════════════════════════
# [1/13] PowerShell 7
# ══════════════════════════════════════════════════════
Write-Host " [1/13] PowerShell 7" -ForegroundColor Cyan
$pwshCmd = Get-Command pwsh -EA SilentlyContinue
if ($pwshCmd) {
try {
$pwshVer = (& pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>$null).Trim()
Report "[1] PowerShell 7" "PASS" "v$pwshVer ($($pwshCmd.Source))"
} catch {
Report "[1] PowerShell 7" "WARN" "pwsh 可达但版本查询失败"
}
} else {
Report "[1] PowerShell 7" "FAIL" "pwsh 不在 PATH — 桌面 .lnk 强依赖 pwsh.exe"
}
# ══════════════════════════════════════════════════════
# [2/13] Node.js
# ══════════════════════════════════════════════════════
Write-Host " [2/13] Node.js" -ForegroundColor Cyan
if (Test-Cmd "node") {
try {
$nodeVer = (& node --version 2>$null).Trim()
Report "[2] Node.js" "PASS" "$nodeVer"
} catch {
Report "[2] Node.js" "WARN" "node 可达但版本查询失败"
}
} else {
Report "[2] Node.js" "FAIL" "node 不在 PATH — Claude Code 强依赖"
}
# ══════════════════════════════════════════════════════
# [3/13] Git
# ══════════════════════════════════════════════════════
Write-Host " [3/13] Git" -ForegroundColor Cyan
if (Test-Cmd "git") {
try {
$gitVer = (& git --version 2>$null).Trim()
Report "[3] Git" "PASS" "$gitVer"
} catch {
Report "[3] Git" "WARN" "git 可达但版本查询失败"
}
} else {
Report "[3] Git" "FAIL" "git 不在 PATH — 配置同步强依赖"
}
# ══════════════════════════════════════════════════════
# [4/13] Claude Code
# ══════════════════════════════════════════════════════
Write-Host " [4/13] Claude Code" -ForegroundColor Cyan
$claudePs1Found = $null
$claudeCmd = Get-Command claude -EA SilentlyContinue
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
$claudePs1Found = $claudeCmd.Source
}
if (-not $claudePs1Found) {
try {
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
$candidate = Join-Path $npmPrefix "claude.ps1"
if (Test-Path $candidate) { $claudePs1Found = $candidate }
} catch {}
}
if (-not $claudePs1Found) {
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1", "$env:LOCALAPPDATA\npm\claude.ps1")) {
if (Test-Path $p) { $claudePs1Found = $p; break }
}
}
if ($claudePs1Found) {
try {
$claudeVer = (& claude --version 2>$null | Select-Object -First 1).Trim()
Report "[4] Claude Code" "PASS" "$claudeVer ($claudePs1Found)"
} catch {
Report "[4] Claude Code" "PASS" "claude.ps1 存在: $claudePs1Found (版本查询跳过)"
}
} else {
Report "[4] Claude Code" "FAIL" "claude.ps1 不可达 — 运行 npm i -g @anthropic-ai/claude-code"
}
# ══════════════════════════════════════════════════════
# [5/13] 桌面 .lnk
# ══════════════════════════════════════════════════════
Write-Host " [5/13] 桌面 .lnk" -ForegroundColor Cyan
$desktop = [Environment]::GetFolderPath('Desktop')
$requiredLnks = @('启动Bookworm.lnk', '更新Bookworm.lnk')
$optionalLnks = @('体检Bookworm.lnk', '卸载Bookworm.lnk')
$lnkMissing = @()
$lnkPresent = @()
foreach ($n in $requiredLnks) {
$p = Join-Path $desktop $n
if (Test-Path $p) { $lnkPresent += $n } else { $lnkMissing += $n }
}
$optPresent = @()
foreach ($n in $optionalLnks) {
$p = Join-Path $desktop $n
if (Test-Path $p) { $optPresent += $n }
}
if ($lnkMissing.Count -eq 0) {
# 验证启动 .lnk Args 契约 (4 项: pwsh TargetPath / bw-launch.ps1 / --dangerously-skip-permissions / -ExecutionPolicy Bypass)
$launchLnk = Join-Path $desktop '启动Bookworm.lnk'
$argsOK = $true
$argsDetail = ""
try {
$shell = New-Object -ComObject WScript.Shell
$sc = $shell.CreateShortcut($launchLnk)
$checks = @(
@{ Name = "TargetPath=pwsh"; OK = ($sc.TargetPath -match 'pwsh\.exe$') }
@{ Name = "bw-launch.ps1"; OK = ($sc.Arguments -match 'bw-launch\.ps1') }
@{ Name = "--dangerously-skip-permissions"; OK = ($sc.Arguments -match '--dangerously-skip-permissions') }
@{ Name = "-ExecutionPolicy Bypass"; OK = ($sc.Arguments -match '-ExecutionPolicy Bypass') }
)
$badChecks = $checks | Where-Object { -not $_.OK }
if ($badChecks) {
$argsOK = $false
$argsDetail = "契约失败: " + (($badChecks | ForEach-Object { $_.Name }) -join ', ')
}
} catch { $argsOK = $false; $argsDetail = "读取 .lnk 异常" }
if ($argsOK) {
$extra = if ($optPresent.Count -gt 0) { " + $($optPresent -join '/')" } else { "" }
Report "[5] 桌面 .lnk" "PASS" "$($lnkPresent -join ' + ')$extra — 4 项契约 OK"
} else {
Report "[5] 桌面 .lnk" "FAIL" "$argsDetail"
}
} else {
Report "[5] 桌面 .lnk" "FAIL" "缺失: $($lnkMissing -join ', ') — 重跑 Bookworm-Setup.exe"
}
# ══════════════════════════════════════════════════════
# [6/13] DPAPI 凭证
# ══════════════════════════════════════════════════════
Write-Host " [6/13] DPAPI 凭证" -ForegroundColor Cyan
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
if (Test-Path $regPath) {
try {
Add-Type -AssemblyName System.Security -EA Stop
$props = Get-ItemProperty $regPath -EA Stop
$envNames = $props.PSObject.Properties | Where-Object { $_.Name -match '^[A-Z_]+$' }
$decrypted = 0
foreach ($ev in $envNames) {
try {
$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect(
[Convert]::FromBase64String($ev.Value), $null,
[System.Security.Cryptography.DataProtectionScope]::CurrentUser)
$decrypted++
} catch {}
}
if ($envNames.Count -gt 0 -and $decrypted -eq $envNames.Count) {
Report "[6] DPAPI 凭证" "PASS" "$decrypted/$($envNames.Count) 密钥可解密"
} elseif ($decrypted -gt 0) {
Report "[6] DPAPI 凭证" "WARN" "$decrypted/$($envNames.Count) 可解密 (部分失败)"
} else {
Report "[6] DPAPI 凭证" "FAIL" "0/$($envNames.Count) 可解密 — DPAPI 可能跨用户失效"
}
} catch {
Report "[6] DPAPI 凭证" "WARN" "HKCU 存在但读取异常: $($_.Exception.Message)"
}
} else {
Report "[6] DPAPI 凭证" "FAIL" "HKCU:\Software\Bookworm\CachedEnv 不存在 — 未安装或已卸载"
}
# ══════════════════════════════════════════════════════
# [7/13] Profile BW_CRED 块
# ══════════════════════════════════════════════════════
Write-Host " [7/13] Profile BW_CRED" -ForegroundColor Cyan
$credBlockOK = 0; $credBlockMissing = @()
foreach ($entry in @(
@{ Name = "PS7"; Path = (Join-Path $env:USERPROFILE "Documents\PowerShell\profile.ps1") }
@{ Name = "PS5.1"; Path = (Join-Path $env:USERPROFILE "Documents\WindowsPowerShell\profile.ps1") }
)) {
if (Test-Path $entry.Path) {
$c = Get-Content $entry.Path -Raw -EA SilentlyContinue
if ($c -match 'BW_CRED_START' -and $c -match 'BW_CRED_END') {
$credBlockOK++
} else {
$credBlockMissing += $entry.Name
}
} else {
$credBlockMissing += "$($entry.Name)(文件不存在)"
}
}
if ($credBlockOK -ge 1 -and $credBlockMissing.Count -eq 0) {
Report "[7] Profile BW_CRED" "PASS" "PS7 + PS5.1 双 profile sentinel 完整"
} elseif ($credBlockOK -ge 1) {
Report "[7] Profile BW_CRED" "WARN" "部分缺失: $($credBlockMissing -join ', ')"
} else {
Report "[7] Profile BW_CRED" "FAIL" "BW_CRED 块不存在 — 启动时无法自动加载凭证"
}
# ══════════════════════════════════════════════════════
# [8/13] Profile BW_CLIP 块
# ══════════════════════════════════════════════════════
Write-Host " [8/13] Profile BW_CLIP" -ForegroundColor Cyan
$clipProfile = Join-Path $env:USERPROFILE "Documents\PowerShell\profile.ps1"
if (Test-Path $clipProfile) {
$c = Get-Content $clipProfile -Raw -EA SilentlyContinue
if ($c -match 'BW_CLIP_START' -and $c -match 'BW_CLIP_END') {
Report "[8] Profile BW_CLIP" "PASS" "截图粘贴助手 sentinel 完整"
} else {
Report "[8] Profile BW_CLIP" "WARN" "BW_CLIP 块缺失 (截图粘贴功能不可用, 非核心)"
}
} else {
Report "[8] Profile BW_CLIP" "WARN" "PS7 profile.ps1 不存在"
}
# ══════════════════════════════════════════════════════
# [9/13] 凭证注入链路 (DPAPI → profile → 运行时 Key)
# ══════════════════════════════════════════════════════
Write-Host " [9/13] 凭证注入链路" -ForegroundColor Cyan
# Bookworm 的设计: DPAPI 加密存 HKCU → profile.ps1 启动时解密 export → 运行时 env 可用
# 不应检查 User 持久环境变量 (Key 明文存 User env 反而不安全)
$chainOK = $true; $chainDetail = @()
# 环节 1: DPAPI 存储有 Key
$regPath = "HKCU:\Software\Bookworm\CachedEnv"
$dpapiHasKey = $false
if (Test-Path $regPath) {
try {
$props = Get-ItemProperty $regPath -EA SilentlyContinue
$apiKeyProp = $props.PSObject.Properties | Where-Object { $_.Name -eq 'ANTHROPIC_API_KEY' }
if ($apiKeyProp) { $dpapiHasKey = $true; $chainDetail += "DPAPI=OK" }
} catch {}
}
if (-not $dpapiHasKey) { $chainOK = $false; $chainDetail += "DPAPI=MISSING" }
# 环节 2: profile 有 BW_CRED 块
$ps7Profile = Join-Path $env:USERPROFILE "Documents\PowerShell\profile.ps1"
$profileHasCred = $false
if (Test-Path $ps7Profile) {
$c = Get-Content $ps7Profile -Raw -EA SilentlyContinue
if ($c -match 'BW_CRED_START') { $profileHasCred = $true; $chainDetail += "Profile=OK" }
}
if (-not $profileHasCred) { $chainOK = $false; $chainDetail += "Profile=MISSING" }
# 环节 3: BASE_URL (可选但推荐)
$baseUrl = [Environment]::GetEnvironmentVariable('ANTHROPIC_BASE_URL', 'User')
if (-not $baseUrl) { $baseUrl = $env:ANTHROPIC_BASE_URL }
if ($baseUrl) { $chainDetail += "BaseURL=$baseUrl" } else { $chainDetail += "BaseURL=default" }
if ($chainOK) {
Report "[9] 凭证注入链路" "PASS" ($chainDetail -join ' → ')
} else {
Report "[9] 凭证注入链路" "FAIL" ($chainDetail -join ' → ') + " — 重跑 Bookworm-Setup.exe"
}
# ══════════════════════════════════════════════════════
# [10/13] ~/.claude 完整性
# ══════════════════════════════════════════════════════
Write-Host " [10/13] ~/.claude 完整性" -ForegroundColor Cyan
$claudeDir = Join-Path $env:USERPROFILE ".claude"
$intChecks = @()
# CLAUDE.md
$claudeMdPath = Join-Path $claudeDir "CLAUDE.md"
$claudeMdOK = $false
if (Test-Path $claudeMdPath) {
$cm = Get-Content $claudeMdPath -Raw -EA SilentlyContinue
$claudeMdOK = $cm -match "Bookworm"
}
$intChecks += @{ Name = "CLAUDE.md"; OK = $claudeMdOK }
# Skills
$skillsDir = Join-Path $claudeDir "skills"
$skillCount = 0
if (Test-Path $skillsDir) { $skillCount = @(Get-ChildItem $skillsDir -Directory -EA SilentlyContinue).Count }
$intChecks += @{ Name = "Skills ($skillCount, 需>=10)"; OK = ($skillCount -ge 10) }
# Hooks
$hooksDir = Join-Path $claudeDir "hooks"
$hookCount = 0
if (Test-Path $hooksDir) { $hookCount = @(Get-ChildItem $hooksDir -Filter "*.js" -File -EA SilentlyContinue).Count }
$intChecks += @{ Name = "Hooks ($hookCount, 需>=3)"; OK = ($hookCount -ge 3) }
# Settings hooks
$settingsFile = Join-Path $claudeDir "settings.json"
$settingsOK = $false
if (Test-Path $settingsFile) {
$sc = Get-Content $settingsFile -Raw -EA SilentlyContinue
$settingsOK = $sc -match '"hooks"'
}
$intChecks += @{ Name = "Settings hooks"; OK = $settingsOK }
$intFails = $intChecks | Where-Object { -not $_.OK }
if ($intFails.Count -eq 0) {
Report "[10] ~/.claude 完整性" "PASS" "CLAUDE.md + $skillCount Skills + $hookCount Hooks + Settings"
} else {
$failNames = ($intFails | ForEach-Object { $_.Name }) -join ', '
Report "[10] ~/.claude 完整性" "FAIL" "缺失: $failNames"
}
# ══════════════════════════════════════════════════════
# [11/13] API 中转站连通
# ══════════════════════════════════════════════════════
Write-Host " [11/13] API 中转站" -ForegroundColor Cyan
$apiBaseUrl = [Environment]::GetEnvironmentVariable('ANTHROPIC_BASE_URL', 'User')
if (-not $apiBaseUrl) { $apiBaseUrl = $env:ANTHROPIC_BASE_URL }
if (-not $apiBaseUrl) { $apiBaseUrl = "https://bww.letcareme.com" }
try {
# 中转站对境外 IP 返 503, NO_PROXY 直连
$savedProxy = $env:NO_PROXY
$env:NO_PROXY = "bww.letcareme.com,letcareme.com,localhost,127.0.0.1"
$resp = Invoke-WebRequest -Uri "$apiBaseUrl/v1/models" -Method Head -TimeoutSec 10 -UseBasicParsing -EA Stop
$env:NO_PROXY = $savedProxy
Report "[11] API 中转站" "PASS" "$apiBaseUrl (HTTP $($resp.StatusCode))"
} catch {
$env:NO_PROXY = $savedProxy
$errMsg = $_.Exception.Message
if ($errMsg -match '40[0-9]|301|302|503') {
Report "[11] API 中转站" "PASS" "$apiBaseUrl 可达 (HTTP 错误码 = 网络通)"
} else {
Report "[11] API 中转站" "FAIL" "$apiBaseUrl 不可达: $errMsg"
}
}
# ══════════════════════════════════════════════════════
# [12/13] Worker 连通
# ══════════════════════════════════════════════════════
Write-Host " [12/13] Worker" -ForegroundColor Cyan
try {
$workerUrl = "https://bookworm-router.bookworm-api.workers.dev/config"
$resp = Invoke-RestMethod -Uri $workerUrl -TimeoutSec 10 -EA Stop
Report "[12] Worker" "PASS" "bookworm-router /config 可达"
} catch {
$errMsg = $_.Exception.Message
if ($errMsg -match '403|404') {
Report "[12] Worker" "PASS" "Worker 可达 (HTTP 错误码 = 网络通)"
} else {
Report "[12] Worker" "WARN" "Worker 不可达: $errMsg (非核心, 仅影响默认 model)"
}
}
# ══════════════════════════════════════════════════════
# [13/13] Gitea 连通
# ══════════════════════════════════════════════════════
Write-Host " [13/13] Gitea" -ForegroundColor Cyan
try {
$savedProxy = $env:NO_PROXY
$env:NO_PROXY = "code.letcareme.com,letcareme.com,localhost,127.0.0.1"
$resp = Invoke-WebRequest -Uri "https://code.letcareme.com" -Method Head -TimeoutSec 10 -UseBasicParsing -EA Stop
$env:NO_PROXY = $savedProxy
Report "[13] Gitea" "PASS" "code.letcareme.com 可达"
} catch {
$env:NO_PROXY = $savedProxy
$errMsg = $_.Exception.Message
if ($errMsg -match '40[0-9]|301|302|503') {
Report "[13] Gitea" "PASS" "code.letcareme.com 可达 (HTTP 错误码 = 网络通)"
} else {
Report "[13] Gitea" "FAIL" "code.letcareme.com 不可达: $errMsg — 配置同步将失败"
}
}
# ══════════════════════════════════════════════════════
# Summary
# ══════════════════════════════════════════════════════
$total = $pass + $warn + $fail
Write-Host ""
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host " | 体检报告 |" -ForegroundColor Cyan
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
$passLine = " | PASS: $pass / $total"
$warnLine = " | WARN: $warn"
$failLine = " | FAIL: $fail"
Write-Host $passLine -ForegroundColor Green
if ($warn -gt 0) { Write-Host $warnLine -ForegroundColor Yellow }
if ($fail -gt 0) { Write-Host $failLine -ForegroundColor Red }
Write-Host " |" -ForegroundColor Cyan
if ($fail -eq 0 -and $warn -eq 0) {
Write-Host " | [ALL GREEN] Bookworm 完全健康" -ForegroundColor Green
} elseif ($fail -eq 0) {
Write-Host " | [HEALTHY] 核心功能正常, 有 $warn 项可优化" -ForegroundColor Yellow
} else {
Write-Host " | [NEEDS FIX] $fail 项异常需修复" -ForegroundColor Red
Write-Host " | 修复: 重跑 Bookworm-Setup.exe 或联系管理员" -ForegroundColor Red
}
Write-Host " | 日志: $doctorLog" -ForegroundColor Gray
Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host ""
Log-Doctor "=== DONE: PASS=$pass WARN=$warn FAIL=$fail ==="
if ($fail -gt 0) { exit 1 } else { exit 0 }

163
bw-launch.ps1 Normal file
View File

@ -0,0 +1,163 @@
<#
.SYNOPSIS
Bookworm 启动 wrapper (v3.1.0 引入)
.DESCRIPTION
桌面 .lnk 调用此 wrapper 而非直调 claude.ps1, 让启动逻辑可热修复 ( wrapper 不需重 bake .lnk).
职责:
1. 动态查找 claude.ps1 真实路径 (npm config get prefix 兜底候选)
2. claude.ps1 stale (npm uninstall/换版本/ prefix) 弹清晰 GUI 引导, 不再"快捷方式失效"
3. 失败时不静默, 写日志 + GUI 弹窗
.lnk 的契约:
.lnk Args = -NoLogo -NoExit -ExecutionPolicy Bypass -File "<bw-launch.ps1 绝对路径>"
--dangerously-skip-permissions
$args[0..N] 转发给 claude.ps1
.NOTES
v3.1.0 (2026-04-25) 引入 wrapper 模式 (闭合 L4 局限: bake claude.ps1 路径 stale)
v3.1.2 (2026-04-26) nvm/fnm/volta shim 探测 (闭合 L9)
分发: Phase 7 安装时复制到 $BootDir\bw-launch.ps1
#>
$ErrorActionPreference = "Continue"
$bwLaunchLog = Join-Path $env:TEMP "bw-launch.log"
function Write-BwLaunchLog {
param([string]$Level, [string]$Msg)
try {
$line = "[$([DateTime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))] [$Level] $Msg"
$line | Out-File -FilePath $bwLaunchLog -Append -Encoding UTF8 -EA SilentlyContinue
} catch {}
}
# v3.1.2 L12: log rotation — >1MB 归档, 保留最近 3 个 .bak
foreach ($logName in @("bw-launch.log", "bw-crash.log", "bw-phase4-validate.log", "bw-doctor.log")) {
$logPath = Join-Path $env:TEMP $logName
try {
if ((Test-Path $logPath) -and ((Get-Item $logPath -EA SilentlyContinue).Length -gt 1MB)) {
$ts = [DateTime]::Now.ToString('yyyyMMdd-HHmmss')
Copy-Item $logPath "$logPath.bak.$ts" -Force -EA Stop
Remove-Item $logPath -Force -EA Stop
# 保留最近 3 个 .bak, 删旧的
$baks = Get-ChildItem "$logPath.bak.*" -EA SilentlyContinue | Sort-Object LastWriteTime -Descending
if ($baks.Count -gt 3) { $baks | Select-Object -Skip 3 | Remove-Item -Force -EA SilentlyContinue }
}
} catch {}
}
function Show-LaunchError {
param([string]$Title, [string]$Body)
Write-BwLaunchLog "ERROR" "$Title :: $Body"
Write-Host ""
Write-Host " [!] $Title" -ForegroundColor Red
Write-Host ""
Write-Host $Body -ForegroundColor Yellow
Write-Host ""
Write-Host " 日志: $bwLaunchLog" -ForegroundColor Gray
Write-Host ""
try {
Add-Type -AssemblyName System.Windows.Forms -EA Stop
[System.Windows.Forms.MessageBox]::Show("$Body`n`n详情见: $bwLaunchLog", "Bookworm 启动失败 — $Title", 'OK', 'Error') | Out-Null
} catch {}
Read-Host "按回车关闭"
}
Write-BwLaunchLog "INFO" "bw-launch wrapper 启动 args=$($args -join ' ')"
# ── 1. PATH 三层重载 (即便桌面 .lnk 不依赖 PATH, 子 claude.ps1 调 node 仍依赖) ──
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
try {
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
if ($npmPrefix -and (Test-Path $npmPrefix) -and ($env:Path -notlike "*$npmPrefix*")) {
$env:Path = "$npmPrefix;$env:Path"
}
} catch {
Write-BwLaunchLog "WARN" "npm config get prefix 失败: $_"
}
foreach ($p in @("$env:APPDATA\npm", "$env:ProgramFiles\nodejs", "$env:LOCALAPPDATA\npm")) {
if ((Test-Path $p) -and ($env:Path -notlike "*$p*")) {
$env:Path = "$p;$env:Path"
}
}
# v3.1.2 L9: nvm/fnm/volta shim 探测 (版本管理器装的 node 不在默认 PATH)
$shimPaths = @()
if ($env:NVM_HOME) { $shimPaths += "$env:NVM_HOME"; $shimPaths += (Join-Path $env:NVM_HOME "nodejs") }
if ($env:NVM_SYMLINK) { $shimPaths += $env:NVM_SYMLINK }
if ($env:FNM_DIR) { $shimPaths += (Join-Path $env:FNM_DIR "aliases\default") }
if ($env:FNM_MULTISHELL_PATH) { $shimPaths += $env:FNM_MULTISHELL_PATH }
if ($env:VOLTA_HOME) { $shimPaths += (Join-Path $env:VOLTA_HOME "bin") }
foreach ($sp in $shimPaths) {
if ($sp -and (Test-Path $sp) -and ($env:Path -notlike "*$sp*")) {
$env:Path = "$sp;$env:Path"
Write-BwLaunchLog "INFO" "shim PATH 补充: $sp"
}
}
# ── 2. 动态定位 claude.ps1 ──
$claudePs1 = $null
# 优先 Get-Command (PATH 已重载)
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1') -and (Test-Path $claudeCmd.Source)) {
$claudePs1 = $claudeCmd.Source
Write-BwLaunchLog "INFO" "claude.ps1 from Get-Command: $claudePs1"
}
# 兜底 npm config get prefix
if (-not $claudePs1) {
try {
$candidate = Join-Path $npmPrefix "claude.ps1"
if (Test-Path $candidate) {
$claudePs1 = $candidate
Write-BwLaunchLog "INFO" "claude.ps1 from npm prefix: $claudePs1"
}
} catch {}
}
# 最终硬编码兜底
if (-not $claudePs1) {
foreach ($p in @("$env:APPDATA\npm\claude.ps1", "$env:ProgramFiles\nodejs\claude.ps1", "$env:LOCALAPPDATA\npm\claude.ps1")) {
if (Test-Path $p) {
$claudePs1 = $p
Write-BwLaunchLog "INFO" "claude.ps1 from hardcoded: $claudePs1"
break
}
}
}
if (-not $claudePs1 -or -not (Test-Path $claudePs1)) {
$diag = "未找到 claude.ps1 文件.`n`n"
$diag += "诊断信息:`n"
$diag += " npm config get prefix: $(if ($npmPrefix) { $npmPrefix } else { '(查询失败)' })`n"
$diag += " PATH 中 npm/nodejs 片段:`n"
foreach ($p in (($env:Path -split ';') | Where-Object { $_ -match 'npm|nodejs' })) {
$diag += " $p`n"
}
$diag += "`n修复方案:`n"
$diag += " 方案 A: 命令行运行 npm i -g @anthropic-ai/claude-code`n"
$diag += " 方案 B: 重新双击 Bookworm-Setup.exe 修复安装"
Show-LaunchError "Claude Code 未找到" $diag
exit 1
}
# ── 3. 启动 claude.ps1 + 转发 $args (--dangerously-skip-permissions 等) ──
Write-BwLaunchLog "INFO" "调用 claude.ps1 args=$($args -join ' ')"
try {
& $claudePs1 @args
$exitCode = $LASTEXITCODE
Write-BwLaunchLog "INFO" "claude.ps1 退出码: $exitCode"
if ($exitCode -ne 0) {
Write-Host ""
Write-Host " [!] Claude 进程退出码: $exitCode" -ForegroundColor Yellow
Write-Host " 日志: $bwLaunchLog" -ForegroundColor Gray
}
exit $exitCode
} catch {
Show-LaunchError "Claude 启动异常" "异常信息: $($_.Exception.Message)"
exit 1
}

387
bw-ota.ps1 Normal file
View File

@ -0,0 +1,387 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Bookworm Portable OTA 更新检查器 (启动时自动调用)
.DESCRIPTION
轻量版同步: 检查远端版本 用户确认 增量同步 验签 原子替换.
设计原则: fail-open (任何异常不阻断启动), 24h 冷却, 用户确认制.
bookworm-sync.ps1 的区别:
- Token DPAPI 加密文件读取 (安装时写入)
- Pubkey 内嵌安装目录
- 失败不阻断启动
- 版本相同跳过
.NOTES
Author: Bookworm Admin
Version: 1.0.0 (2026-04-27)
License: Private
#>
[CmdletBinding()]
param(
[switch]$Force,
[switch]$SkipConfirm,
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$OtaDir = Join-Path $env:USERPROFILE '.claude\.bw-ota'
$ClaudeRoot = Join-Path $env:USERPROFILE '.claude'
$ConfigFile = Join-Path $OtaDir 'config.json'
$CredFile = Join-Path $OtaDir 'pull-cred.dpapi'
$PubKeyFile = Join-Path $OtaDir 'signing-pubkey.pem'
# ========== 日志 ==========
function Write-Ota ($m, $c = 'Cyan') { Write-Host "[Bookworm OTA] $m" -ForegroundColor $c }
function Write-OtaOk ($m) { Write-Ota $m 'Green' }
function Write-OtaWarn ($m) { Write-Ota $m 'Yellow' }
function Write-OtaErr ($m) { Write-Ota $m 'Red' }
# ========== OTA 基础设施检查 ==========
function Test-OtaReady {
if (-not (Test-Path $OtaDir)) { return $false }
if (-not (Test-Path $ConfigFile)) { return $false }
if (-not (Test-Path $CredFile)) { return $false }
if (-not (Test-Path $PubKeyFile)) { return $false }
return $true
}
# ========== 配置读写 ==========
function Read-OtaConfig {
try {
$raw = Get-Content -Raw $ConfigFile
if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
return $raw | ConvertFrom-Json
} catch {
return $null
}
}
function Save-OtaConfig ($cfg) {
$json = $cfg | ConvertTo-Json -Depth 4
[IO.File]::WriteAllText($ConfigFile, $json, [Text.UTF8Encoding]::new($false))
}
# ========== 冷却检查 (24h) ==========
function Test-Cooldown ($cfg) {
if ($Force) { return $false }
$interval = if ($cfg.checkInterval) { $cfg.checkInterval } else { 86400 }
$lastCheck = if ($cfg.lastCheck) { $cfg.lastCheck } else { 0 }
$now = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
return ($now - $lastCheck) -lt $interval
}
# ========== DPAPI 凭证解密 ==========
function Read-Credential {
try {
Add-Type -AssemblyName System.Security
$encrypted = [IO.File]::ReadAllBytes($CredFile)
$plain = [Security.Cryptography.ProtectedData]::Unprotect(
$encrypted,
[Text.Encoding]::UTF8.GetBytes('bookworm-ota-salt'),
[Security.Cryptography.DataProtectionScope]::CurrentUser
)
$json = [Text.Encoding]::UTF8.GetString($plain) | ConvertFrom-Json
return $json
} catch {
return $null
}
}
# ========== 语义版本比较 (major.minor.patch) ==========
function Compare-SemVer ($local, $remote) {
$lParts = ($local -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ }
$rParts = ($remote -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ }
for ($i = 0; $i -lt [Math]::Max($lParts.Count, $rParts.Count); $i++) {
$l = if ($i -lt $lParts.Count) { $lParts[$i] } else { 0 }
$r = if ($i -lt $rParts.Count) { $rParts[$i] } else { 0 }
if ($r -gt $l) { return 1 }
if ($r -lt $l) { return -1 }
}
return 0
}
# ========== 本地版本号 ==========
function Get-LocalVersion {
$versionFile = Join-Path $ClaudeRoot 'VERSION'
if (Test-Path $versionFile) {
return (Get-Content -Raw $versionFile).Trim()
}
$statsFile = Join-Path $ClaudeRoot 'stats-compiled.json'
if (Test-Path $statsFile) {
try {
$stats = Get-Content -Raw $statsFile | ConvertFrom-Json
return $stats.summary.version
} catch { }
}
return 'unknown'
}
# ========== Basic Auth 头构造 ==========
function Get-AuthHeaders ($cred) {
$pair = "$($cred.user):$($cred.pass)"
$bytes = [Text.Encoding]::UTF8.GetBytes($pair)
$b64 = [Convert]::ToBase64String($bytes)
return @{ Authorization = "Basic $b64" }
}
# ========== 远端版本查询 (Gitea raw, 3s 超时) ==========
function Get-RemoteVersion ($cred, $cfg) {
$repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' }
$apiUrl = "$repoUrl/raw/branch/main/VERSION"
try {
$headers = Get-AuthHeaders $cred
$resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3
return $resp.Content.Trim()
} catch {
return $null
}
}
# ========== 远端最新 tag 查询 (备用) ==========
function Get-RemoteLatestTag ($cred, $cfg) {
$repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' }
$host_ = ([Uri]$repoUrl).Host
$repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/')
$apiUrl = "https://$host_/api/v1/repos/$repoPath/tags?limit=1"
try {
$headers = Get-AuthHeaders $cred
$resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3
$tags = $resp.Content | ConvertFrom-Json
if ($tags.Count -gt 0) { return $tags[0].name }
} catch { }
return $null
}
# ========== 同步核心 (精简版 bookworm-sync.ps1) ==========
function Invoke-OtaSync ($cred, $cfg, $remoteVersion) {
$repoUrl = if ($cfg.repoUrl) { $cfg.repoUrl } else { 'https://code.letcareme.com/bookworm/bookworm-smart-assistant' }
$host_ = ([Uri]$repoUrl).Host
$repoPath = ([Uri]$repoUrl).AbsolutePath.TrimStart('/')
$ref = if ($remoteVersion -and $remoteVersion -match '^v?\d') { $remoteVersion } else { 'main' }
if ($ref -notmatch '^v') { $ref = "v$ref" }
$stageDir = Join-Path $env:TEMP "bw-ota-$([Guid]::NewGuid().ToString().Substring(0,8))"
# 1. Clone (凭证通过 credential manager 传递, 不嵌入 URL)
Write-Ota "下载 $ref ..."
$plainUrl = "https://${host_}/${repoPath}.git"
$credInput = "protocol=https`nhost=${host_}`nusername=$($cred.user)`npassword=$($cred.pass)`n`n"
$credInput | & git credential approve 2>$null
$cloneArgs = @('-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $plainUrl, $stageDir)
& git @cloneArgs 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { throw "clone 失败 (exit $LASTEXITCODE)" }
$gitDir = Join-Path $stageDir '.git'
if (Test-Path $gitDir) { Remove-Item -Recurse -Force $gitDir }
# 2. 验签 (Ed25519)
Write-Ota "验证签名..."
$integrityPath = Join-Path $stageDir 'INTEGRITY.sha256'
$sigPath = Join-Path $stageDir 'INTEGRITY.sha256.sig'
if (-not (Test-Path $integrityPath) -or -not (Test-Path $sigPath)) {
throw "包缺失签名文件"
}
$verifyScript = @'
const fs=require('fs'),crypto=require('crypto');
const d=fs.readFileSync(process.argv[2]),s=Buffer.from(fs.readFileSync(process.argv[3],'utf8').trim(),'hex');
const k=crypto.createPublicKey(fs.readFileSync(process.argv[4],'utf8'));
process.exit(crypto.verify(null,d,k,s)?0:1);
'@
$tmpV = Join-Path $env:TEMP "bw-ota-verify-$([Guid]::NewGuid().ToString().Substring(0,8)).js"
[IO.File]::WriteAllText($tmpV, $verifyScript, [Text.UTF8Encoding]::new($false))
node $tmpV $integrityPath $sigPath $PubKeyFile 2>&1 | Out-Null
$sigOk = $LASTEXITCODE -eq 0
Remove-Item $tmpV -Force -ErrorAction SilentlyContinue
if (-not $sigOk) { throw "Ed25519 验签失败! 包可能被篡改." }
Write-OtaOk "签名验证通过"
# 3. 逐文件哈希
Write-Ota "校验文件完整性..."
$lines = Get-Content $integrityPath
$fail = 0
foreach ($line in $lines) {
if ($line -notmatch '^([a-f0-9]{64})\s{2}(.+)$') { throw "INTEGRITY 格式错误" }
$expected = $Matches[1]; $relPath = $Matches[2]
$abs = Join-Path $stageDir $relPath
if (-not (Test-Path $abs -PathType Leaf)) { continue }
$actual = (Get-FileHash -Path $abs -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $expected) { $fail++ }
}
if ($fail -gt 0) { throw "完整性校验失败: $fail 处哈希不匹配" }
Write-OtaOk "文件完整性校验通过 ($($lines.Count) 个文件)"
# 4. 渲染 settings.template.json
$tplPath = Join-Path $stageDir 'settings.template.json'
if (Test-Path $tplPath) {
$raw = Get-Content -Raw $tplPath
if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
$rootFwd = $ClaudeRoot -replace '\\', '/'
$homeFwd = $env:USERPROFILE -replace '\\', '/'
$rendered = $raw -replace '\{\{CLAUDE_ROOT\}\}', $rootFwd -replace '\{\{HOME\}\}', $homeFwd
$outPath = Join-Path $stageDir 'settings.json'
[IO.File]::WriteAllText($outPath, $rendered, [Text.UTF8Encoding]::new($false))
}
# 5. 保留本机私有
$preserveList = @(
'memory', 'projects', 'sessions', 'session-env', 'session-state', 'sessions.db',
'tasks', 'teams',
'pinned-sessions.json', 'pinned-sessions.json.tmp',
'history.jsonl', 'evolution-log.jsonl',
'.credentials.json', '.bw-token', '.hmac-key', '.skill-cache',
'file-history', 'image-cache', 'paste-cache', 'debug', 'telemetry',
'cache', 'plans', 'plugins', 'shell-snapshots', 'vendor',
'repos', 'backups', 'archives',
'mcp-servers', 'node_modules',
'settings.local.json', 'settings.json.bak.*',
'auto-sync-repos.json', 'scheduled_tasks.lock',
'.bw-ota'
)
$preserved = 0
if (Test-Path $ClaudeRoot) {
foreach ($pat in $preserveList) {
$items = @(Get-ChildItem -Path $ClaudeRoot -Filter $pat -Force -ErrorAction SilentlyContinue)
foreach ($item in $items) {
$target = Join-Path $stageDir $item.Name
if (Test-Path $target) { Remove-Item -Recurse -Force $target }
Copy-Item -Path $item.FullName -Destination $target -Recurse -Force
$preserved++
}
}
}
Write-Ota "已保留 $preserved 项本机数据 (copy)"
# DryRun: 验证到此为止, 不做原子替换
if ($DryRun) {
$fileCount = (Get-ChildItem -Path $stageDir -Recurse -File).Count
Write-OtaOk "[DryRun] 验证通过! staging 含 $fileCount 个文件, 跳过原子替换"
Remove-Item -Recurse -Force $stageDir -ErrorAction SilentlyContinue
return
}
# 6. 备份 + 原子替换 (先备份再替换, 任一步失败不丢数据)
$bakFull = $null
if (Test-Path $ClaudeRoot) {
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
$bakLeaf = ".claude.bak-$ts"
$bakFull = Join-Path (Split-Path -Parent $ClaudeRoot) $bakLeaf
try {
Rename-Item -Path $ClaudeRoot -NewName $bakLeaf -ErrorAction Stop
} catch {
throw "无法备份 .claude (可能被占用): $_"
}
}
try {
Copy-Item -Path $stageDir -Destination $ClaudeRoot -Recurse -Force -ErrorAction Stop
Remove-Item -Path $stageDir -Recurse -Force -ErrorAction SilentlyContinue
} catch {
if ($bakFull -and (Test-Path $bakFull)) {
Remove-Item -Path $ClaudeRoot -Recurse -Force -ErrorAction SilentlyContinue
Rename-Item -Path $bakFull -NewName (Split-Path -Leaf $ClaudeRoot)
Write-OtaWarn "替换失败, 已回滚到原版本"
}
throw "替换失败: $_"
}
# 7. 清理旧备份 (保留最近 3 个)
$parent = Split-Path -Parent $ClaudeRoot
$baks = @(Get-ChildItem -Path $parent -Directory -Filter ".claude.bak-*" -ErrorAction SilentlyContinue | Sort-Object Name -Descending)
if ($baks.Count -gt 3) {
$baks | Select-Object -Skip 3 | ForEach-Object {
Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue
}
}
# 8. 写入 VERSION 到新 .claude
$versionPath = Join-Path $ClaudeRoot 'VERSION'
if (-not (Test-Path $versionPath)) {
[IO.File]::WriteAllText($versionPath, "$remoteVersion`n", [Text.UTF8Encoding]::new($false))
}
Write-OtaOk "更新完成! $ref"
}
# ========== 主流程 (fail-open 包裹) ==========
function Invoke-OtaMain {
try {
# 基础设施检查
if (-not (Test-OtaReady)) {
return
}
$cfg = Read-OtaConfig
if (-not $cfg) { return }
# 冷却检查
if (Test-Cooldown $cfg) {
return
}
# 解密凭证
$cred = Read-Credential
if (-not $cred -or -not $cred.user -or -not $cred.pass) {
Write-OtaWarn "凭证解密失败, 跳过更新检查"
return
}
# 本地 vs 远端版本
$localVer = Get-LocalVersion
if ($DryRun) { Write-Ota "[DryRun 模式] 仅验证, 不替换文件" 'Yellow' }
Write-Ota "当前版本: $localVer"
$remoteVer = Get-RemoteVersion $cred $cfg
if (-not $remoteVer) {
$remoteVer = Get-RemoteLatestTag $cred $cfg
if ($remoteVer) { $remoteVer = $remoteVer -replace '^v', '' }
}
# 更新 lastCheck
$cfg.lastCheck = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
Save-OtaConfig $cfg
if (-not $remoteVer) {
Write-OtaWarn "无法获取远端版本, 跳过"
return
}
$localClean = ($localVer -replace '^v', '').Trim()
$remoteClean = ($remoteVer -replace '^v', '').Trim()
if ($localClean -eq $remoteClean) {
Write-OtaOk "v$localClean 已是最新"
return
}
$cmp = Compare-SemVer $localClean $remoteClean
if ($cmp -le 0) {
Write-OtaOk "v$localClean 已是最新 (远端 v$remoteClean 非更高版本)"
return
}
# 发现新版本 (远端 > 本地)
Write-Host ""
Write-Ota "发现新版本 v$remoteClean (当前 v$localClean)" 'Magenta'
if (-not $SkipConfirm) {
Write-Host "[Bookworm OTA] 按 Enter 更新 / Ctrl+C 跳过: " -ForegroundColor Yellow -NoNewline
try { [void][Console]::ReadLine() }
catch {
Write-Ota "已跳过更新"
return
}
}
Invoke-OtaSync $cred $cfg $remoteClean
Write-Host ""
}
catch {
Write-OtaWarn "更新检查异常: $($_.Exception.Message)"
Write-OtaWarn "跳过更新, 继续启动..."
}
}
Invoke-OtaMain

179
diagnose-tooluse.js Normal file
View File

@ -0,0 +1,179 @@
#!/usr/bin/env node
/**
* Bookworm tool_use 诊断脚本
* 测试中转站是否正确透传 Anthropic tool_use 协议
* 用法: node diagnose-tooluse.js
*/
const https = require('https');
const http = require('http');
const API_KEY = process.env.ANTHROPIC_API_KEY;
const BASE_URL = process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
if (!API_KEY) {
console.error('[FAIL] ANTHROPIC_API_KEY 未设置');
process.exit(1);
}
console.log(`[INFO] 中转站: ${BASE_URL}`);
console.log(`[INFO] API Key: ${API_KEY.substring(0, 8)}...`);
console.log('');
// 测试 1: 非流式 tool_use
async function testToolUse() {
const url = new URL('/v1/messages', BASE_URL);
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const body = JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 200,
tools: [{
name: 'get_disk_info',
description: 'Get disk space information',
input_schema: {
type: 'object',
properties: { drive: { type: 'string', description: 'Drive letter' } },
required: ['drive']
}
}],
tool_choice: { type: 'tool', name: 'get_disk_info' },
messages: [{ role: 'user', content: 'Check disk C:' }]
});
return new Promise((resolve, reject) => {
const req = lib.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01'
}
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve({ status: res.statusCode, body: json });
} catch (e) {
resolve({ status: res.statusCode, raw: data.substring(0, 500) });
}
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
req.write(body);
req.end();
});
}
// 测试 2: 流式 tool_use
async function testStreamToolUse() {
const url = new URL('/v1/messages', BASE_URL);
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const body = JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 200,
stream: true,
tools: [{
name: 'get_disk_info',
description: 'Get disk space information',
input_schema: {
type: 'object',
properties: { drive: { type: 'string', description: 'Drive letter' } },
required: ['drive']
}
}],
tool_choice: { type: 'tool', name: 'get_disk_info' },
messages: [{ role: 'user', content: 'Check disk C:' }]
});
return new Promise((resolve, reject) => {
const req = lib.request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
'anthropic-version': '2023-06-01'
}
}, (res) => {
let data = '';
let hasToolUseStart = false;
let hasInputJsonDelta = false;
res.on('data', chunk => {
const text = chunk.toString();
data += text;
if (text.includes('"type":"tool_use"') || text.includes('"type": "tool_use"')) hasToolUseStart = true;
if (text.includes('input_json_delta')) hasInputJsonDelta = true;
});
res.on('end', () => {
resolve({
status: res.statusCode,
hasToolUseStart,
hasInputJsonDelta,
sample: data.substring(0, 800)
});
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
req.write(body);
req.end();
});
}
(async () => {
// 测试 1: 非流式
console.log('=== 测试 1: 非流式 tool_use ===');
try {
const r = await testToolUse();
if (r.status !== 200) {
console.log(`[FAIL] HTTP ${r.status}`);
console.log(JSON.stringify(r.body || r.raw, null, 2).substring(0, 300));
} else {
const content = r.body?.content || [];
const toolUseBlock = content.find(b => b.type === 'tool_use');
if (toolUseBlock) {
console.log(`[PASS] 收到 tool_use block: name=${toolUseBlock.name}, id=${toolUseBlock.id}`);
console.log(` input: ${JSON.stringify(toolUseBlock.input)}`);
} else {
console.log('[FAIL] 未收到 tool_use block!');
console.log(' content types:', content.map(b => b.type).join(', ') || '(empty)');
console.log(' stop_reason:', r.body?.stop_reason);
if (content[0]?.text) console.log(' text:', content[0].text.substring(0, 200));
}
}
} catch (e) {
console.log(`[FAIL] 请求失败: ${e.message}`);
}
console.log('');
// 测试 2: 流式
console.log('=== 测试 2: 流式 tool_use (Claude Code 实际使用模式) ===');
try {
const r = await testStreamToolUse();
if (r.status !== 200) {
console.log(`[FAIL] HTTP ${r.status}`);
console.log(r.sample?.substring(0, 300));
} else {
console.log(` tool_use block in stream: ${r.hasToolUseStart ? '[PASS]' : '[FAIL]'}`);
console.log(` input_json_delta events: ${r.hasInputJsonDelta ? '[PASS]' : '[FAIL]'}`);
if (!r.hasToolUseStart) {
console.log(' stream sample:', r.sample?.substring(0, 400));
}
}
} catch (e) {
console.log(`[FAIL] 请求失败: ${e.message}`);
}
console.log('');
console.log('=== 诊断结论 ===');
console.log('如果两项测试均 PASS: 中转站 tool_use 正常');
console.log('如果非流式 PASS 流式 FAIL: 中转站流式 SSE 处理有 bug, 联系管理员升级 NewAPI');
console.log('如果均 FAIL: 中转站不支持 Anthropic tool_use, 需开启原生 Anthropic 透传模式');
})();

View File

@ -1,85 +0,0 @@
<!-- DOWNLOAD Panel -->
<section class="panel" data-panel="download" style="display:none">
<div style="max-width:640px;margin:0 auto;padding:2rem 1rem">
<div style="text-align:center;margin-bottom:2rem">
<div style="font-size:3rem;margin-bottom:0.5rem">&#128218;</div>
<h2 style="font-size:1.6rem;margin-bottom:0.3rem">Bookworm <span style="color:var(--accent,#58a6ff)">Portable</span></h2>
<p style="color:var(--text-secondary,#8b949e);font-size:0.95rem">AI 编程助手桌面版 &#8212; 一键安装到你的电脑</p>
</div>
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap;margin-bottom:1.5rem">
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>92</b> Skills</span>
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>18</b> Agents</span>
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>34</b> Hooks</span>
<span style="background:var(--bg-tertiary,#21262d);border:1px solid var(--border,#30363d);border-radius:20px;padding:0.25rem 0.75rem;font-size:0.8rem"><b>AES-256</b> &#21152;&#23494;</span>
</div>
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:12px;padding:1.5rem;text-align:center;margin-bottom:1.5rem">
<h3 style="margin-bottom:0.8rem;font-size:1.1rem">Windows &#26700;&#38754;&#29256;</h3>
<p style="color:var(--text-secondary,#8b949e);font-size:0.9rem;margin-bottom:1.2rem">
&#22312;&#20320;&#30340;&#30005;&#33041;&#19978;&#36816;&#34892; Bookworm&#65292;&#23436;&#25972;&#30340; Claude Code + 97 &#20010;&#19987;&#23478; Skills<br>
&#31163;&#32447;&#37197;&#32622;&#65292;API &#36890;&#36807;&#21152;&#23494;&#20013;&#36716;&#31449;&#36716;&#21457;
</p>
<a id="downloadSetupBtn" href="/api/download/setup" download="Bookworm-Setup.bat"
style="display:inline-block;background:linear-gradient(135deg,#238636,#2ea043);color:#fff;font-size:1rem;font-weight:600;padding:0.75rem 2rem;border-radius:8px;text-decoration:none;cursor:pointer">
&#11015; &#19979;&#36733;&#23433;&#35013;&#31243;&#24207; (4 KB)
</a>
<p style="color:var(--text-secondary,#8b949e);font-size:0.75rem;margin-top:0.5rem">Bookworm-Setup.bat &#8212; &#21452;&#20987;&#21363;&#21487;&#23433;&#35013;</p>
</div>
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:12px;padding:1.2rem 1.5rem;margin-bottom:1.5rem">
<h4 style="margin-bottom:0.8rem;color:var(--accent,#58a6ff)">&#23433;&#35013;&#21482;&#38656; 3 &#27493;</h4>
<div style="display:flex;gap:0.8rem;align-items:flex-start;margin-bottom:0.6rem">
<span style="background:var(--accent,#58a6ff);color:#000;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0">1</span>
<span>&#19979;&#36733;&#19978;&#26041;&#23433;&#35013;&#31243;&#24207;&#65292;&#21452;&#20987;&#36816;&#34892;</span>
</div>
<div style="display:flex;gap:0.8rem;align-items:flex-start;margin-bottom:0.6rem">
<span style="background:var(--accent,#58a6ff);color:#000;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0">2</span>
<span>&#36755;&#20837;&#31649;&#29702;&#21592;&#25552;&#20379;&#30340;&#23494;&#30721;</span>
</div>
<div style="display:flex;gap:0.8rem;align-items:flex-start">
<span style="background:#3fb950;color:#000;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0">&#10003;</span>
<span>&#23436;&#25104;&#65281;&#26700;&#38754;&#20986;&#29616; Bookworm &#22270;&#26631;&#65292;&#21452;&#20987;&#21551;&#21160;</span>
</div>
</div>
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:12px;padding:1.2rem 1.5rem;margin-bottom:1.5rem">
<h4 style="margin-bottom:0.5rem">&#31995;&#32479;&#35201;&#27714;</h4>
<table style="width:100%;font-size:0.85rem;border-collapse:collapse">
<tr style="border-bottom:1px solid var(--border,#30363d)"><td style="padding:0.4rem 0"><b>&#25805;&#20316;&#31995;&#32479;</b></td><td>Windows 10/11</td></tr>
<tr style="border-bottom:1px solid var(--border,#30363d)"><td style="padding:0.4rem 0"><b>Node.js</b></td><td>18+ (<a href="https://nodejs.org" target="_blank" style="color:var(--accent,#58a6ff)">&#19979;&#36733;</a>)</td></tr>
<tr style="border-bottom:1px solid var(--border,#30363d)"><td style="padding:0.4rem 0"><b>Git</b></td><td><a href="https://git-scm.com" target="_blank" style="color:var(--accent,#58a6ff)">&#19979;&#36733;</a></td></tr>
<tr><td style="padding:0.4rem 0"><b>&#32593;&#32476;</b></td><td>&#38656;&#35201;&#20195;&#29702;/VPN (&#22269;&#20869;)</td></tr>
</table>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.8rem;margin-bottom:1.5rem">
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
<div style="font-size:1.5rem;margin-bottom:0.3rem">&#9889;</div>
<div style="font-weight:600;font-size:0.9rem">30 &#31186;&#21551;&#21160;</div>
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">&#23433;&#35013;&#21518;&#27599;&#27425;&#21452;&#20987;&#21363;&#21487;</div>
</div>
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
<div style="font-size:1.5rem;margin-bottom:0.3rem">&#128274;</div>
<div style="font-weight:600;font-size:0.9rem">AES-256 &#21152;&#23494;</div>
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">&#20973;&#35777;&#19981;&#33853;&#30424;&#65292;&#36827;&#31243;&#32423;&#38548;&#31163;</div>
</div>
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
<div style="font-size:1.5rem;margin-bottom:0.3rem">&#128640;</div>
<div style="font-weight:600;font-size:0.9rem">97 &#20010; AI &#19987;&#23478;</div>
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">&#20840;&#26632;&#24320;&#21457;/&#23433;&#20840;/&#26550;&#26500;/&#27979;&#35797;</div>
</div>
<div style="background:var(--bg-secondary,#161b22);border:1px solid var(--border,#30363d);border-radius:8px;padding:1rem;text-align:center">
<div style="font-size:1.5rem;margin-bottom:0.3rem">&#127760;</div>
<div style="font-weight:600;font-size:0.9rem">&#20013;&#36716;&#31449; API</div>
<div style="color:var(--text-secondary,#8b949e);font-size:0.75rem">&#26080;&#38656; Claude &#36134;&#21495;</div>
</div>
</div>
<div style="text-align:center;color:var(--text-secondary,#8b949e);font-size:0.75rem">
Bookworm Portable v1.3 &#8212; &#22914;&#38656;&#24110;&#21161;&#35831;&#32852;&#31995;&#31649;&#29702;&#21592;
</div>
</div>
</section>

View File

@ -1,125 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookworm - 下载安装</title>
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32.png">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0d1117;
color: #e6edf3;
font-family: -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 2rem;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 16px;
padding: 3rem;
max-width: 520px;
width: 100%;
text-align: center;
}
.logo {
font-size: 3rem;
margin-bottom: 1rem;
}
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
h1 span { color: #58a6ff; }
.subtitle { color: #8b949e; margin-bottom: 2rem; font-size: 0.95rem; }
.badges {
display: flex; gap: 0.5rem; justify-content: center;
margin-bottom: 2rem; flex-wrap: wrap;
}
.badge {
background: #0d1117; border: 1px solid #30363d; border-radius: 20px;
padding: 0.25rem 0.75rem; font-size: 0.8rem; color: #8b949e;
}
.badge b { color: #e6edf3; }
.btn {
display: inline-block;
background: linear-gradient(135deg, #238636, #2ea043);
color: white;
font-size: 1.1rem;
font-weight: 600;
padding: 0.9rem 2.5rem;
border-radius: 8px;
text-decoration: none;
border: none;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
margin-bottom: 1rem;
}
.btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(35,134,54,0.4); }
.btn:active { transform: translateY(0); }
.size { color: #8b949e; font-size: 0.8rem; margin-bottom: 2rem; }
.steps {
text-align: left;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 8px;
padding: 1.2rem 1.5rem;
margin-top: 1rem;
font-size: 0.9rem;
line-height: 2;
}
.steps .num {
display: inline-block;
background: #58a6ff;
color: #000;
width: 20px; height: 20px;
border-radius: 50%;
text-align: center;
font-size: 0.75rem;
font-weight: 700;
line-height: 20px;
margin-right: 0.5rem;
}
.steps .dim { color: #8b949e; }
.req {
text-align: left;
margin-top: 1.5rem;
font-size: 0.8rem;
color: #8b949e;
}
.req b { color: #d29922; }
</style>
</head>
<body>
<div class="card">
<div class="logo"><img src="assets/logo-200x200.png" alt="Bookworm" style="width:80px;height:80px;border-radius:18%;"></div>
<h1>Bookworm <span>Portable</span></h1>
<p class="subtitle">AI 编程助手 — 一键安装,即刻使用</p>
<div class="badges">
<span class="badge"><b>92</b> Skills</span>
<span class="badge"><b>18</b> Agents</span>
<span class="badge"><b>34</b> Hooks</span>
<span class="badge"><b>AES-256</b> 加密</span>
</div>
<a class="btn" href="https://code.letcareme.com/bookworm/bookworm-boot/raw/branch/main/Bookworm-Setup.bat" download="Bookworm-Setup.bat">
&#11015; 下载安装程序
</a>
<p class="size">Bookworm-Setup.bat (4 KB) — 双击即可安装</p>
<div class="steps">
<span class="num">1</span> 下载上方 .bat 文件<br>
<span class="num">2</span> 双击运行 <span class="dim">(如提示安全警告,选 "仍要运行")</span><br>
<span class="num">3</span> 输入管理员提供的密码<br>
<span class="num">&#10003;</span> 完成!桌面出现 Bookworm 图标
</div>
<div class="req">
<b>&#9888; 前置要求:</b><br>
&#8226; Node.js (<a href="https://nodejs.org" style="color:#58a6ff">下载</a>) + Git (<a href="https://git-scm.com" style="color:#58a6ff">下载</a>)<br>
&#8226; 代理/VPN 软件 (国内必须,用于首次连接验证)
</div>
</div>
</body>
</html>

View File

@ -1,177 +0,0 @@
<#
.SYNOPSIS
Bookworm Portable - 凭证加密工具
.DESCRIPTION
API Key MCP 凭证加密为 secrets.enc,
存放于 Gitea/USB , 安装时解密为进程级环境变量.
.USAGE
# 交互式创建加密凭证文件
.\encrypt-secrets.ps1
# 从现有 .env 文件加密
.\encrypt-secrets.ps1 -FromFile "C:\path\to\.env"
# 解密验证
.\encrypt-secrets.ps1 -Decrypt
#>
param(
[string]$FromFile,
[switch]$Decrypt
)
$ScriptDir = if ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else { $PWD.Path }
$SecretsEnc = Join-Path $ScriptDir "secrets.enc"
$TempFile = Join-Path $env:TEMP "bw-secrets-$([guid]::NewGuid().ToString('N').Substring(0,8)).tmp"
# 检查 openssl
$opensslCmd = Get-Command openssl -ErrorAction SilentlyContinue
if (-not $opensslCmd) {
# 搜索常见 Git 安装路径
$searchPaths = @(
"C:\Program Files\Git\usr\bin\openssl.exe",
"D:\Git\usr\bin\openssl.exe",
"D:\Git\mingw64\bin\openssl.exe",
"C:\Program Files\Git\mingw64\bin\openssl.exe",
"C:\Program Files (x86)\Git\usr\bin\openssl.exe"
)
$found = $searchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($found) {
$opensslCmd = $found
Write-Host "[INFO] 使用 Git 内置 openssl: $found" -ForegroundColor Gray
}
else {
Write-Host "[ERROR] openssl 未找到。请确认 Git for Windows 已安装。" -ForegroundColor Red
exit 1
}
} else {
$opensslCmd = $opensslCmd.Source
}
# ─── 解密模式 ─────────────────────────────────────────
if ($Decrypt) {
if (-not (Test-Path $SecretsEnc)) {
Write-Host "[ERROR] secrets.enc 不存在" -ForegroundColor Red
exit 1
}
$password = Read-Host "输入主密码" -AsSecureString
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
$plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
Write-Host "`n=== 解密内容 ===" -ForegroundColor Cyan
# 通过 stdin 传入密码,避免进程列表泄露
$plainPwd | & $opensslCmd enc -aes-256-cbc -d -pbkdf2 -iter 600000 -in $SecretsEnc -pass stdin
Write-Host "`n=== 结束 ===" -ForegroundColor Cyan
$plainPwd = $null
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
exit 0
}
# ─── 加密模式 ─────────────────────────────────────────
Write-Host ""
Write-Host " Bookworm Portable - 凭证加密工具" -ForegroundColor Cyan
Write-Host " =================================" -ForegroundColor Cyan
Write-Host ""
try {
if ($FromFile -and (Test-Path $FromFile)) {
Write-Host "从文件加载: $FromFile" -ForegroundColor Gray
$invalidLines = Get-Content $FromFile | Where-Object { $_.Trim() -and $_ -notmatch '^[A-Z][A-Z0-9_]+=.+$' }
if ($invalidLines) {
Write-Host "[WARN] 以下行格式不正确 (应为 KEY=value):" -ForegroundColor Yellow
$invalidLines | ForEach-Object { Write-Host " $_" -ForegroundColor Yellow }
}
Copy-Item $FromFile $TempFile
}
else {
Write-Host "请输入凭证 (key=value 格式, 每行一个, 空行结束):" -ForegroundColor White
Write-Host "常见 Key:" -ForegroundColor Gray
Write-Host " ANTHROPIC_API_KEY=sk-ant-..." -ForegroundColor DarkGray
Write-Host " ANTHROPIC_BASE_URL=https://your-relay.com/v1" -ForegroundColor DarkGray
Write-Host " GITHUB_PERSONAL_ACCESS_TOKEN=ghp_..." -ForegroundColor DarkGray
Write-Host " SLACK_BOT_TOKEN=xoxb-..." -ForegroundColor DarkGray
Write-Host " ATLASSIAN_API_TOKEN=..." -ForegroundColor DarkGray
Write-Host " BROWSERBASE_API_KEY=..." -ForegroundColor DarkGray
Write-Host " BROWSERBASE_PROJECT_ID=..." -ForegroundColor DarkGray
Write-Host " FIRECRAWL_API_KEY=..." -ForegroundColor DarkGray
Write-Host ""
$lines = @()
while ($true) {
$line = Read-Host ">"
if ([string]::IsNullOrWhiteSpace($line)) { break }
if ($line -match '^[A-Z][A-Z0-9_]+=.+$') {
$lines += $line
$key = $line.Substring(0, $line.IndexOf('='))
Write-Host " [+] $key" -ForegroundColor Green
}
else {
Write-Host " [!] 格式不正确,应为 KEY=value" -ForegroundColor Yellow
}
}
if ($lines.Count -eq 0) {
Write-Host "[!] 未输入任何凭证,退出" -ForegroundColor Yellow
exit 0
}
$lines -join "`n" | Set-Content $TempFile -NoNewline
}
# 设置密码并加密
Write-Host ""
$password1 = Read-Host "设置主密码 (用于解密, 至少 12 位)" -AsSecureString
$password2 = Read-Host "确认主密码" -AsSecureString
$bstr1 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password1)
$bstr2 = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password2)
$pwd1 = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr1)
$pwd2 = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr2)
if ($pwd1 -ne $pwd2) {
Write-Host "[ERROR] 密码不匹配" -ForegroundColor Red
exit 1
}
if ($pwd1.Length -lt 12) {
Write-Host "[ERROR] 密码至少 12 位 (推荐 16+ 位混合字符)" -ForegroundColor Red
exit 1
}
# AES-256-CBC 加密, PBKDF2 600000 迭代 (OWASP 2023), 通过 stdin 传入密码
$pwd1 | & $opensslCmd enc -aes-256-cbc -pbkdf2 -iter 600000 -salt -in $TempFile -out $SecretsEnc -pass stdin
# 安全删除临时文件
if (Test-Path $TempFile) {
$bytes = [System.IO.File]::ReadAllBytes($TempFile)
[Array]::Clear($bytes, 0, $bytes.Length)
[System.IO.File]::WriteAllBytes($TempFile, $bytes)
Remove-Item $TempFile -Force
}
# 清除内存
$pwd1 = $null; $pwd2 = $null
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr1)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2)
$size = (Get-Item $SecretsEnc).Length
Write-Host ""
Write-Host " [OK] secrets.enc 已生成 ($size bytes)" -ForegroundColor Green
Write-Host " 加密: AES-256-CBC + PBKDF2 (600000 迭代)" -ForegroundColor Gray
Write-Host " 路径: $SecretsEnc" -ForegroundColor Gray
Write-Host ""
Write-Host " 重要提醒:" -ForegroundColor Yellow
Write-Host " - 主密码无法找回,请牢记" -ForegroundColor Yellow
Write-Host " - 推送到 Gitea bookworm-boot 仓库即可" -ForegroundColor Yellow
Write-Host " - 验证: .\encrypt-secrets.ps1 -Decrypt" -ForegroundColor Yellow
} finally {
# 确保任何退出路径都清理临时文件
if (Test-Path $TempFile -ErrorAction SilentlyContinue) {
Remove-Item $TempFile -Force -ErrorAction SilentlyContinue
}
}

View File

@ -1,788 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookworm Portable for Mac - 保姆式安装手册</title>
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32.png">
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #e6edf3;
--text-dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #bc8cff;
--cyan: #39d2c0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, 'SF Pro Display', 'PingFang SC', 'Helvetica Neue', sans-serif;
line-height: 1.7;
padding: 0;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.header {
background: linear-gradient(135deg, #1a1f35 0%, #0d1117 100%);
border-bottom: 1px solid var(--border);
padding: 3rem 2rem;
text-align: center;
}
.header pre {
color: var(--cyan);
font-size: 0.65rem;
line-height: 1.2;
margin-bottom: 1rem;
display: inline-block;
text-align: left;
}
.header h1 { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; }
.header h1 span { color: var(--accent); }
.header .platform-badge {
display: inline-flex; align-items: center; gap: 0.4rem;
background: rgba(88,166,255,0.15); border: 1px solid rgba(88,166,255,0.3);
border-radius: 20px; padding: 0.3rem 0.9rem; font-size: 0.9rem;
color: var(--accent); margin-bottom: 0.8rem;
}
.header p { color: var(--text-dim); font-size: 1.05rem; }
.badge-row { display: flex; gap: 0.8rem; justify-content: center; margin-top: 1rem; flex-wrap: wrap; }
.badge {
display: inline-flex; align-items: center; gap: 0.4rem;
background: var(--card); border: 1px solid var(--border);
border-radius: 20px; padding: 0.3rem 0.9rem; font-size: 0.85rem; color: var(--text-dim);
}
.badge strong { color: var(--text); }
.platform-switch {
display: flex; gap: 0.6rem; justify-content: center; margin-top: 1.2rem;
}
.platform-switch a {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.5rem 1.2rem; border-radius: 8px; font-size: 0.9rem;
font-weight: 600; text-decoration: none; transition: all 0.2s;
}
.platform-switch .active {
background: var(--accent); color: #000;
}
.platform-switch .inactive {
background: var(--card); color: var(--text-dim);
border: 1px solid var(--border);
}
.platform-switch .inactive:hover {
border-color: var(--accent); color: var(--text); text-decoration: none;
}
.container { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.section { margin-bottom: 2.5rem; }
.section h2 {
font-size: 1.4rem; margin-bottom: 1rem; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 0.6rem;
}
.section h2 .num {
background: var(--accent); color: #000; width: 28px; height: 28px;
border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
font-size: 0.85rem; font-weight: 700; flex-shrink: 0;
}
.code-block {
background: #0d1117; border: 1px solid var(--border); border-radius: 8px;
padding: 1rem 1.2rem; margin: 0.8rem 0; overflow-x: auto; position: relative;
cursor: pointer; transition: border-color 0.2s;
}
.code-block:hover { border-color: var(--accent); }
.code-block::after {
content: "点击复制"; position: absolute; top: 0.4rem; right: 0.6rem;
font-size: 0.7rem; color: var(--text-dim); background: var(--card);
padding: 0.15rem 0.5rem; border-radius: 4px; border: 1px solid var(--border);
opacity: 0; transition: opacity 0.2s;
}
.code-block:hover::after { opacity: 1; }
.code-block.copied::after { content: "已复制!"; color: var(--green); opacity: 1; }
.code-block code {
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Cascadia Code', monospace;
font-size: 0.9rem; line-height: 1.6; color: var(--text);
white-space: pre-wrap; word-break: break-all;
}
.code-block .label {
position: absolute; top: 0.4rem; left: 0.6rem;
font-size: 0.65rem; color: var(--text-dim); background: var(--card);
padding: 0.1rem 0.5rem; border-radius: 4px; border: 1px solid var(--border);
text-transform: uppercase; letter-spacing: 0.5px;
}
.code-block.has-label { padding-top: 2rem; }
.cmd { color: var(--green); }
.flag { color: var(--yellow); }
.url { color: var(--accent); }
.comment { color: var(--text-dim); }
.card {
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: 1.2rem 1.5rem; margin: 0.8rem 0;
}
.card h3 { font-size: 1rem; margin-bottom: 0.5rem; color: var(--accent); }
.card p { color: var(--text-dim); font-size: 0.95rem; }
.step { display: flex; gap: 1rem; margin: 1.2rem 0; align-items: flex-start; }
.step-icon {
width: 40px; height: 40px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; font-weight: 700; flex-shrink: 0; margin-top: 0.1rem;
}
.step-icon.green { background: rgba(63,185,80,0.15); border: 1px solid rgba(63,185,80,0.3); color: var(--green); }
.step-icon.blue { background: rgba(88,166,255,0.15); border: 1px solid rgba(88,166,255,0.3); color: var(--accent); }
.step-icon.yellow { background: rgba(210,153,34,0.15); border: 1px solid rgba(210,153,34,0.3); color: var(--yellow); }
.step-icon.red { background: rgba(248,81,73,0.15); border: 1px solid rgba(248,81,73,0.3); color: var(--red); }
.step-icon.purple { background: rgba(188,140,255,0.15); border: 1px solid rgba(188,140,255,0.3); color: var(--purple); }
.step-content { flex: 1; }
.step-content h4 { font-size: 1.05rem; margin-bottom: 0.3rem; }
.step-content p { color: var(--text-dim); font-size: 0.9rem; }
table { width: 100%; border-collapse: collapse; margin: 0.8rem 0; font-size: 0.9rem; }
th, td { padding: 0.6rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
th { color: var(--text-dim); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
td code { background: var(--bg); padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; border: 1px solid var(--border); }
.alert {
border-radius: 8px; padding: 1rem 1.2rem; margin: 1rem 0;
font-size: 0.9rem; display: flex; gap: 0.8rem; align-items: flex-start;
}
.alert-icon { font-size: 1.2rem; flex-shrink: 0; line-height: 1.5; }
.alert.warning { background: rgba(210,153,34,0.08); border: 1px solid rgba(210,153,34,0.3); }
.alert.danger { background: rgba(248,81,73,0.08); border: 1px solid rgba(248,81,73,0.3); }
.alert.info { background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.3); }
.alert.success { background: rgba(63,185,80,0.08); border: 1px solid rgba(63,185,80,0.3); }
.checklist { list-style: none; padding: 0; }
.checklist li {
padding: 0.5rem 0; padding-left: 2rem; position: relative;
border-bottom: 1px solid rgba(48,54,61,0.5); color: var(--text-dim);
}
.checklist li::before {
content: ""; position: absolute; left: 0; top: 0.65rem;
width: 18px; height: 18px; border: 2px solid var(--border); border-radius: 4px;
}
.checklist li.done::before {
background: var(--green); border-color: var(--green);
content: ""; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
background-size: 14px; background-position: center; background-repeat: no-repeat;
}
.checklist li.done { color: var(--text); }
.checklist li strong { color: var(--text); }
.flow {
display: flex; align-items: center; justify-content: center;
gap: 0; margin: 1.5rem 0; flex-wrap: wrap;
}
.flow-node {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 0.6rem 1rem; font-size: 0.85rem; text-align: center; min-width: 100px;
}
.flow-node.active { border-color: var(--green); box-shadow: 0 0 8px rgba(63,185,80,0.2); }
.flow-arrow { color: var(--text-dim); font-size: 1.2rem; padding: 0 0.3rem; }
.screenshot-note {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 0.8rem 1rem; margin: 0.5rem 0; font-size: 0.85rem; color: var(--text-dim);
border-left: 3px solid var(--yellow);
}
.screenshot-note strong { color: var(--yellow); }
.footer {
text-align: center; padding: 2rem; color: var(--text-dim);
font-size: 0.8rem; border-top: 1px solid var(--border);
}
@media (max-width: 600px) {
.header { padding: 2rem 1rem; }
.header pre { font-size: 0.5rem; }
.header h1 { font-size: 1.5rem; }
.container { padding: 1.5rem 1rem; }
.flow { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
.step { flex-direction: column; gap: 0.5rem; }
}
</style>
</head>
<body>
<div class="header">
<pre> ____ _
| __ ) ___ ___ | | ____ _____ _ __ _ __ ___
| _ \ / _ \ / _ \| |/ /\ \ /\ / / _ \| '__| '_ ` _ \
| |_) | (_) | (_) | < \ V V / (_) | | | | | | | |
|____/ \___/ \___/|_|\_\ \_/\_/ \___/|_| |_| |_| |_|
</pre>
<div class="platform-badge"> macOS Edition</div>
<h1>Bookworm <span>Portable</span> 保姆式安装手册</h1>
<p>从零开始,一步步教你在任意 Mac 电脑上激活 Bookworm</p>
<div class="badge-row">
<span class="badge"><strong>92</strong> Skills</span>
<span class="badge"><strong>18</strong> Agents</span>
<span class="badge"><strong>34</strong> Hooks</span>
<span class="badge"><strong>AES-256</strong> 加密</span>
<span class="badge"><strong>HTTPS</strong> 传输</span>
</div>
<div class="platform-switch">
<a href="/mac" class="active"> macOS</a>
<a href="/" class="inactive"> Windows</a>
</div>
<a href="/Bookworm-Setup.sh" download style="display:inline-block;margin-top:1.2rem;padding:0.7rem 2rem;background:var(--accent);color:#000;font-weight:700;border-radius:8px;font-size:1rem;text-decoration:none;transition:opacity 0.2s" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">&#11015; 下载一键安装脚本</a>
</div>
<div class="container">
<!-- 整体流程 -->
<div class="section">
<h2>整体流程概览</h2>
<div class="card" style="text-align:center;border-color:var(--green);margin-bottom:1.5rem">
<h3 style="color:var(--green);margin-bottom:0.5rem">最快方式:一键安装脚本</h3>
<p style="color:var(--text-dim);font-size:0.9rem;margin-bottom:0.8rem">下载 <strong>Bookworm-Setup.sh</strong> → 在终端运行 → 输入密码 → 完成</p>
<p style="font-size:0.8rem;color:var(--text-dim)">脚本自动检测依赖、安装 Homebrew/Node.js/Git、下载配置、启动 Claude Code</p>
</div>
<p style="text-align:center;color:var(--text-dim);font-size:0.85rem;margin-bottom:0.5rem">手动安装流程:</p>
<div class="flow">
<div class="flow-node">安装依赖<br><small style="color:var(--text-dim)">Homebrew + Node.js</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">安装 Claude Code<br><small style="color:var(--text-dim)">npm 全局安装</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">运行安装脚本<br><small style="color:var(--text-dim)">或 git clone</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">输入密码<br><small style="color:var(--text-dim)">主密码</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node active">开始使用<br><small style="color:var(--green)">Bookworm 激活</small></div>
</div>
<p style="text-align:center;color:var(--text-dim);font-size:0.9rem">首次安装约 10 分钟(含依赖下载),之后每次启动约 5-15 秒</p>
</div>
<!-- ============================================================ -->
<!-- 第一步:安装依赖 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">1</span>安装依赖软件</h2>
<div class="alert danger">
<span class="alert-icon">&#9888;</span>
<div>
<strong>国内必须:代理/VPN 软件</strong><br>
Claude Code 启动时会检查 <code>api.anthropic.com</code>,国内无法直连。<br>
请先安装并启动代理软件ClashX / Surge / V2Ray / 任意 VPN安装脚本会<strong>自动检测</strong>系统代理。<br>
无代理 = Claude Code 无法启动。
</div>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>中转站不走代理</strong><br>
API 中转站 <code>bww.letcareme.com</code> 部署在国内阿里云,<strong>不需要通过代理访问</strong><br>
安装脚本已自动设置 <code>NO_PROXY=bww.letcareme.com,code.letcareme.com</code>,无需手动配置。<br>
如果代理软件有"绕过规则"设置,建议把 <code>*.letcareme.com</code> 加入直连列表。
</div>
</div>
<p>需要安装以下软件。如果已装过可跳到下一步。</p>
<!-- Homebrew -->
<div class="step">
<div class="step-icon blue">A</div>
<div class="step-content">
<h4>安装 HomebrewmacOS 包管理器)</h4>
<p>Homebrew 是 macOS 上最常用的包管理器,后续用它安装 Node.js 和 Git。</p>
</div>
</div>
<p>打开 <strong>终端</strong>(按 <code>&#8984; + 空格</code> 搜索 "终端" 或 "Terminal"),执行:</p>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">/bin/bash</span> -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"</code>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>国内加速</strong><br>
如果下载太慢,可以使用清华镜像:<br>
<code>export HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"</code><br>
<code>export HOMEBREW_CORE_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git"</code><br>
设置后重新执行上面的安装命令。
</div>
</div>
<div class="alert warning">
<span class="alert-icon">&#9888;</span>
<div>
<strong>Apple Silicon (M1/M2/M3/M4) 用户注意!</strong><br>
安装完成后,需要将 Homebrew 添加到 PATH。终端会提示你执行以下命令
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># Apple Silicon Mac 需要执行Intel Mac 不需要)</span>
<span class="cmd">echo</span> 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
<span class="cmd">eval</span> "$(/opt/homebrew/bin/brew shellenv)"</code>
</div>
<p style="margin-top:0.8rem">验证安装成功:</p>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">brew</span> --version <span class="comment"># 应显示 Homebrew 4.x.x</span></code>
</div>
<!-- Node.js -->
<div class="step" style="margin-top:1.5rem">
<div class="step-icon blue">B</div>
<div class="step-content">
<h4>安装 Node.js必须</h4>
<p>用 Homebrew 一行命令安装。</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">brew</span> install node</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">也可以从 <a href="https://nodejs.org/zh-cn" target="_blank">nodejs.org</a> 下载 .pkg 安装包。</p>
<p style="margin-top:0.5rem">验证安装成功:</p>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">node</span> -v <span class="comment"># 应显示 v22.x.x</span>
<span class="cmd">npm</span> -v <span class="comment"># 应显示 10.x.x</span></code>
</div>
<!-- Git -->
<div class="step" style="margin-top:1.5rem">
<div class="step-icon blue">C</div>
<div class="step-content">
<h4>安装 Git必须</h4>
<p>macOS 通常自带 Git如没有则用 Homebrew 安装。</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 检查是否已安装</span>
<span class="cmd">git</span> --version
<span class="comment"># 如果提示安装 Xcode Command Line Tools点击"安装"即可</span>
<span class="comment"># 或者用 Homebrew 安装:</span>
<span class="cmd">brew</span> install git</code>
</div>
</div>
<!-- ============================================================ -->
<!-- 第二步:安装 Claude Code -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">2</span>安装 Claude Code</h2>
<div class="step">
<div class="step-icon green">1</div>
<div class="step-content">
<h4>全局安装</h4>
<p>在终端中执行:</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">安装过程需要几分钟,等待完成即可。如果报权限错误,在前面加 <code>sudo</code></p>
<div class="step">
<div class="step-icon green">2</div>
<div class="step-content">
<h4>验证安装</h4>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">claude</span> --version <span class="comment"># 应显示版本号</span></code>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>不需要登录 Claude 账号!</strong><br>
Bookworm 使用中转站 API安装 Claude Code 后直接进入下一步,不用执行 <code>claude login</code>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 第三步:安装 Bookworm -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">3</span>安装 Bookworm核心步骤</h2>
<div class="step">
<div class="step-icon purple">1</div>
<div class="step-content">
<h4>克隆引导仓库</h4>
<p>在终端中执行以下命令。系统会提示输入用户名和密码。</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">git clone</span> <span class="url">https://code.letcareme.com/bookworm/bookworm-boot.git</span>
<span class="cmd">cd</span> bookworm-boot</code>
</div>
<div class="screenshot-note">
<strong>弹出用户名密码?</strong> 输入管理员提供给你的 Gitea 账号密码。这是 Gitea 的密码,不是主密码。
</div>
<div class="step">
<div class="step-icon purple">2</div>
<div class="step-content">
<h4>运行安装脚本</h4>
<p>在终端中执行安装脚本:</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">bash</span> Bookworm-Setup.sh</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">如果提示权限不足:<code>chmod +x Bookworm-Setup.sh && ./Bookworm-Setup.sh</code></p>
<div class="step">
<div class="step-icon purple">3</div>
<div class="step-content">
<h4>输入主密码</h4>
<p>脚本会提示 <strong>"输入主密码解密凭证"</strong>,输入管理员提供的<strong>主密码</strong>(不是 Gitea 密码),按回车。</p>
<p>密码输入时不显示字符,这是正常的。<strong>输错了可以重试,最多 3 次。</strong></p>
</div>
</div>
<div class="screenshot-note">
<strong>两个密码不要搞混:</strong><br>
<strong>Gitea 密码</strong> = 克隆仓库时输入的,用于下载文件<br>
<strong>主密码</strong> = 解密 API 凭证时输入的,用于启动 Claude Code
</div>
<div class="step">
<div class="step-icon green">4</div>
<div class="step-content">
<h4>等待完成</h4>
<p>脚本会显示步骤进度 [1/8] 到 [8/8],自动完成:</p>
<ul style="color:var(--text-dim);font-size:0.9rem;padding-left:1.2rem;margin-top:0.3rem">
<li>[1/6] 检查依赖 (Homebrew / Node.js / Git / OpenSSL / Claude Code)</li>
<li>[2/6] 自动检测代理 + 设置 NO_PROXY</li>
<li>[3/6] 同步配置 (克隆/更新 92 个 Skills)</li>
<li>[4/6] 解密凭证 (输入主密码) + 渲染配置模板</li>
<li>[5/6] 配置终端别名 (bw / bw-update)</li>
<li>[6/6] 完成 — 可选立即启动</li>
</ul>
</div>
</div>
<div class="alert success">
<span class="alert-icon">&#10003;</span>
<div>
<strong>看到 "Bookworm 就绪" 绿色横幅就说明成功了!</strong><br>
Claude Code 启动后,脚本会验证 Skills/Hooks/配置 完整性,全部 [OK] 后进入 Bookworm 模式。<br>
所有 API 请求通过中转站转发,不需要自己的 Claude 账号。
</div>
</div>
<div class="alert warning">
<span class="alert-icon">&#9888;</span>
<div>
<strong>看到 "原生模式启动" 黄色横幅?</strong><br>
说明 Bookworm 配置不完整。请重新运行安装脚本,或联系管理员。
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 日常使用 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">4</span>日常使用</h2>
<div class="card">
<h3>方法一:终端别名(推荐,最简单)</h3>
<p>安装脚本已自动添加别名到 <code>~/.zshrc</code>,直接在终端输入:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">bw</span> <span class="comment"># 快速启动</span>
<span class="cmd">bw-update</span> <span class="comment"># 同步更新</span></code>
</div>
<div class="card" style="margin-top:1rem">
<h3>方法二:脚本命令</h3>
<p>如果别名不可用,在终端中手动执行:</p>
</div>
<table>
<tr><th>操作</th><th>命令</th><th>说明</th></tr>
<tr>
<td><strong>快速启动</strong></td>
<td><code>bw</code></td>
<td>直接启动 Claude Code + Bookworm</td>
</tr>
<tr>
<td><strong>同步更新</strong></td>
<td><code>bw-update</code></td>
<td>更新 boot + 配置仓库</td>
</tr>
</table>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>启动时显示 "有 N 个新更新可用"</strong><br>
说明管理员更新了 Skills 或 Hooks。执行 <code>bw-update</code> 即可同步。
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 密码说明 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">5</span>密码说明</h2>
<div class="card">
<h3>本系统有两个密码,不要搞混</h3>
<table>
<tr><th>名称</th><th>用途</th><th>何时输入</th></tr>
<tr><td><strong>Gitea 密码</strong></td><td>下载文件(克隆仓库)</td><td>首次安装时 git 弹出要求</td></tr>
<tr><td><strong>主密码</strong></td><td>解密 API 凭证</td><td>每次启动脚本提示输入</td></tr>
</table>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>密码输错了?</strong> 最多可以重试 3 次不用紧张。3 次都错才会退出。
</div>
</div>
<div class="card" style="margin-top:0.8rem">
<h3>本日免密功能</h3>
<p>首次解密成功后,脚本会询问 <strong>"今日内免密启动? (y/n)"</strong></p>
<p><strong>y</strong> 后,当天再次启动无需输入主密码,次日自动过期。</p>
<p style="color:var(--text-dim);font-size:0.85rem;margin-top:0.3rem">凭证缓存在 macOS 钥匙串 (Keychain) 中,仅当前用户可读。</p>
</div>
<div class="alert warning" style="margin-top:0.8rem">
<span class="alert-icon">&#128274;</span>
<div><strong>主密码无法找回</strong> — 忘记后联系管理员重新生成 secrets.enc。</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 使用完毕 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">6</span>使用完毕 — 清理 / 卸载</h2>
<p>在终端中执行清理命令:</p>
<table>
<tr><th>场景</th><th>命令</th><th>说明</th></tr>
<tr>
<td><strong>基础清理</strong></td>
<td><code>rm -rf ~/.claude</code></td>
<td>删除 Bookworm 配置,保留引导仓库供重新安装</td>
</tr>
<tr>
<td><strong>完整恢复</strong></td>
<td><code>rm -rf ~/.claude ~/bookworm-boot</code></td>
<td>删除所有 Bookworm 文件</td>
</tr>
<tr>
<td><strong>深度清理</strong></td>
<td><code>rm -rf ~/.claude ~/bookworm-boot && sed -i '' '/Bookworm Portable/,+2d' ~/.zshrc && git credential-osxkeychain erase &lt;&lt;&lt; "host=code.letcareme.com"</code></td>
<td>完整恢复 + 清除别名 + 清除 Git 凭证</td>
</tr>
</table>
<div class="alert danger">
<span class="alert-icon">&#9888;</span>
<div>
<strong>在他人电脑/公用电脑上务必清理:</strong><br>
执行深度清理命令,确保不留下任何凭证或配置
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 故障排查 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">!</span>常见问题排查</h2>
<div class="card">
<h3>&#10060; brew 命令找不到</h3>
<p><strong>原因:</strong>Homebrew 未添加到 PATHApple Silicon Mac 常见)。</p>
<p><strong>解决:</strong></p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># Apple Silicon (M1/M2/M3/M4)</span>
<span class="cmd">eval</span> "$(/opt/homebrew/bin/brew shellenv)"
<span class="comment"># Intel Mac</span>
<span class="cmd">eval</span> "$(/usr/local/bin/brew shellenv)"</code>
</div>
<div class="card">
<h3>&#10060; npm 全局安装报 Permission denied</h3>
<p><strong>原因:</strong>macOS 默认目录权限限制。</p>
<p><strong>解决方式一(推荐):</strong>修改 npm 全局目录:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">mkdir</span> -p ~/.npm-global
<span class="cmd">npm</span> config set prefix '~/.npm-global'
<span class="cmd">echo</span> 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
<span class="cmd">source</span> ~/.zshrc
<span class="comment"># 然后重新安装</span>
<span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">解决方式二:在命令前加 <code>sudo</code>(简单但不推荐长期使用)。</p>
<div class="card">
<h3>&#10060; git clone 失败 / 认证失败</h3>
<p><strong>解决步骤:</strong></p>
<ol style="color:var(--text-dim);padding-left:1.5rem;margin-top:0.3rem">
<li>浏览器打开 <code>https://code.letcareme.com</code> 确认网站可访问</li>
<li>确认用户名密码正确(区分大小写)</li>
<li>如果 macOS 弹出钥匙串对话框,点"始终允许"</li>
<li>检查网络是否需要代理</li>
</ol>
</div>
<div class="card">
<h3>&#10060; 解密凭证失败 / 主密码错误</h3>
<p><strong>原因:</strong>主密码区分大小写,且无法找回。</p>
<p><strong>解决:</strong>仔细检查密码是否正确。如确认忘记,联系管理员重新生成 <code>secrets.enc</code></p>
</div>
<div class="card">
<h3>&#10060; openssl 版本不兼容</h3>
<p><strong>原因:</strong>macOS 自带 LibreSSL部分加密参数可能不同。</p>
<p><strong>解决:</strong>安装 OpenSSL</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">brew</span> install openssl
<span class="comment"># 脚本会自动检测 Homebrew 安装的 openssl 路径</span></code>
</div>
<div class="card">
<h3>&#10060; ECONNRESET / "Unable to connect to API"</h3>
<p><strong>原因:</strong>代理软件把国内中转站 <code>bww.letcareme.com</code> 的流量也走了国际线路。</p>
<p><strong>解决:</strong>手动设置 NO_PROXY 后重试:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 设置中转站直连(不走代理)</span>
<span class="cmd">export</span> NO_PROXY="bww.letcareme.com,code.letcareme.com"
<span class="comment"># 重新启动</span>
<span class="cmd">bw</span></code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">或在代理软件中将 <code>*.letcareme.com</code> 加入直连规则。</p>
<div class="card">
<h3>&#10060; "Not logged in" / 直接运行 claude 报错</h3>
<p><strong>原因:</strong>API 凭证是进程级环境变量,只在安装脚本启动的进程中有效。</p>
<p><strong>解决:</strong><strong>不要直接运行 <code>claude</code></strong>,必须通过以下方式启动:</p>
<ul style="color:var(--text-dim);font-size:0.9rem;padding-left:1.2rem;margin-top:0.3rem">
<li>终端输入 <code>bw</code>(推荐)</li>
<li><code>cd ~/bookworm-boot && bash Bookworm-Setup.sh</code></li>
</ul>
</div>
<div class="card">
<h3>&#10060; 安装包下载太慢</h3>
<p><strong>解决:</strong>设置淘宝镜像:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># npm 淘宝镜像</span>
<span class="cmd">npm</span> config set registry https://registry.npmmirror.com
<span class="comment"># Homebrew 清华镜像</span>
<span class="cmd">export</span> HOMEBREW_BREW_GIT_REMOTE="https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git"
<span class="comment"># 然后重新安装</span>
<span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
</div>
<div class="card">
<h3>&#10060; 需要自己的 Claude 账号吗?</h3>
<p><strong>不需要。</strong>所有 API 请求通过中转站转发,消耗中转站额度。目标机不需要任何 Anthropic 账号或订阅。</p>
</div>
</div>
<!-- ============================================================ -->
<!-- 安装检查清单 -->
<!-- ============================================================ -->
<div class="section">
<h2>安装检查清单</h2>
<p style="color:var(--text-dim);margin-bottom:0.5rem">逐项确认,全部打勾即可开始使用:</p>
<ul class="checklist">
<li><strong>Homebrew 已安装</strong><code>brew --version</code> 显示版本号</li>
<li><strong>Node.js 已安装</strong><code>node -v</code> 显示版本号</li>
<li><strong>Git 已安装</strong><code>git --version</code> 显示版本号</li>
<li><strong>npm 可用</strong><code>npm -v</code> 显示版本号</li>
<li><strong>Claude Code 已安装</strong><code>claude --version</code> 显示版本号</li>
<li><strong>已获取 Gitea 账号密码</strong> — 管理员提供</li>
<li><strong>已获取主密码</strong> — 管理员提供(用于解密 API 凭证)</li>
<li><strong>能访问 code.letcareme.com</strong> — 浏览器打开确认</li>
<li><strong>代理/VPN 已启动</strong> — 国内必须,脚本自动检测 (ClashX/Surge/V2Ray 等)</li>
</ul>
</div>
<!-- ============================================================ -->
<!-- 快速参考卡片 -->
<!-- ============================================================ -->
<div class="section">
<h2>快速参考</h2>
<table>
<tr><th>操作</th><th>快捷方式</th><th>完整命令</th></tr>
<tr><td>首次安装</td><td>git clone + <code>bash Bookworm-Setup.sh</code></td><td><code>cd ~/bookworm-boot && bash Bookworm-Setup.sh</code></td></tr>
<tr><td>快速启动</td><td><code>bw</code></td><td><code>NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude</code></td></tr>
<tr><td>同步更新</td><td><code>bw-update</code></td><td><code>cd ~/bookworm-boot && git pull && cd ~/.claude && git pull</code></td></tr>
<tr><td>基础清理</td><td colspan="2"><code>rm -rf ~/.claude</code></td></tr>
<tr><td>完整恢复</td><td colspan="2"><code>rm -rf ~/.claude ~/bookworm-boot</code></td></tr>
<tr><td>深度清理</td><td colspan="2"><code>rm -rf ~/.claude ~/bookworm-boot && sed -i '' '/Bookworm/,+2d' ~/.zshrc</code></td></tr>
</table>
</div>
<!-- ============================================================ -->
<!-- 安全须知 -->
<!-- ============================================================ -->
<div class="section">
<h2>安全须知</h2>
<table>
<tr><th>特性</th><th>规格</th></tr>
<tr><td>凭证加密</td><td>AES-256-CBC + PBKDF2 (600,000 迭代)</td></tr>
<tr><td>传输加密</td><td>HTTPS (TLS 1.2+, Let's Encrypt 证书)</td></tr>
<tr><td>凭证存储</td><td>进程级环境变量 + 可选本日缓存 (macOS Keychain, 当日 23:59 过期)</td></tr>
<tr><td>登录保护</td><td>fail2ban (5 次失败/小时 → 封禁 24 小时)</td></tr>
</table>
<div class="alert warning" style="margin-top:1rem">
<span class="alert-icon">&#128274;</span>
<div>
<strong>主密码无法找回</strong> — 请妥善保管。忘记后需管理员重新生成加密凭证。
</div>
</div>
</div>
</div>
<div class="footer">
Bookworm Portable v1.5 — macOS 保姆式安装手册<br>
&copy; 2026 Bookworm Smart Assistant
</div>
<script>
function copyCode(el) {
const code = el.querySelector('code').innerText;
navigator.clipboard.writeText(code).then(() => {
el.classList.add('copied');
setTimeout(() => el.classList.remove('copied'), 1500);
});
}
</script>
</body>
</html>

View File

@ -1,787 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bookworm Portable - 保姆式安装手册</title>
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32.png">
<style>
:root {
--bg: #0d1117;
--card: #161b22;
--border: #30363d;
--text: #e6edf3;
--text-dim: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--yellow: #d29922;
--red: #f85149;
--purple: #bc8cff;
--cyan: #39d2c0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
line-height: 1.7;
padding: 0;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.header {
background: linear-gradient(135deg, #1a1f35 0%, #0d1117 100%);
border-bottom: 1px solid var(--border);
padding: 3rem 2rem;
text-align: center;
}
.header pre {
color: var(--cyan);
font-size: 0.65rem;
line-height: 1.2;
margin-bottom: 1rem;
display: inline-block;
text-align: left;
}
.header h1 { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; }
.header h1 span { color: var(--accent); }
.header p { color: var(--text-dim); font-size: 1.05rem; }
.badge-row { display: flex; gap: 0.8rem; justify-content: center; margin-top: 1rem; flex-wrap: wrap; }
.badge {
display: inline-flex; align-items: center; gap: 0.4rem;
background: var(--card); border: 1px solid var(--border);
border-radius: 20px; padding: 0.3rem 0.9rem; font-size: 0.85rem; color: var(--text-dim);
}
.badge strong { color: var(--text); }
.container { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.section { margin-bottom: 2.5rem; }
.section h2 {
font-size: 1.4rem; margin-bottom: 1rem; padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 0.6rem;
}
.section h2 .num {
background: var(--accent); color: #000; width: 28px; height: 28px;
border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
font-size: 0.85rem; font-weight: 700; flex-shrink: 0;
}
.code-block {
background: #0d1117; border: 1px solid var(--border); border-radius: 8px;
padding: 1rem 1.2rem; margin: 0.8rem 0; overflow-x: auto; position: relative;
cursor: pointer; transition: border-color 0.2s;
}
.code-block:hover { border-color: var(--accent); }
.code-block::after {
content: "点击复制"; position: absolute; top: 0.4rem; right: 0.6rem;
font-size: 0.7rem; color: var(--text-dim); background: var(--card);
padding: 0.15rem 0.5rem; border-radius: 4px; border: 1px solid var(--border);
opacity: 0; transition: opacity 0.2s;
}
.code-block:hover::after { opacity: 1; }
.code-block.copied::after { content: "已复制!"; color: var(--green); opacity: 1; }
.code-block code {
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
font-size: 0.9rem; line-height: 1.6; color: var(--text);
white-space: pre-wrap; word-break: break-all;
}
.code-block .label {
position: absolute; top: 0.4rem; left: 0.6rem;
font-size: 0.65rem; color: var(--text-dim); background: var(--card);
padding: 0.1rem 0.5rem; border-radius: 4px; border: 1px solid var(--border);
text-transform: uppercase; letter-spacing: 0.5px;
}
.code-block.has-label { padding-top: 2rem; }
.cmd { color: var(--green); }
.flag { color: var(--yellow); }
.url { color: var(--accent); }
.comment { color: var(--text-dim); }
.card {
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: 1.2rem 1.5rem; margin: 0.8rem 0;
}
.card h3 { font-size: 1rem; margin-bottom: 0.5rem; color: var(--accent); }
.card p { color: var(--text-dim); font-size: 0.95rem; }
.step { display: flex; gap: 1rem; margin: 1.2rem 0; align-items: flex-start; }
.step-icon {
width: 40px; height: 40px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 1.2rem; font-weight: 700; flex-shrink: 0; margin-top: 0.1rem;
}
.step-icon.green { background: rgba(63,185,80,0.15); border: 1px solid rgba(63,185,80,0.3); color: var(--green); }
.step-icon.blue { background: rgba(88,166,255,0.15); border: 1px solid rgba(88,166,255,0.3); color: var(--accent); }
.step-icon.yellow { background: rgba(210,153,34,0.15); border: 1px solid rgba(210,153,34,0.3); color: var(--yellow); }
.step-icon.red { background: rgba(248,81,73,0.15); border: 1px solid rgba(248,81,73,0.3); color: var(--red); }
.step-icon.purple { background: rgba(188,140,255,0.15); border: 1px solid rgba(188,140,255,0.3); color: var(--purple); }
.step-content { flex: 1; }
.step-content h4 { font-size: 1.05rem; margin-bottom: 0.3rem; }
.step-content p { color: var(--text-dim); font-size: 0.9rem; }
table { width: 100%; border-collapse: collapse; margin: 0.8rem 0; font-size: 0.9rem; }
th, td { padding: 0.6rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
th { color: var(--text-dim); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
td code { background: var(--bg); padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; border: 1px solid var(--border); }
.alert {
border-radius: 8px; padding: 1rem 1.2rem; margin: 1rem 0;
font-size: 0.9rem; display: flex; gap: 0.8rem; align-items: flex-start;
}
.alert-icon { font-size: 1.2rem; flex-shrink: 0; line-height: 1.5; }
.alert.warning { background: rgba(210,153,34,0.08); border: 1px solid rgba(210,153,34,0.3); }
.alert.danger { background: rgba(248,81,73,0.08); border: 1px solid rgba(248,81,73,0.3); }
.alert.info { background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.3); }
.alert.success { background: rgba(63,185,80,0.08); border: 1px solid rgba(63,185,80,0.3); }
.checklist { list-style: none; padding: 0; }
.checklist li {
padding: 0.5rem 0; padding-left: 2rem; position: relative;
border-bottom: 1px solid rgba(48,54,61,0.5); color: var(--text-dim);
}
.checklist li::before {
content: ""; position: absolute; left: 0; top: 0.65rem;
width: 18px; height: 18px; border: 2px solid var(--border); border-radius: 4px;
}
.checklist li.done::before {
background: var(--green); border-color: var(--green);
content: ""; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
background-size: 14px; background-position: center; background-repeat: no-repeat;
}
.checklist li.done { color: var(--text); }
.checklist li strong { color: var(--text); }
.flow {
display: flex; align-items: center; justify-content: center;
gap: 0; margin: 1.5rem 0; flex-wrap: wrap;
}
.flow-node {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 0.6rem 1rem; font-size: 0.85rem; text-align: center; min-width: 100px;
}
.flow-node.active { border-color: var(--green); box-shadow: 0 0 8px rgba(63,185,80,0.2); }
.flow-arrow { color: var(--text-dim); font-size: 1.2rem; padding: 0 0.3rem; }
.screenshot-note {
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
padding: 0.8rem 1rem; margin: 0.5rem 0; font-size: 0.85rem; color: var(--text-dim);
border-left: 3px solid var(--yellow);
}
.screenshot-note strong { color: var(--yellow); }
.footer {
text-align: center; padding: 2rem; color: var(--text-dim);
font-size: 0.8rem; border-top: 1px solid var(--border);
}
@media (max-width: 600px) {
.header { padding: 2rem 1rem; }
.header pre { font-size: 0.5rem; }
.header h1 { font-size: 1.5rem; }
.container { padding: 1.5rem 1rem; }
.flow { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
.step { flex-direction: column; gap: 0.5rem; }
}
</style>
</head>
<body>
<div class="header">
<pre>
____ _
| __ ) ___ ___ | | ____ _____ _ __ _ __ ___
| _ \ / _ \ / _ \| |/ /\ \ /\ / / _ \| '__| '_ ` _ \
| |_) | (_) | (_) | < \ V V / (_) | | | | | | | |
|____/ \___/ \___/|_|\_\ \_/\_/ \___/|_| |_| |_| |_|
</pre>
<h1>Bookworm <span>Portable</span> 保姆式安装手册</h1>
<p>从零开始,一步步教你在任意 Windows 电脑上激活 Bookworm</p>
<div class="badge-row">
<span class="badge"><strong>92</strong> Skills</span>
<span class="badge"><strong>18</strong> Agents</span>
<span class="badge"><strong>34</strong> Hooks</span>
<span class="badge"><strong>AES-256</strong> 加密</span>
<span class="badge"><strong>NDA</strong> 技术保密</span>
</div>
<a href="/Bookworm-Setup.bat" download style="display:inline-block;margin-top:1.2rem;padding:0.7rem 2rem;background:var(--accent);color:#000;font-weight:700;border-radius:8px;font-size:1rem;text-decoration:none;transition:opacity 0.2s" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">&#11015; 下载一键安装器</a>
</div>
<div class="container">
<!-- 整体流程 -->
<div class="section">
<h2>整体流程概览</h2>
<div class="card" style="text-align:center;border-color:var(--green);margin-bottom:1.5rem">
<h3 style="color:var(--green);margin-bottom:0.5rem">最快方式:一键安装器</h3>
<p style="color:var(--text-dim);font-size:0.9rem;margin-bottom:0.8rem">获取 <strong>Bookworm-Setup.bat</strong> (4KB) &#8594; 双击运行 &#8594; 输入密码 &#8594; 完成</p>
<p style="font-size:0.8rem;color:var(--text-dim)">安装器自动检测依赖、下载配置、创建桌面快捷方式、启动 Claude Code</p>
</div>
<p style="text-align:center;color:var(--text-dim);font-size:0.85rem;margin-bottom:0.5rem">手动安装流程:</p>
<div class="flow">
<div class="flow-node">安装依赖<br><small style="color:var(--text-dim)">Node.js + Git</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">安装 Claude Code<br><small style="color:var(--text-dim)">npm 全局安装</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">双击安装器<br><small style="color:var(--text-dim)">或 git clone</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node">输入密码<br><small style="color:var(--text-dim)">主密码</small></div>
<span class="flow-arrow">&#10132;</span>
<div class="flow-node active">开始使用<br><small style="color:var(--green)">Bookworm 激活</small></div>
</div>
<p style="text-align:center;color:var(--text-dim);font-size:0.9rem">首次安装约 10 分钟(含依赖下载),之后每次双击启动约 10-30 秒</p>
</div>
<!-- ============================================================ -->
<!-- 第一步:安装依赖 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">1</span>安装依赖软件</h2>
<div class="alert danger">
<span class="alert-icon">&#9888;</span>
<div>
<strong>国内必须:代理/VPN 软件</strong><br>
Claude Code 启动时会检查 <code>api.anthropic.com</code>,国内无法直连。<br>
请先安装并启动代理软件Clash / V2Ray / 快柠檬 / 任意 VPN安装脚本会<strong>自动检测</strong>系统代理。<br>
无代理 = Claude Code 无法启动。
</div>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>中转站不走代理</strong><br>
API 中转站 <code>bww.letcareme.com</code> 部署在国内阿里云,<strong>不需要通过代理访问</strong><br>
安装脚本已自动设置 <code>NO_PROXY=bww.letcareme.com,code.letcareme.com</code>,无需手动配置。<br>
如果代理软件有"绕过规则"设置,建议把 <code>*.letcareme.com</code> 加入直连列表。
</div>
</div>
<p>需要安装以下软件。如果已装过可跳到下一步。</p>
<!-- Node.js -->
<div class="step">
<div class="step-icon blue">A</div>
<div class="step-content">
<h4>安装 Node.js必须</h4>
<p>去官网下载 LTS 版本安装包,双击安装,一路 Next 即可。</p>
</div>
</div>
<div class="card">
<h3>方式一:官网下载(推荐)</h3>
<p>打开浏览器访问 <a href="https://nodejs.org/zh-cn" target="_blank">https://nodejs.org</a>,点击绿色的 <strong>"LTS 推荐"</strong> 按钮下载,双击 .msi 文件安装,全部默认 Next。</p>
</div>
<div class="card">
<h3>方式二PowerShell 命令安装</h3>
<p>右键开始菜单 → 选择 <strong>"PowerShell (管理员)"</strong><strong>"终端 (管理员)"</strong>,然后执行:</p>
</div>
<div class="code-block has-label" onclick="copyCode(this)">
<span class="label">PowerShell (管理员)</span>
<code><span class="comment"># 下载 Node.js 安装包</span>
Invoke-WebRequest -Uri "https://nodejs.org/dist/v22.15.0/node-v22.15.0-x64.msi" -OutFile "$env:TEMP\node-install.msi"
<span class="comment"># 运行安装(会弹出安装向导,一路 Next</span>
Start-Process msiexec.exe -ArgumentList "/i $env:TEMP\node-install.msi" -Wait</code>
</div>
<div class="alert warning">
<span class="alert-icon">&#9888;</span>
<div>
<strong>安装完成后必须重开 PowerShell</strong><br>
关闭当前 PowerShell 窗口,重新打开一个新的,否则 <code>node</code><code>npm</code> 命令找不到。
</div>
</div>
<p style="margin-top:0.8rem">验证安装成功:</p>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">node</span> -v <span class="comment"># 应显示 v22.x.x</span>
<span class="cmd">npm</span> -v <span class="comment"># 应显示 10.x.x</span></code>
</div>
<div class="screenshot-note">
<strong>如果 npm 报 "执行策略" 错误?</strong> 执行以下命令后重试:<br>
<code>Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned</code><br>
提示确认时输入 <strong>Y</strong> 回车。
</div>
<!-- Git -->
<div class="step" style="margin-top:1.5rem">
<div class="step-icon blue">B</div>
<div class="step-content">
<h4>安装 Git必须</h4>
<p>去官网下载安装,全部默认设置即可。</p>
</div>
</div>
<div class="card">
<p>打开 <a href="https://git-scm.com/download/win" target="_blank">https://git-scm.com/download/win</a>,下载 <strong>"64-bit Git for Windows Setup"</strong>,双击安装,全部 Next。</p>
</div>
<p>验证:</p>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">git</span> --version <span class="comment"># 应显示 git version 2.x.x</span></code>
</div>
<!-- PowerShell 7 -->
<div class="step" style="margin-top:1.5rem">
<div class="step-icon yellow">C</div>
<div class="step-content">
<h4>安装 PowerShell 7推荐</h4>
<p>Windows 自带的 PowerShell 5.1 有中文兼容问题,建议升级到 7。</p>
</div>
</div>
<div class="code-block has-label" onclick="copyCode(this)">
<span class="label">PowerShell (管理员)</span>
<code><span class="cmd">winget</span> install Microsoft.PowerShell</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">如果没有 winget<a href="https://github.com/PowerShell/PowerShell/releases" target="_blank">GitHub Releases</a> 下载 .msi 安装包。安装后用 <code>pwsh</code> 命令启动新版 PowerShell。</p>
</div>
<!-- ============================================================ -->
<!-- 第二步:安装 Claude Code -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">2</span>安装 Claude Code</h2>
<div class="step">
<div class="step-icon green">1</div>
<div class="step-content">
<h4>全局安装</h4>
<p>在 PowerShell 中执行(不需要管理员权限):</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">安装过程需要几分钟,等待完成即可。</p>
<div class="step">
<div class="step-icon green">2</div>
<div class="step-content">
<h4>验证安装</h4>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">claude</span> --version <span class="comment"># 应显示版本号</span></code>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>不需要登录 Claude 账号!</strong><br>
Bookworm 使用中转站 API安装 Claude Code 后直接进入下一步,不用执行 <code>claude login</code>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 第三步:安装 Bookworm -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">3</span>安装 Bookworm核心步骤</h2>
<div class="step">
<div class="step-icon purple">1</div>
<div class="step-content">
<h4>克隆引导仓库</h4>
<p>在 PowerShell 中执行以下命令。系统会提示输入用户名和密码。</p>
</div>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">git clone</span> <span class="url">https://code.letcareme.com/bookworm/bookworm-boot.git</span>
<span class="cmd">cd</span> bookworm-boot</code>
</div>
<div class="screenshot-note">
<strong>弹出用户名密码?</strong> 输入管理员提供给你的 Gitea 账号密码。这是 Gitea 的密码,不是主密码。
</div>
<div class="step">
<div class="step-icon purple">2</div>
<div class="step-content">
<h4>双击运行安装脚本</h4>
<p>双击文件夹里的 <strong>更新并启动Bookworm.bat</strong>,脚本会自动完成所有配置。</p>
</div>
</div>
<div class="card">
<h3>或者用命令行运行</h3>
<p>如果双击 .bat 不起作用,在 PowerShell 中手动执行:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">没有 pwsh 可用 <code>powershell</code> 替代。</p>
<div class="step">
<div class="step-icon purple">3</div>
<div class="step-content">
<h4>输入主密码</h4>
<p>脚本会提示 <strong>"输入主密码解密凭证"</strong>,输入管理员提供的<strong>主密码</strong>(不是 Gitea 密码),按回车。</p>
<p>密码输入时不显示字符,这是正常的。<strong>输错了可以重试,最多 3 次。</strong></p>
</div>
</div>
<div class="screenshot-note">
<strong>两个密码不要搞混:</strong><br>
<strong>Gitea 密码</strong> = 克隆仓库时输入的,用于下载文件<br>
<strong>主密码</strong> = 解密 API 凭证时输入的,用于启动 Claude Code
</div>
<div class="step">
<div class="step-icon green">4</div>
<div class="step-content">
<h4>等待完成</h4>
<p>脚本会显示步骤进度 [1/9] 到 [9/9],自动完成:</p>
<ul style="color:var(--text-dim);font-size:0.9rem;padding-left:1.2rem;margin-top:0.3rem">
<li>[1/9] 前置检查 (Claude Code / Node.js / Git)</li>
<li>[2/9] 自动检测代理 + 设置 NO_PROXY</li>
<li>[3/9] 解密凭证 (输入主密码)</li>
<li>[4/9] 同步配置 (下载专家技能库)</li>
<li>[5/9] 完整性校验 (SHA256 哈希验证)</li>
<li>[6/9] 渲染配置模板</li>
<li>[7/9] 初始化本地目录</li>
<li>[8/9] Bookworm 系统验证 + MCP 检查</li>
<li>[9/9] 启动 Claude Code</li>
</ul>
</div>
</div>
<div class="alert success">
<span class="alert-icon">&#10003;</span>
<div>
<strong>看到 "Bookworm 就绪" 绿色横幅就说明成功了!</strong><br>
Claude Code 启动后,脚本会验证配置完整性,全部 [OK] 后进入 Bookworm 模式。<br>
所有 API 请求通过中转站转发,不需要自己的 Claude 账号。
</div>
</div>
<div class="alert warning">
<span class="alert-icon">&#9888;</span>
<div>
<strong>看到 "原生模式启动" 黄色横幅?</strong><br>
说明 Bookworm 配置不完整。请不加 -StartOnly 重新运行安装脚本,或联系管理员。
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 日常使用 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">4</span>日常使用</h2>
<div class="card">
<h3>方法一:双击 .bat 文件(推荐,最简单)</h3>
<p>bookworm-boot 文件夹里有两个 .bat 文件:</p>
</div>
<table>
<tr><th>文件</th><th>作用</th><th>适用场景</th></tr>
<tr>
<td><strong>启动Bookworm.bat</strong></td>
<td>快速启动,不更新配置</td>
<td>每天日常使用</td>
</tr>
<tr>
<td><strong>更新并启动Bookworm.bat</strong></td>
<td>先同步最新 Skills 再启动</td>
<td>管理员通知有更新时</td>
</tr>
</table>
<p style="color:var(--text-dim);font-size:0.85rem;margin-top:0.5rem">双击即可,无需打开 PowerShell无需记命令。脚本会自动检测 PowerShell 7/5.1。</p>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>启动时显示 "有新更新可用"</strong><br>
说明管理员推送了更新。双击 <strong>更新并启动Bookworm.bat</strong> 即可同步。
</div>
</div>
<div class="card" style="margin-top:1rem">
<h3>方法二:命令行(备用)</h3>
<p>如果 .bat 文件无法运行,在 PowerShell 中手动执行:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 快速启动</span>
<span class="cmd">cd</span> bookworm-boot
<span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1 <span class="flag">-StartOnly</span>
<span class="comment"># 同步更新后启动</span>
<span class="cmd">cd</span> bookworm-boot
<span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1</code>
</div>
</div>
<!-- ============================================================ -->
<!-- 使用完毕 -->
<!-- ============================================================ -->
<!-- ============================================================ -->
<!-- 密码说明 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">5</span>密码说明</h2>
<div class="card">
<h3>本系统有两个密码,不要搞混</h3>
<table>
<tr><th>名称</th><th>用途</th><th>何时输入</th></tr>
<tr><td><strong>Gitea 密码</strong></td><td>下载文件(克隆仓库)</td><td>首次安装时 git 弹出要求</td></tr>
<tr><td><strong>主密码</strong></td><td>解密 API 凭证</td><td>每次启动脚本提示输入</td></tr>
</table>
</div>
<div class="alert info">
<span class="alert-icon">&#128161;</span>
<div>
<strong>密码输错了?</strong> 最多可以重试 3 次不用紧张。3 次都错才会退出。
</div>
</div>
<div class="card" style="margin-top:0.8rem">
<h3>本日免密功能</h3>
<p>首次解密成功后,脚本会询问 <strong>"今日内免密启动? (y/n)"</strong></p>
<p><strong>y</strong> 后,当天再次启动无需输入主密码,次日自动过期。</p>
<p style="color:var(--text-dim);font-size:0.85rem;margin-top:0.3rem">凭证缓存在 Windows Credential Manager 中DPAPI 加密,仅当前用户可读)。</p>
</div>
<div class="alert warning" style="margin-top:0.8rem">
<span class="alert-icon">&#128274;</span>
<div><strong>主密码无法找回</strong> &#8212; 忘记后联系管理员重新生成 secrets.enc。</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 使用完毕 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">6</span>使用完毕 &mdash; 清理 / 卸载</h2>
<div class="card" style="border-color:var(--green)">
<h3 style="color:var(--green)">最简单:双击 卸载Bookworm.bat</h3>
<p>bookworm-boot 文件夹里的 <strong>卸载Bookworm.bat</strong>,双击即可一键完整卸载:终止进程 + 清除凭证 + 恢复原始配置 + 删除桌面快捷方式。</p>
</div>
<p style="margin-top:1rem;color:var(--text-dim);font-size:0.9rem">或者用命令行精细控制:</p>
<table>
<tr><th>场景</th><th>命令</th><th>说明</th></tr>
<tr>
<td><strong>基础清理</strong></td>
<td><code>pwsh -File stop.ps1</code></td>
<td>清除环境变量,保留配置供下次快速启动</td>
</tr>
<tr>
<td><strong>完整恢复</strong></td>
<td><code>pwsh -File stop.ps1 -Restore</code></td>
<td>删除 Bookworm恢复电脑原始状态</td>
</tr>
<tr>
<td><strong>深度清理</strong></td>
<td><code>pwsh -File stop.ps1 -Restore -Deep</code></td>
<td>完整恢复 + 清除历史 + 清除 Git/凭证缓存</td>
</tr>
</table>
<div class="alert danger">
<span class="alert-icon">&#9888;</span>
<div>
<strong>在他人电脑/公用电脑上务必卸载:</strong><br>
双击 <strong>卸载Bookworm.bat</strong> 或执行 <code>pwsh -File stop.ps1 -Restore -Deep</code>
</div>
</div>
</div>
<!-- ============================================================ -->
<!-- 故障排查 -->
<!-- ============================================================ -->
<div class="section">
<h2><span class="num">!</span>常见问题排查</h2>
<div class="card">
<h3>&#10060; 输入 node -v 或 npm -v 提示 "无法识别"</h3>
<p><strong>原因:</strong>安装 Node.js 后没有重开 PowerShell 窗口PATH 没刷新。</p>
<p><strong>解决:</strong>关闭当前 PowerShell重新打开一个新的窗口再试。</p>
<p>如果还不行,手动刷新 PATH</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code>$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
<span class="cmd">node</span> -v</code>
</div>
<div class="card">
<h3>&#10060; npm 报 "执行策略" / "Execution Policy" 错误</h3>
<p><strong>原因:</strong>Windows 默认禁止运行脚本。</p>
<p><strong>解决:</strong></p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code>Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned</code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">提示确认时输入 <strong>Y</strong> 回车。之后 npm 和 pwsh 脚本都能正常运行。</p>
<div class="card">
<h3>&#10060; 提示 "openssl 未找到"</h3>
<p><strong>原因:</strong>解密凭证需要 openssl它随 Git for Windows 一起安装。</p>
<p><strong>解决:</strong>确认 Git 已安装。脚本会自动搜索 <code>C:\Program Files\Git</code><code>D:\Git</code> 下的 openssl。</p>
</div>
<div class="card">
<h3>&#10060; git clone 失败 / 认证失败</h3>
<p><strong>解决步骤:</strong></p>
<ol style="color:var(--text-dim);padding-left:1.5rem;margin-top:0.3rem">
<li>浏览器打开 <code>https://code.letcareme.com</code> 确认网站可访问</li>
<li>确认用户名密码正确(区分大小写)</li>
<li>如果开了 2FA需要用 Access Token 替代密码</li>
<li>检查网络是否需要代理</li>
</ol>
</div>
<div class="card">
<h3>&#10060; 解密凭证失败 / 主密码错误</h3>
<p><strong>原因:</strong>主密码区分大小写,且无法找回。</p>
<p><strong>解决:</strong>仔细检查密码是否正确。如确认忘记,联系管理员重新生成 <code>secrets.enc</code></p>
</div>
<div class="card">
<h3>&#10060; Claude Code 启动后没有 Bookworm 横幅</h3>
<p><strong>原因:</strong>配置文件未正确同步。</p>
<p><strong>解决:</strong>不加 <code>-StartOnly</code> 重新运行安装脚本,让它重新 clone</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1</code>
</div>
<div class="card">
<h3>&#10060; 安装包下载太慢</h3>
<p><strong>解决:</strong>Node.js 官网在国内可能较慢,可以用淘宝镜像:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 设置 npm 淘宝镜像(加速下载)</span>
<span class="cmd">npm</span> config set registry https://registry.npmmirror.com
<span class="comment"># 然后重新安装 Claude Code</span>
<span class="cmd">npm</span> i -g @anthropic-ai/claude-code</code>
</div>
<div class="card">
<h3>&#10060; ECONNRESET / "Unable to connect to API"</h3>
<p><strong>原因:</strong>代理软件把国内中转站 <code>bww.letcareme.com</code> 的流量也走了国际线路,导致连接被重置。</p>
<p><strong>解决:</strong>在 PowerShell 中手动设置 NO_PROXY 后重试:</p>
</div>
<div class="code-block" onclick="copyCode(this)">
<code><span class="comment"># 设置中转站直连(不走代理)</span>
$env:NO_PROXY = "bww.letcareme.com,code.letcareme.com"
<span class="comment"># 重新启动</span>
<span class="cmd">cd</span> bookworm-boot
<span class="cmd">pwsh</span> <span class="flag">-ExecutionPolicy Bypass</span> -File install.ps1 <span class="flag">-StartOnly</span></code>
</div>
<p style="color:var(--text-dim);font-size:0.85rem">新版安装脚本已自动设置 NO_PROXY<code>git pull</code> 更新后此问题不再出现。</p>
<div class="card">
<h3>&#10060; "Not logged in" / 直接运行 claude 报错</h3>
<p><strong>原因:</strong>API 凭证是进程级环境变量,只在安装脚本启动的进程中有效。新开 PowerShell 窗口直接运行 <code>claude</code> 没有凭证。</p>
<p><strong>解决:</strong><strong>不要直接运行 <code>claude</code></strong>,必须通过以下方式启动:</p>
<ul style="color:var(--text-dim);font-size:0.9rem;padding-left:1.2rem;margin-top:0.3rem">
<li>双击桌面 <strong>Bookworm</strong> 快捷方式</li>
<li>双击 <strong>启动Bookworm.bat</strong></li>
<li>命令行:<code>pwsh -ExecutionPolicy Bypass -File install.ps1 -StartOnly</code></li>
</ul>
</div>
<div class="card">
<h3>&#10060; 完整性校验不匹配(大量文件 WARN</h3>
<p><strong>原因:</strong>本地配置文件已被更新(管理员推送了新版本),但 <code>integrity.sha256</code> 未同步更新。</p>
<p><strong>解决:</strong><strong>y</strong> 继续即可,不影响使用。管理员会在下个版本同步哈希文件。</p>
</div>
<div class="card">
<h3>&#10060; 需要自己的 Claude 账号吗?</h3>
<p><strong>不需要。</strong>所有 API 请求通过中转站转发,消耗中转站额度。目标机不需要任何 Anthropic 账号或订阅。</p>
</div>
<div class="card">
<h3>&#10060; 询问 AI 系统内部信息时被拒绝了?</h3>
<p><strong>这是正常行为。</strong>Bookworm 的技能库、路由引擎、配置架构属于技术保密范围AI 被设定为不披露这些信息。</p>
<p><strong>解决:</strong>直接告诉 AI 你要完成的任务(写代码、分析问题、设计方案等),它会自动调用最合适的专家能力来帮你。无需了解内部机制即可获得完整服务。</p>
</div>
</div>
<!-- ============================================================ -->
<!-- 安装检查清单 -->
<!-- ============================================================ -->
<div class="section">
<h2>安装检查清单</h2>
<p style="color:var(--text-dim);margin-bottom:0.5rem">逐项确认,全部打勾即可开始使用:</p>
<ul class="checklist">
<li><strong>Node.js 已安装</strong> &mdash; <code>node -v</code> 显示版本号</li>
<li><strong>Git 已安装</strong> &mdash; <code>git --version</code> 显示版本号</li>
<li><strong>npm 可用</strong> &mdash; <code>npm -v</code> 显示版本号(如报错先设 ExecutionPolicy</li>
<li><strong>Claude Code 已安装</strong> &mdash; <code>claude --version</code> 显示版本号</li>
<li><strong>PowerShell 7 已安装</strong> &mdash; <code>pwsh --version</code> 显示 7.x推荐但非必须</li>
<li><strong>已获取 Gitea 账号密码</strong> &mdash; 管理员提供</li>
<li><strong>已获取主密码</strong> &mdash; 管理员提供(用于解密 API 凭证)</li>
<li><strong>能访问 code.letcareme.com</strong> &mdash; 浏览器打开确认</li>
<li><strong>代理/VPN 已启动</strong> &mdash; 国内必须,脚本自动检测 (Clash/V2Ray/快柠檬等)</li>
</ul>
</div>
<!-- ============================================================ -->
<!-- 快速参考卡片 -->
<!-- ============================================================ -->
<div class="section">
<h2>快速参考</h2>
<table>
<tr><th>操作</th><th>最简方式</th><th>命令行方式</th></tr>
<tr><td>首次安装</td><td>git clone + 双击<br><strong>更新并启动Bookworm.bat</strong></td><td><code>pwsh -ExecutionPolicy Bypass -File install.ps1</code></td></tr>
<tr><td>快速启动</td><td>双击 <strong>启动Bookworm.bat</strong></td><td><code>pwsh -File install.ps1 -StartOnly</code></td></tr>
<tr><td>同步更新</td><td>双击 <strong>更新并启动Bookworm.bat</strong></td><td><code>pwsh -File install.ps1</code></td></tr>
<tr><td>基础清理</td><td colspan="2"><code>pwsh -ExecutionPolicy Bypass -File stop.ps1</code></td></tr>
<tr><td>完整恢复</td><td colspan="2"><code>pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore</code></td></tr>
<tr><td>深度清理</td><td colspan="2"><code>pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep</code></td></tr>
</table>
</div>
<!-- ============================================================ -->
<!-- 安全须知 -->
<!-- ============================================================ -->
<div class="section">
<h2>安全须知</h2>
<table>
<tr><th>特性</th><th>规格</th></tr>
<tr><td>凭证加密</td><td>AES-256-CBC + PBKDF2 (600,000 迭代)</td></tr>
<tr><td>传输加密</td><td>HTTPS (TLS 1.2+, Let's Encrypt 证书)</td></tr>
<tr><td>凭证存储</td><td>进程级环境变量 + 可选本日缓存 (Windows Credential Manager, DPAPI 加密, 当日 23:59 过期)</td></tr>
<tr><td>登录保护</td><td>fail2ban (5 次失败/小时 &#8594; 封禁 24 小时)</td></tr>
</table>
<div class="alert warning" style="margin-top:1rem">
<span class="alert-icon">&#128274;</span>
<div>
<strong>主密码无法找回</strong> &mdash; 请妥善保管。忘记后需管理员重新生成加密凭证。
</div>
</div>
</div>
</div>
<div class="footer">
Bookworm Portable v1.5-NDA &mdash; 保姆式安装手册<br>
&copy; 2026 Bookworm Smart Assistant
</div>
<script>
function copyCode(el) {
const code = el.querySelector('code').innerText;
navigator.clipboard.writeText(code).then(() => {
el.classList.add('copied');
setTimeout(() => el.classList.remove('copied'), 1500);
});
}
</script>
</body>
</html>

104
inject-mcp.js Normal file
View File

@ -0,0 +1,104 @@
/**
* Bookworm MCP 注入脚本 v2 同步 Bookworm v6.5.1 全部 22 portable MCP
* 安全合并到 ~/.claude.json ()
* 用法: node inject-mcp.js
*/
const fs = require("fs");
const path = require("path");
const HOME = process.env.USERPROFILE || process.env.HOME || "";
const f = path.join(HOME, ".claude.json");
let d = {};
try { d = JSON.parse(fs.readFileSync(f, "utf8")); } catch (e) {}
d.mcpServers = {
// ── Tier 1: npx (无需 API Key) ──
"context7": {
command: "npx.cmd", args: ["--yes", "@upstash/context7-mcp@2.1.1"], type: "stdio"
},
"sequential-thinking": {
command: "npx.cmd", args: ["--yes", "@modelcontextprotocol/server-sequential-thinking@2025.12.18"], type: "stdio"
},
"playwright": {
command: "npx.cmd", args: ["--yes", "@playwright/mcp@0.0.68", "--headless"], type: "stdio"
},
"session-continuity": {
command: "npx.cmd", args: ["--yes", "claude-session-continuity-mcp@1.13.0"], type: "stdio"
},
"browser-mcp": {
command: "npx.cmd", args: ["--yes", "@browsermcp/mcp@latest"], type: "stdio"
},
"desktop-commander": {
command: "npx.cmd", args: ["--yes", "@wonderwhy-er/desktop-commander@latest"], type: "stdio",
env: { PUPPETEER_SKIP_DOWNLOAD: "true", PUPPETEER_EXECUTABLE_PATH: "C:/Program Files/Google/Chrome/Application/chrome.exe" }
},
"chrome-devtools": {
command: "npx.cmd",
args: ["--yes", "chrome-devtools-mcp@0.18.1",
"--executablePath", "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"--viewport", "1280x720", "--proxyServer", "http://127.0.0.1:7893"],
type: "stdio"
},
"mobile": {
command: "npx.cmd", args: ["--yes", "@mobilenext/mobile-mcp@0.0.35"], type: "stdio",
env: { ANDROID_HOME: path.join(HOME, "android-sdk") }
},
// ── Tier 2: npx + API Key (凭证从环境变量读取) ──
"github": {
command: "npx.cmd", args: ["--yes", "@modelcontextprotocol/server-github"], type: "stdio",
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN || "" }
},
"slack": {
command: "npx.cmd", args: ["--yes", "@modelcontextprotocol/server-slack"], type: "stdio",
env: { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN || "", SLACK_TEAM_ID: "T0A4L1JLEER" }
},
"firecrawl": {
command: "npx.cmd", args: ["--yes", "firecrawl-mcp"], type: "stdio",
env: { FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY || "" }
},
"mcp-image": {
command: "npx.cmd", args: ["--yes", "mcp-image"], type: "stdio",
env: { GEMINI_API_KEY: process.env.GEMINI_API_KEY || "", IMAGE_OUTPUT_DIR: path.join(HOME, "Pictures/mcp-images") }
},
"google-drive": {
command: "npx.cmd", args: ["--yes", "@piotr-agier/google-drive-mcp"], type: "stdio",
env: { GOOGLE_DRIVE_OAUTH_CREDENTIALS: path.join(HOME, ".config/google-drive-mcp/gcp-oauth.keys.json") }
},
"browserbase": {
command: "npx.cmd", args: ["--yes", "@anthropic-ai/browserbase-mcp"], type: "stdio",
env: { BROWSERBASE_API_KEY: process.env.BROWSERBASE_API_KEY || "", BROWSERBASE_PROJECT_ID: "d3dbb32f-be2f-4e3a-b9ec-68e27474763c" }
},
// ── Tier 3: npx + 代理 (需要外网访问) ──
"notebooklm": {
command: "npx.cmd", args: ["--yes", "notebooklm-mcp@latest"], type: "stdio",
env: { https_proxy: "http://127.0.0.1:7893", http_proxy: "http://127.0.0.1:7893" }
},
"cloudflare": {
command: "npx.cmd", args: ["--yes", "mcp-remote", "https://docs.mcp.cloudflare.com/sse"], type: "stdio",
env: { https_proxy: "http://127.0.0.1:7893", http_proxy: "http://127.0.0.1:7893" }
},
// ── Tier 4: HTTP (零安装, 云端托管) ──
"linear": { type: "http", url: "https://mcp.linear.app/mcp" },
"supabase": { type: "http", url: "https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo" },
"figma": { type: "http", url: "https://mcp.figma.com/mcp" },
// ── Tier 5: Python/uvx (需要 Python + uv) ──
"windows-mcp": { command: "uvx", args: ["--python", "3.13", "windows-mcp"], type: "stdio" },
"atlassian": {
command: "uvx", args: ["mcp-atlassian"], type: "stdio",
env: {
JIRA_URL: "https://huakoh.atlassian.net", JIRA_USERNAME: "huakoh449@gmail.com",
JIRA_API_TOKEN: process.env.ATLASSIAN_API_TOKEN || "",
CONFLUENCE_URL: "https://huakoh.atlassian.net/wiki", CONFLUENCE_USERNAME: "huakoh449@gmail.com",
CONFLUENCE_API_TOKEN: process.env.ATLASSIAN_API_TOKEN || ""
}
},
"computer-control-mcp": { command: "uvx", args: ["computer-control-mcp@latest"], type: "stdio" }
};
fs.writeFileSync(f, JSON.stringify(d, null, 2));
const count = Object.keys(d.mcpServers).length;
console.log("OK: " + count + " MCP servers -> " + f);

View File

@ -50,8 +50,8 @@ if (-not $opensslCmd) {
function Write-Banner { function Write-Banner {
Write-Host "" Write-Host ""
Write-Host " +------------------------------------------+" -ForegroundColor Cyan Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host " | Bookworm Portable Installer v1.5 |" -ForegroundColor Cyan Write-Host " | Bookworm Portable Installer v1.6 |" -ForegroundColor Cyan
Write-Host " | 92 Skills / 18 Agents / 34 Hooks |" -ForegroundColor Cyan Write-Host " | Claude Code 国内一键就绪 |" -ForegroundColor Cyan
Write-Host " +------------------------------------------+" -ForegroundColor Cyan Write-Host " +------------------------------------------+" -ForegroundColor Cyan
Write-Host "" Write-Host ""
} }
@ -172,28 +172,79 @@ function Install-MissingDeps {
# ─── 桌面快捷方式 ──────────────────────────────────── # ─── 桌面快捷方式 ────────────────────────────────────
function New-DesktopShortcuts { function New-DesktopShortcuts {
# v3.0.11 架构重构: .lnk 直调 pwsh + claude.ps1 绝对路径 (1 跳直链)
$desktop = [System.Environment]::GetFolderPath("Desktop") $desktop = [System.Environment]::GetFolderPath("Desktop")
$bootDir = $ScriptDir $bootDir = $ScriptDir
$lnkPath = Join-Path $desktop "启动Bookworm.lnk"
# 定位 pwsh.exe
$pwshExe = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
if (-not $pwshExe) {
foreach ($p in @("$env:ProgramFiles\PowerShell\7\pwsh.exe", "${env:ProgramFiles(x86)}\PowerShell\7\pwsh.exe")) {
if (Test-Path $p) { $pwshExe = $p; break }
}
}
if (-not $pwshExe) {
Write-Host " [!] pwsh.exe 未找到, 跳过桌面快捷方式 (建议先装 PS7)" -ForegroundColor Yellow
return
}
# v3.1.0: 优先用 wrapper bw-launch.ps1, 闭合 claude.ps1 路径 stale (L4)
$bwLaunchPs1 = Join-Path $bootDir "bw-launch.ps1"
if (-not (Test-Path $bwLaunchPs1)) {
Write-Host " [!] bw-launch.ps1 wrapper 未找到, 跳过桌面快捷方式" -ForegroundColor Yellow
Write-Host " $bwLaunchPs1 应由 bookworm-boot git 仓库提供" -ForegroundColor Gray
return
}
# 装机时自检 claude.ps1 当前可达 (运行时由 wrapper 兜底)
$claudeCheck = $null
$claudeCmd = Get-Command claude -ErrorAction SilentlyContinue
if ($claudeCmd -and $claudeCmd.Source -and $claudeCmd.Source.EndsWith('.ps1')) { $claudeCheck = $claudeCmd.Source }
if (-not $claudeCheck) {
try {
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
$cand = Join-Path $npmPrefix "claude.ps1"
if (Test-Path $cand) { $claudeCheck = $cand }
} catch {}
}
if (-not $claudeCheck) {
Write-Host " [!] claude.ps1 装机时不可达, 跳过桌面快捷方式" -ForegroundColor Yellow
return
}
# 启动Bookworm 快捷方式 — 优先 pwsh (PS7),回退 powershell (PS5)
$lnkPath = Join-Path $desktop "Bookworm.lnk"
if (-not (Test-Path $lnkPath)) {
try { try {
$shell = New-Object -ComObject WScript.Shell $shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($lnkPath) $shortcut = $shell.CreateShortcut($lnkPath)
$scriptPath = Join-Path $bootDir "install.ps1" $shortcut.TargetPath = $pwshExe
$hasPwsh = [bool](Get-Command pwsh -ErrorAction SilentlyContinue) $shortcut.Arguments = "-NoLogo -NoExit -ExecutionPolicy Bypass -File `"$bwLaunchPs1`" --dangerously-skip-permissions"
$psExe = if ($hasPwsh) { (Get-Command pwsh).Source } else { "powershell.exe" } $shortcut.WorkingDirectory = $env:USERPROFILE
$shortcut.TargetPath = $psExe $shortcut.Description = "Bookworm Smart Assistant (v3.1.0 wrapper)"
$shortcut.Arguments = "-NoLogo -ExecutionPolicy Bypass -Command `"Set-Item Env:NO_PROXY 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1'; & '$scriptPath' -StartOnly -AutoAccept`"" $iconPath = Join-Path $bootDir "bookworm-desktop.ico"
$shortcut.WorkingDirectory = $bootDir if (-not (Test-Path $iconPath)) { $iconPath = Join-Path $bootDir "bookworm.ico" }
$shortcut.Description = "Bookworm Smart Assistant" if (Test-Path $iconPath) { $shortcut.IconLocation = "$iconPath,0" }
$shortcut.Save() $shortcut.Save()
$psVer = if ($hasPwsh) { "PowerShell 7" } else { "PowerShell 5.1" }
Write-Host " [OK] 桌面快捷方式已创建: Bookworm ($psVer)" -ForegroundColor Green # 自验证 (4 项)
} catch { $verify = $shell.CreateShortcut($lnkPath)
Write-Host " [!] 桌面快捷方式创建失败 (不影响使用)" -ForegroundColor Gray $okTarget = $verify.TargetPath -eq $pwshExe
$okPath = $verify.Arguments -match [regex]::Escape($bwLaunchPs1)
$okPerm = $verify.Arguments -match "--dangerously-skip-permissions"
$okBypass = $verify.Arguments -match "-ExecutionPolicy Bypass"
if ($okTarget -and $okPath -and $okPerm -and $okBypass) {
Write-Host " [OK] 桌面快捷方式已创建并通过 4 项自验证 (v3.1.0 wrapper)" -ForegroundColor Green
} else {
Write-Host " [!] 桌面快捷方式自验证失败 (Target=$okTarget Path=$okPath Perm=$okPerm Bypass=$okBypass)" -ForegroundColor Yellow
Remove-Item $lnkPath -Force -EA SilentlyContinue
} }
} catch {
Write-Host " [!] 桌面快捷方式创建失败: $_" -ForegroundColor Gray
}
# 迁移清理老 Bookworm.lnk
$oldLnk = Join-Path $desktop "Bookworm.lnk"
if ((Test-Path $oldLnk) -and (Test-Path $lnkPath)) {
try { Remove-Item -LiteralPath $oldLnk -Force -ErrorAction Stop } catch {}
} }
} }
@ -418,14 +469,46 @@ foreach ($c in $checks) {
if (-not (Test-Command "claude") -or -not (Test-Command "node") -or -not (Test-Command "git")) { if (-not (Test-Command "claude") -or -not (Test-Command "node") -or -not (Test-Command "git")) {
Install-MissingDeps Install-MissingDeps
} }
# 再次验证 # v3.0.5: 再次验证 — StartOnly 场景加 GUI 弹窗 (防止 console 闪退用户看不见)
# 触发条件: 用户双击老快捷方式, 但 Phase 1 之前失败导致 claude/node 未装
function Show-MissingDepGui {
param([string]$depName, [string]$installCmd)
try {
Add-Type -AssemblyName System.Windows.Forms -EA Stop
$msg = @"
检测到 $depName 未安装无法启动 Bookworm
最可能原因:
上次安装器未完成 (Phase 1 环境检测被中断)
推荐修复
1. 双击桌面或下载目录的 Bookworm-Setup.exe
2. 安装器会自动补装缺失的依赖
3. 已装好的部分会被跳过, 不会重复
手动修复
$installCmd
完成后再次点击启动快捷方式即可
"@
[System.Windows.Forms.MessageBox]::Show($msg, "Bookworm 启动失败 — $depName 未安装", 'OK', 'Error') | Out-Null
} catch { }
}
if (-not (Test-Command "claude")) { if (-not (Test-Command "claude")) {
Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red
Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray
if ($StartOnly) { Show-MissingDepGui "Claude Code" "npm i -g @anthropic-ai/claude-code" }
exit 1 exit 1
} }
if (-not (Test-Command "node")) { if (-not (Test-Command "node")) {
Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red
if ($StartOnly) { Show-MissingDepGui "Node.js" "https://nodejs.org/zh-cn/download 下载 LTS .msi" }
exit 1
}
if (-not (Test-Command "git")) {
Write-Host "`n [ABORT] Git 未安装" -ForegroundColor Red
if ($StartOnly) { Show-MissingDepGui "Git" "https://git-scm.com/download/win 下载 64-bit" }
exit 1 exit 1
} }
@ -637,19 +720,20 @@ if (Test-Path $claudeMd) {
$bwChecks += @{ Name = "CLAUDE.md (文件不存在!)"; OK = $false } $bwChecks += @{ Name = "CLAUDE.md (文件不存在!)"; OK = $false }
} }
# 检查 Skills # v3.0.5: 阈值按脱敏分发版 (bookworm-portable-config.git) 实际内容定
# 管理员自用的完整版 (bookworm-config.git) 含 90+ skills, 分发版精简到核心 14+
$skillCount = 0 $skillCount = 0
if (Test-Path $skillsDir) { if (Test-Path $skillsDir) {
$skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count $skillCount = (Get-ChildItem $skillsDir -Directory -ErrorAction SilentlyContinue).Count
} }
$bwChecks += @{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -gt 50) } $bwChecks += @{ Name = "Skills ($skillCount 个)"; OK = ($skillCount -ge 10) }
# 检查 Hooks # 检查 Hooks
$hookCount = 0 $hookCount = 0
if (Test-Path $hooksDir) { if (Test-Path $hooksDir) {
$hookCount = (Get-ChildItem $hooksDir -Filter "*.js" -File -ErrorAction SilentlyContinue).Count $hookCount = (Get-ChildItem $hooksDir -Filter "*.js" -File -ErrorAction SilentlyContinue).Count
} }
$bwChecks += @{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -gt 10) } $bwChecks += @{ Name = "Hooks ($hookCount 个)"; OK = ($hookCount -ge 3) }
# 检查 settings.json hooks 配置 # 检查 settings.json hooks 配置
$hasHooks = $false $hasHooks = $false
@ -671,12 +755,12 @@ foreach ($c in $bwChecks) {
if (-not $allOK) { if (-not $allOK) {
Write-Host "" Write-Host ""
Write-Host " ╔══════════════════════════════════════════════════════╗" -ForegroundColor Yellow Write-Host " ╔══════════════════════════════════════════════════════╗" -ForegroundColor Yellow
Write-Host " ║ [!] 警告: Bookworm 系统不完整 " -ForegroundColor Yellow Write-Host " ║ [!] 警告: Bookworm 系统核心资产不足" -ForegroundColor Yellow
Write-Host " ║ Claude Code 将以原生模式启动 (无 Skills/Hooks) ║" -ForegroundColor Yellow Write-Host " ║ Claude Code 将以原生模式启动 (无 Skills/Hooks) ║" -ForegroundColor Yellow
Write-Host " ║ 建议: 不加 -StartOnly 重新运行 install.ps1 同步 ║" -ForegroundColor Yellow Write-Host " ║ 建议: 检查网络后不加 -StartOnly 重新运行同步 " -ForegroundColor Yellow
Write-Host " ╚══════════════════════════════════════════════════════╝" -ForegroundColor Yellow Write-Host " ╚══════════════════════════════════════════════════════╝" -ForegroundColor Yellow
} else { } else {
Write-Host " [OK] Bookworm 系统完整 ($skillCount Skills / $hookCount Hooks)" -ForegroundColor Green Write-Host " [OK] Bookworm 分发版就绪 ($skillCount Skills / $hookCount Hooks / Settings)" -ForegroundColor Green
} }
# --- MCP 依赖检查 (中文提醒) --- # --- MCP 依赖检查 (中文提醒) ---
@ -771,6 +855,10 @@ if (-not $StartOnly) {
Start-Process $guidePath Start-Process $guidePath
Write-Host " [OK] 使用教程已在浏览器打开" -ForegroundColor Gray Write-Host " [OK] 使用教程已在浏览器打开" -ForegroundColor Gray
} }
} else {
# StartOnly 路径 (老 Bookworm.lnk 指向此): 跑幂等迁移, 单次 ~10ms
# 让只从不点「更新Bookworm」的老用户也自动完成快捷方式命名统一
New-DesktopShortcuts
} }
# 启动 Claude Code (同步执行, 窗口类型由调用方 .bat 决定) # 启动 Claude Code (同步执行, 窗口类型由调用方 .bat 决定)

View File

@ -1,206 +0,0 @@
# Bookworm Portable 项目经验与避坑指南
> 2026-04-01 | 从可行性评估到 E2E 验证的完整实战记录
---
## 一、项目概要
**目标**: 让任意 Windows 电脑通过一行命令激活 Bookworm (97 Skills + 18 Agents + 28 Hooks) + 中转站 API
**最终架构**: Gitea 私有仓 (code.letcareme.com) + AES-256 加密凭证 + 自动代理检测 + 一键安装脚本
**耗时**: 1 天 (评估 → 设计 → 三路专家审查 → P0/P1 修复 → 部署 → E2E 验证 → 踩坑修复)
---
## 二、关键决策与转折点
### 决策 1: USB → Git 方案切换
- **初始方案**: U盘存储全部系统文件 + 加密凭证
- **问题**: Windows NTFS Junction 跨驱动器失败 (实测 Accessible: False)、USB IOPS 瓶颈 (hook 延迟 200-600ms)、体积 2.1GB
- **最终方案**: Git + 模板渲染USB 变为可选 (纯云端即可)
- **教训**: 方案评估阶段多投入时间做实测,不要假设跨驱动器操作能工作
### 决策 2: GitHub → 国内 Gitea
- **问题**: 国内无 VPN 无法访问 GitHub
- **方案**: 阿里云 ECS 自建 Gitea国内直连
- **教训**: 面向国内用户的工具链必须全程可达,不能依赖海外服务
### 决策 3: bookworm.letcareme.com → code.letcareme.com
- **问题**: bookworm.letcareme.com 已被 Bookworm Web 项目 Nginx 配置占用
- **解决**: 换用 code.letcareme.com申请新证书
- **教训**: 部署前检查域名占用,`grep -r "域名" /etc/nginx/` 先扫一遍
---
## 三、必踩的坑 (按严重度排序)
### 坑 1: Claude Code 国内启动检查 (CRITICAL)
```
现象: "Failed to connect to api.anthropic.com: ERR_BAD_REQUEST"
原因: Claude Code 启动时硬编码检查 api.anthropic.com与 ANTHROPIC_BASE_URL 无关
影响: 国内无代理 = Claude Code 完全无法启动
解决: 必须有代理/VPN且必须设 HTTPS_PROXY 环境变量 (Node.js 不读系统代理)
耗时: 2 小时排查
```
### 坑 2: Node.js 不读 Windows 系统代理 (CRITICAL)
```
现象: PowerShell (Invoke-WebRequest) 能通Claude Code 不通
原因: Node.js 只读 HTTPS_PROXY/HTTP_PROXY 环境变量,不读 Windows IE/系统代理设置
解决: 用 [System.Net.WebRequest]::DefaultWebProxy.GetProxy() 发现实际代理端口
然后 $env:HTTPS_PROXY = "http://127.0.0.1:端口"
教训: 系统代理 ≠ 进程代理Node.js 应用必须显式设环境变量
```
### 坑 3: settings.json 中 ${VAR} 不展开 (CRITICAL)
```
现象: "API Error: Invalid URL"
原因: settings.template.json 中 "ANTHROPIC_BASE_URL": "${ANTHROPIC_BASE_URL}"
Claude Code 把 ${ANTHROPIC_BASE_URL} 当字面字符串,不展开环境变量
实际发送的 URL 是 "${ANTHROPIC_BASE_URL}/v1/messages"
解决: 从 settings.json env 段删除 ANTHROPIC_API_KEY 和 ANTHROPIC_BASE_URL
让 install.ps1 注入的进程环境变量直接生效
教训: Claude Code settings.json 的 ${} 语法行为未文档化,不要假设它能展开
```
### 坑 4: PowerShell 5.1 的 ?. 语法 (HIGH)
```
现象: "表达式或语句中包含意外的标记 '?.Source'"
原因: ?. (null-conditional operator) 是 PS 7+ 专属语法PS 5.1 不支持
解决: 改为 if ($x) { $x.Property } else { $null }
教训: 目标机可能只有 PS 5.1 (Windows 自带),所有脚本必须 PS 5.1 兼容
```
### 坑 5: PowerShell 5.1 的 git stderr 问题 (HIGH)
```
现象: git clone 实际成功但脚本报 [ERROR] 克隆失败
原因: git 把进度信息写到 stderrPS 5.1 的 $ErrorActionPreference="Stop"
把 stderr 输出当成终止性错误抛出异常
解决: git 命令前临时设 $ErrorActionPreference = "Continue"
用 $LASTEXITCODE 判断实际成功/失败
教训: PS 5.1 + 外部命令 + Stop 模式 = 必炸,所有外部命令都要处理
```
### 坑 6: UTF-8 BOM 问题 (HIGH)
```
现象: "<# : 无法将 '<#' 项识别为 cmdlet" 或中文乱码
原因: Write 工具生成的文件无 BOMPS 5.1 按 ANSI 解析 → 中文乱码
修复时 BOM 加了两次 → 双 BOM 导致 <# 注释块被当成代码
解决: 统一用 bash 添加一次 BOM: printf '\xEF\xBB\xBF' > tmp && cat file >> tmp
每次编辑后检查: head -c 3 file | xxd -p 应为 efbbbf
教训: 面向 PS 5.1 的 .ps1 文件必须有且仅有一个 UTF-8 BOM
```
### 坑 7: openssl 路径不在默认位置 (MEDIUM)
```
现象: "[ERROR] openssl 未找到"
原因: Git for Windows 安装在 D:\Git 而非默认 C:\Program Files\Git
解决: 搜索多个路径: C:\Program Files\Git, D:\Git, D:\Git\mingw64\bin
教训: 不要硬编码第三方软件路径,提供多路径搜索
```
### 坑 8: Gitea 端口被占用 (MEDIUM)
```
现象: 部署脚本设 3000 端口但已被 my-oa-frontend 容器占用
解决: 改为 3300 端口
教训: 部署前 netstat -tlnp | grep 端口 先检查
```
### 坑 9: Nginx 反代不传 Authorization 头 (MEDIUM)
```
现象: git push 通过 Nginx 反代时 404
原因: Nginx 默认传 Authorization 但旧的 bookworm-web.conf 先匹配了域名
解决: 换域名 + 添加 proxy_set_header Authorization $http_authorization
教训: 多个 Nginx 配置文件可能竞争同一个 server_name
```
### 坑 10: git clone 私有仓需要凭证 (LOW)
```
现象: install.ps1 中 git clone 静默失败 (无密码提示)
解决: 先手动 git clone 触发凭证输入 + git config credential.helper store
教训: 自动化脚本中的 git 操作需要预配置凭证
```
---
## 四、安全经验
### 三路专家审查的价值
- **红队**: 发现 14 个攻击向量,其中 INSTALL_LOCK=false 成功率 95%
- **代码审查**: 发现 5 个 Blocker + 13 个 Warning
- **架构审查**: 发现仓库体积失控 (预估 50MB 实际 500MB+)
- **交叉验证**: 三路独立发现的 openssl 命令行密码泄露问题,置信度极高
- **教训**: 多专家并行审查在 P0 修复前执行,投入产出比极高
### 安全加固清单 (实际落地的)
- [x] Gitea INSTALL_LOCK=true + 注册关闭 + CLI 创建管理员
- [x] Gitea 二进制 SHA256 校验
- [x] HTTPS (Let's Encrypt + Nginx 反代 + HSTS)
- [x] fail2ban (5次/小时 → 封24h)
- [x] Gitea 仅 127.0.0.1 监听
- [x] openssl 密码通过 stdin 管道 (不暴露进程列表)
- [x] PBKDF2 600,000 迭代 (OWASP 2023)
- [x] 凭证仅进程级环境变量 (不写磁盘)
- [x] .gitignore 排除凭证/大文件/敏感数据
- [x] git add 前人工确认 + 大文件检测
- [x] stop.ps1 清理 Git Credential Manager 缓存
---
## 五、PowerShell 跨版本兼容速查表
| PS 7+ 写法 | PS 5.1 兼容写法 |
|---|---|
| `$obj?.Property` | `if ($obj) { $obj.Property } else { $null }` |
| `$cmd?.Source` | `$cmd = Get-Command x -EA 0; if ($cmd) { $cmd.Source }` |
| `??` 空合并 | `if ($x) { $x } else { $default }` |
| UTF-8 无 BOM | 必须有 BOM: `\xEF\xBB\xBF` |
| `$ErrorActionPreference="Stop"` + git | 临时改 `Continue`,用 `$LASTEXITCODE` 判断 |
---
## 六、代理/VPN 检测方法速查
```powershell
# 最可靠: .NET DefaultWebProxy (覆盖所有系统代理)
[System.Net.WebRequest]::DefaultWebProxy.GetProxy("https://api.anthropic.com")
# IE 注册表
Get-ItemProperty "HKCU:\...\Internet Settings" | Select ProxyServer, ProxyEnable
# 端口扫描 (Clash 7890, V2Ray 10808, 快柠檬 10792)
Test-NetConnection 127.0.0.1 -Port 7890
```
---
## 七、项目最终产出
```
13 个文件 (bookworm-portable/)
2 个 Gitea 仓库 (bookworm-config 514文件/14MB, bookworm-boot 6文件)
3 个 ECS 部署脚本 (Gitea + HTTPS + 防火墙)
7 项 P0 安全修复
4 项 P1 安全加固
1 个保姆式 HTML 教程
1 个快速参考 TXT 手册
1 份经验避坑指南 (本文档)
E2E 验证: 远程 Windows Server → 从零安装 → Claude Code 启动成功
```
---
## 八、如果重新来过,我会...
1. **一开始就做 PS 5.1 兼容测试** — 不假设目标机有 PS 7
2. **一开始就加代理检测** — 国内 Claude Code 的核心门槛
3. **不在 settings.json env 段放 ${} 变量** — 行为未文档化
4. **先检查域名和端口占用** — 再写部署脚本
5. **三路专家审查提前到设计阶段** — 而非代码写完后再审
---
*Bookworm Portable v1.2 | code.letcareme.com | 2026-04-01*

View File

@ -137,7 +137,7 @@ function startSSEHeartbeat(res) {
async function proxyChat(opts, res) { async function proxyChat(opts, res) {
const { const {
apiKey, apiKey,
model = 'claude-sonnet-4-5-20250514', model = 'claude-opus-4-7',
messages, messages,
maxTokens = 8192, maxTokens = 8192,
stream = false, stream = false,

View File

@ -77,6 +77,30 @@
<div class="ver">v1.5 | 92 Skills / 18 Agents / 34 Hooks<br>打印后贴在显示器旁边</div> <div class="ver">v1.5 | 92 Skills / 18 Agents / 34 Hooks<br>打印后贴在显示器旁边</div>
</div> </div>
<!-- ==================== 安装前准备 ==================== -->
<div class="section">
<span class="section-title red">安装前准备 (仅首次)</span>
<div class="card warn">
<h3>&#9888; 首次运行 Bookworm-Setup.exe 前,请确认以下两项</h3>
<table>
<tr><th style="width:30%">检查项</th><th>操作</th></tr>
<tr>
<td><strong>1. PowerShell 执行策略</strong></td>
<td><kbd>Win</kbd>+<kbd>R</kbd> 输入 <code>powershell</code> 回车,执行:<br>
<code>Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force</code><br>
<span style="font-size:8pt;color:#64748b">否则 npm/node 脚本会被系统阻止运行</span></td>
</tr>
<tr>
<td><strong>2. 代理已开启</strong></td>
<td>启动 Clash / V2Ray / 快柠檬(任选一个),安装器需要下载依赖</td>
</tr>
</table>
<p style="font-size:8pt;color:#64748b;margin-top:4px">如果电脑没有 wingetWin10 旧版本),安装器可能无法自动装 Node.js 和 Git。请手动安装<br>
Node.js &rarr; <code>https://nodejs.org</code> &nbsp;|&nbsp; Git &rarr; <code>https://git-scm.com</code></p>
</div>
</div>
<!-- ==================== 打开 PowerShell ==================== --> <!-- ==================== 打开 PowerShell ==================== -->
<div class="section"> <div class="section">
<span class="section-title">第零步:打开 PowerShell</span> <span class="section-title">第零步:打开 PowerShell</span>

View File

@ -0,0 +1,218 @@
# Bookworm Portable 启动器 bat 生成工具 (v3.0.6)
# 用途: 从单一明文 PowerShell 脚本生成两个 bat, 避免手工同步 Base64 字符串不一致
# 用法: pwsh -NoProfile -File tools/gen-launcher-bats.ps1
# 输出: 启动Bookworm.bat + 更新并启动Bookworm.bat (覆盖写入)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$launchBat = Join-Path $repoRoot "启动Bookworm.bat"
$updateBat = Join-Path $repoRoot "更新并启动Bookworm.bat"
# ─── 明文: 三层 PATH 修复 + DPAPI 加载 + claude 诊断 + 启动 ─────────
# v3.0.9: 增加 npm config get prefix 动态查询, 兼容 nvm/fnm/Program Files 等非标准 npm 位置
$plainScript = @'
Add-Type -AssemblyName System.Security
# 层 1: Machine + User env PATH (标准 Windows 环境变量)
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
# 层 2: npm config get prefix (真实 npm 全局目录, 兼容 nvm/fnm/标准安装/Program Files)
try {
$npmPrefix = (& npm config get prefix 2>$null | Select-Object -First 1).Trim()
if ($npmPrefix -and (Test-Path $npmPrefix) -and ($env:Path -notlike "*$npmPrefix*")) {
$env:Path = "$npmPrefix;$env:Path"
}
} catch {}
# 层 3: 常见 npm global 硬编码兜底 (npm 本身不在 PATH 时无法 query)
$npmCandidates = @(
"$env:APPDATA\npm",
"$env:ProgramFiles\nodejs",
"${env:ProgramFiles(x86)}\nodejs",
"$env:LOCALAPPDATA\npm"
)
foreach ($p in $npmCandidates) {
if (-not (Test-Path $p)) { continue }
$hasClaude = (Test-Path (Join-Path $p 'claude.ps1')) -or (Test-Path (Join-Path $p 'claude.cmd')) -or (Test-Path (Join-Path $p 'claude'))
if ($hasClaude -and ($env:Path -notlike "*$p*")) {
$env:Path = "$p;$env:Path"
}
}
# DPAPI 加载缓存凭证
$r = 'HKCU:\Software\Bookworm\CachedEnv'
try {
(Get-ItemProperty $r -EA Stop).PSObject.Properties | Where-Object { $_.Name -match '^[A-Z_]+$' } | ForEach-Object {
$v = $_.Value
try {
$b = [Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v), $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
$v = [Text.Encoding]::UTF8.GetString($b)
} catch {}
[Environment]::SetEnvironmentVariable($_.Name, $v, 'Process')
}
} catch {}
if (-not (Get-Command claude -ErrorAction SilentlyContinue)) {
Write-Host ''
Write-Host ' [!] claude 命令未找到 (已尝试 3 层 PATH 修复仍失败)' -ForegroundColor Red
Write-Host ''
Write-Host ' 诊断信息:' -ForegroundColor Yellow
Write-Host " npm prefix: $(try { (& npm config get prefix 2>$null) } catch { '(npm 不可用)' })" -ForegroundColor Gray
Write-Host ' PATH 片段 (npm/nodejs/pwsh/Git):' -ForegroundColor Gray
($env:Path -split ';') | Where-Object { $_ -match 'npm|nodejs|pwsh|Git' } | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray }
Write-Host ''
Write-Host ' 修复: 重新运行 Bookworm-Setup.exe (v3.0.9+) 即可自动补全' -ForegroundColor Green
Write-Host ''
Read-Host '按回车关闭'
return
}
& claude --dangerously-skip-permissions
'@
# ─── Base64-UTF-16LE 编码 ─────────────────────────────────
$bytes = [System.Text.Encoding]::Unicode.GetBytes($plainScript)
$enc = [Convert]::ToBase64String($bytes)
# 健康检查
if ($enc.Length -gt 7500) { throw "Base64 长度 $($enc.Length) 超 bat 变量安全上限 7500" }
$bad = $enc -replace '[A-Za-z0-9+/=]', ''
if ($bad) { throw "Base64 含非法字符: [$bad]" }
Write-Host "[gen-launcher-bats] Base64 长度: $($enc.Length), 纯字符集检查 OK" -ForegroundColor Green
# ─── bat 1: 启动Bookworm.bat ──────────────────────────────
$launch = @"
@echo off
chcp 65001 > nul
cd /d "%~dp0"
:: 中转站在国内,不走代理
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
:: 静默自动更新 (bookworm-boot + .claude 配置, 失败不阻断启动)
echo [..] 检查更新...
git pull --rebase >nul 2>nul
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
set USE_WT=0
where wt >nul 2>nul && set USE_WT=1
set USE_PWSH7=0
where pwsh >nul 2>nul && set USE_PWSH7=1
:: v3.0.6: Base64-UTF-16LE (PATH 重载 + DPAPI 凭证加载 + claude 诊断 + 启动)
:: 纯 A-Za-z0-9+/= 字符集, 避免 wt.exe 的 ';' 切 tab 误切 (修复 64856bc 症状一)
:: -d "%CD%" 无尾反斜杠, 避免 -d "%~dp0" 的转义引号 (修复 0c33109 症状二)
:: 重新生成: pwsh -NoProfile -File tools/gen-launcher-bats.ps1
set ENC=$enc
:: 优先路径: wt + pwsh7
if %USE_WT% equ 1 if %USE_PWSH7% equ 1 (
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- pwsh -NoLogo -NoExit -EncodedCommand %ENC%
exit
)
:: 路径 2: wt + powershell 5.1
if %USE_WT% equ 1 if %USE_PWSH7% equ 0 (
start "" wt new-tab --title "Bookworm Smart Assistant" -d "%CD%" -- powershell -NoLogo -ExecutionPolicy Bypass -NoExit -EncodedCommand %ENC%
exit
)
:: 路径 3: conhost + pwsh7 (无 wt 就不会有 ; 切 tab 问题, 但仍用 Base64 统一)
if %USE_PWSH7% equ 1 (
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC%
exit
)
:: 路径 4: 回退 PowerShell 5.1 (最低保障, 交给 install.ps1 -StartOnly 处理)
title Bookworm Portable
echo.
echo [!] PowerShell 7 未安装, 使用 PowerShell 5.1
echo.
powershell -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept
if %errorlevel% neq 0 (
echo.
echo 启动失败,按任意键退出...
pause > nul
)
"@
# ─── bat 2: 更新并启动Bookworm.bat ───────────────────────
$update = @"
@echo off
chcp 65001 > nul
cd /d "%~dp0"
:: 中转站在国内,不走代理
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
:: 静默自动更新 (bookworm-boot + .claude 配置)
echo [..] 同步更新...
git pull --rebase >nul 2>nul
git -C "%USERPROFILE%\.claude" pull --rebase >nul 2>nul
:: v3.0.6: 同启动Bookworm.bat 的 Base64 (DPAPI + PATH 重载 + claude 启动)
set ENC=$enc
:: 检测 pwsh7 可用性
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
:: pwsh7: 先同步配置 (SkipLaunch 不启动 claude), 再用 -EncodedCommand 在新窗口启动
pwsh -NoLogo -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept -SkipLaunch
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -EncodedCommand %ENC%
exit
)
:: 回退 PowerShell 5.1: 一次调用完成更新+加载凭证+启动 (消除双次调用)
title Bookworm Portable
powershell -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept
if %errorlevel% neq 0 (
echo.
echo 启动失败,按任意键退出...
pause > nul
)
"@
# ─── 写入 ─────────────────────────────────────────────────
# bat 文件默认期望 GBK/ANSI, 但脚本顶部 chcp 65001 已切换到 UTF-8, 用无 BOM UTF-8 写入
[System.IO.File]::WriteAllText($launchBat, $launch, [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($updateBat, $update, [System.Text.UTF8Encoding]::new($false))
Write-Host "[gen-launcher-bats] ✓ 启动Bookworm.bat ($((Get-Item $launchBat).Length) bytes)" -ForegroundColor Green
Write-Host "[gen-launcher-bats] ✓ 更新并启动Bookworm.bat ($((Get-Item $updateBat).Length) bytes)" -ForegroundColor Green
# ─── Round-trip 验证 (v3.0.10: 除了 PARSE 还要 lint + 实跑不启动 claude) ────
$decoded = [System.Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc))
$err = $null
[void][System.Management.Automation.Language.Parser]::ParseInput($decoded, [ref]$null, [ref]$err)
if ($err) { throw "解码后脚本 PARSE ERR: $($err[0])" }
Write-Host "[gen-launcher-bats] ✓ PARSE OK ($($decoded.Length) chars)" -ForegroundColor Green
# 实跑验证 (v3.0.10: 截断到 & claude 之前, 只跑 PATH 修复 + DPAPI 加载, 不启动 claude)
# 这能抓出 PARSE 通过但运行时报错的 bug (例如 -or 被当 Test-Path 参数)
$runnable = $decoded -replace '& claude --dangerously-skip-permissions', 'Write-Host "__BW_DRYRUN_OK__"'
$tmpPs1 = Join-Path $env:TEMP "bw-launcher-dryrun-$(Get-Random).ps1"
Set-Content -Path $tmpPs1 -Value $runnable -Encoding UTF8
try {
$dryRunOutput = (& pwsh -NoProfile -ExecutionPolicy Bypass -File $tmpPs1 2>&1 | Out-String)
# 只抓真正的 PS 错误 (ErrorRecord / cannot be found / parameter name / 等)
$errorPatterns = @(
'cannot be found that matches parameter name',
'A parameter cannot be found',
'CommandNotFoundException',
'ParameterBindingException',
'is not recognized as',
'cannot find.*because it does not exist',
'RuntimeException'
)
$hasError = $false
foreach ($pat in $errorPatterns) {
if ($dryRunOutput -match $pat) { $hasError = $true; break }
}
# 必须看到 dry-run 成功标记才算通过
$reachedEnd = $dryRunOutput -match '__BW_DRYRUN_OK__'
if ($hasError -or -not $reachedEnd) {
Write-Host "[gen-launcher-bats] ✗ 实跑验证失败:" -ForegroundColor Red
Write-Host $dryRunOutput -ForegroundColor DarkRed
throw "Base64 解码后脚本运行时错误 (hasError=$hasError, reachedEnd=$reachedEnd)"
}
Write-Host "[gen-launcher-bats] ✓ 实跑通过 (dry-run 到达 __BW_DRYRUN_OK__ 标记)" -ForegroundColor Green
} finally {
Remove-Item $tmpPs1 -Force -EA SilentlyContinue
}

123
tools/test-launcher-e2e.ps1 Normal file
View File

@ -0,0 +1,123 @@
<#
.SYNOPSIS
v3.1.1: 启动器 .lnk 端到端行为测试
.DESCRIPTION
模拟双击桌面 .lnk pwsh bw-launch.ps1 claude.ps1 链路.
检测点:
1. PS 进程能拉起 ( ExecutionPolicy 拒绝 / wt tab )
2. bw-launch.ps1 PATH 三层重载执行无错
3. claude.ps1 解析能找到 (不一定真启动 Claude TUI, --version 探测)
4. 整体退出码 = 0 (任何运行时错就拒绝)
替代手动双击, 集成到 build.ps1 后自动跑, 提前抓 v3.0.10 -or bug.
.NOTES
用法: pwsh -NoProfile -File tools/test-launcher-e2e.ps1
退出码: 0=PASS / 1=FAIL
日志: $env:TEMP\bw-e2e-test.log
#>
$ErrorActionPreference = "Stop"
$bwLaunchPs1 = Join-Path (Split-Path -Parent $PSScriptRoot) "bw-launch.ps1"
$logFile = Join-Path $env:TEMP "bw-e2e-test.log"
if (-not (Test-Path $bwLaunchPs1)) {
Write-Host "[FAIL] bw-launch.ps1 缺失: $bwLaunchPs1" -ForegroundColor Red
exit 1
}
Write-Host "[e2e] 测试目标: $bwLaunchPs1" -ForegroundColor Cyan
# Test 1: PS 解析 wrapper 文件 (静态 syntax)
$err = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($bwLaunchPs1, [ref]$null, [ref]$err)
if ($err) {
Write-Host "[FAIL] bw-launch.ps1 PARSE 错: $($err.Count)" -ForegroundColor Red
$err | Select-Object -First 3 | ForEach-Object { Write-Host " L$($_.Extent.StartLineNumber): $($_.Message)" -ForegroundColor Red }
exit 1
}
Write-Host "[e2e ✓] Test 1 — bw-launch.ps1 PARSE OK" -ForegroundColor Green
# Test 2: 实跑 wrapper, claude 不可用场景 (期望 GUI 弹窗 + 退出码 1, 不是闪退)
# 用临时 PATH 隔离 claude
$pwshExe = (Get-Command pwsh -EA SilentlyContinue).Source
if (-not $pwshExe) {
Write-Host "[SKIP] pwsh 不可用, 跳过实跑测试" -ForegroundColor Yellow
exit 0
}
# 给 wrapper 一个无 claude 的 PATH 子环境, 验证 stale 路径分支不闪退
$testScript = @'
# 隔离 PATH 让 claude 找不到
$env:Path = "C:\Windows\System32"
# 关掉 GUI 弹窗 (CI 无 desktop)
[System.Reflection.Assembly]::Load('System.Windows.Forms') | Out-Null
$env:BW_E2E_NOGUI = "1"
'@
Write-Host "[e2e] Test 2 — wrapper 静态分析 (运行时缺 claude 应清晰报错而非闪退)" -ForegroundColor Cyan
$content = Get-Content $bwLaunchPs1 -Raw
$expectedFeatures = @{
"PATH 三层重载" = ($content -match 'GetEnvironmentVariable.*Machine' -and $content -match 'npm config get prefix')
"claude.ps1 fallback 链" = ($content -match 'claude\.ps1' -and $content -match 'Get-Command claude')
"失败 GUI 弹窗" = ($content -match 'MessageBox\]::Show')
"失败日志写入" = ($content -match 'bw-launch\.log')
"args 转发到 claude" = ($content -match '\$claudePs1.*@args' -or $content -match '\$args')
"exit code 传播" = ($content -match 'exit \$exitCode' -or $content -match 'exit \$LASTEXITCODE')
}
$failedFeatures = $expectedFeatures.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key }
if ($failedFeatures) {
Write-Host "[FAIL] wrapper 缺关键特性:" -ForegroundColor Red
$failedFeatures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
exit 1
}
Write-Host "[e2e ✓] Test 2 — wrapper 6 项特性齐全" -ForegroundColor Green
# Test 3: .lnk Args 4 项契约验证 (auto-setup.ps1 / install.ps1 一致性)
Write-Host "[e2e] Test 3 — .lnk Args 契约 (auto-setup + install 双向一致)" -ForegroundColor Cyan
$autoSetup = Get-Content (Join-Path (Split-Path -Parent $PSScriptRoot) "auto-setup.ps1") -Raw
$installPs1 = Get-Content (Join-Path (Split-Path -Parent $PSScriptRoot) "install.ps1") -Raw
$contractChecks = @{
"auto-setup.ps1 .lnk Args 含 -ExecutionPolicy Bypass" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*-ExecutionPolicy Bypass')
"auto-setup.ps1 .lnk Args 含 bwLaunchPs1 变量" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*\$bwLaunchPs1')
"auto-setup.ps1 .lnk Args 含 --skip-permissions" = ($autoSetup -match '\$shortcut\.Arguments\s*=[^\r\n]*dangerously-skip-permissions')
"install.ps1 .lnk Args 含 -ExecutionPolicy Bypass" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*-ExecutionPolicy Bypass')
"install.ps1 .lnk Args 含 bwLaunchPs1 变量" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*\$bwLaunchPs1')
"install.ps1 .lnk Args 含 --skip-permissions" = ($installPs1 -match '\$shortcut\.Arguments\s*=[^\r\n]*dangerously-skip-permissions')
}
$contractFails = $contractChecks.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key }
if ($contractFails) {
Write-Host "[FAIL] .lnk Args 契约不一致:" -ForegroundColor Red
$contractFails | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
exit 1
}
Write-Host "[e2e ✓] Test 3 — .lnk Args 6 项契约一致" -ForegroundColor Green
# Test 4: profile 双注入契约
Write-Host "[e2e] Test 4 — profile 双注入契约 (PS7 + PS5.1)" -ForegroundColor Cyan
$profileChecks = @{
"auto-setup.ps1 注入 PS7 profile (Documents\PowerShell)" = ($autoSetup -match 'Documents\\PowerShell.*profile\.ps1' -or $autoSetup -match 'Documents\\\\PowerShell')
"auto-setup.ps1 注入 PS5.1 profile (Documents\WindowsPowerShell)" = ($autoSetup -match 'Documents\\WindowsPowerShell' -or $autoSetup -match 'WindowsPowerShell')
"auto-setup.ps1 sentinel BW_CRED_START v3.1.0" = ($autoSetup -match 'BW_CRED_START v3\.1\.0')
"auto-setup.ps1 字面替换 (String.Replace)" = ($autoSetup -match '\.Replace\(\$match\.Value')
}
$profileFails = $profileChecks.GetEnumerator() | Where-Object { -not $_.Value } | ForEach-Object { $_.Key }
if ($profileFails) {
Write-Host "[FAIL] profile 注入契约缺失:" -ForegroundColor Red
$profileFails | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
exit 1
}
Write-Host "[e2e ✓] Test 4 — profile 双注入契约齐全" -ForegroundColor Green
# All passed
Write-Host ""
Write-Host "━━━ E2E 测试 SUMMARY ━━━" -ForegroundColor Green
Write-Host " Test 1: bw-launch.ps1 PARSE ✓"
Write-Host " Test 2: wrapper 6 项特性齐全 ✓"
Write-Host " Test 3: .lnk Args 6 项契约一致 ✓"
Write-Host " Test 4: profile 双注入契约齐全 ✓"
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Green
Write-Host "[PASS] E2E 测试通过 (4/4)" -ForegroundColor Green
exit 0

162
卸载Bookworm-impl.ps1 Normal file
View File

@ -0,0 +1,162 @@
<#
.SYNOPSIS
v3.1.3: Bookworm 完整卸载实<EFBFBD><EFBFBD>脚本
.DESCRIPTION
卸载Bookworm.bat 调用. 集中所有 PS 清理逻辑, 避免 cmd 多行 PS 转义.
清理项:
[1] 桌面 4-5 .lnk (启动/更新/体检/卸载/ Bookworm.lnk)
[2] ~/.claude/ 整个目录 (skills/hooks/agents/凭证)
[3] HKCU:\Software\Bookworm (DPAPI 缓存 + Toast 备份)
[4] PS7 + PS5.1 profile.ps1 BW_CRED + BW_CLIP
[5] HKCU 截图 Toast 设置还原 (从备份)
[6] ANTHROPIC_* + BW_LICENSE_KEY User 环境变量
[7] 提示用户手动删 bookworm-boot 目录
保留 (公共依赖, 不主动卸):
- Node.js / Git / PowerShell 7 / Claude Code (npm i -g)
#>
$ErrorActionPreference = "Continue"
Add-Type -AssemblyName System.Windows.Forms -EA SilentlyContinue
# ── 二次确认 ──
$msg = "确定要卸载 Bookworm 吗?`n`n"
$msg += "将删除:`n"
$msg += " - 桌面 4-5 个快捷方式`n"
$msg += " - ~/.claude 中 Bookworm 注入的内容 (skills/hooks/agents/scripts)`n"
$msg += " - HKCU DPAPI 凭证缓存`n"
$msg += " - PowerShell profile (PS7+PS5.1) 中的 BW_* 块`n"
$msg += " - 还原截图 Toast 默认设置`n"
$msg += " - ANTHROPIC_* User 环境变量`n`n"
$msg += "保留:`n"
$msg += " - ~/.claude/CLAUDE.md, memory/, projects/ (用户自有内容)`n"
$msg += " - Node.js / Git / PowerShell 7 / Claude Code"
$confirm = [System.Windows.Forms.MessageBox]::Show($msg, 'Bookworm 卸载二次确认', 'YesNo', 'Warning')
if ($confirm -ne 'Yes') {
Write-Host "Bookworm 卸载已取消" -ForegroundColor Yellow
exit 0
}
$logLines = @()
function Log { param([string]$m); Write-Host $m -ForegroundColor Cyan; $script:logLines += $m }
# ── [1/7] 桌面 .lnk ──
Log "[1/7] 删桌面快捷方式"
$desk = [Environment]::GetFolderPath('Desktop')
foreach ($n in @('启动Bookworm.lnk','更新Bookworm.lnk','体检Bookworm.lnk','卸载Bookworm.lnk','Bookworm.lnk')) {
$p = Join-Path $desk $n
if (Test-Path $p) {
try { Remove-Item $p -Force -EA Stop; Log " ✓ 删 $n" }
catch { Log " [!] 删 $n 失败: $_" }
}
}
# ── [2/7] ~/.claude Bookworm 注入内容 (精准删除, 保留用户自有配置) ──
Log "[2/7] 清 ~/.claude/ 中 Bookworm 注入的文件"
$claudeDir = Join-Path $env:USERPROFILE '.claude'
if (Test-Path $claudeDir) {
# 只删 Bookworm 管理的子目录和文件, 不删用户自己的 CLAUDE.md / memory / projects
$bwManagedDirs = @('skills', 'hooks', 'agents', 'scripts', 'constitution', 'patches', 'session-state')
foreach ($d in $bwManagedDirs) {
$dp = Join-Path $claudeDir $d
if (Test-Path $dp) {
try { Remove-Item $dp -Recurse -Force -EA Stop; Log " ✓ 删 $d/" }
catch { Log " [!] 删 $d/ 失败: $_" }
}
}
# 删 Bookworm 的配置文件 (settings.json 含 hooks 注册)
foreach ($f in @('settings.json', 'settings.local.json', 'stats-compiled.json')) {
$fp = Join-Path $claudeDir $f
if (Test-Path $fp) {
try { Remove-Item $fp -Force -EA Stop; Log " ✓ 删 $f" }
catch { Log " [!] 删 $f 失败: $_" }
}
}
Log " · 保留 ~/.claude/CLAUDE.md, memory/, projects/ (用户自有内容)"
}
# ── [3/7] HKCU DPAPI 凭证 + Toast 备份元数据 ──
Log "[3/7] 清 HKCU:\Software\Bookworm DPAPI 凭证"
# 先读 Toast 备份, 后面 [5] 用
$toastOrig = $null
$bak = 'HKCU:\Software\Bookworm\ToastBackup'
if (Test-Path $bak) {
try { $toastOrig = (Get-ItemProperty -Path $bak -Name 'ScreenSketchToast_Original' -EA SilentlyContinue).ScreenSketchToast_Original } catch {}
}
foreach ($r in @('HKCU:\Software\Bookworm\CachedEnv','HKCU:\Software\Bookworm\ToastBackup','HKCU:\Software\Bookworm')) {
if (Test-Path $r) {
try { Remove-Item $r -Recurse -Force -EA Stop; Log " ✓ 删 $r" }
catch { Log " [!] 删 $r 失败: $_" }
}
}
# ── [4/7] profile.ps1 BW_CRED + BW_CLIP 块清理 (PS7+PS5.1) ──
Log "[4/7] 清 profile.ps1 BW_CRED + BW_CLIP 块"
foreach ($pdir in @(
(Join-Path $env:USERPROFILE 'Documents\PowerShell'),
(Join-Path $env:USERPROFILE 'Documents\WindowsPowerShell')
)) {
$pp = Join-Path $pdir 'profile.ps1'
if (-not (Test-Path $pp)) { continue }
try {
$c = Get-Content $pp -Raw -Encoding UTF8
$orig = $c.Length
# 兼容 v3.0.x / v3.1.x 所有 sentinel 版本
$c = [regex]::Replace($c, '# BW_CRED_START[\s\S]*?# BW_CRED_END\r?\n?', '')
$c = [regex]::Replace($c, '# BW_CLIP_START[\s\S]*?# BW_CLIP_END\r?\n?', '')
if ($c.Length -ne $orig) {
[System.IO.File]::WriteAllText($pp, $c, [System.Text.UTF8Encoding]::new($false))
Log " ✓ 清理 $pp (移除 $($orig - $c.Length) 字节)"
} else {
Log " · $pp 无 BW_* 块, 跳过"
}
} catch { Log " [!] $pp 处理失败: $_" }
}
# ── [5/7] 还原截图 Toast ──
Log "[5/7] 还原截图 Toast 设置"
$toastReg = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.ScreenSketch_8wekyb3d8bbwe!App'
if ($toastOrig -eq '__ABSENT__') {
if (Test-Path $toastReg) {
try { Remove-ItemProperty -Path $toastReg -Name 'Enabled' -EA Stop; Log " ✓ Toast Enabled 项已删除 (恢复默认)" }
catch { Log " [!] Toast 还原失败: $_" }
}
} elseif ($toastOrig) {
try {
if (-not (Test-Path $toastReg)) { New-Item -Path $toastReg -Force | Out-Null }
Set-ItemProperty -Path $toastReg -Name 'Enabled' -Value ([int]$toastOrig) -Type DWord -Force
Log " ✓ Toast Enabled 还原为 $toastOrig"
} catch { Log " [!] Toast 还原失败: $_" }
} else {
Log " · 无 Toast 备份记录, 跳过"
}
# ── [6/7] ANTHROPIC_* User env ──
Log "[6/7] 清 ANTHROPIC_* / BW_LICENSE_KEY User 环境变量"
foreach ($ev in @('ANTHROPIC_API_KEY','ANTHROPIC_BASE_URL','ANTHROPIC_MODEL','BW_LICENSE_KEY')) {
$v = [Environment]::GetEnvironmentVariable($ev, 'User')
if ($v) {
try { [Environment]::SetEnvironmentVariable($ev, $null, 'User'); Log " ✓ 清 $ev" }
catch { Log " [!] 清 $ev 失败: $_" }
}
}
# ── [7/7] 提示手动删 bookworm-boot ──
Log "[7/7] 手动收尾"
Log " 请手动删除 bookworm-boot 目录 (本脚本无法删除自身所在目录):"
Log " 例如: $env:USERPROFILE\Downloads\bookworm-boot"
# ── 完成弹窗 ──
$endMsg = "Bookworm 卸载完成.`n`n"
$endMsg += "执行摘要:`n"
$endMsg += ($logLines -join "`n")
$endMsg += "`n`n保留的公共依赖 (如不再需要可手动卸):`n"
$endMsg += " - Node.js: %ProgramFiles%\nodejs`n"
$endMsg += " - Git for Windows: %ProgramFiles%\Git`n"
$endMsg += " - PowerShell 7: %ProgramFiles%\PowerShell\7`n"
$endMsg += " - Claude Code: 命令行运行 npm uninstall -g @anthropic-ai/claude-code"
[System.Windows.Forms.MessageBox]::Show($endMsg, 'Bookworm 卸载完成', 'OK', 'Information') | Out-Null
exit 0

View File

@ -1,50 +1,22 @@
@echo off @echo off
chcp 65001 > nul chcp 65001 > nul
title Bookworm Portable - 卸载 :: v3.1.3: Bookworm 完整卸载脚本
cd /d "%~dp0" :: 删除: 桌面 .lnk × 4 / ~/.claude / DPAPI HKCU 凭证 /
:: 双 profile (PS7+PS5.1) 的 BW_CRED + BW_CLIP 块 / Toast 备份还原 /
:: ANTHROPIC_* User 环境变量
:: 保留: Node/Git/PS7/Claude Code (公共依赖, 不主动卸)
:: 调用配套 PS 脚本完成所有清理 (cmd 不擅长 PS 多行)
echo. setlocal
echo ==================================== where pwsh >nul 2>nul && set "PSH=pwsh" || set "PSH=powershell"
echo Bookworm Portable - 完整卸载
echo ====================================
echo.
echo 将执行:
echo - 终止 Claude Code 进程
echo - 清除所有环境变量和凭证缓存
echo - 恢复原始 .claude 目录
echo - 清除 PowerShell 历史和 Git 凭证
echo - 删除桌面快捷方式
echo.
:: AutoAccept: 卸载确认已豁免 set "UNINST_PS1=%~dp0卸载Bookworm-impl.ps1"
:: set /p confirm=" 确认卸载? (y/n): " if not exist "%UNINST_PS1%" (
:: if /i not "%confirm%"=="y" ( echo [!] 卸载实现脚本缺失: %UNINST_PS1%
:: echo 已取消 echo [!] 请重跑 Bookworm-Setup.exe Phase 3 重新克隆 bookworm-boot
:: pause pause
:: exit /b exit /b 1
:: )
echo [AutoAccept] 自动确认卸载
echo.
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
pwsh -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep
) else (
powershell -ExecutionPolicy Bypass -File stop.ps1 -Restore -Deep
) )
:: 删除桌面快捷方式 %PSH% -NoProfile -ExecutionPolicy Bypass -File "%UNINST_PS1%"
del "%USERPROFILE%\Desktop\Bookworm.lnk" 2>nul endlocal
del "%USERPROFILE%\Desktop\更新Bookworm.lnk" 2>nul
:: 清除凭证缓存注册表
reg delete "HKCU\Software\Bookworm" /f 2>nul
echo.
echo ====================================
echo Bookworm 已完全卸载
echo 可安全删除 bookworm-boot 文件夹
echo ====================================
echo.
pause

View File

@ -1,17 +0,0 @@
@echo off
setlocal
chcp 65001 > nul 2>&1
title Bookworm Smart Assistant
:: 日常启动入口: 静默更新 + 直接启动 (无需管理员权限)
set "NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1"
where node >nul 2>nul
if %errorlevel% equ 0 goto :RUN
echo [!!] Node.js 未安装, 请先运行 Bookworm-Install.bat
pause
exit /b 1
:RUN
node "%~dp0setup-all.js" --start
endlocal

View File

@ -1,26 +1,44 @@
@echo off @echo off
chcp 65001 > nul chcp 65001 > nul
cd /d "%~dp0" :: v3.0.11 架构重构: 桌面 .lnk 已直调 pwsh + claude.ps1 绝对路径, 不再走本 bat.
:: 此文件仅保留作为兼容入口 (老用户已经把 .bat 加到收藏夹/开始菜单的场景),
:: 内部行为简化为转发到桌面 .lnk 触发统一启动路径.
::
:: 如果用户双击本 .bat (而非桌面 .lnk), 直接 invoke pwsh + claude.ps1 启动:
:: 中转站在国内,不走代理 setlocal
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
:: 优先 pwsh7: 新窗口启动 claude, 先从注册表缓存加载凭证 (DPAPI 解密) :: 1. 定位 pwsh.exe (PS7 必需, 启动器 v3.0.11 强依赖)
where pwsh >nul 2>nul set "PWSH_EXE="
if %errorlevel% equ 0 ( where pwsh >nul 2>nul && for /f "delims=" %%i in ('where pwsh') do if not defined PWSH_EXE set "PWSH_EXE=%%i"
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -Command "Add-Type -AssemblyName System.Security;$r='HKCU:\Software\Bookworm\CachedEnv';try{(Get-ItemProperty $r -EA Stop).PSObject.Properties|Where-Object{$_.Name-match'^[A-Z_]+$'}|ForEach-Object{$v=$_.Value;try{$b=[Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v),$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);$v=[Text.Encoding]::UTF8.GetString($b)}catch{};[Environment]::SetEnvironmentVariable($_.Name,$v,'Process')}}catch{};& claude --dangerously-skip-permissions" if not defined PWSH_EXE if exist "%ProgramFiles%\PowerShell\7\pwsh.exe" set "PWSH_EXE=%ProgramFiles%\PowerShell\7\pwsh.exe"
exit if not defined PWSH_EXE if exist "%LOCALAPPDATA%\Microsoft\PowerShell\pwsh.exe" set "PWSH_EXE=%LOCALAPPDATA%\Microsoft\PowerShell\pwsh.exe"
if not defined PWSH_EXE (
echo [!] PowerShell 7 未安装. 请先重跑 Bookworm-Setup.exe 装好 PS7.
pause
exit /b 1
) )
:: 回退 PowerShell 5.1 :: 2. 定位 claude.ps1 (优先 npm prefix, 兜底常见位置)
title Bookworm Portable set "CLAUDE_PS1="
echo. for /f "delims=" %%i in ('npm config get prefix 2^>nul') do set "NPM_PREFIX=%%i"
echo [!] PowerShell 7 未安装, 使用 PowerShell 5.1 if defined NPM_PREFIX if exist "%NPM_PREFIX%\claude.ps1" set "CLAUDE_PS1=%NPM_PREFIX%\claude.ps1"
echo. if not defined CLAUDE_PS1 if exist "%APPDATA%\npm\claude.ps1" set "CLAUDE_PS1=%APPDATA%\npm\claude.ps1"
powershell -ExecutionPolicy Bypass -File install.ps1 -StartOnly -AutoAccept if not defined CLAUDE_PS1 if exist "%ProgramFiles%\nodejs\claude.ps1" set "CLAUDE_PS1=%ProgramFiles%\nodejs\claude.ps1"
if %errorlevel% neq 0 ( if not defined CLAUDE_PS1 (
echo. echo [!] claude.ps1 未找到. 请重跑 Bookworm-Setup.exe 修复 Claude Code 安装.
echo 启动失败,按任意键退出... pause
pause > nul exit /b 1
) )
:: 3. OTA 自动更新检查 (fail-open: 脚本不存在或报错均不阻断启动)
set "OTA_SCRIPT=%USERPROFILE%\.claude\.bw-ota\bw-ota.ps1"
if exist "%OTA_SCRIPT%" (
"%PWSH_EXE%" -NoLogo -ExecutionPolicy Bypass -File "%OTA_SCRIPT%"
)
:: 4. 直调 pwsh + claude.ps1 (无 wt / 无 Base64 / 无 DPAPI in-bat)
:: 凭证由 pwsh profile.ps1 BW_CRED_START..END 块自动加载
"%PWSH_EXE%" -NoLogo -NoExit -File "%CLAUDE_PS1%" --dangerously-skip-permissions
endlocal

45
更新Bookworm.bat Normal file
View File

@ -0,0 +1,45 @@
@echo off
chcp 65001 > nul
cd /d "%~dp0"
:: v3.1.1 架构: 更新 .bat 仅做 git pull, 完成后弹 GUI 让用户决定是否立即启动.
:: 启动 claude 由独立的 启动Bookworm.lnk → pwsh + bw-launch.ps1 完成 (1 跳直链)
echo.
echo Bookworm 配置同步
echo ============================================
echo.
set HAS_FAIL=0
:: 同步 bookworm-boot 仓库 (本目录)
echo [1/2] 同步启动器目录 (bookworm-boot)...
git pull --rebase 2>&1
if %errorlevel% neq 0 (
echo [!] bookworm-boot git pull 失败 ^(不影响启动 lnk^)
set HAS_FAIL=1
)
:: 同步 ~/.claude 配置仓库 (Skill/hook/agents)
echo.
echo [2/2] 同步 Claude 配置 (.claude/)...
git -C "%USERPROFILE%\.claude" stash -q 2>nul
git -C "%USERPROFILE%\.claude" pull --rebase 2>&1
if %errorlevel% neq 0 (
echo [!] .claude git pull 失败 ^(不影响启动 lnk^)
set HAS_FAIL=1
)
git -C "%USERPROFILE%\.claude" stash pop -q 2>nul
echo.
echo ============================================
if %HAS_FAIL% equ 0 (
echo [OK] 所有同步完成
) else (
echo [!] 部分同步失败 ^(详见上方日志, 启动仍可正常使用^)
)
echo ============================================
:: v3.1.1 (闭合 L6): 完成后 GUI 询问是否立即启动 Claude (闭合 "更新完了不知道下一步")
:: 用 PowerShell 弹 MessageBox YesNo, Yes → 触发桌面启动 lnk; No → 直接退出
where pwsh >nul 2>nul && set "PSH=pwsh" || set "PSH=powershell"
%PSH% -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $r = [System.Windows.Forms.MessageBox]::Show('配置同步完成. 是否立即启动 Bookworm Claude?', 'Bookworm 同步完成', 'YesNo', 'Question'); if ($r -eq 'Yes') { $lnk = Join-Path ([Environment]::GetFolderPath('Desktop')) '启动Bookworm.lnk'; if (Test-Path $lnk) { Start-Process $lnk } else { Write-Host 'lnk 缺失, 请重跑 Bookworm-Setup.exe' -ForegroundColor Yellow; pause } }"

View File

@ -1,25 +0,0 @@
@echo off
chcp 65001 > nul
cd /d "%~dp0"
:: 中转站在国内,不走代理
set NO_PROXY=bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1
set no_proxy=%NO_PROXY%
:: 检测 pwsh7 可用性
where pwsh >nul 2>nul
if %errorlevel% equ 0 (
:: pwsh7: 先同步配置, 再在新窗口启动 claude (从注册表缓存加载 DPAPI 凭证)
pwsh -NoLogo -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept -SkipLaunch
start "Bookworm Smart Assistant" pwsh -NoLogo -NoExit -Command "Add-Type -AssemblyName System.Security;$r='HKCU:\Software\Bookworm\CachedEnv';try{(Get-ItemProperty $r -EA Stop).PSObject.Properties|Where-Object{$_.Name-match'^[A-Z_]+$'}|ForEach-Object{$v=$_.Value;try{$b=[Security.Cryptography.ProtectedData]::Unprotect([Convert]::FromBase64String($v),$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);$v=[Text.Encoding]::UTF8.GetString($b)}catch{};[Environment]::SetEnvironmentVariable($_.Name,$v,'Process')}}catch{};& claude --dangerously-skip-permissions"
exit
)
:: 回退 PowerShell 5.1: 一次调用完成更新+加载凭证+启动 (消除双次调用)
title Bookworm Portable
powershell -ExecutionPolicy Bypass -File "%~dp0install.ps1" -AutoAccept
if %errorlevel% neq 0 (
echo.
echo 启动失败,按任意键退出...
pause > nul
)