@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 +1 -1
- package/src/commands/claude-proxy.js +4 -3
- package/src/tools/openclaw.js +5 -7
- package/src/webui/index.html +28 -4
- package/src/webui/server.js +144 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.7.
|
|
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 "
|
|
65
|
-
: `curl -
|
|
66
|
-
{ stdio: 'ignore', timeout:
|
|
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 {
|
package/src/tools/openclaw.js
CHANGED
|
@@ -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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
// 未运行,启动它
|
package/src/webui/index.html
CHANGED
|
@@ -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
|
-
|
|
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')
|
package/src/webui/server.js
CHANGED
|
@@ -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'
|
|
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'
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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: '⚠️
|
|
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)
|