@simonyea/holysheep-cli 1.7.116 → 1.7.118

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.116",
3
+ "version": "1.7.118",
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",
@@ -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,12 +503,30 @@ async function handleUpgrade(_req, res) {
502
503
  newVer = m ? m[1] : null
503
504
  } catch {}
504
505
 
505
- // OpenClaw 升级后自动重启 Bridge + Gateway
506
+ // OpenClaw 升级后:Gateway 更新 PID → Bridge
506
507
  if (ok && tool.id === 'openclaw') {
507
508
  try { execSync('openclaw daemon stop', { stdio: 'ignore', timeout: 10000 }) } catch {}
509
+ try { execSync('openclaw daemon start', { stdio: 'ignore', timeout: 30000 }) } catch {}
508
510
  const openclawTool = TOOLS.find(t => t.id === 'openclaw')
511
+ try {
512
+ const gPort = openclawTool?.getGatewayPort?.() || 18789
513
+ let gPid = null
514
+ if (process.platform === 'win32') {
515
+ const o = execSync(`powershell -NonInteractive -Command "(Get-NetTCPConnection -LocalPort ${gPort} -State Listen -ErrorAction SilentlyContinue).OwningProcess"`, { stdio: 'pipe', timeout: 5000 }).toString().trim()
516
+ gPid = Number(o.split(/\r?\n/)[0]) || null
517
+ } else {
518
+ const o = execSync(`lsof -iTCP:${gPort} -sTCP:LISTEN -t 2>/dev/null | head -1`, { stdio: 'pipe', timeout: 5000 }).toString().trim()
519
+ gPid = Number(o) || null
520
+ }
521
+ if (gPid) {
522
+ const bridgeMod = require('../tools/openclaw-bridge')
523
+ const bc = bridgeMod.readBridgeConfig()
524
+ bc.gatewayPid = gPid
525
+ bc.gatewayStartedAt = new Date().toISOString()
526
+ fs.writeFileSync(bridgeMod.BRIDGE_CONFIG_FILE, JSON.stringify(bc, null, 2), 'utf8')
527
+ }
528
+ } catch {}
509
529
  try { openclawTool?.ensureBridgeRunning?.() } catch {}
510
- try { execSync('openclaw daemon start', { stdio: 'ignore', timeout: 30000 }) } catch {}
511
530
  }
512
531
 
513
532
  sseEmit(res, { type: 'tool', name: tool.name, status: ok ? 'ok' : 'error', localVer, newVer })
