@simonyea/holysheep-cli 1.7.13 → 1.7.15

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/README.md CHANGED
@@ -76,6 +76,7 @@ You'll be prompted for your API Key (`cr_xxx`), then select the tools to configu
76
76
 
77
77
  > **Keep the gateway window open** while using OpenClaw. The gateway must be running for the browser UI to work.
78
78
  > If only `npx openclaw` is available, HolySheep will start Gateway as a direct process and will not install a persistent daemon from a temporary `npx` cache path.
79
+ > HolySheep Bridge now watches the local OpenClaw Gateway and exits automatically if the Gateway stays unavailable, so it won't keep a blank browser shell alive after OpenClaw has stopped.
79
80
 
80
81
  > **OpenClaw itself requires Node.js 20+**. If setup fails, first check `node --version`.
81
82
 
@@ -164,6 +165,7 @@ hs setup
164
165
 
165
166
  > ⚠️ **保持 Gateway 窗口开启**,关闭后 Gateway 停止,浏览器界面无法使用。
166
167
  > 如果机器上只有 `npx openclaw`,HolySheep 会直接启动 Gateway 进程,不会把 daemon 安装到临时 `npx` 缓存路径上。
168
+ > HolySheep Bridge 会持续检查本地 OpenClaw Gateway;如果 Gateway 持续不可用,Bridge 会自动退出,避免空白浏览器壳窗口一直残留。
167
169
 
168
170
  > ⚠️ **OpenClaw 自身要求 Node.js 20+**。如果配置失败,请先运行 `node --version` 检查版本。
169
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.13",
3
+ "version": "1.7.15",
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",
@@ -5,5 +5,6 @@ module.exports = [
5
5
  require('./claude-code'),
6
6
  require('./codex'),
7
7
  require('./droid'),
8
+ require('./opencode'),
8
9
  require('./openclaw'),
9
10
  ]
@@ -9,6 +9,10 @@ const fetch = global.fetch || require('node-fetch')
9
9
 
10
10
  const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw')
11
11
  const BRIDGE_CONFIG_FILE = path.join(OPENCLAW_DIR, 'holysheep-bridge.json')
12
+ const DEFAULT_WATCHDOG_INTERVAL_MS = 3000
13
+ const DEFAULT_WATCHDOG_FAILURE_THRESHOLD = 3
14
+ const DEFAULT_WATCHDOG_STARTUP_GRACE_MS = 30000
15
+ const DEFAULT_WATCHDOG_REQUEST_TIMEOUT_MS = 1500
12
16
 
13
17
  function readBridgeConfig(configPath = BRIDGE_CONFIG_FILE) {
14
18
  return JSON.parse(fs.readFileSync(configPath, 'utf8'))
@@ -581,6 +585,93 @@ function buildModelsResponse(config) {
581
585
  }
582
586
  }
583
587
 
