@simonyea/holysheep-cli 1.7.58 → 1.7.60

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.58",
3
+ "version": "1.7.60",
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",
@@ -199,6 +199,21 @@ function waitForBridge(port) {
199
199
  return false
200
200
  }
201
201
 
202
+ function stopBridge() {
203
+ // 杀掉所有已有的 bridge 进程,避免重新配置时端口冲突
204
+ try {
205
+ if (isWin) {
206
+ execSync('taskkill /F /FI "WINDOWTITLE eq openclaw-bridge*" 2>nul', { shell: true, stdio: 'ignore' })
207
+ // 按命令行匹配
208
+ const out = execSync('wmic process where "commandline like \'%openclaw-bridge%\'" get processid', { shell: true, stdio: 'pipe', encoding: 'utf8' })
209
+ const pids = out.match(/\d+/g)
210
+ if (pids) pids.forEach(pid => { try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' }) } catch {} })
211
+ } else {
212
+ execSync("pkill -f 'openclaw-bridge' 2>/dev/null || true", { shell: true, stdio: 'ignore' })
213
+ }
214
+ } catch {}
215
+ }
216
+
202
217
  function startBridge(port) {
203
218
  if (waitForBridge(port)) return true
204
219
 
@@ -611,6 +626,14 @@ module.exports = {
611
626
  this._lastRuntimeCommand = runtime.command
612
627
  this._lastRuntimeVia = runtime.via
613
628
 
629
+ // 重新配置前先停掉旧的 Bridge 和 Gateway,释放端口
630
+ console.log(chalk.gray(' → 停止旧的 Bridge 和 Gateway...'))
631
+ stopBridge()
632
+ runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
633
+ // 等端口释放
634
+ const t0 = Date.now()
635
+ while (Date.now() - t0 < 1000) {}
636
+
614
637
  if (runtime.via === 'npx' && removeBrokenLaunchAgent()) {
615
638
  console.log(chalk.gray(' → 已清理旧的 OpenClaw 守护进程配置(失效的 npx 缓存路径)'))
616
639
  }
@@ -654,8 +677,6 @@ module.exports = {
654
677
  }
655
678
  const bridgeBaseUrl = getBridgeBaseUrl(bridgePort)
656
679
 
657
- runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
658
-
659
680
  if (gatewayPort !== DEFAULT_GATEWAY_PORT) {
660
681
  console.log(chalk.yellow(` ⚠️ 端口 ${DEFAULT_GATEWAY_PORT} 已占用,自动切换到 ${gatewayPort}`))
661
682
  const listeners = listPortListeners(DEFAULT_GATEWAY_PORT)
@@ -129,6 +129,9 @@ a:hover { text-decoration: underline; }
129
129
  .footer-links { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; font-size: 0.85rem; margin-bottom: 8px; }
130
130
  .footer-sub { font-size: 0.78rem; color: var(--text2); }
131
131
 
132
+ /* Update banner */
133
+ .update-banner { background: var(--primary-dim); border: 1px solid var(--primary); border-radius: 8px; padding: 10px 16px; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem; }
134
+
132
135
  /* Misc */
133
136
  .loading { color: var(--text2); font-size: 0.85rem; }
134
137
  .hidden { display: none !important; }
@@ -184,7 +187,8 @@ const I18N = {
184
187
  installSuccess: '安装完成', installFailed: '安装失败',
185
188
  needLogin: '请先登录', cleanDone: '已清理',
186
189
  hotReload: '已生效,无需重启', needRestart: '重启终端后生效',
187
- launch: '启动命令',
190
+ launch: '启动命令', upgradeOne: '升级',
191
+ updateAvailable: '有新版本可用', updateNow: '立即升级',
188
192
  },
189
193
  en: {
190
194
  loggedIn: 'Logged in', notLoggedIn: 'Not logged in', login: 'Login', logout: 'Logout',
@@ -206,7 +210,8 @@ const I18N = {
206
210
  installSuccess: 'Installed', installFailed: 'Install failed',
207
211
  needLogin: 'Please login first', cleanDone: 'Cleaned',
208
212
  hotReload: 'Active, no restart needed', needRestart: 'Restart terminal to apply',
209
- launch: 'Launch',
213
+ launch: 'Launch', upgradeOne: 'Upgrade',
214
+ updateAvailable: 'Update available', updateNow: 'Update now',
210
215
  },
211
216
  }
212
217
 
@@ -251,6 +256,11 @@ async function loadAccount() {
251
256
 
252
257
  document.getElementById('version').textContent = 'v' + (s.version || '')
253
258
 
259
+ // 版本更新提示
260
+ if (s.updateAvailable) {
261
+ document.getElementById('version').innerHTML = `v${s.version} <span style="color:var(--primary);cursor:pointer" onclick="doUpgradeTool('holysheep','HolySheep CLI')" title="${t('updateNow')}">→ v${s.updateAvailable} ${t('updateNow')}</span>`
262
+ }
263
+
254
264
  if (s.loggedIn) {
255
265
  const hasBalance = !b.error && typeof b.balance === 'number'
256
266
  el.innerHTML = `<div class="card account-card account-logged-in">
@@ -333,12 +343,15 @@ function renderToolCard(tool) {
333
343
  } else if (!tool.configured) {
334
344
  dotClass = 'dot-warn'
335
345
  statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-warn">${t('notConfigured')}</span>`
336
- actions = `<button class="btn btn-primary btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('configure')}</button>`
346
+ const upgradeBtn = tool.canUpgrade ? `<button class="btn btn-outline btn-sm" onclick="doUpgradeTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('upgradeOne')}</button>` : ''
347
+ actions = `<button class="btn btn-primary btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('configure')}</button>${upgradeBtn}`
337
348
  hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
338
349
  } else {
339
350
  dotClass = 'dot-ok'
340
351
  statusBadges = `<span class="badge badge-ok">${t('installed')}</span> <span class="badge badge-ok">${t('configured')}</span>`
352
+ const upgradeBtn = tool.canUpgrade ? `<button class="btn btn-outline btn-sm" onclick="doUpgradeTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('upgradeOne')}</button>` : ''
341
353
  actions = `<button class="btn btn-outline btn-sm" onclick="doConfigureTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reconfigure')}</button>
354
+ ${upgradeBtn}
342
355
  <button class="btn btn-danger btn-sm" onclick="doResetTool('${tool.id}','${esc(tool.name)}')" ${busy ? 'disabled' : ''}>${t('reset')}</button>`
343
356
  hintLine = tool.version ? `<span class="mono" style="font-size:0.78rem;color:var(--text2)">${esc(tool.version)}</span>` : ''
344
357
  }
@@ -445,6 +458,25 @@ async function doUpgrade() {
445
458
  loadTools()
446
459
  }
447
460
 
461
+ async function doUpgradeTool(id, name) {
462
+ if (busy) return
463
+ busy = true
464
+ openConsole(`${t('upgradeOne')}: ${name}`)
465
+ document.getElementById('console-section').classList.add('busy')
466
+
467
+ await streamSSE('/api/tool/upgrade', { toolId: id }, (ev) => {
468
+ if (ev.type === 'progress') appendLog(ev.message, 'info')
469
+ else if (ev.type === 'output') appendLogRaw(ev.text)
470
+ else if (ev.type === 'done') {
471
+ appendLog(ev.success ? `\n✓ ${t('configSuccess')}` : `\n✗ ${t('configFailed')}`, ev.success ? 'ok' : 'err')
472
+ }
473
+ })
474
+
475
+ document.getElementById('console-section').classList.remove('busy')
476
+ busy = false
477
+ loadTools()
478
+ }
479
+
448
480
  // ── Console ──────────────────────────────────────────────────────────────────
449
481
  function openConsole(title) {
450
482
  const area = document.getElementById('console-section')
@@ -96,6 +96,7 @@ const AUTO_INSTALL = {
96
96
  // ── UPGRADABLE_TOOLS (from upgrade.js) ───────────────────────────────────────
97
97
 
98
98
  const UPGRADABLE_TOOLS = [
99
+ { name: 'HolySheep CLI', id: 'holysheep', command: 'hs', versionCmd: 'hs --version', npmPkg: '@simonyea/holysheep-cli', installCmd: 'npm install -g @simonyea/holysheep-cli@latest' },
99
100
  {
100
101
  name: 'Claude Code', id: 'claude-code', command: 'claude',
101
102
  versionCmd: 'claude --version', npmPkg: null,
@@ -104,19 +105,49 @@ const UPGRADABLE_TOOLS = [
104
105
  : 'curl -fsSL https://claude.ai/install.sh | bash',
105
106
  },
106
107
  { name: 'Codex CLI', id: 'codex', command: 'codex', versionCmd: 'codex --version', npmPkg: '@openai/codex', installCmd: 'npm install -g @openai/codex@latest' },
108
+ { name: 'Droid CLI', id: 'droid', command: 'droid', versionCmd: 'droid --version', npmPkg: null, installCmd: 'brew install --cask droid' },
109
+ { name: 'OpenCode', id: 'opencode', command: 'opencode', versionCmd: 'opencode --version', npmPkg: 'opencode-ai', installCmd: 'npm install -g opencode-ai@latest' },
110
+ { name: 'OpenClaw', id: 'openclaw', command: 'openclaw', versionCmd: 'openclaw --version', npmPkg: 'openclaw', installCmd: 'npm install -g openclaw@latest --ignore-scripts' },
107
111
  { name: 'Gemini CLI', id: 'gemini-cli', command: 'gemini', versionCmd: 'gemini --version', npmPkg: '@google/gemini-cli', installCmd: 'npm install -g @google/gemini-cli@latest' },
108
112
  ]
109
113
 
114
+ // ── Update check (cached, refreshes every 30min) ────────────────────────────
115
+
116
+ let _latestVersion = null
117
+ let _lastCheckTime = 0
118
+ const UPDATE_CHECK_INTERVAL = 30 * 60 * 1000
119
+
120
+ async function checkLatestVersion() {
121
+ const now = Date.now()
122
+ if (_latestVersion && now - _lastCheckTime < UPDATE_CHECK_INTERVAL) return _latestVersion
123
+ try {
124
+ const fetch = require('node-fetch')
125
+ const r = await fetch('https://registry.npmjs.org/@simonyea/holysheep-cli/latest', { timeout: 5000 })
126
+ if (r.ok) {
127
+ const data = await r.json()
128
+ _latestVersion = data.version || null
129
+ _lastCheckTime = now
130
+ }
131
+ } catch {}
132
+ return _latestVersion
133
+ }
134
+
135
+ // 启动时立即检查一次
136
+ checkLatestVersion()
137
+
110
138
  // ── API Handlers ─────────────────────────────────────────────────────────────
111
139
 
112
140
  async function handleStatus(_req, res) {
113
141
  const apiKey = getApiKey()
114
142
  const config = loadConfig()
143
+ const latest = await checkLatestVersion()
115
144
  json(res, {
116
145
  loggedIn: !!apiKey,
117
146
  apiKey: apiKey ? maskKey(apiKey) : null,
118
147
  savedAt: config.savedAt || null,
119
148
  version: pkg.version,
149
+ latestVersion: latest || null,
150
+ updateAvailable: latest && latest !== pkg.version ? latest : null,
120
151
  })
121
152
  }
122
153
 
@@ -243,6 +274,7 @@ async function handleTools(_req, res) {
243
274
  hint: t.hint || null,
244
275
  launchCmd: t.launchCmd || null,
245
276
  canAutoInstall: !!AUTO_INSTALL[t.id],
277
+ canUpgrade: !!UPGRADABLE_TOOLS.find(u => u.id === t.id),
246
278
  }
247
279
  })
248
280
  json(res, tools)
@@ -466,11 +498,10 @@ async function handleToolConfigure(req, res) {
466
498
 
467
499
  const allModelIds = [
468
500
  'gpt-5.4', 'gpt-5.3-codex-spark',
469
- 'claude-sonnet-4-6', 'claude-sonnet-4-6[1m]',
470
- 'claude-opus-4-6', 'claude-opus-4-6[1m]',
501
+ 'claude-sonnet-4-6', 'claude-opus-4-6',
471
502
  'MiniMax-M2.7-highspeed', 'claude-haiku-4-5',
472
503
  ]
473
- const primaryModel = toolId === 'openclaw' ? 'claude-sonnet-4-6[1m]' : 'claude-sonnet-4-6'
504
+ const primaryModel = 'claude-sonnet-4-6'
474
505
 
475
506
  sseEmit(res, { type: 'progress', message: `正在配置 ${tool.name}...` })
476
507
 
@@ -510,6 +541,51 @@ async function handleToolConfigure(req, res) {
510
541
  })
511
542
  }
512
543
 
544
+ // ── Single-tool upgrade (SSE) ────────────────────────────────────────────────
545
+
546
+ async function handleToolUpgrade(req, res) {
547
+ const body = await parseBody(req)
548
+ const { toolId } = body
549
+ const entry = UPGRADABLE_TOOLS.find(t => t.id === toolId)
550
+ if (!entry) return json(res, { error: '不支持升级此工具' }, 400)
551
+
552
+ sseStart(res)
553
+
554
+ let localVer = null
555
+ try {
556
+ const out = execSync(entry.versionCmd, { stdio: 'pipe', timeout: 10000 }).toString().trim()
557
+ const m = out.match(/(\d+\.\d+\.\d+[\w.-]*)/)
558
+ localVer = m ? m[1] : out.split('\n')[0].slice(0, 30)
559
+ } catch {}
560
+
561
+ sseEmit(res, { type: 'progress', message: `${entry.name} 当前版本: ${localVer || '未知'}` })
562
+ sseEmit(res, { type: 'progress', message: `正在升级 ${entry.name}...` })
563
+
564
+ const ok = await new Promise(resolve => {
565
+ const child = spawn(entry.installCmd, [], { shell: true })
566
+ child.stdout?.on('data', chunk => sseEmit(res, { type: 'output', text: chunk.toString() }))
567
+ child.stderr?.on('data', chunk => sseEmit(res, { type: 'output', text: chunk.toString() }))
568
+ child.on('close', code => resolve(code === 0))
569
+ child.on('error', () => resolve(false))
570
+ })
571
+
572
+ let newVer = null
573
+ try {
574
+ const out = execSync(entry.versionCmd, { stdio: 'pipe', timeout: 10000 }).toString().trim()
575
+ const m = out.match(/(\d+\.\d+\.\d+[\w.-]*)/)
576
+ newVer = m ? m[1] : null
577
+ } catch {}
578
+
579
+ if (ok) {
580
+ sseEmit(res, { type: 'progress', message: `✓ ${entry.name} 升级成功: ${localVer || '?'} → ${newVer || 'latest'}` })
581
+ } else {
582
+ sseEmit(res, { type: 'progress', message: `✗ ${entry.name} 升级失败` })
583
+ }
584
+
585
+ sseEmit(res, { type: 'done', success: ok, localVer, newVer })
586
+ res.end()
587
+ }
588
+
513
589
  // ── Single-tool reset ────────────────────────────────────────────────────────
514
590
 
515
591
  async function handleToolReset(req, res) {
@@ -605,6 +681,7 @@ async function handleRequest(req, res) {
605
681
  if (route === '/api/tool/install' && req.method === 'POST') return await handleToolInstall(req, res)
606
682
  if (route === '/api/tool/configure' && req.method === 'POST') return await handleToolConfigure(req, res)
607
683
  if (route === '/api/tool/reset' && req.method === 'POST') return await handleToolReset(req, res)
684
+ if (route === '/api/tool/upgrade' && req.method === 'POST') return await handleToolUpgrade(req, res)
608
685
  if (route === '/api/env' && req.method === 'GET') return handleEnv(req, res)
609
686
  if (route === '/api/env/clean' && req.method === 'POST') return handleEnvClean(req, res)
610
687