feat: Keychain 免密 + 版本号同步 + bw-update 修复

- Bookworm-Setup.sh: macOS Keychain 本日免密缓存(对标 Windows Credential Manager)
- Bookworm-OneClick-Mac.sh: bw-update 补 config 仓库更新
- sync-version.js: 新建版本号同步脚本(从 stats-compiled.json 自动注入)
- quick-start.html/quick-reference.txt: Hooks 29→34 修正

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
bookworm 2026-04-06 14:05:29 +08:00
parent d499342271
commit 23b369f99b
5 changed files with 168 additions and 5 deletions

View File

@ -277,7 +277,7 @@ if ! grep -q "$ALIAS_MARKER" "$SHELL_RC" 2>/dev/null; then
# Bookworm Portable aliases
alias bw='cd ~/bookworm-boot && NO_PROXY="bww.letcareme.com,code.letcareme.com,localhost,127.0.0.1" claude'
alias bw-update='cd ~/bookworm-boot && git pull && echo "Updated!"'
alias bw-update='cd ~/bookworm-boot && git pull && cd ~/.claude && git pull && echo "Updated!"'
ALIASES
success "已添加到 $SHELL_RC:"
info " bw — 启动 Bookworm"

View File

@ -198,11 +198,73 @@ for d in debug sessions cache backups telemetry memory projects; do
done
# ============================================================
# Step 4: 解密凭证
# Step 4: 解密凭证 (含 Keychain 本日免密)
# ============================================================
step 4 "解密凭证"
if [ -f "$SECRETS_ENC" ] && [ -n "$OPENSSL_CMD" ]; then
# Keychain 缓存相关
KEYCHAIN_SERVICE="bookworm-secrets"
KEYCHAIN_ACCOUNT="$(whoami)"
CACHE_LOADED=false
# 尝试从 Keychain 加载缓存
load_cached_secrets() {
local cached
cached=$(security find-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w 2>/dev/null) || return 1
# 检查是否过期 (缓存格式: EXPIRY=ISO日期\nKEY=VALUE\n...)
local expiry
expiry=$(echo "$cached" | head -1)
local expiry_date="${expiry#EXPIRY=}"
local today
today=$(date +%Y-%m-%d)
if [ "$expiry_date" != "$today" ]; then
# 已过期,删除缓存
security delete-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" 2>/dev/null || true
return 1
fi
# 加载环境变量 (跳过 EXPIRY 行)
local count=0
while IFS= read -r line; do
[ -z "$line" ] && continue
[[ "$line" == EXPIRY=* ]] && continue
local key="${line%%=*}"
local value="${line#*=}"
key=$(echo "$key" | tr -d ' ')
if [ -n "$key" ] && [ -n "$value" ]; then
export "$key=$value"
count=$((count + 1))
fi
done <<< "$cached"
if [ $count -gt 0 ] && [ -n "$ANTHROPIC_API_KEY" ]; then
success "从 Keychain 缓存加载 $count 个凭证 (免密)"
CACHE_LOADED=true
return 0
fi
return 1
}
# 保存凭证到 Keychain
save_secrets_to_cache() {
local today
today=$(date +%Y-%m-%d)
local data="EXPIRY=$today"
local env_keys="ANTHROPIC_API_KEY ANTHROPIC_BASE_URL GITHUB_PERSONAL_ACCESS_TOKEN SLACK_BOT_TOKEN ATLASSIAN_API_TOKEN BROWSERBASE_API_KEY FIRECRAWL_API_KEY"
for k in $env_keys; do
local v="${!k}"
if [ -n "$v" ]; then
data="$data
$k=$v"
fi
done
security add-generic-password -s "$KEYCHAIN_SERVICE" -a "$KEYCHAIN_ACCOUNT" -w "$data" -U 2>/dev/null && \
success "凭证已缓存至今日 23:59 (下次免密)" || \
warn "Keychain 缓存失败 (不影响使用)"
}
# 先尝试缓存
if load_cached_secrets 2>/dev/null; then
: # 缓存加载成功
elif [ -f "$SECRETS_ENC" ] && [ -n "$OPENSSL_CMD" ]; then
DECRYPTED=""
for attempt in 1 2 3; do
echo ""
@ -222,6 +284,13 @@ if [ -f "$SECRETS_ENC" ] && [ -n "$OPENSSL_CMD" ]; then
fi
done <<< "$DECRYPTED"
DECRYPTED=""
# 询问是否缓存
echo ""
read -p " 今日内免密启动? (y/n): " CACHE_CHOICE
if [ "$CACHE_CHOICE" = "y" ] || [ "$CACHE_CHOICE" = "Y" ]; then
save_secrets_to_cache
fi
break
else
if [ $attempt -lt 3 ]; then

View File