588
+ function isProcessAlive(pid) {
589
+ if (!Number.isInteger(pid) || pid <= 0) return null
590
+ try {
591
+ process.kill(pid, 0)
592
+ return true
593
+ } catch (error) {
594
+ if (error && error.code === 'EPERM') return true
595
+ return false
596
+ }
597
+ }
598
+
599
+ async function checkGatewayHealth(config) {
600
+ const gatewayPort = Number(config.gatewayPort)
601
+ if (!Number.isInteger(gatewayPort) || gatewayPort <= 0) {
602
+ return { ok: true, reason: 'no_gateway_port' }
603
+ }
604
+
605
+ const gatewayPid = Number(config.gatewayPid)
606
+ const pidAlive = isProcessAlive(gatewayPid)
607
+ if (pidAlive === false) {
608
+ return { ok: false, reason: 'gateway_pid_exited' }
609
+ }
610
+
611
+ const host = config.gatewayHost || '127.0.0.1'
612
+ const timeout = Number(config.watchdog?.requestTimeoutMs) || DEFAULT_WATCHDOG_REQUEST_TIMEOUT_MS
613
+
614
+ try {
615
+ const response = await fetch(`http://${host}:${gatewayPort}/`, { method: 'GET', timeout })
616
+ return response.ok
617
+ ? { ok: true, reason: 'gateway_http_ok' }
618
+ : { ok: false, reason: `gateway_http_${response.status}` }
619
+ } catch {
620
+ return { ok: false, reason: 'gateway_http_unreachable' }
621
+ }
622
+ }
623
+
624
+ function stopBridge(server, reason) {
625
+ process.stdout.write(`HolySheep OpenClaw bridge stopping: ${reason}\n`)
626
+ server.close(() => process.exit(0))
627
+ setTimeout(() => process.exit(0), 250).unref()
628
+ }
629
+
630
+ function startGatewayWatchdog(server, configPath = BRIDGE_CONFIG_FILE) {
631
+ const bridgeStartedAt = Date.now()
632
+ let consecutiveFailures = 0
633
+ let stopping = false
634
+
635
+ const timer = setInterval(async () => {
636
+ if (stopping) return
637
+
638
+ let config
639
+ try {
640
+ config = readBridgeConfig(configPath)
641
+ } catch {
642
+ stopping = true
643
+ stopBridge(server, 'bridge config missing')
644
+ return
645
+ }
646
+
647
+ const watchdog = config.watchdog || {}
648
+ if (watchdog.enabled === false) return
649
+
650
+ const startupGraceMs = Number(watchdog.startupGraceMs) || DEFAULT_WATCHDOG_STARTUP_GRACE_MS
651
+ const failureThreshold = Number(watchdog.failureThreshold) || DEFAULT_WATCHDOG_FAILURE_THRESHOLD
652
+ const health = await checkGatewayHealth(config)
653
+
654
+ if (health.ok) {
655
+ consecutiveFailures = 0
656
+ return
657
+ }
658
+
659
+ const gatewayStartedAt = Date.parse(config.gatewayStartedAt || '') || bridgeStartedAt
660
+ if (Date.now() - gatewayStartedAt < startupGraceMs) {
661
+ return
662
+ }
663
+
664
+ consecutiveFailures += 1
665
+ if (consecutiveFailures < failureThreshold) return
666
+
667
+ stopping = true
668
+ stopBridge(server, `OpenClaw Gateway unavailable (${health.reason})`)
669
+ }, DEFAULT_WATCHDOG_INTERVAL_MS)
670
+
671
+ timer.unref()
672
+ server.on('close', () => clearInterval(timer))
673
+ }
674
+
584
675
  function createBridgeServer(configPath = BRIDGE_CONFIG_FILE) {
585
676
  return http.createServer(async (req, res) => {
586
677
  if (req.method === 'OPTIONS') {
@@ -627,6 +718,7 @@ function startBridge(args = parseArgs(process.argv.slice(2))) {
627
718
  server.listen(port, host, () => {
628
719
  process.stdout.write(`HolySheep OpenClaw bridge listening on http://${host}:${port}\n`)
629
720
  })
721
+ startGatewayWatchdog(server, args.config)
630
722
 
631
723
  return server
632
724
  }
@@ -163,6 +163,14 @@ function writeBridgeConfig(data) {
163
163
  fs.writeFileSync(BRIDGE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8')
164
164
  }
165
165
 
166
+ function updateBridgeConfig(patch) {
167
+ const current = readBridgeConfig()
168
+ writeBridgeConfig({
169
+ ...current,
170
+ ...patch,
171
+ })
172
+ }
173
+
166
174
  function getConfiguredBridgePort(config = readBridgeConfig()) {
167
175
  const port = Number(config?.port)
168
176
  return Number.isInteger(port) && port > 0 ? port : DEFAULT_BRIDGE_PORT
@@ -495,13 +503,15 @@ function _startGateway(port, preferNpx = false, preferService = true) {
495
503
  ? runOpenClaw(['gateway', 'start'], { preferNpx, timeout: 20000 })
496
504
  : { status: 1 }
497
505
 
506
+ let directChild = null
507
+
498
508
  if (serviceResult.status !== 0) {
499
- const child = spawnOpenClaw(['gateway', '--port', String(port)], {
509
+ directChild = spawnOpenClaw(['gateway', '--port', String(port)], {
500
510
  preferNpx,
501
511
  detached: true,
502
512
  stdio: 'ignore',
503
513
  })
504
- child.unref()
514
+ directChild.unref()
505
515
  }
506
516
 
507
517
  for (let i = 0; i < 8; i++) {
@@ -515,11 +525,19 @@ function _startGateway(port, preferNpx = false, preferService = true) {
515
525
  : `curl -sf http://127.0.0.1:${port}/ -o /dev/null --max-time 1`,
516
526
  { stdio: 'ignore', timeout: 3000 }
517
527
  )
518
- return true
528
+ return {
529
+ ok: true,
530
+ pid: directChild?.pid || null,
531
+ mode: directChild ? 'direct-process' : 'daemon',
532
+ }
519
533
  } catch {}
520
534
  }
521
535
 
522
- return false
536
+ return {
537
+ ok: false,
538
+ pid: directChild?.pid || null,
539
+ mode: directChild ? 'direct-process' : 'daemon',
540
+ }
523
541
  }
524
542
 
525
543
  function getDashboardUrl(port, preferNpx = false) {
@@ -572,6 +590,12 @@ module.exports = {
572
590
  }
573
591
 
574
592
  const resolvedPrimaryModel = pickPrimaryModel(primaryModel, selectedModels)
593
+ const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
594
+ if (!gatewayPort) {
595
+ throw new Error(`找不到可用端口(已检查 ${DEFAULT_GATEWAY_PORT}-${DEFAULT_GATEWAY_PORT + MAX_PORT_SCAN - 1})`)
596
+ }
597
+ this._lastGatewayPort = gatewayPort
598
+
575
599
  const bridgePort = findAvailableGatewayPort(DEFAULT_BRIDGE_PORT)
576
600
  if (!bridgePort) {
577
601
  throw new Error(`找不到可用桥接端口(已检查 ${DEFAULT_BRIDGE_PORT}-${DEFAULT_BRIDGE_PORT + MAX_PORT_SCAN - 1})`)
@@ -580,10 +604,22 @@ module.exports = {
580
604
 
581
605
  writeBridgeConfig({
582
606
  port: bridgePort,
607
+ host: '127.0.0.1',
583
608
  apiKey,
584
609
  baseUrlAnthropic,
585
610
  baseUrlOpenAI,
586
611
  models: normalizeRequestedModels(selectedModels),
612
+ gatewayPort,
613
+ gatewayHost: '127.0.0.1',
614
+ gatewayPid: null,
615
+ gatewayLaunchMode: null,
616
+ watchdog: {
617
+ enabled: true,
618
+ intervalMs: 3000,
619
+ failureThreshold: 3,
620
+ startupGraceMs: 30000,
621
+ requestTimeoutMs: 1500,
622
+ },
587
623
  })
588
624
 
589
625
  console.log(chalk.gray(' → 正在启动 HolySheep Bridge...'))
@@ -594,12 +630,6 @@ module.exports = {
594
630
 
595
631
  runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
596
632
 
597
- const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
598
- if (!gatewayPort) {
599
- throw new Error(`找不到可用端口(已检查 ${DEFAULT_GATEWAY_PORT}-${DEFAULT_GATEWAY_PORT + MAX_PORT_SCAN - 1})`)
600
- }
601
- this._lastGatewayPort = gatewayPort
602
-
603
633
  if (gatewayPort !== DEFAULT_GATEWAY_PORT) {
604
634
  console.log(chalk.yellow(` ⚠️ 端口 ${DEFAULT_GATEWAY_PORT} 已占用,自动切换到 ${gatewayPort}`))
605
635
  const listeners = listPortListeners(DEFAULT_GATEWAY_PORT)
@@ -649,21 +679,28 @@ module.exports = {
649
679
  }
650
680
 
651
681
  console.log(chalk.gray(' → 正在启动 Gateway...'))
652
- const ok = _startGateway(gatewayPort, runtime.via === 'npx', serviceReady)
682
+ const gatewayState = _startGateway(gatewayPort, runtime.via === 'npx', serviceReady)
683
+ updateBridgeConfig({
684
+ gatewayPort,
685
+ gatewayPid: gatewayState.pid,
686
+ gatewayLaunchMode: gatewayState.mode,
687
+ gatewayStartedAt: new Date().toISOString(),
688
+ })
653
689
 
654
- if (ok) {
690
+ if (gatewayState.ok) {
655
691
  console.log(chalk.green(' ✓ OpenClaw Gateway 已启动'))
656
692
  } else {
657
693
  console.log(chalk.yellow(' ⚠️ Gateway 未就绪;当前不要打开 about:blank 或裸浏览器壳窗口'))
658
694
  }
659
695
 
660
- const dashUrl = ok ? getDashboardUrl(gatewayPort, runtime.via === 'npx') : getDashboardUrlForPort(gatewayPort)
696
+ const dashUrl = gatewayState.ok ? getDashboardUrl(gatewayPort, runtime.via === 'npx') : getDashboardUrlForPort(gatewayPort)
661
697
  console.log(chalk.cyan('\n → 浏览器打开(推荐使用此地址):'))
662
698
  console.log(chalk.bold.cyan(` ${dashUrl}`))
663
699
  console.log(chalk.gray(` Bridge 地址: ${bridgeBaseUrl}`))
664
700
  console.log(chalk.gray(` 默认模型: ${plan.primaryRef || OPENCLAW_DEFAULT_MODEL}`))
665
- console.log(chalk.gray(` Gateway 启动方式: ${serviceReady ? 'daemon' : 'direct process'}`))
701
+ console.log(chalk.gray(` Gateway 启动方式: ${gatewayState.mode}`))
666
702
  console.log(chalk.gray(' 浏览器应直接打开 dashboard URL,不应停在 about:blank'))
703
+ console.log(chalk.gray(' Bridge 会在检测到 OpenClaw Gateway 持续不可用后自动退出'))
667
704
  console.log(chalk.gray(' 如在 Windows 上打开裸 http://127.0.0.1:PORT/ 仍报 Unauthorized,请使用上面的 dashboard 地址'))
668
705
 
669
706
  return {
@@ -671,8 +708,8 @@ module.exports = {
671
708
  hot: false,
672
709
  dashboardUrl: dashUrl,
673
710
  gatewayPort,
674
- gatewayReady: ok,
675
- gatewayLaunchMode: serviceReady ? 'daemon' : 'direct-process',
711
+ gatewayReady: gatewayState.ok,
712
+ gatewayLaunchMode: gatewayState.mode,
676
713
  launchCmd: getLaunchCommand(gatewayPort),
677
714
  }
678
715
  },