@simonyea/holysheep-cli 1.7.12 → 1.7.14
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 +9 -2
- package/package.json +1 -1
- package/src/commands/doctor.js +28 -2
- package/src/commands/setup.js +3 -0
- package/src/tools/openclaw-bridge.js +92 -0
- package/src/tools/openclaw.js +138 -19
package/README.md
CHANGED
|
@@ -70,11 +70,13 @@ You'll be prompted for your API Key (`cr_xxx`), then select the tools to configu
|
|
|
70
70
|
1. HolySheep configures OpenClaw to use HolySheep API
|
|
71
71
|
2. The OpenClaw Gateway starts on **`http://127.0.0.1:18789/` by default**
|
|
72
72
|
3. If `18789` is occupied, `hs setup` automatically picks the next available local port
|
|
73
|
-
4. Open the exact
|
|
73
|
+
4. Open the exact dashboard URL shown in the terminal and start chatting — do not use a blank browser shell or `about:blank`
|
|
74
74
|
|
|
75
75
|
**Default OpenClaw model:** `gpt-5.4`
|
|
76
76
|
|
|
77
77
|
> **Keep the gateway window open** while using OpenClaw. The gateway must be running for the browser UI to work.
|
|
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.
|
|
78
80
|
|
|
79
81
|
> **OpenClaw itself requires Node.js 20+**. If setup fails, first check `node --version`.
|
|
80
82
|
|
|
@@ -157,11 +159,13 @@ hs setup
|
|
|
157
159
|
1. HolySheep 会自动把 OpenClaw 接到 HolySheep API
|
|
158
160
|
2. 默认启动在 **`http://127.0.0.1:18789/`**
|
|
159
161
|
3. 如果 `18789` 被占用,`hs setup` 会自动切换到下一个可用本地端口
|
|
160
|
-
4.
|
|
162
|
+
4. 按终端里显示的准确 dashboard 地址打开浏览器,直接开始聊天,不要打开空白浏览器壳窗口或 `about:blank`
|
|
161
163
|
|
|
162
164
|
**OpenClaw 默认模型:** `gpt-5.4`
|
|
163
165
|
|
|
164
166
|
> ⚠️ **保持 Gateway 窗口开启**,关闭后 Gateway 停止,浏览器界面无法使用。
|
|
167
|
+
> 如果机器上只有 `npx openclaw`,HolySheep 会直接启动 Gateway 进程,不会把 daemon 安装到临时 `npx` 缓存路径上。
|
|
168
|
+
> HolySheep Bridge 会持续检查本地 OpenClaw Gateway;如果 Gateway 持续不可用,Bridge 会自动退出,避免空白浏览器壳窗口一直残留。
|
|
165
169
|
|
|
166
170
|
> ⚠️ **OpenClaw 自身要求 Node.js 20+**。如果配置失败,请先运行 `node --version` 检查版本。
|
|
167
171
|
|
|
@@ -205,6 +209,9 @@ A: 支持,需要 Node.js 16+。如果 `hs` 命令找不到,请重启终端
|
|
|
205
209
|
**Q: OpenClaw Gateway 窗口可以最小化吗?**
|
|
206
210
|
A: 可以最小化,但不能关闭。关闭后 Gateway 停止,需要按 `hs setup` / `hs doctor` 显示的端口重新运行 `openclaw gateway --port <端口>` 或 `npx openclaw gateway --port <端口>`。
|
|
207
211
|
|
|
212
|
+
**Q: 为什么浏览器会只剩黑屏 / 空白窗口?**
|
|
213
|
+
A: 最常见原因是 Gateway 实际没有启动成功,或者你打开了空白浏览器壳窗口而不是终端输出的 dashboard URL。先运行 `hs doctor` 检查 `gateway.port` 是否正在监听,再打开终端里输出的准确 dashboard 地址。
|
|
214
|
+
|
|
208
215
|
**Q: 18789 端口被占用怎么办?**
|
|
209
216
|
A: `hs setup` 会自动切换到下一个可用本地端口,并把准确访问地址打印出来;也可以运行 `hs doctor` 查看当前 `gateway.port` 和端口占用情况。
|
|
210
217
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.14",
|
|
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/commands/doctor.js
CHANGED
|
@@ -99,9 +99,14 @@ function printCheck(ok, label, detail = '') {
|
|
|
99
99
|
function printOpenClawDetails(tool, installState, nodeMajor) {
|
|
100
100
|
const details = []
|
|
101
101
|
const gatewayPort = typeof tool.getGatewayPort === 'function' ? tool.getGatewayPort() : 18789
|
|
102
|
+
const bridgePort = typeof tool.getBridgePort === 'function' ? tool.getBridgePort() : null
|
|
102
103
|
const primaryModel = typeof tool.getPrimaryModel === 'function' ? tool.getPrimaryModel() : ''
|
|
103
104
|
const listeners = typeof tool.getPortListeners === 'function' ? tool.getPortListeners(gatewayPort) : []
|
|
104
105
|
const foreignListeners = listeners.filter((item) => !String(item.command || '').toLowerCase().includes('openclaw'))
|
|
106
|
+
const launchAgent = typeof tool.getLaunchAgentDiagnosis === 'function' ? tool.getLaunchAgentDiagnosis() : null
|
|
107
|
+
const launchHint = Array.isArray(tool.launchSteps) && tool.launchSteps.length > 1
|
|
108
|
+
? tool.launchSteps[1].cmd
|
|
109
|
+
: tool.launchCmd || 'npx openclaw gateway --port <port>'
|
|
105
110
|
|
|
106
111
|
if (installState.detail === 'npx fallback') {
|
|
107
112
|
details.push({
|
|
@@ -123,6 +128,25 @@ function printOpenClawDetails(tool, installState, nodeMajor) {
|
|
|
123
128
|
})
|
|
124
129
|
}
|
|
125
130
|
|
|
131
|
+
if (bridgePort) {
|
|
132
|
+
details.push({
|
|
133
|
+
level: 'info',
|
|
134
|
+
text: `Bridge 端口:${bridgePort}`,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (launchAgent?.unstable) {
|
|
139
|
+
details.push({
|
|
140
|
+
level: 'warn',
|
|
141
|
+
text: `检测到失效的 LaunchAgent,引用了临时 npx 缓存路径:${launchAgent.unstableArg}`,
|
|
142
|
+
})
|
|
143
|
+
} else if (launchAgent?.exists) {
|
|
144
|
+
details.push({
|
|
145
|
+
level: 'ok',
|
|
146
|
+
text: `LaunchAgent 配置存在:${launchAgent.path}`,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
if (foreignListeners.length) {
|
|
127
151
|
const occupiedBy = foreignListeners
|
|
128
152
|
.slice(0, 2)
|
|
@@ -139,8 +163,10 @@ function printOpenClawDetails(tool, installState, nodeMajor) {
|
|
|
139
163
|
})
|
|
140
164
|
} else {
|
|
141
165
|
details.push({
|
|
142
|
-
level: 'info',
|
|
143
|
-
text:
|
|
166
|
+
level: launchAgent?.unstable ? 'warn' : 'info',
|
|
167
|
+
text: launchAgent?.unstable
|
|
168
|
+
? `Gateway 端口 ${gatewayPort} 当前未监听;这通常会导致浏览器只剩空白/黑屏窗口`
|
|
169
|
+
: `Gateway 端口 ${gatewayPort} 当前空闲;如刚完成配置,可运行 ${launchHint}`,
|
|
144
170
|
})
|
|
145
171
|
}
|
|
146
172
|
|
package/src/commands/setup.js
CHANGED
|
@@ -378,6 +378,9 @@ async function setup(options) {
|
|
|
378
378
|
const hot = r.result?.hot ? chalk.cyan(' (热切换,无需重启)') : chalk.gray(' (重启终端生效)')
|
|
379
379
|
console.log(` ✓ ${r.tool.name}${hot}`)
|
|
380
380
|
if (r.tool.hint) console.log(` ${chalk.gray('💡 ' + r.tool.hint)}`)
|
|
381
|
+
if (r.result?.dashboardUrl) {
|
|
382
|
+
console.log(` ${chalk.gray('🌐 Dashboard:')} ${chalk.cyan.bold(r.result.dashboardUrl)}`)
|
|
383
|
+
}
|
|
381
384
|
// 显示启动命令
|
|
382
385
|
if (r.tool.launchSteps) {
|
|
383
386
|
// 多步骤启动(如 openclaw)
|
|
@@ -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
|
@@ -13,6 +13,8 @@ const { BRIDGE_CONFIG_FILE } = require('./openclaw-bridge')
|
|
|
13
13
|
|
|
14
14
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw')
|
|
15
15
|
const CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json')
|
|
16
|
+
const OPENCLAW_LAUNCH_AGENTS_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents')
|
|
17
|
+
const OPENCLAW_GATEWAY_PLIST = path.join(OPENCLAW_LAUNCH_AGENTS_DIR, 'ai.openclaw.gateway.plist')
|
|
16
18
|
const isWin = process.platform === 'win32'
|
|
17
19
|
const DEFAULT_BRIDGE_PORT = 18788
|
|
18
20
|
const DEFAULT_GATEWAY_PORT = 18789
|
|
@@ -161,6 +163,14 @@ function writeBridgeConfig(data) {
|
|
|
161
163
|
fs.writeFileSync(BRIDGE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8')
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
function updateBridgeConfig(patch) {
|
|
167
|
+
const current = readBridgeConfig()
|
|
168
|
+
writeBridgeConfig({
|
|
169
|
+
...current,
|
|
170
|
+
...patch,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
164
174
|
function getConfiguredBridgePort(config = readBridgeConfig()) {
|
|
165
175
|
const port = Number(config?.port)
|
|
166
176
|
return Number.isInteger(port) && port > 0 ? port : DEFAULT_BRIDGE_PORT
|
|
@@ -299,6 +309,68 @@ function getDashboardCommand() {
|
|
|
299
309
|
return `${runtime} dashboard --no-open`
|
|
300
310
|
}
|
|
301
311
|
|
|
312
|
+
function getDashboardUrlForPort(port) {
|
|
313
|
+
return `http://127.0.0.1:${port}/`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isNpxCachePath(value) {
|
|
317
|
+
return /[\\/]_npx[\\/]/i.test(String(value || ''))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readOpenClawLaunchAgent() {
|
|
321
|
+
if (process.platform !== 'darwin') return null
|
|
322
|
+
try {
|
|
323
|
+
if (!fs.existsSync(OPENCLAW_GATEWAY_PLIST)) return null
|
|
324
|
+
return fs.readFileSync(OPENCLAW_GATEWAY_PLIST, 'utf8')
|
|
325
|
+
} catch {
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseLaunchAgentProgramArguments(content) {
|
|
331
|
+
const match = String(content || '').match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i)
|
|
332
|
+
if (!match) return []
|
|
333
|
+
|
|
334
|
+
return Array.from(match[1].matchAll(/<string>([\s\S]*?)<\/string>/g)).map((item) => item[1])
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getLaunchAgentDiagnosis() {
|
|
338
|
+
const content = readOpenClawLaunchAgent()
|
|
339
|
+
if (!content) {
|
|
340
|
+
return { exists: false, path: OPENCLAW_GATEWAY_PLIST, unstable: false, programArguments: [] }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const programArguments = parseLaunchAgentProgramArguments(content)
|
|
344
|
+
const unstableArg = programArguments.find(isNpxCachePath) || ''
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
exists: true,
|
|
348
|
+
path: OPENCLAW_GATEWAY_PLIST,
|
|
349
|
+
unstable: Boolean(unstableArg),
|
|
350
|
+
unstableArg,
|
|
351
|
+
programArguments,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function removeBrokenLaunchAgent() {
|
|
356
|
+
const diagnosis = getLaunchAgentDiagnosis()
|
|
357
|
+
if (!diagnosis.exists || !diagnosis.unstable) return false
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
execSync(`launchctl bootout "gui/${process.getuid()}" "${diagnosis.path}"`, {
|
|
361
|
+
shell: true,
|
|
362
|
+
stdio: 'ignore',
|
|
363
|
+
})
|
|
364
|
+
} catch {}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
fs.unlinkSync(diagnosis.path)
|
|
368
|
+
return true
|
|
369
|
+
} catch {
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
302
374
|
function buildModelEntry(id) {
|
|
303
375
|
return {
|
|
304
376
|
id,
|
|
@@ -417,6 +489,8 @@ function _disableGatewayAuth(preferNpx = false) {
|
|
|
417
489
|
}
|
|
418
490
|
|
|
419
491
|
function _installGatewayService(port, preferNpx = false) {
|
|
492
|
+
if (preferNpx) return false
|
|
493
|
+
|
|
420
494
|
const result = runOpenClaw(['gateway', 'install', '--force', '--port', String(port)], {
|
|
421
495
|
preferNpx,
|
|
422
496
|
timeout: 60000,
|
|
@@ -429,13 +503,15 @@ function _startGateway(port, preferNpx = false, preferService = true) {
|
|
|
429
503
|
? runOpenClaw(['gateway', 'start'], { preferNpx, timeout: 20000 })
|
|
430
504
|
: { status: 1 }
|
|
431
505
|
|
|
506
|
+
let directChild = null
|
|
507
|
+
|
|
432
508
|
if (serviceResult.status !== 0) {
|
|
433
|
-
|
|
509
|
+
directChild = spawnOpenClaw(['gateway', '--port', String(port)], {
|
|
434
510
|
preferNpx,
|
|
435
511
|
detached: true,
|
|
436
512
|
stdio: 'ignore',
|
|
437
513
|
})
|
|
438
|
-
|
|
514
|
+
directChild.unref()
|
|
439
515
|
}
|
|
440
516
|
|
|
441
517
|
for (let i = 0; i < 8; i++) {
|
|
@@ -449,11 +525,19 @@ function _startGateway(port, preferNpx = false, preferService = true) {
|
|
|
449
525
|
: `curl -sf http://127.0.0.1:${port}/ -o /dev/null --max-time 1`,
|
|
450
526
|
{ stdio: 'ignore', timeout: 3000 }
|
|
451
527
|
)
|
|
452
|
-
return
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
pid: directChild?.pid || null,
|
|
531
|
+
mode: directChild ? 'direct-process' : 'daemon',
|
|
532
|
+
}
|
|
453
533
|
} catch {}
|
|
454
534
|
}
|
|
455
535
|
|
|
456
|
-
return
|
|
536
|
+
return {
|
|
537
|
+
ok: false,
|
|
538
|
+
pid: directChild?.pid || null,
|
|
539
|
+
mode: directChild ? 'direct-process' : 'daemon',
|
|
540
|
+
}
|
|
457
541
|
}
|
|
458
542
|
|
|
459
543
|
function getDashboardUrl(port, preferNpx = false) {
|
|
@@ -466,7 +550,7 @@ function getDashboardUrl(port, preferNpx = false) {
|
|
|
466
550
|
const match = output.match(/Dashboard URL:\s*(\S+)/) || output.match(/(https?:\/\/\S+)/)
|
|
467
551
|
if (match) return match[1]
|
|
468
552
|
}
|
|
469
|
-
return
|
|
553
|
+
return getDashboardUrlForPort(port)
|
|
470
554
|
}
|
|
471
555
|
|
|
472
556
|
module.exports = {
|
|
@@ -499,8 +583,19 @@ module.exports = {
|
|
|
499
583
|
throw new Error('未检测到 OpenClaw;请先全局安装,或确保 npx 可用')
|
|
500
584
|
}
|
|
501
585
|
this._lastRuntimeCommand = runtime.command
|
|
586
|
+
this._lastRuntimeVia = runtime.via
|
|
587
|
+
|
|
588
|
+
if (runtime.via === 'npx' && removeBrokenLaunchAgent()) {
|
|
589
|
+
console.log(chalk.gray(' → 已清理旧的 OpenClaw 守护进程配置(失效的 npx 缓存路径)'))
|
|
590
|
+
}
|
|
502
591
|
|
|
503
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
|
+
|
|
504
599
|
const bridgePort = findAvailableGatewayPort(DEFAULT_BRIDGE_PORT)
|
|
505
600
|
if (!bridgePort) {
|
|
506
601
|
throw new Error(`找不到可用桥接端口(已检查 ${DEFAULT_BRIDGE_PORT}-${DEFAULT_BRIDGE_PORT + MAX_PORT_SCAN - 1})`)
|
|
@@ -509,10 +604,22 @@ module.exports = {
|
|
|
509
604
|
|
|
510
605
|
writeBridgeConfig({
|
|
511
606
|
port: bridgePort,
|
|
607
|
+
host: '127.0.0.1',
|
|
512
608
|
apiKey,
|
|
513
609
|
baseUrlAnthropic,
|
|
514
610
|
baseUrlOpenAI,
|
|
515
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
|
+
},
|
|
516
623
|
})
|
|
517
624
|
|
|
518
625
|
console.log(chalk.gray(' → 正在启动 HolySheep Bridge...'))
|
|
@@ -523,12 +630,6 @@ module.exports = {
|
|
|
523
630
|
|
|
524
631
|
runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
|
|
525
632
|
|
|
526
|
-
const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
|
|
527
|
-
if (!gatewayPort) {
|
|
528
|
-
throw new Error(`找不到可用端口(已检查 ${DEFAULT_GATEWAY_PORT}-${DEFAULT_GATEWAY_PORT + MAX_PORT_SCAN - 1})`)
|
|
529
|
-
}
|
|
530
|
-
this._lastGatewayPort = gatewayPort
|
|
531
|
-
|
|
532
633
|
if (gatewayPort !== DEFAULT_GATEWAY_PORT) {
|
|
533
634
|
console.log(chalk.yellow(` ⚠️ 端口 ${DEFAULT_GATEWAY_PORT} 已占用,自动切换到 ${gatewayPort}`))
|
|
534
635
|
const listeners = listPortListeners(DEFAULT_GATEWAY_PORT)
|
|
@@ -544,7 +645,7 @@ module.exports = {
|
|
|
544
645
|
try { fs.unlinkSync(CONFIG_FILE) } catch {}
|
|
545
646
|
|
|
546
647
|
console.log(chalk.gray(' → 写入配置...'))
|
|
547
|
-
const
|
|
648
|
+
const onboardArgs = [
|
|
548
649
|
'onboard',
|
|
549
650
|
'--non-interactive',
|
|
550
651
|
'--accept-risk',
|
|
@@ -554,8 +655,10 @@ module.exports = {
|
|
|
554
655
|
'--custom-model-id', resolvedPrimaryModel,
|
|
555
656
|
'--custom-compatibility', 'openai',
|
|
556
657
|
'--gateway-port', String(gatewayPort),
|
|
557
|
-
|
|
558
|
-
|
|
658
|
+
]
|
|
659
|
+
if (runtime.via !== 'npx') onboardArgs.push('--install-daemon')
|
|
660
|
+
|
|
661
|
+
const result = runOpenClaw(onboardArgs, { preferNpx: runtime.via === 'npx' })
|
|
559
662
|
|
|
560
663
|
if (result.status !== 0) {
|
|
561
664
|
console.log(chalk.yellow(' ⚠️ onboard 失败,使用备用配置...'))
|
|
@@ -571,21 +674,33 @@ module.exports = {
|
|
|
571
674
|
|
|
572
675
|
_disableGatewayAuth(runtime.via === 'npx')
|
|
573
676
|
const serviceReady = _installGatewayService(gatewayPort, runtime.via === 'npx')
|
|
677
|
+
if (runtime.via === 'npx') {
|
|
678
|
+
console.log(chalk.gray(' → 当前仅检测到 npx openclaw,跳过 daemon 安装,改为直接启动 Gateway 进程'))
|
|
679
|
+
}
|
|
574
680
|
|
|
575
681
|
console.log(chalk.gray(' → 正在启动 Gateway...'))
|
|
576
|
-
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
|
+
})
|
|
577
689
|
|
|
578
|
-
if (ok) {
|
|
690
|
+
if (gatewayState.ok) {
|
|
579
691
|
console.log(chalk.green(' ✓ OpenClaw Gateway 已启动'))
|
|
580
692
|
} else {
|
|
581
|
-
console.log(chalk.yellow(' ⚠️ Gateway
|
|
693
|
+
console.log(chalk.yellow(' ⚠️ Gateway 未就绪;当前不要打开 about:blank 或裸浏览器壳窗口'))
|
|
582
694
|
}
|
|
583
695
|
|
|
584
|
-
const dashUrl = getDashboardUrl(gatewayPort, runtime.via === 'npx')
|
|
696
|
+
const dashUrl = gatewayState.ok ? getDashboardUrl(gatewayPort, runtime.via === 'npx') : getDashboardUrlForPort(gatewayPort)
|
|
585
697
|
console.log(chalk.cyan('\n → 浏览器打开(推荐使用此地址):'))
|
|
586
698
|
console.log(chalk.bold.cyan(` ${dashUrl}`))
|
|
587
699
|
console.log(chalk.gray(` Bridge 地址: ${bridgeBaseUrl}`))
|
|
588
700
|
console.log(chalk.gray(` 默认模型: ${plan.primaryRef || OPENCLAW_DEFAULT_MODEL}`))
|
|
701
|
+
console.log(chalk.gray(` Gateway 启动方式: ${gatewayState.mode}`))
|
|
702
|
+
console.log(chalk.gray(' 浏览器应直接打开 dashboard URL,不应停在 about:blank'))
|
|
703
|
+
console.log(chalk.gray(' Bridge 会在检测到 OpenClaw Gateway 持续不可用后自动退出'))
|
|
589
704
|
console.log(chalk.gray(' 如在 Windows 上打开裸 http://127.0.0.1:PORT/ 仍报 Unauthorized,请使用上面的 dashboard 地址'))
|
|
590
705
|
|
|
591
706
|
return {
|
|
@@ -593,6 +708,8 @@ module.exports = {
|
|
|
593
708
|
hot: false,
|
|
594
709
|
dashboardUrl: dashUrl,
|
|
595
710
|
gatewayPort,
|
|
711
|
+
gatewayReady: gatewayState.ok,
|
|
712
|
+
gatewayLaunchMode: gatewayState.mode,
|
|
596
713
|
launchCmd: getLaunchCommand(gatewayPort),
|
|
597
714
|
}
|
|
598
715
|
},
|
|
@@ -607,6 +724,7 @@ module.exports = {
|
|
|
607
724
|
getGatewayPort() { return getConfiguredGatewayPort() },
|
|
608
725
|
getPrimaryModel() { return getConfiguredPrimaryModel() },
|
|
609
726
|
getPortListeners(port = getConfiguredGatewayPort()) { return listPortListeners(port) },
|
|
727
|
+
getLaunchAgentDiagnosis,
|
|
610
728
|
get hint() {
|
|
611
729
|
return `Bridge + Gateway 已配置,默认模型为 ${getConfiguredPrimaryModel() || OPENCLAW_DEFAULT_MODEL}`
|
|
612
730
|
},
|
|
@@ -620,7 +738,8 @@ module.exports = {
|
|
|
620
738
|
]
|
|
621
739
|
},
|
|
622
740
|
get launchNote() {
|
|
623
|
-
|
|
741
|
+
const runtime = module.exports._lastRuntimeVia === 'npx' ? '当前为 npx 模式,不安装常驻 daemon。' : ''
|
|
742
|
+
return `🌐 请先启动 Bridge,再启动 Gateway;最后运行 ${getDashboardCommand()} ${runtime}`.trim()
|
|
624
743
|
},
|
|
625
744
|
installCmd: 'npm install -g openclaw@latest',
|
|
626
745
|
docsUrl: 'https://docs.openclaw.ai',
|