@simonyea/holysheep-cli 1.7.116 → 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/webui/index.html +28 -4
- package/src/webui/server.js +125 -0
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",
|
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
|
|
|
@@ -689,6 +690,129 @@ async function handleToolUpgrade(req, res) {
|
|
|
689
690
|
res.end()
|
|
690
691
|
}
|
|
691
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
|
+
|
|
692
816
|
// ── Single-tool reset ────────────────────────────────────────────────────────
|
|
693
817
|
|
|
694
818
|
async function handleToolReset(req, res) {
|
|
@@ -866,6 +990,7 @@ async function handleRequest(req, res) {
|
|
|
866
990
|
if (route === '/api/tool/configure' && req.method === 'POST') return await handleToolConfigure(req, res)
|
|
867
991
|
if (route === '/api/tool/reset' && req.method === 'POST') return await handleToolReset(req, res)
|
|
868
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)
|
|
869
994
|
if (route === '/api/tool/launch' && req.method === 'POST') return await handleToolLaunch(req, res)
|
|
870
995
|
if (route === '/api/claude-proxy/start' && req.method === 'POST') return await handleClaudeProxyStart(req, res)
|
|
871
996
|
if (route === '/api/claude-proxy/stop' && req.method === 'POST') return await handleClaudeProxyStop(req, res)
|