@ -47,7 +47,7 @@
# [1/6] 前置检查 (缺依赖自动提示 winget 安装)
# [2/6] 代理自动检测 (无需手动找端口)
# [3/6] 解密凭证 (输入主密码, 最多3次重试, 可选本日免密)
# [4/6] 同步配置 (git clone 92 Skills / 18 Agents / 29 Hooks)
# [4/6] 同步配置 (git clone 92 Skills / 18 Agents / 34 Hooks)
# [5/6] 渲染模板 + 初始化 + Bookworm 完整性验证 + MCP 检查
# [6/6] 启动 Claude Code

View File

@ -74,7 +74,7 @@
<div class="header">
<h1>Bookworm <span>Portable</span> 日常速查卡</h1>
<div class="ver">v1.5 | 92 Skills / 18 Agents / 29 Hooks<br>打印后贴在显示器旁边</div>
<div class="ver">v1.5 | 92 Skills / 18 Agents / 34 Hooks<br>打印后贴在显示器旁边</div>
</div>
<!-- ==================== 打开 PowerShell ==================== -->

94
sync-version.js Normal file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* sync-version.js stats-compiled.json 同步版本号到 boot 仓库文件
*
* 用法: node sync-version.js [--dry-run]
*
* 替换规则:
* {N} Skills stats.summary.skills
* {N} Agents stats.summary.agents
* {N} Hooks stats.summary.hooks (settings.json 注册的总数)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const DRY_RUN = process.argv.includes('--dry-run');
const BOOT_DIR = __dirname;
const STATS_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.claude', 'stats-compiled.json');
// 读取 stats
let stats;
try {
stats = JSON.parse(fs.readFileSync(STATS_PATH, 'utf8'));
} catch (e) {
console.error(` [FAIL] 无法读取 ${STATS_PATH}: ${e.message}`);
process.exit(1);
}
const skills = stats.summary.skills;
const agents = stats.summary.agents;
const hooks = stats.summary.hooks;
const version = stats.summary?.version || stats.version || 'v6.5.1';
console.log(` sync-version: ${skills} Skills / ${agents} Agents / ${hooks} Hooks (${version})`);
// 需要更新的文件 (相对于 boot 仓库)
const FILES = [
'Bookworm-Setup.sh',
'Bookworm-Setup.bat',
'Bookworm-OneClick.bat',
'Bookworm-OneClick-Win10.bat',
'Bookworm-OneClick-Mac.sh',
'install.ps1',
'guide-mac.html',
'quick-start.html',
'quick-reference.txt',
];
// 替换模式: 匹配 "数字 Skills"、"数字 Agents"、"数字 Hooks"
// 支持多种分隔格式: "92 Skills"、"92 个 Skills"、"<strong>92</strong> Skills"
const REPLACEMENTS = [
// 纯文本格式: "92 Skills"、"92 个 Skills"
{ pattern: /\b\d+ Skills/g, replacement: `${skills} Skills` },
{ pattern: /\b\d+ Agents/g, replacement: `${agents} Agents` },
{ pattern: /\b\d+ Hooks/g, replacement: `${hooks} Hooks` },
{ pattern: /\b\d+ 个 Skills/g, replacement: `${skills} 个 Skills` },
// HTML badge 格式: <strong>92</strong> Skills
{ pattern: /<strong>\d+<\/strong> Skills/g, replacement: `<strong>${skills}</strong> Skills` },
{ pattern: /<strong>\d+<\/strong> Agents/g, replacement: `<strong>${agents}</strong> Agents` },
{ pattern: /<strong>\d+<\/strong> Hooks/g, replacement: `<strong>${hooks}</strong> Hooks` },
// PS1 格式: "92 Skills / 18 Agents / 34 Hooks"
{ pattern: /\b\d+ Skills \/ \d+ Agents \/ \d+ Hooks/g, replacement: `${skills} Skills / ${agents} Agents / ${hooks} Hooks` },
// bat 格式: "92 Skills / 18 Agents"
{ pattern: /\b\d+ Skills \/ \d+ Agents/g, replacement: `${skills} Skills / ${agents} Agents` },
// "Bookworm (92 Skills)" / "Bookworm - 92 Skills"
{ pattern: /Bookworm \(\d+ Skills\)/g, replacement: `Bookworm (${skills} Skills)` },
{ pattern: /Bookworm - \d+ Skills/g, replacement: `Bookworm - ${skills} Skills` },
];
let totalChanged = 0;
for (const file of FILES) {
const filePath = path.join(BOOT_DIR, file);
if (!fs.existsSync(filePath)) continue;
let content = fs.readFileSync(filePath, 'utf8');
const original = content;
for (const { pattern, replacement } of REPLACEMENTS) {
content = content.replace(pattern, replacement);
}
if (content !== original) {
if (!DRY_RUN) {
fs.writeFileSync(filePath, content, 'utf8');
}
totalChanged++;
console.log(` ${DRY_RUN ? '[DRY] ' : ''}更新: ${file}`);
}
}
console.log(` 同步完成: ${totalChanged}/${FILES.length} 个文件更新`);
if (DRY_RUN) console.log(' [DRY RUN] 未实际写入');