Path A: client switch to issuer token-exchange + strip orphan legacy files

auto-setup.ps1 / bw-ota.ps1: replace anonymous-first + Gitea-credential
dialog branches with issuer token-exchange (auth.bookwormweb.com); embed
public crypto payloads via base64 (no secrets). build.ps1: regenerate
base64 payloads from source at compile time.

Strip 4 zero-reference legacy/admin orphan files (setup-all.js,
patches/admin-cb-routes.js, tools/legacy-gen-launcher-bats.ps1.archive,
create-admin-powershell.bat). Coupled sensitive sources (gen-authcode.js,
admin-authcode-gui.ps1, crypto-helper.js, install.ps1) deferred: still
referenced by build.ps1 -Admin path and auto-setup priority-4 backward-compat
decrypt chain; require dedicated surgery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
bookworm-admin 2026-06-29 19:50:04 +08:00
parent 25cf34a0eb
commit c6f89aad79
7 changed files with 205 additions and 1029 deletions

File diff suppressed because one or more lines are too long

View File

@ -70,6 +70,53 @@ if ($buildSetup) {
$bwVer = if ($versionLine) { $versionLine.Matches[0].Groups[1].Value } else { "0.0.0" }
Write-Host " 版本: $bwVer" -ForegroundColor Gray
# ── [Path-A] 注入 token-exchange 载荷 base64 (单文件自包含 Setup.exe; 每次 build 与源同步) ──
# 把三个公开加密载荷文件读出 → base64 → 替换 auto-setup.ps1 内 BEGIN/END 标记之间的内容。
# 注意: 载荷全是公开加密代码, 不含任何 secret (authcode 运行时用户输入, pepper 永远服务器端)。
Write-Step "注入 token-exchange 载荷 (base64 内嵌)"
$PayloadSrcRoot = Join-Path $env:USERPROFILE "bookworm-pathA-design\phase1-skeleton"
# 文件名 → 候选源路径 (优先 boot\issuer\ 暂存副本, 回退设计骨架仓)
$payloadMap = [ordered]@{
'seal-cli.js' = @((Join-Path $ScriptDir "issuer\seal-cli.js"), (Join-Path $PayloadSrcRoot "issuer\src\seal-cli.js"))
'crypto-handshake.js' = @((Join-Path $ScriptDir "issuer\crypto-handshake.js"), (Join-Path $PayloadSrcRoot "issuer\src\crypto-handshake.js"))
'bw-tokenexchange.ps1' = @((Join-Path $ScriptDir "issuer\bw-tokenexchange.ps1"), (Join-Path $PayloadSrcRoot "auto-setup-tokenexchange-draft\bw-tokenexchange.ps1"))
}
$b64Map = [ordered]@{}
foreach ($name in $payloadMap.Keys) {
$resolved = $null
foreach ($cand in $payloadMap[$name]) { if (Test-Path $cand) { $resolved = $cand; break } }
if (-not $resolved) {
Write-Fail "载荷源缺失: $name (查找: $($payloadMap[$name] -join ' | '))"
exit 1
}
$bytes = [IO.File]::ReadAllBytes($resolved)
$b64Map[$name] = [Convert]::ToBase64String($bytes)
Write-OK "$name <- $resolved ($($bytes.Length) B -> base64 $($b64Map[$name].Length) ch)"
}
# 生成 BEGIN..END 之间的内容 (4 空格缩进, 与 auto-setup.ps1 内 if 块对齐)
$nl = "`r`n"
$blockLines = New-Object System.Collections.Generic.List[string]
$blockLines.Add("# <BW_PAYLOAD_BASE64_BEGIN> —— 由 build.ps1 从载荷源文件自动注入, 请勿手改")
$blockLines.Add(" `$BwPayloadB64 = @{")
foreach ($name in $b64Map.Keys) {
$blockLines.Add(" '$name' = '$($b64Map[$name])'")
}
$blockLines.Add(" }")
$blockLines.Add(" # <BW_PAYLOAD_BASE64_END>")
$newBlock = $blockLines -join $nl
# 整文件替换 BEGIN..END (含标记) —— 用 MatchEvaluator 避免 $ 被当作正则分组引用
$content = [IO.File]::ReadAllText($inputPs1)
$pattern = '(?s)# <BW_PAYLOAD_BASE64_BEGIN>.*?# <BW_PAYLOAD_BASE64_END>'
if ($content -notmatch $pattern) {
Write-Fail "auto-setup.ps1 缺少 BW_PAYLOAD_BASE64 标记, 无法注入 (检查 BEGIN/END 注释)"
exit 1
}
$evaluator = [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $newBlock }
$content = [regex]::Replace($content, $pattern, $evaluator)
# 保留 auto-setup.ps1 原编码: UTF-8 无 BOM (含中文; PS2EXE/pwsh 默认按 UTF-8 读)
[IO.File]::WriteAllText($inputPs1, $content, (New-Object System.Text.UTF8Encoding($false)))
Write-OK "base64 载荷已注入 auto-setup.ps1 (3 文件, 共 $($newBlock.Length) ch)"
# 优先用桌面专用 B 圆图标, 回退到 galaxy
$iconFile = Join-Path $ScriptDir "bookworm-desktop.ico"
if (-not (Test-Path $iconFile)) {

View File

@ -35,6 +35,10 @@ $ConfigFile = Join-Path $OtaDir 'config.json'
$CredFile = Join-Path $OtaDir 'pull-cred.dpapi'
$PubKeyFile = Join-Path $OtaDir 'signing-pubkey.pem'
# [Path-A] dot-source token-exchange 共享模块 (随核心分发到 ~/.claude/issuer/)
$BwTxModule = (Join-Path $env:USERPROFILE '.claude\issuer\bw-tokenexchange.ps1')
if (Test-Path $BwTxModule) { . $BwTxModule }
# ========== 日志 ==========
function Write-Ota ($m, $c = 'Cyan') { Write-Host "[Bookworm OTA] $m" -ForegroundColor $c }
function Write-OtaOk ($m) { Write-Ota $m 'Green' }
@ -92,6 +96,26 @@ function Read-Credential {
}
}
# ========== [Path-A] DPAPI authcode 读取 (替换 user/pass) ==========
# [需完整宪法确认 OTA-authcode] 持久化 authcode 的安装时写入/轮换/撤销链 (DPAPI, CurrentUser 绑定)。
# 复用 $CredFile, 期望明文 JSON 形如 { "authcode": "BWA-..." }。
function Read-BwAuthcode {
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
if ($json -and $json.authcode) { return [string]$json.authcode }
return $null
} catch {
return $null
}
}
# ========== 语义版本比较 (major.minor.patch) ==========
function Compare-SemVer ($local, $remote) {
$lParts = ($local -replace '^v', '') -split '\.' | ForEach-Object { [int]$_ }
@ -130,11 +154,11 @@ function Get-AuthHeaders ($cred) {
}
# ========== 远端版本查询 (Gitea raw, 3s 超时) ==========
function Get-RemoteVersion ($cred, $cfg) {
function Get-RemoteVersion ($token, $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
$headers = Get-BwTokenAuthHeader -ShortLivedToken $token # [Path-A] token 鉴权替换 Basic
$resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3
return $resp.Content.Trim()
} catch {
@ -143,13 +167,13 @@ function Get-RemoteVersion ($cred, $cfg) {
}
# ========== 远端最新 tag 查询 (备用) ==========
function Get-RemoteLatestTag ($cred, $cfg) {
function Get-RemoteLatestTag ($token, $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
$headers = Get-BwTokenAuthHeader -ShortLivedToken $token # [Path-A] token 鉴权替换 Basic
$resp = Invoke-WebRequest -Uri $apiUrl -Headers $headers -UseBasicParsing -TimeoutSec 3
$tags = $resp.Content | ConvertFrom-Json
if ($tags.Count -gt 0) { return $tags[0].name }
@ -158,7 +182,7 @@ function Get-RemoteLatestTag ($cred, $cfg) {
}
# ========== 同步核心 (精简版 bookworm-sync.ps1) ==========
function Invoke-OtaSync ($cred, $cfg, $remoteVersion) {
function Invoke-OtaSync ($token, $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('/')
@ -167,14 +191,19 @@ function Invoke-OtaSync ($cred, $cfg, $remoteVersion) {
$stageDir = Join-Path $env:TEMP "bw-ota-$([Guid]::NewGuid().ToString().Substring(0,8))"
# 1. Clone (凭证通过 credential manager 传递, 不嵌入 URL)
# 1. [Path-A] token-exchange clone: 短期 token 嵌 URL + credential.helper= 临时覆盖
# 不再 git credential approve / 不写 Credential Manager; clone 后立即清理 token 变量。
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)" }
$uri = [System.Uri]("https://${host_}/${repoPath}.git")
$authedUrl = "$($uri.Scheme)://$([System.Uri]::EscapeDataString('bw-token')):$([System.Uri]::EscapeDataString($token))@$($uri.Host)$($uri.PathAndQuery)"
$cloneArgs = @('-c', 'credential.helper=', '-c', 'core.longpaths=true', 'clone', '--depth', '1', '--branch', $ref, '--single-branch', $authedUrl, $stageDir)
try {
& git @cloneArgs 2>&1 | Out-Null
$cloneExit = $LASTEXITCODE
} finally {
Remove-Variable -Name authedUrl -ErrorAction SilentlyContinue
}
if ($cloneExit -ne 0) { throw "clone 失败 (exit $cloneExit)" }
$gitDir = Join-Path $stageDir '.git'
if (Test-Path $gitDir) { Remove-Item -Recurse -Force $gitDir }
@ -321,21 +350,25 @@ function Invoke-OtaMain {
return
}
# 解密凭证
$cred = Read-Credential
if (-not $cred -or -not $cred.user -or -not $cred.pass) {
Write-OtaWarn "凭证解密失败, 跳过更新检查"
# [Path-A] 取持久化 authcode → 换短期 token (替换 DPAPI user/pass; 模块缺失则 fail-open 跳过)
if (-not (Get-Command Get-BwShortLivedToken -ErrorAction SilentlyContinue)) {
Write-OtaWarn "token-exchange 模块未加载, 跳过更新检查"
return
}
$authcode = Read-BwAuthcode
if (-not $authcode) { Write-OtaWarn "未找到 authcode, 跳过更新检查"; return }
$token = Get-BwShortLivedToken -Authcode $authcode -DeviceId (Get-BwDeviceId)
Remove-Variable -Name authcode -ErrorAction SilentlyContinue
if (-not $token) { Write-OtaWarn "token 换取失败, 跳过更新检查"; return }
# 本地 vs 远端版本
$localVer = Get-LocalVersion
if ($DryRun) { Write-Ota "[DryRun 模式] 仅验证, 不替换文件" 'Yellow' }
Write-Ota "当前版本: $localVer"
$remoteVer = Get-RemoteVersion $cred $cfg
$remoteVer = Get-RemoteVersion $token $cfg
if (-not $remoteVer) {
$remoteVer = Get-RemoteLatestTag $cred $cfg
$remoteVer = Get-RemoteLatestTag $token $cfg
if ($remoteVer) { $remoteVer = $remoteVer -replace '^v', '' }
}
@ -375,7 +408,7 @@ function Invoke-OtaMain {
}
}
Invoke-OtaSync $cred $cfg $remoteClean
Invoke-OtaSync $token $cfg $remoteClean
Write-Host ""
}
catch {

View File

@ -1,54 +0,0 @@
'use strict';
/**
* Circuit Breaker 管理端点补丁
* 追加到 routes/admin.js 末尾 ( module.exports 函数内)
*
* 用法: 部署时将此内容追加到 admin.js registerAdminRoutes 函数体内
*/
// ─── 熔断器状态查看 (admin) ───
// routes['GET:/v1/admin/circuit-breaker'] = async (req, res) => {
// requireAdmin(req);
// const cbStatus = deps.circuitBreaker.getStatus();
// const cbLog = deps.circuitBreaker.getTransitionLog(20);
// json(res, 200, { ok: true, breakers: cbStatus, recentTransitions: cbLog });
// };
// ─── 熔断器重置 (admin) ───
// routes['POST:/v1/admin/circuit-breaker/reset'] = async (req, res) => {
// requireAdmin(req);
// const body = await parseJsonBody(req);
// if (body.provider) {
// deps.circuitBreaker.reset(body.provider);
// json(res, 200, { ok: true, reset: body.provider });
// } else {
// deps.circuitBreaker.resetAll();
// json(res, 200, { ok: true, reset: 'all' });
// }
// };
module.exports = function patchAdminRoutes(routes, deps) {
const { json, parseJsonBody, requireAdmin } = deps;
routes['GET:/v1/admin/circuit-breaker'] = async (req, res) => {
requireAdmin(req);
const cbStatus = deps.circuitBreaker.getStatus();
const cbLog = deps.circuitBreaker.getTransitionLog ? deps.circuitBreaker.getTransitionLog(20) : [];
json(res, 200, { ok: true, breakers: cbStatus, recentTransitions: cbLog });
};
routes['POST:/v1/admin/circuit-breaker/reset'] = async (req, res) => {
requireAdmin(req);
const body = await parseJsonBody(req);
if (body.provider) {
deps.circuitBreaker.reset(body.provider);
json(res, 200, { ok: true, reset: body.provider });
} else if (deps.circuitBreaker.resetAll) {
deps.circuitBreaker.resetAll();
json(res, 200, { ok: true, reset: 'all' });
} else {
json(res, 400, { error: '请指定 provider' });
}
};
};

View File

@ -1,616 +0,0 @@
#!/usr/bin/env node
'use strict';
/**
* Bookworm Portable - 全自动安装引擎 v3.0
* @module setup-all
*/
// W3: Node.js 版本检查
if (parseInt(process.versions.node) < 14) {
console.error(' [!!] Node.js 版本过低 (' + process.version + '), 需要 14.0+');
console.error(' 请更新: https://nodejs.org/');
process.exit(1);
}
const { execSync, spawnSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const crypto = require('crypto');
const os = require('os');
// ─── 配置 ───
const HOME = os.homedir();
const BOOT_DIR = path.join(HOME, 'bookworm-boot');
const CLAUDE_DIR = path.join(HOME, '.claude');
const GITEA_BOOT = 'https://code.letcareme.com/bookworm/bookworm-boot.git';
const GITEA_CONFIG = 'https://code.letcareme.com/bookworm/bookworm-config.git';
const NPM_MIRROR = 'https://registry.npmmirror.com';
const SCRIPT_DIR = __dirname;
// ─── 颜色输出 ───
const c = {
reset: '\x1b[0m', bold: '\x1b[1m',
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
blue: '\x1b[34m', cyan: '\x1b[36m', dim: '\x1b[90m',
};
function ok(msg) { console.log(` ${c.green}[OK]${c.reset} ${msg}`); }
function warn(msg) { console.log(` ${c.yellow}[!]${c.reset} ${msg}`); }
function fail(msg) { console.log(` ${c.red}[!!]${c.reset} ${msg}`); }
function info(msg) { console.log(` ${c.dim}[..]${c.reset} ${msg}`); }
function step(n, total, msg) {
console.log(`\n ${c.bold}[${n}/${total}]${c.reset} ${c.cyan}${msg}${c.reset}`);
}
// ─── 工具函数 ───
function hasCmd(cmd) {
if (!/^[a-zA-Z0-9._-]+$/.test(cmd)) return false; // B3: 防命令注入
try {
execSync(`where ${cmd}`, { stdio: 'pipe' });
return true;
} catch { return false; }
}
function run(cmd, opts = {}) {
try {
return execSync(cmd, {
stdio: opts.silent ? 'pipe' : 'inherit',
encoding: 'utf8',
timeout: opts.timeout || 300000,
env: { ...process.env, ...opts.env },
...opts,
});
} catch (e) {
if (opts.ignoreError) return '';
throw e;
}
}
function runSilent(cmd) {
try {
return execSync(cmd, { stdio: 'pipe', encoding: 'utf8', timeout: 60000 });
} catch { return ''; }
}
function wingetInstall(id, name) {
if (!hasCmd('winget')) {
warn(`winget 不可用, 请手动安装 ${name}`);
return false;
}
info(`通过 winget 安装 ${name}...`);
try {
run(`winget install ${id} --accept-source-agreements --accept-package-agreements --silent`, { timeout: 600000 });
refreshPath();
ok(`${name} 安装成功`);
return true;
} catch (e) {
fail(`${name} 安装失败: ${e.message}`);
return false;
}
}
function refreshPath() {
try {
const sysPath = runSilent('reg query "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" /v Path')
.match(/REG_\w+\s+(.+)/)?.[1] || '';
const usrPath = runSilent('reg query "HKCU\\Environment" /v Path')
.match(/REG_\w+\s+(.+)/)?.[1] || '';
const extra = [
'C:\\Program Files\\nodejs',
'C:\\Program Files\\Git\\cmd',
'C:\\Program Files\\Git\\usr\\bin',
'C:\\Program Files\\PowerShell\\7',
path.join(HOME, 'AppData\\Local\\Microsoft\\WinGet\\Packages'),
path.join(HOME, 'AppData\\Roaming\\npm'),
].join(';');
// I2: 动态扫描 Python 安装路径
const pyBase = path.join(HOME, 'AppData', 'Local', 'Programs', 'Python');
let pyPaths = '';
try {
if (fs.existsSync(pyBase)) {
for (const d of fs.readdirSync(pyBase)) {
pyPaths += ';' + path.join(pyBase, d) + ';' + path.join(pyBase, d, 'Scripts');
}
}
} catch {}
process.env.PATH = `${sysPath};${usrPath};${extra}${pyPaths}`;
} catch {}
}
function askPassword(prompt) {
return new Promise((resolve) => {
process.stdout.write(prompt);
// Windows: 用 PowerShell 隐藏输入
try {
const result = execSync(
'powershell -Command "[Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR((Read-Host -AsSecureString)))"',
{ stdio: ['inherit', 'pipe', 'pipe'], encoding: 'utf8', timeout: 120000 }
).trim();
resolve(result);
} catch {
// W5: 回退明文输入, 给出警告
warn('PowerShell 不可用, 密码将以明文显示');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question('', (ans) => { rl.close(); resolve(ans.trim()); });
}
});
}
// ─── 加密/解密 (与 crypto-helper.js 同格式) ───
function decryptSecrets(filePath, password) {
const data = fs.readFileSync(filePath);
const magic = data.slice(0, 6).toString();
if (magic !== 'BWENC1') throw new Error('WRONG_FORMAT');
const salt = data.slice(6, 22);
const encrypted = data.slice(22);
const derived = crypto.pbkdf2Sync(password, salt, 600000, 48, 'sha256');
const key = derived.slice(0, 32);
const iv = derived.slice(32, 48);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
try {
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
} catch {
throw new Error('WRONG_PASSWORD');
}
}
function escVbs(s) { return s.replace(/"/g, '""'); } // B4: 防 VBS 注入
function createShortcut(name, target, workDir) {
const vbs = path.join(os.tmpdir(), `bw_sc_${Date.now()}.vbs`);
const desktop = path.join(HOME, 'Desktop');
const lnkPath = escVbs(path.join(desktop, name + '.lnk'));
fs.writeFileSync(vbs, `Set ws = CreateObject("WScript.Shell")
Set sc = ws.CreateShortcut("${lnkPath}")
sc.TargetPath = "${escVbs(target)}"
sc.WorkingDirectory = "${escVbs(workDir)}"
sc.Description = "Bookworm Smart Assistant"
sc.Save
`);
try { execSync(`cscript //nologo "${vbs}"`, { stdio: 'pipe' }); return true; }
catch { return false; }
finally { try { fs.unlinkSync(vbs); } catch {} }
}
// W8: 动态检测 Git Bash 路径
function findGitBash() {
const candidates = ['C:\\Program Files\\Git\\bin\\bash.exe', 'C:\\Program Files (x86)\\Git\\bin\\bash.exe'];
try {
const gitPath = runSilent('where git').trim().split('\n')[0];
if (gitPath) candidates.unshift(path.join(path.dirname(path.dirname(gitPath)), 'bin', 'bash.exe'));
} catch {}
return candidates.find(p => fs.existsSync(p)) || candidates[0];
}
// ─── Banner ───
function banner() {
console.log(`
${c.cyan}
____ _
| __ ) ___ ___ | | ____ _____ _ __ ___
| _ \\ / _ \\ / _ \\| |/ /\\ \\ /\\ / / _ \\| '__|
| |_) | (_) | (_) | < \\ V V / (_) | |
|____/ \\___/ \\___/|_|\\_\\ \\_/\\_/ \\___/|_|
${c.bold}全自动安装引擎 v3.0${c.cyan}
双击即装: Node + Git + Python + PS7 + Claude + MCP
${c.reset}
`);
}
// ═══════════════════════════════════════════
// 主流程
// ═══════════════════════════════════════════
async function main() {
// ─── 启动模式: 已安装过 → 静默更新 + 直接启动 ───
const isInstalled = hasCmd('claude') && fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'));
const startOnly = process.argv.includes('--start') || process.argv.includes('-s');
if (isInstalled && startOnly) {
return quickStart();
}
banner();
const TOTAL = 9;
let errors = 0;
// ─── 1. Git ───
step(1, TOTAL, '安装 Git');
if (hasCmd('git')) {
ok('Git 已安装');
} else {
if (!wingetInstall('Git.Git', 'Git')) errors++;
}
// ─── 2. Python ───
step(2, TOTAL, '安装 Python');
if (hasCmd('python') || hasCmd('python3') || hasCmd('py')) {
ok('Python 已安装');
} else {
if (!wingetInstall('Python.Python.3.12', 'Python 3.12')) errors++;
refreshPath();
}
// ─── 3. PowerShell 7 ───
step(3, TOTAL, '安装 PowerShell 7');
if (hasCmd('pwsh')) {
ok('PowerShell 7 已安装');
} else {
if (wingetInstall('Microsoft.PowerShell', 'PowerShell 7')) {
// 设 PS7 为 Windows Terminal 默认配置文件
try {
const wtSettings = path.join(HOME, 'AppData', 'Local', 'Packages',
'Microsoft.WindowsTerminal_8wekyb3d8bbwe', 'LocalState', 'settings.json');
if (fs.existsSync(wtSettings)) {
let wt = JSON.parse(fs.readFileSync(wtSettings, 'utf8'));
// 找到 PS7 的 profile GUID
const ps7Profile = (wt.profiles?.list || []).find(p =>
p.source === 'Windows.Terminal.PowershellCore' || (p.name || '').includes('PowerShell 7')
);
if (ps7Profile && ps7Profile.guid) {
wt.defaultProfile = ps7Profile.guid;
fs.writeFileSync(wtSettings, JSON.stringify(wt, null, 4));
ok('PS7 已设为 Windows Terminal 默认终端');
}
}
} catch { warn('Windows Terminal 默认配置未修改 - 不影响使用'); }
} else {
errors++;
}
}
// ─── 4. Claude Code ───
step(4, TOTAL, '安装 Claude Code');
// I3: 用 --registry 参数, 不污染全局 .npmrc
if (hasCmd('claude')) {
ok('Claude Code 已安装');
} else {
info('通过 npm 安装 Claude Code - 淘宝镜像加速...');
try {
run(`npm i -g @anthropic-ai/claude-code --registry ${NPM_MIRROR}`, { timeout: 600000 });
ok('Claude Code 安装成功');
} catch {
fail('Claude Code 安装失败');
errors++;
}
}
// ─── 5. 克隆 Bookworm 配置 ───
step(5, TOTAL, '同步 Bookworm 配置');
// 设置 git credential helper
run('git config --global credential.helper manager', { ignoreError: true, silent: true });
// 克隆 bookworm-boot
if (fs.existsSync(path.join(BOOT_DIR, '.git'))) {
info('bookworm-boot 已存在, 更新...');
run('git pull', { cwd: BOOT_DIR, ignoreError: true });
} else {
info('首次下载 bookworm-boot (需输入 Gitea 用户名密码)...');
try {
run(`git clone "${GITEA_BOOT}" "${BOOT_DIR}"`);
ok('bookworm-boot 克隆成功');
} catch {
fail('bookworm-boot 克隆失败 - 检查网络和 Gitea 凭证');
errors++;
}
}
// 克隆 bookworm-config → ~/.claude
if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) {
info('.claude 配置已存在, 更新...');
try {
const stashOut = run('git stash', { cwd: CLAUDE_DIR, ignoreError: true, silent: true }) || '';
run('git pull --rebase', { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
if (stashOut.includes('Saved working directory')) {
run('git stash pop', { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
}
} catch {}
} else if (!fs.existsSync(path.join(CLAUDE_DIR, 'CLAUDE.md'))) {
info('首次下载 .claude 配置...');
// 备份现有
if (fs.existsSync(CLAUDE_DIR)) {
const backup = CLAUDE_DIR + '.bak.' + Date.now();
fs.renameSync(CLAUDE_DIR, backup);
ok(`现有 .claude 已备份到 ${path.basename(backup)}`);
}
try {
run(`git clone --depth 1 "${GITEA_CONFIG}" "${CLAUDE_DIR}"`);
ok('.claude 配置克隆成功');
} catch {
fail('.claude 配置克隆失败');
errors++;
}
} else {
ok('.claude 配置已存在');
}
// 确保本地目录
for (const d of ['debug', 'sessions', 'cache', 'backups', 'memory', 'projects']) {
const p = path.join(CLAUDE_DIR, d);
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
}
// ─── 6. 凭证解密 ───
step(6, TOTAL, '解密凭证');
const secretsFile = path.join(BOOT_DIR, 'secrets.enc');
if (!fs.existsSync(secretsFile)) {
warn('secrets.enc 不存在, 跳过凭证解密');
} else if (process.env.ANTHROPIC_API_KEY) {
ok('API Key 已设置 (缓存有效)');
} else {
let decrypted = false;
for (let attempt = 1; attempt <= 3; attempt++) {
const label = attempt > 1
? ` 重新输入主密码 (第 ${attempt}/3 次): `
: ' 输入主密码解密凭证: ';
const password = await askPassword(label);
try {
const text = decryptSecrets(secretsFile, password);
// 注入环境变量 (B5: 不打印 key 名称, 防截屏泄露)
let injectedCount = 0;
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (!trimmed || !trimmed.includes('=')) continue;
const eqIdx = trimmed.indexOf('=');
const key = trimmed.slice(0, eqIdx).trim();
const val = trimmed.slice(eqIdx + 1).trim();
if (key && val) { process.env[key] = val; injectedCount++; }
}
ok(`已注入 ${injectedCount} 个环境变量`);
decrypted = true;
break;
} catch (e) {
if (e.message === 'WRONG_PASSWORD') {
const remaining = 3 - attempt;
if (remaining > 0) fail(`密码错误, 剩余重试: ${remaining}`);
} else if (e.message === 'WRONG_FORMAT') {
fail('secrets.enc 格式不兼容, 请联系管理员');
break;
}
}
}
if (!decrypted) {
fail('凭证解密失败 — Claude Code 将以登录模式启动');
errors++;
}
}
// 渲染 settings.json
const templateFile = path.join(CLAUDE_DIR, 'settings.template.json');
const settingsFile = path.join(CLAUDE_DIR, 'settings.json');
if (fs.existsSync(templateFile)) {
let tpl = fs.readFileSync(templateFile, 'utf8');
const claudeRoot = CLAUDE_DIR.replace(/\\/g, '/');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, claudeRoot);
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠
fs.writeFileSync(settingsFile, tpl);
ok('settings.json 已渲染');
}
// 渲染 settings.local.template.json
const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json');
const localSet = path.join(CLAUDE_DIR, 'settings.local.json');
if (fs.existsSync(localTpl)) {
let tpl = fs.readFileSync(localTpl, 'utf8');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/')); // B6: 统一正斜杠
tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username);
fs.writeFileSync(localSet, tpl);
ok('settings.local.json 已渲染');
}
// ─── 7. MCP + hooks 依赖 ───
step(7, TOTAL, 'MCP 与 hooks 依赖');
// MCP 配置写入 ~/.claude.json (Claude Code 的全局 MCP 存储位置)
const claudeJson = path.join(HOME, '.claude.json');
try {
let globalCfg = {};
if (fs.existsSync(claudeJson)) {
globalCfg = JSON.parse(fs.readFileSync(claudeJson, 'utf8'));
}
// 基础 MCP 列表 (npx 方式, 无需预装, 首次调用自动下载)
const baseMcps = {
'context7': {
command: 'npx.cmd', args: ['-y', '@upstash/context7-mcp@latest'], type: 'stdio'
},
'sequential-thinking': {
command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-sequential-thinking@latest'], type: 'stdio'
},
'playwright': {
command: 'npx.cmd', args: ['-y', '@playwright/mcp@latest', '--headless'], type: 'stdio'
},
'firecrawl': {
command: 'npx.cmd', args: ['-y', 'firecrawl-mcp'], type: 'stdio',
env: { FIRECRAWL_API_KEY: '${FIRECRAWL_API_KEY}' }
},
'github': {
command: 'npx.cmd', args: ['-y', '@modelcontextprotocol/server-github'], type: 'stdio',
env: { GITHUB_PERSONAL_ACCESS_TOKEN: '${GITHUB_PERSONAL_ACCESS_TOKEN}' }
},
'linear': { type: 'http', url: 'https://mcp.linear.app/mcp' },
'figma': { type: 'http', url: 'https://mcp.figma.com/mcp' },
'supabase': { type: 'http', url: 'https://mcp.supabase.com/mcp?project_ref=oepmihbtoylosbsxlmfo' },
};
// 合并: 不覆盖用户已有配置
if (!globalCfg.mcpServers) globalCfg.mcpServers = {};
let added = 0;
for (const [name, cfg] of Object.entries(baseMcps)) {
if (!globalCfg.mcpServers[name]) {
globalCfg.mcpServers[name] = cfg;
added++;
}
}
fs.writeFileSync(claudeJson, JSON.stringify(globalCfg, null, 2));
ok(`MCP 配置已写入 ~/.claude.json (新增 ${added} 个, 总计 ${Object.keys(globalCfg.mcpServers).length} 个)`);
} catch (e) {
warn('MCP 配置写入失败: ' + e.message);
}
// npm install in .claude for hooks
if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) {
info('安装 hooks 依赖...');
run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
ok('hooks 依赖已安装');
}
// Python MCP 依赖
const pyCmd = hasCmd('python') ? 'python' : (hasCmd('python3') ? 'python3' : (hasCmd('py') ? 'py' : null));
if (pyCmd) {
info('安装 Python MCP 依赖...');
run(`${pyCmd} -m pip install askui scrapling --quiet`, { ignoreError: true, silent: true });
ok('Python MCP 依赖已安装');
}
// ─── 8. 桌面快捷方式 ───
step(8, TOTAL, '创建桌面快捷方式');
// 启动脚本: 用 pwsh/powershell 运行 install.ps1
const launchBat = path.join(BOOT_DIR, '启动Bookworm.bat');
if (fs.existsSync(launchBat)) {
if (createShortcut('Bookworm', launchBat, BOOT_DIR)) ok('Bookworm 快捷方式');
} else {
// 回退: 直接创建 claude 启动快捷方式
const claudePath = runSilent('where claude').trim().split('\n')[0];
if (claudePath) {
if (createShortcut('Bookworm', claudePath, HOME)) ok('Bookworm 快捷方式');
}
}
const updateBat = path.join(BOOT_DIR, '更新并启动Bookworm.bat');
if (fs.existsSync(updateBat)) {
if (createShortcut('更新Bookworm', updateBat, BOOT_DIR)) ok('更新Bookworm 快捷方式');
}
// ─── 9. 完成 ───
step(9, TOTAL, '安装完成');
console.log(`
${c.green}
安装完成!
[v] Node.js [v] Git [v] Python
[v] PS7 [v] Claude [v] MCP
[v] Bookworm - 92 Skills / 18 Agents
桌面快捷方式: Bookworm / 更新Bookworm
${c.reset}
`);
if (errors > 0) {
warn(`安装过程中有 ${errors} 个警告, 请查看上方日志`);
}
// 打开使用教程
const guide = path.join(BOOT_DIR, 'guide.html');
if (fs.existsSync(guide)) {
try { execSync(`start "" "${guide}"`, { stdio: 'pipe' }); } catch {}
}
// 询问是否启动
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question('\n 按回车启动 Bookworm (或输入 n 退出): ', (ans) => {
rl.close();
if (ans.trim().toLowerCase() === 'n') return;
console.log(`\n ${c.cyan}正在启动 Claude Code...${c.reset}\n`);
// 设置必要环境变量
const env = { ...process.env };
// W10: 追加而非覆盖用户已有 NO_PROXY
const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || '';
const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1';
env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains;
env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash();
const child = spawn('claude', [], {
stdio: 'inherit',
env,
cwd: HOME,
shell: true,
});
child.on('exit', () => process.exit(0));
});
}
// ═══════════════════════════════════════════
// 快速启动模式: 静默更新 + 直接启动 (已安装过的机器)
// ═══════════════════════════════════════════
async function quickStart() {
console.log(` ${c.cyan}Bookworm 快速启动${c.reset} — 检查更新中...`);
let updated = false;
// 1. 静默更新 bookworm-boot
if (fs.existsSync(path.join(BOOT_DIR, '.git'))) {
try {
const before = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim();
run(`git -C "${BOOT_DIR}" pull --ff-only`, { ignoreError: true, silent: true, timeout: 15000 });
const after = runSilent(`git -C "${BOOT_DIR}" rev-parse HEAD`).trim();
if (before !== after) { ok('bookworm-boot 已更新'); updated = true; }
} catch {}
}
// 2. 静默更新 bookworm-config (.claude)
if (fs.existsSync(path.join(CLAUDE_DIR, '.git', 'config'))) {
try {
const before = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim();
const stashOut = run(`git -C "${CLAUDE_DIR}" stash`, { ignoreError: true, silent: true }) || '';
run(`git -C "${CLAUDE_DIR}" pull --rebase`, { ignoreError: true, silent: true, timeout: 15000 });
if (stashOut.includes('Saved working directory')) {
run(`git -C "${CLAUDE_DIR}" stash pop`, { ignoreError: true, silent: true });
}
const after = runSilent(`git -C "${CLAUDE_DIR}" rev-parse HEAD`).trim();
if (before !== after) { ok('.claude 配置已更新'); updated = true; }
} catch {}
}
// 3. 更新后重新渲染模板
if (updated) {
const templateFile = path.join(CLAUDE_DIR, 'settings.template.json');
const settingsFile = path.join(CLAUDE_DIR, 'settings.json');
if (fs.existsSync(templateFile)) {
let tpl = fs.readFileSync(templateFile, 'utf8');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/'));
fs.writeFileSync(settingsFile, tpl);
}
const localTpl = path.join(CLAUDE_DIR, 'settings.local.template.json');
const localSet = path.join(CLAUDE_DIR, 'settings.local.json');
if (fs.existsSync(localTpl)) {
let tpl = fs.readFileSync(localTpl, 'utf8');
tpl = tpl.replace(/\{\{CLAUDE_ROOT\}\}/g, CLAUDE_DIR.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{HOME\}\}/g, HOME.replace(/\\/g, '/'));
tpl = tpl.replace(/\{\{USERNAME\}\}/g, os.userInfo().username);
fs.writeFileSync(localSet, tpl);
}
ok('配置模板已重新渲染');
// hooks 依赖更新
if (fs.existsSync(path.join(CLAUDE_DIR, 'package.json'))) {
run(`npm install --omit=dev --registry ${NPM_MIRROR}`, { cwd: CLAUDE_DIR, ignoreError: true, silent: true });
}
}
if (!updated) ok('已是最新版本');
// 4. 启动 Claude Code
console.log(`\n ${c.cyan}启动 Claude Code...${c.reset}\n`);
const env = { ...process.env };
const existingNoProxy = process.env.NO_PROXY || process.env.no_proxy || '';
const bwDomains = 'bww.letcareme.com,code.letcareme.com,letcareme.com,localhost,127.0.0.1';
env.NO_PROXY = existingNoProxy ? `${existingNoProxy},${bwDomains}` : bwDomains;
env.CLAUDE_CODE_GIT_BASH_PATH = findGitBash();
const child = spawn('claude', [], { stdio: 'inherit', env, cwd: HOME, shell: true });
child.on('exit', () => process.exit(0));
}
main().catch(e => {
fail(`安装引擎异常: ${e.message}`);
console.error(e.stack);
process.exit(1);
});

View File

@ -1,218 +0,0 @@
# 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
}

View File

@ -1,59 +0,0 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
echo.
echo ========================================================
echo Create Admin PowerShell Shortcut for Claude Code
echo ========================================================
echo.
:: Check admin privileges
net session >nul 2>&1
if %errorlevel% neq 0 (
echo [!] Requesting administrator privileges...
powershell -Command "Start-Process '%~f0' -Verb RunAs"
exit /b
)
:: Detect PowerShell path
set "PS_PATH="
for %%p in (
"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
"%ProgramFiles%\PowerShell\7\pwsh.exe"
"%ProgramFiles(x86)%\PowerShell\7\pwsh.exe"
) do (
if exist %%p (
set "PS_PATH=%%p"
goto :ps_found
)
)
:ps_found
if "!PS_PATH!" == "" (
echo [ERROR] PowerShell not found
pause
exit /b 1
)
echo [OK] PowerShell: !PS_PATH!
:: Create admin shortcut
set "SHORTCUT_PATH=%USERPROFILE%\Desktop\Claude Code (Admin Terminal).lnk"
powershell -NoProfile -Command "$WshShell = New-Object -ComObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%SHORTCUT_PATH%'); $Shortcut.TargetPath = '!PS_PATH!'; $Shortcut.Arguments = '-NoExit -Command \"Write-Host ''Claude Code Admin Terminal Ready'' -ForegroundColor Green; Write-Host ''Type: claude'' -ForegroundColor Yellow\"'; $Shortcut.WorkingDirectory = '%USERPROFILE%'; $Shortcut.Description = 'PowerShell with Admin Rights for Claude Code'; $Shortcut.Save(); $bytes = [System.IO.File]::ReadAllBytes('%SHORTCUT_PATH%'); $bytes[0x15] = $bytes[0x15] -bor 0x20; [System.IO.File]::WriteAllBytes('%SHORTCUT_PATH%', $bytes)"
if exist "%SHORTCUT_PATH%" (
echo.
echo [SUCCESS] Admin terminal shortcut created!
echo.
echo [Usage]
echo 1. Double-click "Claude Code (Admin Terminal)" on desktop
echo 2. Type: claude
echo 3. Claude Code will run with administrator privileges
echo.
) else (
echo [ERROR] Failed to create shortcut
)
pause