@simonyea/holysheep-cli 1.7.115 → 1.7.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.115",
3
+ "version": "1.7.117",
4
4
  "description": "Claude Code/Cursor/Cline API relay for China — ¥1=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
5
5
  "keywords": [
6
6
  "openai-china",
@@ -59,11 +59,12 @@ function isProcessAlive(pid) {
59
59
 
60
60
  function isProxyHealthy(port) {
61
61
  try {
62
+ // Windows: Invoke-WebRequest 遇到 500 会抛异常,改用 TCP 端口检查
62
63
  execSync(
63
64
  isWin
64
- ? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/ -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
65
- : `curl -sf http://127.0.0.1:${port}/ -o /dev/null --max-time 1`,
66
- { stdio: 'ignore', timeout: 3000, windowsHide: true }
65
+ ? `powershell -NonInteractive -Command "if(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue){exit 0}else{exit 1}"`
66
+ : `curl -so /dev/null --max-time 1 http://127.0.0.1:${port}/`,
67
+ { stdio: 'ignore', timeout: 5000, windowsHide: true }
67
68
  )
68
69
  return true
69
70
  } catch {
@@ -781,14 +781,12 @@ module.exports = {
781
781
  },
782
782
  ensureGatewayRunning(port) {
783
783
  port = port || getConfiguredGatewayPort()
784
- // 先检查是否已在运行
784
+ // 检查端口是否有进程在监听(不依赖 HTTP 状态码,500 也算运行中)
785
785
  try {
786
- execSync(
787
- isWin
788
- ? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/ -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
789
- : `curl -sf http://127.0.0.1:${port}/ -o /dev/null --max-time 1`,
790
- { stdio: 'ignore', timeout: 3000 }
791
- )
786
+ const checkCmd = isWin
787
+ ? `powershell -NonInteractive -Command "if(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue){exit 0}else{exit 1}"`
788
+ : `curl -so /dev/null --max-time 1 http://127.0.0.1:${port}/ || lsof -iTCP:${port} -sTCP:LISTEN -t >/dev/null 2>&1`
789
+ execSync(checkCmd, { stdio: 'ignore', timeout: 5000 })
792
790
  return true
793
791
  } catch {}
794
792
  // 未运行,启动它
@@ -187,7 +187,7 @@ const I18N = {
187
187
  installSuccess: '安装完成', installFailed: '安装失败',
188
188
  needLogin: '请先登录', cleanDone: '已清理',
189
189
  hotReload: '已生效,无需重启', needRestart: '重启终端后生效',
190
- launch: '启动命令', upgradeOne: '升级', open: '打开',
190
+ launch: '启动命令', upgradeOne: '升级', rollback: '回退版本', open: '打开',
191
191
  updateAvailable: '有新版本可用', updateNow: '立即升级',
192
192
  },
193
193
  en: {
@@ -210,7 +210,7 @@ const I18N = {
210
210
  installSuccess: 'Installed', installFailed: 'Install failed',
211
211
  needLogin: 'Please login first', cleanDone: 'Cleaned',
212
212
  hotReload: 'Active, no restart needed', needRestart: 'Restart terminal to apply',
213
- launch: 'Launch', upgradeOne: 'Upgrade', open: 'Open',
213
+ launch: 'Launch', upgradeOne: 'Upgrade', rollback: 'Rollback', open: 'Open',
214
214
  updateAvailable: 'Update available', updateNow: 'Update now',
215
215
  },
216
216
  }
@@ -384,15 +384,17 @@ function renderToolCard(tool) {
384
384
  dotClass = 'dot-warn'
385
385
  statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-warn">${t('notConfigured')}</span>`
386
386
  const upgradeBtn = tool.canUpgrade ? `<button class="btn btn-outline btn-sm" onclick="doUpgradeTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('upgradeOne')}</button>` : ''
387
- actions = `<button class="btn btn-primary btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('configure')}</button>${upgradeBtn}`
387
+ const rollbackBtn = tool.canUpgrade && tool.npmPkg ? `<button class="btn btn-outline btn-sm" onclick="doRollbackTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('rollback')}</button>` : ''
388
+ actions = `<button class="btn btn-primary btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('configure')}</button>${upgradeBtn}${rollbackBtn}`
388
389
  hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
389
390
  } else {
390
391
  dotClass = 'dot-ok'
391
392
  statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-ok">${t('configured')}</span>`
392
393
  const upgradeBtn = tool.canUpgrade ? `<button class="btn btn-outline btn-sm" onclick="doUpgradeTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('upgradeOne')}</button>` : ''
394
+ const rollbackBtn = tool.canUpgrade && tool.npmPkg ? `<button class="btn btn-outline btn-sm" onclick="doRollbackTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('rollback')}</button>` : ''
393
395
  actions = `<button class="btn btn-primary btn-sm" onclick="doLaunchTool('${tool.id}')">${t('open')}</button>
394
396
  <button class="btn btn-outline btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reconfigure')}</button>
395
- ${upgradeBtn}
397
+ ${upgradeBtn}${rollbackBtn}
396
398
  <button class="btn btn-danger btn-sm" onclick="doResetTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reset')}</button>`
397
399
  hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
398
400
  }
@@ -548,6 +550,28 @@ async function doUpgradeTool(id, name) {
548
550
  loadTools()
549
551
  }
550
552
 
553
+ async function doRollbackTool(id, name) {
554
+ if (busy) return
555
+ if (!confirm(lang === 'zh' ? `确认回退 ${name} 到上一个版本?` : `Rollback ${name} to previous version?`)) return
556
+ busy = true
557
+ openConsole(`${t('rollback')}: ${name}`)
558
+ document.getElementById('console-section').classList.add('busy')
559
+
560
+ let success = false
561
+ await streamSSE('/api/tool/rollback', { toolId: id }, (ev) => {
562
+ if (ev.type === 'progress') appendLog(ev.message, 'info')
563
+ else if (ev.type === 'output') appendLogRaw(ev.text)
564
+ else if (ev.type === 'done') {
565
+ success = ev.success
566
+ appendLog(ev.success ? `\n✓ ${lang === 'zh' ? '回退成功' : 'Rollback succeeded'}` : `\n✗ ${lang === 'zh' ? '回退失败' : 'Rollback failed'}`, ev.success ? 'ok' : 'err')
567
+ }
568
+ })
569
+
570
+ document.getElementById('console-section').classList.remove('busy')
571
+ busy = false
572
+ loadTools()
573
+ }
574
+
551
575
  // ── Console ──────────────────────────────────────────────────────────────────
552
576
  function openConsole(title) {
553
577
  const area = document.getElementById('console-section')
@@ -310,6 +310,7 @@ async function handleTools(_req, res) {
310
310
  launchCmd: t.launchCmd || null,
311
311
  canAutoInstall: !!AUTO_INSTALL[t.id],
312
312
  canUpgrade: !!UPGRADABLE_TOOLS.find(u => u.id === t.id),
313
+ npmPkg: UPGRADABLE_TOOLS.find(u => u.id === t.id)?.npmPkg || null,
313
314
  }
314
315
  }))
315
316
 
@@ -502,9 +503,11 @@ async function handleUpgrade(_req, res) {
502
503
  newVer = m ? m[1] : null
503
504
  } catch {}
504
505
 
505
- // OpenClaw 升级后自动重启网关
506
- if (ok && tool.id === 'openclaw' && commandExists('openclaw')) {
506
+ // OpenClaw 升级后自动重启 Bridge + Gateway
507
+ if (ok && tool.id === 'openclaw') {
507
508
  try { execSync('openclaw daemon stop', { stdio: 'ignore', timeout: 10000 }) } catch {}
509
+ const openclawTool = TOOLS.find(t => t.id === 'openclaw')
510
+ try { openclawTool?.ensureBridgeRunning?.() } catch {}
508
511
  try { execSync('openclaw daemon start', { stdio: 'ignore', timeout: 30000 }) } catch {}
509
512
  }
510
513
 
@@ -659,17 +662,24 @@ async function handleToolUpgrade(req, res) {
659
662
 
660
663
  if (ok) {
661
664
  sseEmit(res, { type: 'progress', message: `✓ ${entry.name} 升级成功: ${localVer || '?'} → ${newVer || 'latest'}` })
662
- // OpenClaw 升级后自动重启网关,否则运行中的进程还是旧版本
663
- if (entry.id === 'openclaw' && commandExists('openclaw')) {
664
- sseEmit(res, { type: 'progress', message: '正在重启 OpenClaw 网关...' })
665
- try {
666
- execSync('openclaw daemon stop', { stdio: 'ignore', timeout: 10000 })
667
- } catch {}
665
+ // OpenClaw 升级后自动重启 Bridge + Gateway
666
+ if (entry.id === 'openclaw') {
667
+ const openclawTool = TOOLS.find(t => t.id === 'openclaw')
668
+ sseEmit(res, { type: 'progress', message: '正在重启 OpenClaw...' })
669
+ try { execSync('openclaw daemon stop', { stdio: 'ignore', timeout: 10000 }) } catch {}
670
+ if (openclawTool?.ensureBridgeRunning) {
671
+ try {
672
+ openclawTool.ensureBridgeRunning()
673
+ sseEmit(res, { type: 'progress', message: '✓ HolySheep Bridge 已启动' })
674
+ } catch {
675
+ sseEmit(res, { type: 'progress', message: '⚠️ Bridge 启动失败' })
676
+ }
677
+ }
668
678
  try {
669
679
  execSync('openclaw daemon start', { stdio: 'ignore', timeout: 30000 })
670
- sseEmit(res, { type: 'progress', message: '✓ OpenClaw 网关已重启' })
680
+ sseEmit(res, { type: 'progress', message: '✓ OpenClaw Gateway 已重启' })
671
681
  } catch {
672
- sseEmit(res, { type: 'progress', message: '⚠️ 网关重启失败,请手动运行: openclaw daemon start' })
682
+ sseEmit(res, { type: 'progress', message: '⚠️ Gateway 重启失败,请手动运行: openclaw daemon start' })
673
683
  }
674
684
  }
675
685
  } else {
@@ -680,6 +690,129 @@ async function handleToolUpgrade(req, res) {
680
690
  res.end()
681
691
  }
682
692
 
693
+ // ── Single-tool rollback (SSE) ────────────────────────────────────────────────
694
+
695
+ async function handleToolRollback(req, res) {
696
+ const body = await parseBody(req)
697
+ const { toolId } = body
698
+ const entry = UPGRADABLE_TOOLS.find(t => t.id === toolId)
699
+ if (!entry || !entry.npmPkg) return json(res, { error: '不支持回退此工具(仅限 npm 工具)' }, 400)
700
+
701
+ sseStart(res)
702
+
703
+ // 1. 获取当前本地版本
704
+ let localVer = null
705
+ try {
706
+ const out = execSync(entry.versionCmd, { stdio: 'pipe', timeout: 10000 }).toString().trim()
707
+ const m = out.match(/(\d+\.\d+\.\d+[\w.-]*)/)
708
+ localVer = m ? m[1] : null
709
+ } catch {}
710
+ sseEmit(res, { type: 'progress', message: `${entry.name} 当前版本: ${localVer || '未知'}` })
711
+
712
+ // 2. 从 npm registry 获取版本列表,找到倒数第二个
713
+ let targetVer = null
714
+ try {
715
+ sseEmit(res, { type: 'progress', message: '正在查询可用版本...' })
716
+ const r = await fetchWithRetry(`https://registry.npmjs.org/${entry.npmPkg}`, {}, 2, 15000)
717
+ if (r.ok) {
718
+ const data = await r.json()
719
+ const versions = Object.keys(data.versions || {})
720
+ .filter(v => !v.includes('alpha') && !v.includes('beta') && !v.includes('rc') && !v.includes('canary'))
721
+ if (versions.length >= 2) {
722
+ // 找到当前版本的前一个,或者倒数第二个
723
+ const currentIdx = localVer ? versions.indexOf(localVer) : -1
724
+ if (currentIdx > 0) {
725
+ targetVer = versions[currentIdx - 1]
726
+ } else {
727
+ targetVer = versions[versions.length - 2]
728
+ }
729
+ }
730
+ }
731
+ } catch (e) {
732
+ sseEmit(res, { type: 'progress', message: `⚠️ 查询版本失败: ${e.message}` })
733
+ }
734
+
735
+ if (!targetVer) {
736
+ sseEmit(res, { type: 'progress', message: '✗ 无法确定回退版本' })
737
+ sseEmit(res, { type: 'done', success: false })
738
+ return res.end()
739
+ }
740
+
741
+ sseEmit(res, { type: 'progress', message: `回退目标: ${localVer || '?'} → ${targetVer}` })
742
+
743
+ // 3. Windows: kill 进程 + 停止 daemon
744
+ if (entry.id === 'openclaw') {
745
+ try { execSync('openclaw daemon stop', { stdio: 'ignore', timeout: 10000 }) } catch {}
746
+ }
747
+ if (process.platform === 'win32' && entry.command) {
748
+ try {
749
+ execSync(`taskkill /F /IM ${entry.command}.exe /T`, { stdio: 'ignore', shell: true })
750
+ sseEmit(res, { type: 'progress', message: `已关闭 ${entry.name} 进程` })
751
+ } catch {}
752
+ }
753
+
754
+ // 4. 清理残留: npm cache + 删除 node_modules 目录
755
+ sseEmit(res, { type: 'progress', message: '正在清理残留文件...' })
756
+ try { execSync('npm cache clean --force', { stdio: 'ignore', timeout: 30000 }) } catch {}
757
+ try { execSync(`npm uninstall -g ${entry.npmPkg}`, { stdio: 'ignore', timeout: 30000 }) } catch {}
758
+ // Windows 上 npm uninstall 可能残留文件
759
+ if (process.platform === 'win32') {
760
+ const globalPrefix = String(execSync('npm prefix -g', { stdio: 'pipe', timeout: 5000 })).trim()
761
+ const modulePath = path.join(globalPrefix, 'node_modules', ...entry.npmPkg.split('/'))
762
+ try {
763
+ if (fs.existsSync(modulePath)) {
764
+ execSync(`rd /s /q "${modulePath}"`, { stdio: 'ignore', shell: true, timeout: 10000 })
765
+ sseEmit(res, { type: 'progress', message: '已清理残留目录' })
766
+ }
767
+ } catch {}
768
+ }
769
+
770
+ // 5. 安装目标版本
771
+ sseEmit(res, { type: 'progress', message: `正在安装 ${entry.npmPkg}@${targetVer}...` })
772
+ const installCmd = `npm install -g ${entry.npmPkg}@${targetVer}`
773
+ const ok = await new Promise(resolve => {
774
+ const child = spawn(installCmd, [], { shell: true })
775
+ const timer = setTimeout(() => {
776
+ sseEmit(res, { type: 'progress', message: `⚠️ 安装超时` })
777
+ try { child.kill('SIGKILL') } catch {}
778
+ resolve(false)
779
+ }, 5 * 60 * 1000)
780
+ child.stdout?.on('data', chunk => sseEmit(res, { type: 'output', text: chunk.toString() }))
781
+ child.stderr?.on('data', chunk => sseEmit(res, { type: 'output', text: chunk.toString() }))
782
+ child.on('close', code => { clearTimeout(timer); resolve(code === 0) })
783
+ child.on('error', () => { clearTimeout(timer); resolve(false) })
784
+ })
785
+
786
+ // 6. 验证版本
787
+ let newVer = null
788
+ try {
789
+ const out = execSync(entry.versionCmd, { stdio: 'pipe', timeout: 10000 }).toString().trim()
790
+ const m = out.match(/(\d+\.\d+\.\d+[\w.-]*)/)
791
+ newVer = m ? m[1] : null
792
+ } catch {}
793
+
794
+ if (ok) {
795
+ sseEmit(res, { type: 'progress', message: `✓ ${entry.name} 已回退: ${localVer || '?'} → ${newVer || targetVer}` })
796
+ // OpenClaw: 重启 Bridge + Gateway
797
+ if (entry.id === 'openclaw') {
798
+ const openclawTool = TOOLS.find(t => t.id === 'openclaw')
799
+ sseEmit(res, { type: 'progress', message: '正在重启 OpenClaw...' })
800
+ if (openclawTool?.ensureBridgeRunning) {
801
+ try { openclawTool.ensureBridgeRunning() } catch {}
802
+ }
803
+ try {
804
+ execSync('openclaw daemon start', { stdio: 'ignore', timeout: 30000 })
805
+ sseEmit(res, { type: 'progress', message: '✓ OpenClaw 已重启' })
806
+ } catch {}
807
+ }
808
+ } else {
809
+ sseEmit(res, { type: 'progress', message: `✗ 回退失败,请手动运行: ${installCmd}` })
810
+ }
811
+
812
+ sseEmit(res, { type: 'done', success: ok, localVer, newVer: newVer || targetVer })
813
+ res.end()
814
+ }
815
+
683
816
  // ── Single-tool reset ────────────────────────────────────────────────────────
684
817
 
685
818
  async function handleToolReset(req, res) {
@@ -857,6 +990,7 @@ async function handleRequest(req, res) {
857
990
  if (route === '/api/tool/configure' && req.method === 'POST') return await handleToolConfigure(req, res)
858
991
  if (route === '/api/tool/reset' && req.method === 'POST') return await handleToolReset(req, res)
859
992
  if (route === '/api/tool/upgrade' && req.method === 'POST') return await handleToolUpgrade(req, res)
993
+ if (route === '/api/tool/rollback' && req.method === 'POST') return await handleToolRollback(req, res)
860
994
  if (route === '/api/tool/launch' && req.method === 'POST') return await handleToolLaunch(req, res)
861
995
  if (route === '/api/claude-proxy/start' && req.method === 'POST') return await handleClaudeProxyStart(req, res)
862
996
  if (route === '/api/claude-proxy/stop' && req.method === 'POST') return await handleClaudeProxyStop(req, res)