@@ -661,11 +680,37 @@ async function handleToolUpgrade(req, res) {
661
680
 
662
681
  if (ok) {
663
682
  sseEmit(res, { type: 'progress', message: `✓ ${entry.name} 升级成功: ${localVer || '?'} → ${newVer || 'latest'}` })
664
- // OpenClaw 升级后自动重启 Bridge + Gateway
683
+ // OpenClaw 升级后:先停 启动 Gateway(拿 PID)→ 更新 bridge config → 启动 Bridge
665
684
  if (entry.id === 'openclaw') {
666
685
  const openclawTool = TOOLS.find(t => t.id === 'openclaw')
667
686
  sseEmit(res, { type: 'progress', message: '正在重启 OpenClaw...' })
668
687
  try { execSync('openclaw daemon stop', { stdio: 'ignore', timeout: 10000 }) } catch {}
688
+ try {
689
+ execSync('openclaw daemon start', { stdio: 'ignore', timeout: 30000 })
690
+ sseEmit(res, { type: 'progress', message: '✓ OpenClaw Gateway 已启动' })
691
+ } catch {
692
+ sseEmit(res, { type: 'progress', message: '⚠️ Gateway 启动失败' })
693
+ }
694
+ // 获取 Gateway PID 写入 bridge config
695
+ try {
696
+ const gatewayPort = openclawTool?.getGatewayPort?.() || 18789
697
+ let gatewayPid = null
698
+ if (process.platform === 'win32') {
699
+ const out = execSync(`powershell -NonInteractive -Command "(Get-NetTCPConnection -LocalPort ${gatewayPort} -State Listen -ErrorAction SilentlyContinue).OwningProcess"`, { stdio: 'pipe', timeout: 5000 }).toString().trim()
700
+ gatewayPid = Number(out.split(/\r?\n/)[0]) || null
701
+ } else {
702
+ const out = execSync(`lsof -iTCP:${gatewayPort} -sTCP:LISTEN -t 2>/dev/null | head -1`, { stdio: 'pipe', timeout: 5000 }).toString().trim()
703
+ gatewayPid = Number(out) || null
704
+ }
705
+ if (gatewayPid) {
706
+ const bridgeMod = require('../tools/openclaw-bridge')
707
+ const bc = bridgeMod.readBridgeConfig()
708
+ bc.gatewayPid = gatewayPid
709
+ bc.gatewayStartedAt = new Date().toISOString()
710
+ fs.writeFileSync(bridgeMod.BRIDGE_CONFIG_FILE, JSON.stringify(bc, null, 2), 'utf8')
711
+ }
712
+ } catch {}
713
+ // 启动 Bridge
669
714
  if (openclawTool?.ensureBridgeRunning) {
670
715
  try {
671
716
  openclawTool.ensureBridgeRunning()
@@ -674,18 +719,164 @@ async function handleToolUpgrade(req, res) {
674
719
  sseEmit(res, { type: 'progress', message: '⚠️ Bridge 启动失败' })
675
720
  }
676
721
  }
722
+ }
723
+ } else {
724
+ sseEmit(res, { type: 'progress', message: `✗ ${entry.name} 升级失败` })
725
+ }
726
+
727
+ sseEmit(res, { type: 'done', success: ok, localVer, newVer })
728
+ res.end()
729
+ }
730
+
731
+ // ── Single-tool rollback (SSE) ────────────────────────────────────────────────
732
+
733
+ async function handleToolRollback(req, res) {
734
+ const body = await parseBody(req)
735
+ const { toolId } = body
736
+ const entry = UPGRADABLE_TOOLS.find(t => t.id === toolId)
737
+ if (!entry || !entry.npmPkg) return json(res, { error: '不支持回退此工具(仅限 npm 工具)' }, 400)
738
+
739
+ sseStart(res)
740
+
741
+ // 1. 获取当前本地版本
742
+ let localVer = null
743
+ try {
744
+ const out = execSync(entry.versionCmd, { stdio: 'pipe', timeout: 10000 }).toString().trim()
745
+ const m = out.match(/(\d+\.\d+\.\d+[\w.-]*)/)
746
+ localVer = m ? m[1] : null
747
+ } catch {}
748
+ sseEmit(res, { type: 'progress', message: `${entry.name} 当前版本: ${localVer || '未知'}` })
749
+
750
+ // 2. 从 npm registry 获取版本列表,找到倒数第二个
751
+ let targetVer = null
752
+ try {
753
+ sseEmit(res, { type: 'progress', message: '正在查询可用版本...' })
754
+ const r = await fetchWithRetry(`https://registry.npmjs.org/${entry.npmPkg}`, {}, 2, 15000)
755
+ if (r.ok) {
756
+ const data = await r.json()
757
+ const versions = Object.keys(data.versions || {})
758
+ .filter(v => !v.includes('alpha') && !v.includes('beta') && !v.includes('rc') && !v.includes('canary'))
759
+ if (versions.length >= 2) {
760
+ // 找到当前版本的前一个,或者倒数第二个
761
+ const currentIdx = localVer ? versions.indexOf(localVer) : -1
762
+ if (currentIdx > 0) {
763
+ targetVer = versions[currentIdx - 1]
764
+ } else {
765
+ targetVer = versions[versions.length - 2]
766
+ }
767
+ }
768
+ }
769
+ } catch (e) {
770
+ sseEmit(res, { type: 'progress', message: `⚠️ 查询版本失败: ${e.message}` })
771
+ }
772
+
773
+ if (!targetVer) {
774
+ sseEmit(res, { type: 'progress', message: '✗ 无法确定回退版本' })
775
+ sseEmit(res, { type: 'done', success: false })
776
+ return res.end()
777
+ }
778
+
779
+ sseEmit(res, { type: 'progress', message: `回退目标: ${localVer || '?'} → ${targetVer}` })
780
+
781
+ // 3. Windows: kill 进程 + 停止 daemon
782
+ if (entry.id === 'openclaw') {
783
+ try { execSync('openclaw daemon stop', { stdio: 'ignore', timeout: 10000 }) } catch {}
784
+ }
785
+ if (process.platform === 'win32' && entry.command) {
786
+ try {
787
+ execSync(`taskkill /F /IM ${entry.command}.exe /T`, { stdio: 'ignore', shell: true })
788
+ sseEmit(res, { type: 'progress', message: `已关闭 ${entry.name} 进程` })
789
+ } catch {}
790
+ }
791
+
792
+ // 4. 清理残留: npm cache + 删除 node_modules 目录
793
+ sseEmit(res, { type: 'progress', message: '正在清理残留文件...' })
794
+ try { execSync('npm cache clean --force', { stdio: 'ignore', timeout: 30000 }) } catch {}
795
+ try { execSync(`npm uninstall -g ${entry.npmPkg}`, { stdio: 'ignore', timeout: 30000 }) } catch {}
796
+ // Windows 上 npm uninstall 可能残留文件
797
+ if (process.platform === 'win32') {
798
+ const globalPrefix = String(execSync('npm prefix -g', { stdio: 'pipe', timeout: 5000 })).trim()
799
+ const modulePath = path.join(globalPrefix, 'node_modules', ...entry.npmPkg.split('/'))
800
+ try {
801
+ if (fs.existsSync(modulePath)) {
802
+ execSync(`rd /s /q "${modulePath}"`, { stdio: 'ignore', shell: true, timeout: 10000 })
803
+ sseEmit(res, { type: 'progress', message: '已清理残留目录' })
804
+ }
805
+ } catch {}
806
+ }
807
+
808
+ // 5. 安装目标版本
809
+ sseEmit(res, { type: 'progress', message: `正在安装 ${entry.npmPkg}@${targetVer}...` })
810
+ const installCmd = `npm install -g ${entry.npmPkg}@${targetVer}`
811
+ const ok = await new Promise(resolve => {
812
+ const child = spawn(installCmd, [], { shell: true })
813
+ const timer = setTimeout(() => {
814
+ sseEmit(res, { type: 'progress', message: `⚠️ 安装超时` })
815
+ try { child.kill('SIGKILL') } catch {}
816
+ resolve(false)
817
+ }, 5 * 60 * 1000)
818
+ child.stdout?.on('data', chunk => sseEmit(res, { type: 'output', text: chunk.toString() }))
819
+ child.stderr?.on('data', chunk => sseEmit(res, { type: 'output', text: chunk.toString() }))
820
+ child.on('close', code => { clearTimeout(timer); resolve(code === 0) })
821
+ child.on('error', () => { clearTimeout(timer); resolve(false) })
822
+ })
823
+
824
+ // 6. 验证版本
825
+ let newVer = null
826
+ try {
827
+ const out = execSync(entry.versionCmd, { stdio: 'pipe', timeout: 10000 }).toString().trim()
828
+ const m = out.match(/(\d+\.\d+\.\d+[\w.-]*)/)
829
+ newVer = m ? m[1] : null
830
+ } catch {}
831
+
832
+ if (ok) {
833
+ sseEmit(res, { type: 'progress', message: `✓ ${entry.name} 已回退: ${localVer || '?'} → ${newVer || targetVer}` })
834
+ // OpenClaw: 先启动 Gateway(拿 PID),更新 bridge config,再启动 Bridge
835
+ if (entry.id === 'openclaw') {
836
+ sseEmit(res, { type: 'progress', message: '正在重启 OpenClaw...' })
837
+ // 1. 启动 Gateway
677
838
  try {
678
839
  execSync('openclaw daemon start', { stdio: 'ignore', timeout: 30000 })
679
- sseEmit(res, { type: 'progress', message: '✓ OpenClaw Gateway 已重启' })
840
+ sseEmit(res, { type: 'progress', message: '✓ OpenClaw Gateway 已启动' })
680
841
  } catch {
681
- sseEmit(res, { type: 'progress', message: '⚠️ Gateway 重启失败,请手动运行: openclaw daemon start' })
842
+ sseEmit(res, { type: 'progress', message: '⚠️ Gateway 启动失败' })
843
+ }
844
+ // 2. 获取 Gateway PID 并写入 bridge config,否则 bridge watchdog 会自杀
845
+ const openclawTool = TOOLS.find(t => t.id === 'openclaw')
846
+ try {
847
+ const gatewayPort = openclawTool?.getGatewayPort?.() || 18789
848
+ let gatewayPid = null
849
+ if (process.platform === 'win32') {
850
+ const out = execSync(`powershell -NonInteractive -Command "(Get-NetTCPConnection -LocalPort ${gatewayPort} -State Listen -ErrorAction SilentlyContinue).OwningProcess"`, { stdio: 'pipe', timeout: 5000 }).toString().trim()
851
+ gatewayPid = Number(out.split(/\r?\n/)[0]) || null
852
+ } else {
853
+ const out = execSync(`lsof -iTCP:${gatewayPort} -sTCP:LISTEN -t 2>/dev/null | head -1`, { stdio: 'pipe', timeout: 5000 }).toString().trim()
854
+ gatewayPid = Number(out) || null
855
+ }
856
+ if (gatewayPid) {
857
+ const bridgeMod = require('../tools/openclaw-bridge')
858
+ const bc = bridgeMod.readBridgeConfig()
859
+ bc.gatewayPid = gatewayPid
860
+ bc.gatewayStartedAt = new Date().toISOString()
861
+ fs.writeFileSync(bridgeMod.BRIDGE_CONFIG_FILE, JSON.stringify(bc, null, 2), 'utf8')
862
+ sseEmit(res, { type: 'progress', message: `Gateway PID: ${gatewayPid}` })
863
+ }
864
+ } catch {}
865
+ // 3. 启动 Bridge
866
+ if (openclawTool?.ensureBridgeRunning) {
867
+ try {
868
+ openclawTool.ensureBridgeRunning()
869
+ sseEmit(res, { type: 'progress', message: '✓ HolySheep Bridge 已启动' })
870
+ } catch {
871
+ sseEmit(res, { type: 'progress', message: '⚠️ Bridge 启动失败' })
872
+ }
682
873
  }
683
874
  }
684
875
  } else {
685
- sseEmit(res, { type: 'progress', message: `✗ ${entry.name} 升级失败` })
876
+ sseEmit(res, { type: 'progress', message: `✗ 回退失败,请手动运行: ${installCmd}` })
686
877
  }
687
878
 
688
- sseEmit(res, { type: 'done', success: ok, localVer, newVer })
879
+ sseEmit(res, { type: 'done', success: ok, localVer, newVer: newVer || targetVer })
689
880
  res.end()
690
881
  }
691
882
 
@@ -866,6 +1057,7 @@ async function handleRequest(req, res) {
866
1057
  if (route === '/api/tool/configure' && req.method === 'POST') return await handleToolConfigure(req, res)
867
1058
  if (route === '/api/tool/reset' && req.method === 'POST') return await handleToolReset(req, res)
868
1059
  if (route === '/api/tool/upgrade' && req.method === 'POST') return await handleToolUpgrade(req, res)
1060
+ if (route === '/api/tool/rollback' && req.method === 'POST') return await handleToolRollback(req, res)
869
1061
  if (route === '/api/tool/launch' && req.method === 'POST') return await handleToolLaunch(req, res)
870
1062
  if (route === '/api/claude-proxy/start' && req.method === 'POST') return await handleClaudeProxyStart(req, res)
871
1063
  if (route === '/api/claude-proxy/stop' && req.method === 'POST') return await handleClaudeProxyStop(req, res)