@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 +2 -0
- package/package.json +1 -1
- package/src/tools/index.js +1 -0
- package/src/tools/openclaw-bridge.js +92 -0
- package/src/tools/openclaw.js +53 -16
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.
|
|
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",
|
package/src/tools/index.js
CHANGED
|
@@ -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
|
}
|
package/src/tools/openclaw.js
CHANGED
|
@@ -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
|
-
|
|
509
|
+
directChild = spawnOpenClaw(['gateway', '--port', String(port)], {
|
|
500
510
|
preferNpx,
|
|
501
511
|
detached: true,
|
|
502
512
|
stdio: 'ignore',
|
|
503
513
|
})
|
|
504
|
-
|
|
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
|
|
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
|
|
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
|
|
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 启动方式: ${
|
|
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:
|
|
711
|
+
gatewayReady: gatewayState.ok,
|
|
712
|
+
gatewayLaunchMode: gatewayState.mode,
|
|
676
713
|
launchCmd: getLaunchCommand(gatewayPort),
|
|
677
714
|
}
|
|
678
715
|
},
|