From 052cbb63c70bb7ea2a3dfa8d791a29022623baf0 Mon Sep 17 00:00:00 2001 From: li Date: Wed, 11 Mar 2026 20:58:04 +0800 Subject: [PATCH] fix: binary download retry + prominent failure warning v1.7.79 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - downloadBinaryFromRelease: 3 retries + 60s AbortController timeout - Binary failure downgraded to warning (commands + skills still install) - init.ts: red box warning + manual download URL + placement path on failure - update.ts: backup binary before uninstall, restore on failure - update.ts: subprocess timeout 120s → 300s (slow networks / China GFW) - installer.test.ts: contextweaver E2E timeout 5s → 30s --- CHANGELOG.md | 12 +++++ CLAUDE.md | 6 ++- README.md | 2 +- README.zh-CN.md | 2 +- package.json | 2 +- src/commands/init.ts | 40 ++++++++++++++++ src/commands/update.ts | 41 ++++++++++++++-- src/utils/__tests__/installer.test.ts | 2 +- src/utils/installer.ts | 68 +++++++++++++++++++-------- 9 files changed, 147 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1d66fb..d810d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [1.7.79] - 2026-03-11 + +### 🐛 修复 + +- **Binary 下载容错**:`downloadBinaryFromRelease()` 新增 3 次重试 + 60s/次 AbortController 超时,适应中国用户 GitHub Release CDN 慢/不可达的场景 +- **Binary 下载失败醒目告警**:下载失败时在终端显示红框警告 + 手动下载地址 + 放置路径 + chmod 指引,不再静默吞掉错误 +- **Binary 失败不阻塞安装**:binary 下载失败降级为 warning,commands + skills 仍正常安装,不再将整个安装标记为失败 +- **Update binary 备份/恢复**:更新流程先备份旧 binary,新安装失败后自动恢复,避免"删了旧的、新的又没下成" +- **Update subprocess 超时**:`npx init` 子进程超时从 120s 提升到 300s(5 分钟),避免 binary 下载慢时进程被杀导致 commands/skills 丢失 + +--- + ## [1.7.78] - 2026-03-11 ### 🐛 修复 diff --git a/CLAUDE.md b/CLAUDE.md index 7f24c4b..989af70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ > [根目录](../CLAUDE.md) > **skills-v2** -**Last Updated**: 2026-03-11 (v1.7.78) +**Last Updated**: 2026-03-11 (v1.7.79) --- @@ -10,6 +10,10 @@ > 完整变更历史请查看 [CHANGELOG.md](./CHANGELOG.md) +### 2026-03-11 (v1.7.79) +- 🐛 **Binary 下载容错**:3 次重试 + 60s 超时 + 失败醒目告警(红框 + 手动修复指引)+ 不阻塞安装 +- 🐛 **Update 流程加固**:binary 备份/恢复 + subprocess 超时 120s→300s + ### 2026-03-11 (v1.7.78) - 🐛 **Windows Hook exit 255 修复**:Windows 自动授权改用 `permissions.allow`,不再依赖 jq/grep diff --git a/README.md b/README.md index c2de33f..2ca884d 100644 --- a/README.md +++ b/README.md @@ -352,4 +352,4 @@ MIT --- -v1.7.78 | [Issues](https://github.com/fengshao1227/ccg-workflow/issues) | [Contributing](./CONTRIBUTING.md) +v1.7.79 | [Issues](https://github.com/fengshao1227/ccg-workflow/issues) | [Contributing](./CONTRIBUTING.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 9fa7fad..7ae8b95 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -352,4 +352,4 @@ MIT --- -v1.7.78 | [Issues](https://github.com/fengshao1227/ccg-workflow/issues) | [参与贡献](./CONTRIBUTING.md) +v1.7.79 | [Issues](https://github.com/fengshao1227/ccg-workflow/issues) | [参与贡献](./CONTRIBUTING.md) diff --git a/package.json b/package.json index 05ff6fe..d60a825 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ccg-workflow", - "version": "1.7.78", + "version": "1.7.79", "description": "Claude + Codex + Gemini multi-model collaboration system - smart routing development workflow", "type": "module", "packageManager": "pnpm@10.17.1", diff --git a/src/commands/init.ts b/src/commands/init.ts index 2948626..f46a438 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -885,6 +885,46 @@ export async function init(options: InitOptions = {}): Promise { } } } + else { + // Binary download failed — show prominent warning with manual fix instructions + const binDest = join(installDir, 'bin') + const binaryExt = process.platform === 'win32' ? '.exe' : '' + const platformLabel = process.platform === 'darwin' + ? (process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-amd64') + : process.platform === 'linux' + ? (process.arch === 'arm64' ? 'linux-arm64' : 'linux-amd64') + : (process.arch === 'arm64' ? 'windows-arm64' : 'windows-amd64') + const binaryFileName = `codeagent-wrapper-${platformLabel}${binaryExt}` + const destFileName = `codeagent-wrapper${binaryExt}` + const releaseUrl = `https://github.com/fengshao1227/ccg-workflow/releases/tag/preset` + + console.log() + console.log(ansis.red.bold(` ╔════════════════════════════════════════════════════════════╗`)) + console.log(ansis.red.bold(` ║ ⚠ codeagent-wrapper 下载失败 ║`)) + console.log(ansis.red.bold(` ║ Binary download failed (network issue) ║`)) + console.log(ansis.red.bold(` ╚════════════════════════════════════════════════════════════╝`)) + console.log() + console.log(ansis.yellow(` 多模型协作命令 (/ccg:workflow, /ccg:plan 等) 需要此文件才能工作。`)) + console.log(ansis.yellow(` Multi-model commands require this binary to work.`)) + console.log() + console.log(ansis.cyan(` 手动修复 / Manual fix:`)) + console.log() + console.log(ansis.white(` 1. 下载 / Download:`)) + console.log(ansis.cyan(` ${releaseUrl}`)) + console.log(ansis.gray(` → 找到 ${ansis.white(binaryFileName)} 并下载`)) + console.log() + console.log(ansis.white(` 2. 放到 / Place at:`)) + console.log(ansis.cyan(` ${binDest}/${destFileName}`)) + console.log() + if (process.platform !== 'win32') { + console.log(ansis.white(` 3. 加权限 / Make executable:`)) + console.log(ansis.cyan(` chmod +x "${binDest}/${destFileName}"`)) + console.log() + } + console.log(ansis.white(` 或重新安装 / Or re-install:`)) + console.log(ansis.cyan(` npx ccg-workflow@latest`)) + console.log() + } // Show MCP resources if user skipped installation if (mcpProvider === 'skip' || ((mcpProvider === 'ace-tool' || mcpProvider === 'ace-tool-rs') && !aceToolToken) || (mcpProvider === 'contextweaver' && !contextWeaverApiKey)) { diff --git a/src/commands/update.ts b/src/commands/update.ts index 8f9f9a8..87bb448 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -298,11 +298,24 @@ async function performUpdate(fromVersion: string, toVersion: string, isNewVersio } } - // Step 3: Delete old workflows first + // Step 3: Backup binary + Delete old workflows spinner = ora(i18n.t('update:removingOld')).start() + const installDir = join(homedir(), '.claude') + const binDir = join(installDir, 'bin') + const wrapperName = process.platform === 'win32' ? 'codeagent-wrapper.exe' : 'codeagent-wrapper' + const wrapperPath = join(binDir, wrapperName) + const wrapperBackup = join(binDir, `${wrapperName}.bak`) + let binaryBackedUp = false + try { - const installDir = join(homedir(), '.claude') + // Backup existing binary before uninstall (restore if new install fails) + const fsExtra = await import('fs-extra') + if (await fsExtra.pathExists(wrapperPath)) { + await fsExtra.copy(wrapperPath, wrapperBackup) + binaryBackedUp = true + } + const uninstallResult = await uninstallWorkflows(installDir) if (uninstallResult.success) { @@ -324,7 +337,7 @@ async function performUpdate(fromVersion: string, toVersion: string, isNewVersio try { await execAsync(`npx --yes ccg-workflow@latest init --force --skip-mcp --skip-prompt`, { - timeout: 120000, + timeout: 300000, // 5min — binary download from GitHub Release may be slow (especially in China) env: { ...process.env, CCG_UPDATE_MODE: 'true', @@ -332,6 +345,12 @@ async function performUpdate(fromVersion: string, toVersion: string, isNewVersio }) spinner.succeed(i18n.t('update:installDone')) + // Clean up binary backup on success + if (binaryBackedUp) { + const fsExtra = await import('fs-extra') + await fsExtra.remove(wrapperBackup).catch(() => {}) + } + // Read updated config to display installed commands const config = await readCcgConfig() if (config?.workflows?.installed) { @@ -344,6 +363,22 @@ async function performUpdate(fromVersion: string, toVersion: string, isNewVersio } catch (error) { spinner.fail(i18n.t('update:installFailed')) + + // Restore backed-up binary if new install failed + if (binaryBackedUp) { + try { + const fsExtra = await import('fs-extra') + if (await fsExtra.pathExists(wrapperBackup)) { + await fsExtra.ensureDir(binDir) + await fsExtra.move(wrapperBackup, wrapperPath, { overwrite: true }) + console.log(ansis.yellow(` • codeagent-wrapper restored from backup`)) + } + } + catch { + // Backup restore failed — nothing more we can do + } + } + console.log(ansis.red(`${i18n.t('common:error')}: ${error}`)) console.log() console.log(ansis.yellow(i18n.t('update:manualRetry'))) diff --git a/src/utils/__tests__/installer.test.ts b/src/utils/__tests__/installer.test.ts index 303e9ae..6d79897 100644 --- a/src/utils/__tests__/installer.test.ts +++ b/src/utils/__tests__/installer.test.ts @@ -226,7 +226,7 @@ describe('installWorkflows E2E — mcpProvider="contextweaver"', () => { }) expect(result.success).toBe(true) expect(result.errors).toEqual([]) - }) + }, 30_000) it('generated command files contain contextweaver references', async () => { const planContent = readFileSync(join(tmpDir, 'commands', 'ccg', 'plan.md'), 'utf-8') diff --git a/src/utils/installer.ts b/src/utils/installer.ts index 1a6a0a4..ee8c032 100644 --- a/src/utils/installer.ts +++ b/src/utils/installer.ts @@ -33,25 +33,53 @@ const PACKAGE_ROOT = findPackageRoot(__dirname) * Download codeagent-wrapper binary from GitHub Release. * Uses the fixed `preset` tag release for pre-compiled binaries. * Supports redirect-following (GitHub releases redirect to CDN). + * + * Retry: 3 attempts with exponential backoff (2s, 4s, 6s). + * Timeout: 60s per attempt via AbortController. */ async function downloadBinaryFromRelease(binaryName: string, destPath: string): Promise { const url = `${BINARY_DOWNLOAD_URL}/${binaryName}` + const MAX_ATTEMPTS = 3 + const TIMEOUT_MS = 60_000 // 60s per attempt - // Use dynamic import for https to follow redirects manually (Node.js fetch follows automatically) - const response = await fetch(url, { redirect: 'follow' }) - if (!response.ok) { - return false + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + + const response = await fetch(url, { redirect: 'follow', signal: controller.signal }) + if (!response.ok) { + clearTimeout(timer) + if (attempt < MAX_ATTEMPTS) { + await new Promise(resolve => setTimeout(resolve, attempt * 2000)) + continue + } + return false + } + + const buffer = Buffer.from(await response.arrayBuffer()) + clearTimeout(timer) + + await fs.writeFile(destPath, buffer) + + // Set executable permission on Unix-like systems + if (process.platform !== 'win32') { + await fs.chmod(destPath, 0o755) + } + + return true + } + catch { + if (attempt < MAX_ATTEMPTS) { + // Exponential backoff before retry + await new Promise(resolve => setTimeout(resolve, attempt * 2000)) + continue + } + return false + } } - const buffer = Buffer.from(await response.arrayBuffer()) - await fs.writeFile(destPath, buffer) - - // Set executable permission on Unix-like systems - if (process.platform !== 'win32') { - await fs.chmod(destPath, 0o755) - } - - return true + return false } // All available commands (20 total after adding spec commands) @@ -845,7 +873,7 @@ ${workflow.description} const destBinary = join(binDir, platform === 'win32' ? 'codeagent-wrapper.exe' : 'codeagent-wrapper') - // Download from GitHub Release (only source) + // Download from GitHub Release (3 attempts with retry) const installed = await downloadBinaryFromRelease(binaryName, destBinary) if (installed) { @@ -857,18 +885,18 @@ ${workflow.description} result.binInstalled = true } catch (verifyError) { - result.errors.push(`Binary verification failed: ${verifyError}`) - result.success = false + // Binary downloaded but verification failed — warning, not blocking + result.errors.push(`Binary verification failed (non-blocking): ${verifyError}`) } } else { - result.errors.push(`Failed to download binary: ${binaryName} from GitHub Release. Check network or visit https://github.com/${GITHUB_REPO}/releases/tag/${RELEASE_TAG}`) - result.success = false + // Binary download failed — warning, not blocking (commands + skills still work) + result.errors.push(`Failed to download binary: ${binaryName} from GitHub Release (after 3 attempts). Check network or visit https://github.com/${GITHUB_REPO}/releases/tag/${RELEASE_TAG}`) } } catch (error) { - result.errors.push(`Failed to install codeagent-wrapper: ${error}`) - result.success = false + // Binary install exception — warning, not blocking + result.errors.push(`Failed to install codeagent-wrapper (non-blocking): ${error}`) } result.configPath = commandsDir