@simonyea/holysheep-cli 2.1.40 → 2.1.41
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/dist/configure-worker.js +4491 -0
- package/dist/index.js +9591 -0
- package/dist/process-proxy-inject.js +117 -0
- package/package.json +20 -7
- package/.gitea/workflows/sanity.yml +0 -125
- package/scripts/check-tarball-size.js +0 -44
- package/src/commands/balance.js +0 -57
- package/src/commands/claude-proxy.js +0 -248
- package/src/commands/claude.js +0 -135
- package/src/commands/doctor.js +0 -282
- package/src/commands/login.js +0 -211
- package/src/commands/openclaw.js +0 -258
- package/src/commands/reset.js +0 -53
- package/src/commands/setup.js +0 -493
- package/src/commands/upgrade.js +0 -168
- package/src/commands/webui.js +0 -622
- package/src/index.js +0 -226
- package/src/tools/aider.js +0 -78
- package/src/tools/antigravity.js +0 -42
- package/src/tools/claude-code.js +0 -228
- package/src/tools/claude-process-proxy.js +0 -1030
- package/src/tools/codex.js +0 -254
- package/src/tools/continue.js +0 -146
- package/src/tools/cursor.js +0 -71
- package/src/tools/droid.js +0 -281
- package/src/tools/env-config.js +0 -185
- package/src/tools/gemini-cli.js +0 -82
- package/src/tools/hermes.js +0 -354
- package/src/tools/index.js +0 -13
- package/src/tools/openclaw-bridge.js +0 -987
- package/src/tools/openclaw.js +0 -925
- package/src/tools/opencode.js +0 -227
- package/src/tools/process-proxy-inject.js +0 -142
- package/src/utils/config.js +0 -54
- package/src/utils/shell.js +0 -342
- package/src/utils/which.js +0 -176
- package/src/webui/aionui-runtime-fetcher.js +0 -429
- package/src/webui/aionui-runtime.js +0 -139
- package/src/webui/aionui-wrapper.js +0 -734
- package/src/webui/configure-worker.js +0 -67
- package/src/webui/server.js +0 -1572
- package/src/webui/workspace-runtime.js +0 -288
- package/src/webui/workspace-store.js +0 -325
- /package/{src/webui → dist}/index.html +0 -0
- /package/{src/tools → dist}/pty-hermes-wrapper.py +0 -0
package/src/tools/openclaw.js
DELETED
|
@@ -1,925 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw 适配器 (v2 — 基于实测的正确配置格式)
|
|
3
|
-
*
|
|
4
|
-
* 正确方案:写入 HolySheep 的 OpenAI + Anthropic + MiniMax provider,
|
|
5
|
-
* 默认模型固定为 GPT-5.4,同时保留 Claude / MiniMax 模型供 /model 切换。
|
|
6
|
-
*/
|
|
7
|
-
const fs = require('fs')
|
|
8
|
-
const path = require('path')
|
|
9
|
-
const os = require('os')
|
|
10
|
-
const { spawnSync, spawn, execSync } = require('child_process')
|
|
11
|
-
const { commandExists } = require('../utils/which')
|
|
12
|
-
const { BRIDGE_CONFIG_FILE } = require('./openclaw-bridge')
|
|
13
|
-
|
|
14
|
-
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw')
|
|
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')
|
|
18
|
-
const isWin = process.platform === 'win32'
|
|
19
|
-
const DEFAULT_BRIDGE_PORT = 18788
|
|
20
|
-
const DEFAULT_GATEWAY_PORT = 18789
|
|
21
|
-
const MAX_PORT_SCAN = 40
|
|
22
|
-
const OPENCLAW_DEFAULT_MODEL = 'gpt-5.4'
|
|
23
|
-
const OPENCLAW_DEFAULT_CODEX_SPARK_MODEL = 'gpt-5.3-codex-spark'
|
|
24
|
-
const OPENCLAW_DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6'
|
|
25
|
-
const OPENCLAW_DEFAULT_MINIMAX_MODEL = 'MiniMax-M2.7-highspeed'
|
|
26
|
-
const OPENCLAW_PROVIDER_NAME = 'holysheep'
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* [HolySheep fork v2.1.38 / hs26] Atomic JSON write.
|
|
30
|
-
*
|
|
31
|
-
* Background: `~/.openclaw/` accumulated 30+ `openclaw.json.clobbered.*`
|
|
32
|
-
* backup files (mtime span hours apart, content identical) — OpenClaw's
|
|
33
|
-
* own config layer detected that two processes raced `writeFileSync`
|
|
34
|
-
* against the same path and renamed the half-written file aside to avoid
|
|
35
|
-
* corruption. Each race produced a backup.
|
|
36
|
-
*
|
|
37
|
-
* Root cause: `fs.writeFileSync(path, data)` is NOT atomic — it opens +
|
|
38
|
-
* truncates + writes; a concurrent reader/writer can observe a partial
|
|
39
|
-
* file or overwrite between truncation and final bytes. POSIX
|
|
40
|
-
* `rename(tmp, final)` IS atomic on the same filesystem, so we:
|
|
41
|
-
* 1. Write to `${final}.tmp.${pid}.${rand}`
|
|
42
|
-
* 2. `fs.renameSync(tmp, final)`
|
|
43
|
-
* Windows: `rename` fails if target exists → retry via copyFile + unlink.
|
|
44
|
-
*
|
|
45
|
-
* Never leaves the final path in a half-written state. If two procs race,
|
|
46
|
-
* one's write wins atomically and the other's wins the next one; no
|
|
47
|
-
* .clobbered.* files get produced by OpenClaw.
|
|
48
|
-
*/
|
|
49
|
-
function atomicWriteJson(filePath, data) {
|
|
50
|
-
const dir = path.dirname(filePath)
|
|
51
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
52
|
-
const body = JSON.stringify(data, null, 2)
|
|
53
|
-
const tmp = `${filePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 10)}`
|
|
54
|
-
fs.writeFileSync(tmp, body, 'utf8')
|
|
55
|
-
try {
|
|
56
|
-
fs.renameSync(tmp, filePath)
|
|
57
|
-
return
|
|
58
|
-
} catch (renameErr) {
|
|
59
|
-
// Windows: rename fails if target exists. Fall back to copy+unlink.
|
|
60
|
-
// Also handles cross-device rename (EXDEV) in edge cases.
|
|
61
|
-
if (process.platform === 'win32' || renameErr.code === 'EXDEV' || renameErr.code === 'EEXIST') {
|
|
62
|
-
try {
|
|
63
|
-
fs.copyFileSync(tmp, filePath)
|
|
64
|
-
try { fs.unlinkSync(tmp) } catch {}
|
|
65
|
-
return
|
|
66
|
-
} catch (copyErr) {
|
|
67
|
-
try { fs.unlinkSync(tmp) } catch {}
|
|
68
|
-
throw copyErr
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
try { fs.unlinkSync(tmp) } catch {}
|
|
72
|
-
throw renameErr
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* [HolySheep fork v2.1.38 / hs26] Prune stale OpenClaw config backup files.
|
|
78
|
-
*
|
|
79
|
-
* Removes `~/.openclaw/openclaw.json.clobbered.*` older than 7 days. These
|
|
80
|
-
* are produced by OpenClaw itself when a racy `writeFileSync` is detected,
|
|
81
|
-
* but once our atomicWriteJson is in place no new ones should appear — this
|
|
82
|
-
* cleanup just garbage-collects the historical accumulation without touching
|
|
83
|
-
* `*.last-good` / `*.bak` / `*.pre-*` which users or other tooling may need.
|
|
84
|
-
*
|
|
85
|
-
* Safe: scoped to ONLY the exact glob `openclaw.json.clobbered.*` in the
|
|
86
|
-
* known OpenClaw dir. No-op if dir doesn't exist or user has no matching
|
|
87
|
-
* files. Best-effort (each unlink wrapped in try) so a single locked file
|
|
88
|
-
* won't block the rest of configure().
|
|
89
|
-
*/
|
|
90
|
-
function pruneClobberedBackups(maxAgeMs = 7 * 24 * 3600 * 1000) {
|
|
91
|
-
try {
|
|
92
|
-
if (!fs.existsSync(OPENCLAW_DIR)) return { scanned: 0, removed: 0 }
|
|
93
|
-
const entries = fs.readdirSync(OPENCLAW_DIR)
|
|
94
|
-
const cutoff = Date.now() - maxAgeMs
|
|
95
|
-
let scanned = 0
|
|
96
|
-
let removed = 0
|
|
97
|
-
for (const name of entries) {
|
|
98
|
-
if (!/^openclaw\.json\.clobbered\./.test(name)) continue
|
|
99
|
-
scanned++
|
|
100
|
-
const abs = path.join(OPENCLAW_DIR, name)
|
|
101
|
-
try {
|
|
102
|
-
const st = fs.statSync(abs)
|
|
103
|
-
if (st.mtimeMs < cutoff) {
|
|
104
|
-
fs.unlinkSync(abs)
|
|
105
|
-
removed++
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
// locked / race — skip
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return { scanned, removed }
|
|
112
|
-
} catch {
|
|
113
|
-
return { scanned: 0, removed: 0 }
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
function getOpenClawBinaryCandidates() {
|
|
119
|
-
return isWin ? ['openclaw.cmd', 'openclaw'] : ['openclaw']
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function getBinaryRunner() {
|
|
123
|
-
return isWin
|
|
124
|
-
? { cmd: 'openclaw.cmd', argsPrefix: [], shell: true, label: 'openclaw', via: 'binary' }
|
|
125
|
-
: { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function hasOpenClawBinary() {
|
|
129
|
-
return getOpenClawBinaryCandidates().some((cmd) => commandExists(cmd))
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function hasNpx() {
|
|
133
|
-
return commandExists('npx')
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getRunner(preferNpx = false) {
|
|
137
|
-
const binaryRunner = hasOpenClawBinary() ? getBinaryRunner() : null
|
|
138
|
-
|
|
139
|
-
if (!preferNpx && hasOpenClawBinary()) {
|
|
140
|
-
return binaryRunner
|
|
141
|
-
}
|
|
142
|
-
if (hasNpx()) {
|
|
143
|
-
return { cmd: 'npx', argsPrefix: ['openclaw'], shell: isWin, label: 'npx openclaw', via: 'npx' }
|
|
144
|
-
}
|
|
145
|
-
if (binaryRunner) {
|
|
146
|
-
return binaryRunner
|
|
147
|
-
}
|
|
148
|
-
return null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function runWithRunner(runner, args, opts = {}) {
|
|
152
|
-
return spawnSync(runner.cmd, [...runner.argsPrefix, ...args], {
|
|
153
|
-
shell: runner.shell,
|
|
154
|
-
timeout: opts.timeout || 30000,
|
|
155
|
-
stdio: opts.stdio || 'pipe',
|
|
156
|
-
encoding: 'utf8',
|
|
157
|
-
windowsHide: true,
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function normalizeVersionOutput(text) {
|
|
162
|
-
return firstLine(text).replace(/^openclaw\s+/i, '').trim()
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function probeRunner(runner, timeout) {
|
|
166
|
-
const result = runWithRunner(runner, ['--version'], { timeout })
|
|
167
|
-
if (result.error || result.status !== 0) return null
|
|
168
|
-
|
|
169
|
-
const version = normalizeVersionOutput(result.stdout || result.stderr || '')
|
|
170
|
-
return version || null
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** 运行 openclaw CLI(优先全局命令,可切换到 npx 回退) */
|
|
174
|
-
function runOpenClaw(args, opts = {}) {
|
|
175
|
-
const runner = getRunner(Boolean(opts.preferNpx))
|
|
176
|
-
if (!runner) {
|
|
177
|
-
return { status: 1, stdout: '', stderr: 'OpenClaw CLI not found' }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return runWithRunner(runner, args, opts)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function spawnOpenClaw(args, opts = {}) {
|
|
184
|
-
const runner = getRunner(Boolean(opts.preferNpx))
|
|
185
|
-
if (!runner) throw new Error('OpenClaw CLI not found')
|
|
186
|
-
|
|
187
|
-
const { preferNpx: _preferNpx, ...spawnOpts } = opts
|
|
188
|
-
return spawn(runner.cmd, [...runner.argsPrefix, ...args], {
|
|
189
|
-
shell: runner.shell,
|
|
190
|
-
windowsHide: true,
|
|
191
|
-
...spawnOpts,
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function getPreferredRuntime() {
|
|
196
|
-
return module.exports._useNpx || !hasOpenClawBinary()
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function firstLine(text) {
|
|
200
|
-
return String(text || '').trim().split('\n')[0] || ''
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function getOpenClawVersion(preferNpx = false) {
|
|
204
|
-
const runner = getRunner(preferNpx)
|
|
205
|
-
if (!runner) return null
|
|
206
|
-
return probeRunner(runner, preferNpx ? 60000 : 15000)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function detectRuntime() {
|
|
210
|
-
const preferNpx = getPreferredRuntime()
|
|
211
|
-
const runnerOrder = preferNpx ? [getRunner(true), getRunner(false)] : [getRunner(false), getRunner(true)]
|
|
212
|
-
const seen = new Set()
|
|
213
|
-
|
|
214
|
-
for (const runner of runnerOrder) {
|
|
215
|
-
if (!runner) continue
|
|
216
|
-
const key = `${runner.via}:${runner.cmd}:${runner.argsPrefix.join(' ')}`
|
|
217
|
-
if (seen.has(key)) continue
|
|
218
|
-
seen.add(key)
|
|
219
|
-
|
|
220
|
-
const version = probeRunner(runner, runner.via === 'npx' ? 60000 : 15000)
|
|
221
|
-
if (version) {
|
|
222
|
-
return {
|
|
223
|
-
available: true,
|
|
224
|
-
via: runner.via,
|
|
225
|
-
command: runner.label,
|
|
226
|
-
version,
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const fallbackRunner = getRunner(preferNpx)
|
|
232
|
-
if (fallbackRunner) {
|
|
233
|
-
return {
|
|
234
|
-
available: false,
|
|
235
|
-
via: fallbackRunner.via,
|
|
236
|
-
command: fallbackRunner.label,
|
|
237
|
-
version: null,
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return { available: false, via: null, command: null, version: null }
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function readBridgeConfig() {
|
|
245
|
-
try {
|
|
246
|
-
if (fs.existsSync(BRIDGE_CONFIG_FILE)) {
|
|
247
|
-
return JSON.parse(fs.readFileSync(BRIDGE_CONFIG_FILE, 'utf8'))
|
|
248
|
-
}
|
|
249
|
-
} catch {}
|
|
250
|
-
return {}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function writeBridgeConfig(data) {
|
|
254
|
-
// [HolySheep fork v2.1.38 / hs26] Atomic write — see atomicWriteJson.
|
|
255
|
-
atomicWriteJson(BRIDGE_CONFIG_FILE, data)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function updateBridgeConfig(patch) {
|
|
259
|
-
const current = readBridgeConfig()
|
|
260
|
-
writeBridgeConfig({
|
|
261
|
-
...current,
|
|
262
|
-
...patch,
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function getConfiguredBridgePort(config = readBridgeConfig()) {
|
|
267
|
-
const port = Number(config?.port)
|
|
268
|
-
return Number.isInteger(port) && port > 0 ? port : DEFAULT_BRIDGE_PORT
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function getBridgeBaseUrl(port = getConfiguredBridgePort()) {
|
|
272
|
-
return `http://127.0.0.1:${port}/v1`
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function waitForBridge(port) {
|
|
276
|
-
for (let i = 0; i < 10; i++) {
|
|
277
|
-
const t0 = Date.now()
|
|
278
|
-
while (Date.now() - t0 < 500) {}
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
execSync(
|
|
282
|
-
isWin
|
|
283
|
-
? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/health -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
|
|
284
|
-
: `curl -sf http://127.0.0.1:${port}/health -o /dev/null --max-time 1`,
|
|
285
|
-
{ stdio: 'ignore', timeout: 3000 }
|
|
286
|
-
)
|
|
287
|
-
return true
|
|
288
|
-
} catch {}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return false
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function stopBridge() {
|
|
295
|
-
// 杀掉所有已有的 bridge 进程,避免重新配置时端口冲突
|
|
296
|
-
try {
|
|
297
|
-
if (isWin) {
|
|
298
|
-
execSync('taskkill /F /FI "WINDOWTITLE eq openclaw-bridge*" 2>nul', { shell: true, stdio: 'ignore' })
|
|
299
|
-
// 按命令行匹配
|
|
300
|
-
const out = execSync('wmic process where "commandline like \'%openclaw-bridge%\'" get processid', { shell: true, stdio: 'pipe', encoding: 'utf8' })
|
|
301
|
-
const pids = out.match(/\d+/g)
|
|
302
|
-
if (pids) pids.forEach(pid => { try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' }) } catch {} })
|
|
303
|
-
} else {
|
|
304
|
-
execSync("pkill -f 'openclaw-bridge' 2>/dev/null || true", { shell: true, stdio: 'ignore' })
|
|
305
|
-
}
|
|
306
|
-
} catch {}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function startBridge(port) {
|
|
310
|
-
if (waitForBridge(port)) return true
|
|
311
|
-
|
|
312
|
-
const scriptPath = path.join(__dirname, '..', 'index.js')
|
|
313
|
-
// Windows: use shell+node command to avoid ERROR_FILE_NOT_FOUND with process.execPath
|
|
314
|
-
// (Windows Store / nvm paths can be unresolvable when spawning detached)
|
|
315
|
-
// Windows: use 'node' (resolved via PATH by CreateProcess) without shell:true.
|
|
316
|
-
// shell:true spawns cmd.exe /c which exits after the command, breaking detach.
|
|
317
|
-
const spawnCmd = isWin ? 'node' : process.execPath
|
|
318
|
-
const spawnOpts = { detached: true, stdio: 'ignore', windowsHide: true }
|
|
319
|
-
const child = spawn(spawnCmd, [scriptPath, 'openclaw-bridge', '--port', String(port)], spawnOpts)
|
|
320
|
-
child.unref()
|
|
321
|
-
return waitForBridge(port)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function getBridgeCommand(port = getConfiguredBridgePort()) {
|
|
325
|
-
return `hs openclaw-bridge --port ${port}`
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function pickPrimaryModel(primaryModel, selectedModels) {
|
|
329
|
-
const models = Array.isArray(selectedModels) ? selectedModels : []
|
|
330
|
-
return primaryModel || models[0] || OPENCLAW_DEFAULT_MODEL
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function readConfig() {
|
|
334
|
-
try {
|
|
335
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
336
|
-
const raw = fs.readFileSync(CONFIG_FILE, 'utf8')
|
|
337
|
-
try {
|
|
338
|
-
return JSON.parse(raw)
|
|
339
|
-
} catch {
|
|
340
|
-
// 兼容极少数带注释的配置,但不要误伤 https:// 之类的 URL
|
|
341
|
-
return JSON.parse(raw.replace(/^\s*\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, ''))
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} catch {}
|
|
345
|
-
return {}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function getConfiguredGatewayPort(config = readConfig()) {
|
|
349
|
-
const port = Number(config?.gateway?.port)
|
|
350
|
-
return Number.isInteger(port) && port > 0 ? port : DEFAULT_GATEWAY_PORT
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function getConfiguredPrimaryModel(config = readConfig()) {
|
|
354
|
-
return config?.agents?.defaults?.model?.primary || ''
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function normalizePrimaryModelRef(ref = getConfiguredPrimaryModel()) {
|
|
358
|
-
const value = String(ref || '')
|
|
359
|
-
const parts = value.split('/')
|
|
360
|
-
return parts[parts.length - 1] || ''
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function getPrimaryModelRoute(modelRef = getConfiguredPrimaryModel()) {
|
|
364
|
-
const model = normalizePrimaryModelRef(modelRef)
|
|
365
|
-
if (model.startsWith('gpt-')) {
|
|
366
|
-
return 'openai /chat/completions'
|
|
367
|
-
}
|
|
368
|
-
if (model.startsWith('claude-')) {
|
|
369
|
-
return 'anthropic /v1/messages'
|
|
370
|
-
}
|
|
371
|
-
if (model.startsWith('MiniMax-')) {
|
|
372
|
-
return 'minimax /minimax/v1/messages'
|
|
373
|
-
}
|
|
374
|
-
return 'unknown'
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function isPortInUse(port) {
|
|
378
|
-
try {
|
|
379
|
-
if (isWin) {
|
|
380
|
-
const out = execSync(`netstat -ano | findstr :${port}`, { shell: true, stdio: 'pipe', encoding: 'utf8' })
|
|
381
|
-
return out.trim().length > 0
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN`, { shell: true, stdio: 'ignore' })
|
|
385
|
-
return true
|
|
386
|
-
} catch {
|
|
387
|
-
return false
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function listPortListeners(port) {
|
|
392
|
-
try {
|
|
393
|
-
if (isWin) {
|
|
394
|
-
const out = execSync(`netstat -ano | findstr :${port}`, { shell: true, stdio: 'pipe', encoding: 'utf8' })
|
|
395
|
-
return out
|
|
396
|
-
.trim()
|
|
397
|
-
.split('\n')
|
|
398
|
-
.filter(Boolean)
|
|
399
|
-
.map((line) => {
|
|
400
|
-
const parts = line.trim().split(/\s+/)
|
|
401
|
-
return { pid: parts[parts.length - 1], command: 'pid', detail: parts[1] || '' }
|
|
402
|
-
})
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN`, { shell: true, stdio: 'pipe', encoding: 'utf8' })
|
|
406
|
-
return out
|
|
407
|
-
.trim()
|
|
408
|
-
.split('\n')
|
|
409
|
-
.slice(1)
|
|
410
|
-
.filter(Boolean)
|
|
411
|
-
.map((line) => {
|
|
412
|
-
const parts = line.trim().split(/\s+/)
|
|
413
|
-
return {
|
|
414
|
-
command: parts[0] || 'unknown',
|
|
415
|
-
pid: parts[1] || '?',
|
|
416
|
-
detail: parts[parts.length - 1] || '',
|
|
417
|
-
}
|
|
418
|
-
})
|
|
419
|
-
} catch {
|
|
420
|
-
return []
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function findAvailableGatewayPort(startPort = DEFAULT_GATEWAY_PORT, excludedPorts = []) {
|
|
425
|
-
const excluded = new Set((excludedPorts || []).map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0))
|
|
426
|
-
for (let offset = 0; offset < MAX_PORT_SCAN; offset++) {
|
|
427
|
-
const port = startPort + offset
|
|
428
|
-
if (excluded.has(port)) continue
|
|
429
|
-
if (!isPortInUse(port)) return port
|
|
430
|
-
}
|
|
431
|
-
return null
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function getLaunchCommand(port = getConfiguredGatewayPort()) {
|
|
435
|
-
const runtime = module.exports._lastRuntimeCommand || (hasOpenClawBinary() ? 'openclaw' : 'npx openclaw')
|
|
436
|
-
return `${runtime} gateway --port ${port}`
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function getDashboardCommand() {
|
|
440
|
-
const runtime = module.exports._lastRuntimeCommand || (hasOpenClawBinary() ? 'openclaw' : 'npx openclaw')
|
|
441
|
-
return `${runtime} dashboard --no-open`
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function getDashboardUrlForPort(port) {
|
|
445
|
-
return `http://127.0.0.1:${port}/`
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function isNpxCachePath(value) {
|
|
449
|
-
return /[\\/]_npx[\\/]/i.test(String(value || ''))
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function readOpenClawLaunchAgent() {
|
|
453
|
-
if (process.platform !== 'darwin') return null
|
|
454
|
-
try {
|
|
455
|
-
if (!fs.existsSync(OPENCLAW_GATEWAY_PLIST)) return null
|
|
456
|
-
return fs.readFileSync(OPENCLAW_GATEWAY_PLIST, 'utf8')
|
|
457
|
-
} catch {
|
|
458
|
-
return null
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function parseLaunchAgentProgramArguments(content) {
|
|
463
|
-
const match = String(content || '').match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/i)
|
|
464
|
-
if (!match) return []
|
|
465
|
-
|
|
466
|
-
return Array.from(match[1].matchAll(/<string>([\s\S]*?)<\/string>/g)).map((item) => item[1])
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function getLaunchAgentDiagnosis() {
|
|
470
|
-
const content = readOpenClawLaunchAgent()
|
|
471
|
-
if (!content) {
|
|
472
|
-
return { exists: false, path: OPENCLAW_GATEWAY_PLIST, unstable: false, programArguments: [] }
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const programArguments = parseLaunchAgentProgramArguments(content)
|
|
476
|
-
const unstableArg = programArguments.find(isNpxCachePath) || ''
|
|
477
|
-
|
|
478
|
-
return {
|
|
479
|
-
exists: true,
|
|
480
|
-
path: OPENCLAW_GATEWAY_PLIST,
|
|
481
|
-
unstable: Boolean(unstableArg),
|
|
482
|
-
unstableArg,
|
|
483
|
-
programArguments,
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function removeBrokenLaunchAgent() {
|
|
488
|
-
const diagnosis = getLaunchAgentDiagnosis()
|
|
489
|
-
if (!diagnosis.exists || !diagnosis.unstable) return false
|
|
490
|
-
|
|
491
|
-
try {
|
|
492
|
-
execSync(`launchctl bootout "gui/${process.getuid()}" "${diagnosis.path}"`, {
|
|
493
|
-
shell: true,
|
|
494
|
-
stdio: 'ignore',
|
|
495
|
-
})
|
|
496
|
-
} catch {}
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
fs.unlinkSync(diagnosis.path)
|
|
500
|
-
return true
|
|
501
|
-
} catch {
|
|
502
|
-
return false
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function buildModelEntry(id) {
|
|
507
|
-
return {
|
|
508
|
-
id,
|
|
509
|
-
name: `${id} (HolySheep)`,
|
|
510
|
-
reasoning: false,
|
|
511
|
-
input: ['text'],
|
|
512
|
-
contextWindow: 200000,
|
|
513
|
-
maxTokens: id.startsWith('gpt-') ? 8192 : 16000,
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function normalizeRequestedModels(selectedModels) {
|
|
518
|
-
const requestedModels = Array.isArray(selectedModels) && selectedModels.length > 0
|
|
519
|
-
? [...selectedModels]
|
|
520
|
-
: [OPENCLAW_DEFAULT_MODEL, OPENCLAW_DEFAULT_CODEX_SPARK_MODEL, OPENCLAW_DEFAULT_CLAUDE_MODEL, OPENCLAW_DEFAULT_MINIMAX_MODEL]
|
|
521
|
-
|
|
522
|
-
if (!requestedModels.includes(OPENCLAW_DEFAULT_MODEL)) requestedModels.unshift(OPENCLAW_DEFAULT_MODEL)
|
|
523
|
-
return Array.from(new Set(requestedModels))
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function buildManagedPlan(baseUrlBridge, apiKey, primaryModel, selectedModels) {
|
|
527
|
-
const requestedModels = normalizeRequestedModels(selectedModels)
|
|
528
|
-
const managedModelRefs = requestedModels.map((model) => `${OPENCLAW_PROVIDER_NAME}/${model}`)
|
|
529
|
-
const fallbackPrimaryModel = pickPrimaryModel(primaryModel, requestedModels)
|
|
530
|
-
const primaryRef = managedModelRefs.includes(`${OPENCLAW_PROVIDER_NAME}/${fallbackPrimaryModel}`)
|
|
531
|
-
? `${OPENCLAW_PROVIDER_NAME}/${fallbackPrimaryModel}`
|
|
532
|
-
: managedModelRefs[0] || `${OPENCLAW_PROVIDER_NAME}/${OPENCLAW_DEFAULT_MODEL}`
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
providers: {
|
|
536
|
-
[OPENCLAW_PROVIDER_NAME]: {
|
|
537
|
-
baseUrl: baseUrlBridge,
|
|
538
|
-
apiKey,
|
|
539
|
-
api: 'openai-completions',
|
|
540
|
-
models: requestedModels.map(buildModelEntry),
|
|
541
|
-
},
|
|
542
|
-
},
|
|
543
|
-
managedModelRefs,
|
|
544
|
-
models: requestedModels,
|
|
545
|
-
primaryRef,
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function isHolySheepProvider(provider) {
|
|
550
|
-
return typeof provider?.baseUrl === 'string' && (
|
|
551
|
-
provider.baseUrl.includes('api.holysheep.ai') ||
|
|
552
|
-
provider.baseUrl.includes('127.0.0.1')
|
|
553
|
-
)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function writeManagedConfig(baseConfig, bridgeBaseUrl, apiKey, primaryModel, selectedModels, gatewayPort) {
|
|
557
|
-
fs.mkdirSync(OPENCLAW_DIR, { recursive: true })
|
|
558
|
-
|
|
559
|
-
const plan = buildManagedPlan(bridgeBaseUrl, apiKey, primaryModel, selectedModels)
|
|
560
|
-
const existingProviders = baseConfig?.models?.providers || {}
|
|
561
|
-
const managedProviderIds = Object.entries(existingProviders)
|
|
562
|
-
.filter(([providerId, provider]) => providerId === OPENCLAW_PROVIDER_NAME || isHolySheepProvider(provider))
|
|
563
|
-
.map(([providerId]) => providerId)
|
|
564
|
-
|
|
565
|
-
const preservedProviders = Object.fromEntries(
|
|
566
|
-
Object.entries(existingProviders).filter(([, provider]) => !isHolySheepProvider(provider))
|
|
567
|
-
)
|
|
568
|
-
|
|
569
|
-
const existingModelMap = baseConfig?.agents?.defaults?.models || {}
|
|
570
|
-
const preservedModelMap = Object.fromEntries(
|
|
571
|
-
Object.entries(existingModelMap).filter(([ref]) => {
|
|
572
|
-
return !managedProviderIds.some((providerId) => ref.startsWith(`${providerId}/`))
|
|
573
|
-
})
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
const managedModelMap = Object.fromEntries(plan.managedModelRefs.map((ref) => [ref, {}]))
|
|
577
|
-
|
|
578
|
-
const nextConfig = {
|
|
579
|
-
...baseConfig,
|
|
580
|
-
models: {
|
|
581
|
-
...(baseConfig.models || {}),
|
|
582
|
-
mode: 'merge',
|
|
583
|
-
providers: {
|
|
584
|
-
...preservedProviders,
|
|
585
|
-
...plan.providers,
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
agents: {
|
|
589
|
-
...(baseConfig.agents || {}),
|
|
590
|
-
defaults: {
|
|
591
|
-
...(baseConfig.agents?.defaults || {}),
|
|
592
|
-
model: {
|
|
593
|
-
...(baseConfig.agents?.defaults?.model || {}),
|
|
594
|
-
primary: plan.primaryRef,
|
|
595
|
-
},
|
|
596
|
-
models: {
|
|
597
|
-
...preservedModelMap,
|
|
598
|
-
...managedModelMap,
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
},
|
|
602
|
-
gateway: {
|
|
603
|
-
...(baseConfig.gateway || {}),
|
|
604
|
-
mode: 'local',
|
|
605
|
-
port: gatewayPort,
|
|
606
|
-
bind: 'loopback',
|
|
607
|
-
auth: {
|
|
608
|
-
...(baseConfig.gateway?.auth || {}),
|
|
609
|
-
mode: 'none',
|
|
610
|
-
},
|
|
611
|
-
},
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// [HolySheep fork v2.1.38 / hs26] Atomic write — prevents the
|
|
615
|
-
// openclaw.json.clobbered.* pile-up from racing writeFileSync.
|
|
616
|
-
atomicWriteJson(CONFIG_FILE, nextConfig)
|
|
617
|
-
return plan
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function _disableGatewayAuth(preferNpx = false) {
|
|
621
|
-
try {
|
|
622
|
-
runOpenClaw(['config', 'set', 'gateway.auth.mode', 'none'], { preferNpx })
|
|
623
|
-
} catch {}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function _installGatewayService(port, preferNpx = false) {
|
|
627
|
-
if (preferNpx) return false
|
|
628
|
-
|
|
629
|
-
const result = runOpenClaw(['gateway', 'install', '--force', '--port', String(port)], {
|
|
630
|
-
preferNpx,
|
|
631
|
-
timeout: 60000,
|
|
632
|
-
})
|
|
633
|
-
return result.status === 0
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function _startGateway(port, preferNpx = false, preferService = true) {
|
|
637
|
-
const serviceResult = preferService
|
|
638
|
-
? runOpenClaw(['gateway', 'start'], { preferNpx, timeout: 20000 })
|
|
639
|
-
: { status: 1 }
|
|
640
|
-
|
|
641
|
-
let directChild = null
|
|
642
|
-
|
|
643
|
-
if (serviceResult.status !== 0) {
|
|
644
|
-
directChild = spawnOpenClaw(['gateway', '--port', String(port)], {
|
|
645
|
-
preferNpx,
|
|
646
|
-
detached: true,
|
|
647
|
-
stdio: 'ignore',
|
|
648
|
-
})
|
|
649
|
-
directChild.unref()
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
for (let i = 0; i < 8; i++) {
|
|
653
|
-
const t0 = Date.now()
|
|
654
|
-
while (Date.now() - t0 < 1000) {}
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
execSync(
|
|
658
|
-
isWin
|
|
659
|
-
? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/ -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
|
|
660
|
-
: `curl -sf http://127.0.0.1:${port}/ -o /dev/null --max-time 1`,
|
|
661
|
-
{ stdio: 'ignore', timeout: 3000 }
|
|
662
|
-
)
|
|
663
|
-
return {
|
|
664
|
-
ok: true,
|
|
665
|
-
pid: directChild?.pid || null,
|
|
666
|
-
mode: directChild ? 'direct-process' : 'daemon',
|
|
667
|
-
}
|
|
668
|
-
} catch {}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return {
|
|
672
|
-
ok: false,
|
|
673
|
-
pid: directChild?.pid || null,
|
|
674
|
-
mode: directChild ? 'direct-process' : 'daemon',
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function getDashboardUrl(port, preferNpx = false) {
|
|
679
|
-
const result = runOpenClaw(['dashboard', '--no-open'], {
|
|
680
|
-
preferNpx,
|
|
681
|
-
timeout: preferNpx ? 60000 : 20000,
|
|
682
|
-
})
|
|
683
|
-
if (result.status === 0) {
|
|
684
|
-
const output = String(result.stdout || '')
|
|
685
|
-
const match = output.match(/Dashboard URL:\s*(\S+)/) || output.match(/(https?:\/\/\S+)/)
|
|
686
|
-
if (match) return match[1]
|
|
687
|
-
}
|
|
688
|
-
return getDashboardUrlForPort(port)
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
module.exports = {
|
|
692
|
-
name: 'OpenClaw',
|
|
693
|
-
id: 'openclaw',
|
|
694
|
-
|
|
695
|
-
checkInstalled() {
|
|
696
|
-
return detectRuntime().available
|
|
697
|
-
},
|
|
698
|
-
|
|
699
|
-
detectRuntime,
|
|
700
|
-
|
|
701
|
-
getVersion() {
|
|
702
|
-
return detectRuntime().version
|
|
703
|
-
},
|
|
704
|
-
|
|
705
|
-
isConfigured() {
|
|
706
|
-
const cfg = readConfig()
|
|
707
|
-
const hasProvider = cfg?.models?.providers?.[OPENCLAW_PROVIDER_NAME]?.baseUrl?.includes('127.0.0.1')
|
|
708
|
-
const bridge = readBridgeConfig()
|
|
709
|
-
return Boolean(hasProvider && bridge?.apiKey)
|
|
710
|
-
},
|
|
711
|
-
|
|
712
|
-
configure(apiKey, baseUrlAnthropic, baseUrlOpenAI, primaryModel, selectedModels) {
|
|
713
|
-
const chalk = require('chalk')
|
|
714
|
-
console.log(chalk.gray('\n ⚙️ 正在配置 OpenClaw...'))
|
|
715
|
-
|
|
716
|
-
// [HolySheep fork v2.1.38 / hs26] Garbage-collect stale OpenClaw
|
|
717
|
-
// backup files from pre-atomic-write builds. Scoped to exact glob
|
|
718
|
-
// `openclaw.json.clobbered.*` older than 7 days — leaves .last-good
|
|
719
|
-
// and .bak files alone.
|
|
720
|
-
try {
|
|
721
|
-
const pruned = pruneClobberedBackups()
|
|
722
|
-
if (pruned.removed > 0) {
|
|
723
|
-
console.log(chalk.gray(` → 已清理 ${pruned.removed} 个过期的 OpenClaw 配置备份(>7 天的 .clobbered.* 文件)`))
|
|
724
|
-
}
|
|
725
|
-
} catch {}
|
|
726
|
-
|
|
727
|
-
const runtime = detectRuntime()
|
|
728
|
-
if (!runtime.available) {
|
|
729
|
-
throw new Error('未检测到 OpenClaw;请先全局安装,或确保 npx 可用')
|
|
730
|
-
}
|
|
731
|
-
this._lastRuntimeCommand = runtime.command
|
|
732
|
-
this._lastRuntimeVia = runtime.via
|
|
733
|
-
|
|
734
|
-
// 重新配置前先停掉旧的 Bridge 和 Gateway,释放端口
|
|
735
|
-
console.log(chalk.gray(' → 停止旧的 Bridge 和 Gateway...'))
|
|
736
|
-
stopBridge()
|
|
737
|
-
runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
|
|
738
|
-
// 等端口释放
|
|
739
|
-
const t0 = Date.now()
|
|
740
|
-
while (Date.now() - t0 < 1000) {}
|
|
741
|
-
|
|
742
|
-
if (runtime.via === 'npx' && removeBrokenLaunchAgent()) {
|
|
743
|
-
console.log(chalk.gray(' → 已清理旧的 OpenClaw 守护进程配置(失效的 npx 缓存路径)'))
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const resolvedPrimaryModel = pickPrimaryModel(primaryModel, selectedModels)
|
|
747
|
-
const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
|
|
748
|
-
if (!gatewayPort) {
|
|
749
|
-
throw new Error(`找不到可用端口(已检查 ${DEFAULT_GATEWAY_PORT}-${DEFAULT_GATEWAY_PORT + MAX_PORT_SCAN - 1})`)
|
|
750
|
-
}
|
|
751
|
-
this._lastGatewayPort = gatewayPort
|
|
752
|
-
|
|
753
|
-
const bridgePort = findAvailableGatewayPort(DEFAULT_BRIDGE_PORT, [gatewayPort])
|
|
754
|
-
if (!bridgePort) {
|
|
755
|
-
throw new Error(`找不到可用桥接端口(已检查 ${DEFAULT_BRIDGE_PORT}-${DEFAULT_BRIDGE_PORT + MAX_PORT_SCAN - 1})`)
|
|
756
|
-
}
|
|
757
|
-
this._lastBridgePort = bridgePort
|
|
758
|
-
|
|
759
|
-
writeBridgeConfig({
|
|
760
|
-
port: bridgePort,
|
|
761
|
-
host: '127.0.0.1',
|
|
762
|
-
apiKey,
|
|
763
|
-
baseUrlAnthropic,
|
|
764
|
-
baseUrlOpenAI,
|
|
765
|
-
models: normalizeRequestedModels(selectedModels),
|
|
766
|
-
gatewayPort,
|
|
767
|
-
gatewayHost: '127.0.0.1',
|
|
768
|
-
gatewayPid: null,
|
|
769
|
-
gatewayLaunchMode: null,
|
|
770
|
-
watchdog: {
|
|
771
|
-
enabled: true,
|
|
772
|
-
intervalMs: 3000,
|
|
773
|
-
failureThreshold: 3,
|
|
774
|
-
startupGraceMs: 30000,
|
|
775
|
-
requestTimeoutMs: 1500,
|
|
776
|
-
},
|
|
777
|
-
})
|
|
778
|
-
|
|
779
|
-
console.log(chalk.gray(' → 正在启动 HolySheep Bridge...'))
|
|
780
|
-
if (!startBridge(bridgePort)) {
|
|
781
|
-
throw new Error('HolySheep OpenClaw Bridge 启动失败')
|
|
782
|
-
}
|
|
783
|
-
const bridgeBaseUrl = getBridgeBaseUrl(bridgePort)
|
|
784
|
-
|
|
785
|
-
if (gatewayPort !== DEFAULT_GATEWAY_PORT) {
|
|
786
|
-
console.log(chalk.yellow(` ⚠️ 端口 ${DEFAULT_GATEWAY_PORT} 已占用,自动切换到 ${gatewayPort}`))
|
|
787
|
-
const listeners = listPortListeners(DEFAULT_GATEWAY_PORT)
|
|
788
|
-
if (listeners.length) {
|
|
789
|
-
const summary = listeners
|
|
790
|
-
.slice(0, 2)
|
|
791
|
-
.map((item) => `${item.command}(${item.pid})`)
|
|
792
|
-
.join(', ')
|
|
793
|
-
console.log(chalk.gray(` 占用进程: ${summary}`))
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
try { fs.unlinkSync(CONFIG_FILE) } catch {}
|
|
798
|
-
|
|
799
|
-
console.log(chalk.gray(' → 写入配置...'))
|
|
800
|
-
const onboardArgs = [
|
|
801
|
-
'onboard',
|
|
802
|
-
'--non-interactive',
|
|
803
|
-
'--accept-risk',
|
|
804
|
-
'--auth-choice', 'custom-api-key',
|
|
805
|
-
'--custom-base-url', bridgeBaseUrl,
|
|
806
|
-
'--custom-api-key', apiKey,
|
|
807
|
-
'--custom-model-id', resolvedPrimaryModel.replace(/\[.*\]/, ''),
|
|
808
|
-
'--custom-compatibility', 'openai',
|
|
809
|
-
'--gateway-port', String(gatewayPort),
|
|
810
|
-
]
|
|
811
|
-
if (runtime.via !== 'npx') onboardArgs.push('--install-daemon')
|
|
812
|
-
|
|
813
|
-
const result = runOpenClaw(onboardArgs, { preferNpx: runtime.via === 'npx' })
|
|
814
|
-
|
|
815
|
-
if (result.status !== 0) {
|
|
816
|
-
console.log(chalk.yellow(' ⚠️ onboard 失败,使用备用配置...'))
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const plan = writeManagedConfig(
|
|
820
|
-
result.status === 0 ? readConfig() : {},
|
|
821
|
-
bridgeBaseUrl,
|
|
822
|
-
apiKey,
|
|
823
|
-
resolvedPrimaryModel,
|
|
824
|
-
selectedModels,
|
|
825
|
-
gatewayPort,
|
|
826
|
-
)
|
|
827
|
-
|
|
828
|
-
_disableGatewayAuth(runtime.via === 'npx')
|
|
829
|
-
const serviceReady = _installGatewayService(gatewayPort, runtime.via === 'npx')
|
|
830
|
-
if (runtime.via === 'npx') {
|
|
831
|
-
console.log(chalk.gray(' → 当前仅检测到 npx openclaw,跳过 daemon 安装,改为直接启动 Gateway 进程'))
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
console.log(chalk.gray(' → 正在启动 Gateway...'))
|
|
835
|
-
const gatewayState = _startGateway(gatewayPort, runtime.via === 'npx', serviceReady)
|
|
836
|
-
updateBridgeConfig({
|
|
837
|
-
gatewayPort,
|
|
838
|
-
gatewayPid: gatewayState.pid,
|
|
839
|
-
gatewayLaunchMode: gatewayState.mode,
|
|
840
|
-
gatewayStartedAt: new Date().toISOString(),
|
|
841
|
-
})
|
|
842
|
-
|
|
843
|
-
if (gatewayState.ok) {
|
|
844
|
-
console.log(chalk.green(' ✓ OpenClaw Gateway 已启动'))
|
|
845
|
-
} else {
|
|
846
|
-
console.log(chalk.yellow(' ⚠️ Gateway 未就绪;当前不要打开 about:blank 或裸浏览器壳窗口'))
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const dashUrl = gatewayState.ok ? getDashboardUrl(gatewayPort, runtime.via === 'npx') : getDashboardUrlForPort(gatewayPort)
|
|
850
|
-
console.log(chalk.cyan('\n → 浏览器打开(推荐使用此地址):'))
|
|
851
|
-
console.log(chalk.bold.cyan(` ${dashUrl}`))
|
|
852
|
-
console.log(chalk.gray(` Bridge 地址: ${bridgeBaseUrl}`))
|
|
853
|
-
console.log(chalk.gray(` 默认模型: ${plan.primaryRef || OPENCLAW_DEFAULT_MODEL}`))
|
|
854
|
-
console.log(chalk.gray(` Gateway 启动方式: ${gatewayState.mode}`))
|
|
855
|
-
console.log(chalk.gray(' 浏览器应直接打开 dashboard URL,不应停在 about:blank'))
|
|
856
|
-
console.log(chalk.gray(' Bridge 会在检测到 OpenClaw Gateway 持续不可用后自动退出'))
|
|
857
|
-
console.log(chalk.gray(' 如在 Windows 上打开裸 http://127.0.0.1:PORT/ 仍报 Unauthorized,请使用上面的 dashboard 地址'))
|
|
858
|
-
|
|
859
|
-
return {
|
|
860
|
-
file: CONFIG_FILE,
|
|
861
|
-
hot: false,
|
|
862
|
-
dashboardUrl: dashUrl,
|
|
863
|
-
gatewayPort,
|
|
864
|
-
gatewayReady: gatewayState.ok,
|
|
865
|
-
gatewayLaunchMode: gatewayState.mode,
|
|
866
|
-
launchCmd: getLaunchCommand(gatewayPort),
|
|
867
|
-
}
|
|
868
|
-
},
|
|
869
|
-
|
|
870
|
-
reset() {
|
|
871
|
-
// 先停进程再删配置,避免残留进程指向已删除的配置导致 timeout
|
|
872
|
-
stopBridge()
|
|
873
|
-
try { runOpenClaw(['gateway', 'stop'], { preferNpx: detectRuntime().via === 'npx' }) } catch {}
|
|
874
|
-
try { fs.unlinkSync(CONFIG_FILE) } catch {}
|
|
875
|
-
try { fs.unlinkSync(BRIDGE_CONFIG_FILE) } catch {}
|
|
876
|
-
},
|
|
877
|
-
|
|
878
|
-
getConfigPath() { return CONFIG_FILE },
|
|
879
|
-
getBridgePort() { return getConfiguredBridgePort() },
|
|
880
|
-
getGatewayPort() { return getConfiguredGatewayPort() },
|
|
881
|
-
ensureBridgeRunning(port) {
|
|
882
|
-
port = port || getConfiguredBridgePort()
|
|
883
|
-
return startBridge(port)
|
|
884
|
-
},
|
|
885
|
-
ensureGatewayRunning(port) {
|
|
886
|
-
port = port || getConfiguredGatewayPort()
|
|
887
|
-
// 检查端口是否有进程在监听(不依赖 HTTP 状态码,500 也算运行中)
|
|
888
|
-
try {
|
|
889
|
-
const checkCmd = isWin
|
|
890
|
-
? `powershell -NonInteractive -Command "if(Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue){exit 0}else{exit 1}"`
|
|
891
|
-
: `curl -so /dev/null --max-time 1 http://127.0.0.1:${port}/ || lsof -iTCP:${port} -sTCP:LISTEN -t >/dev/null 2>&1`
|
|
892
|
-
execSync(checkCmd, { stdio: 'ignore', timeout: 5000 })
|
|
893
|
-
return true
|
|
894
|
-
} catch {}
|
|
895
|
-
// 未运行,启动它
|
|
896
|
-
const preferNpx = !hasOpenClawBinary()
|
|
897
|
-
const result = _startGateway(port, preferNpx, !preferNpx)
|
|
898
|
-
return result.ok
|
|
899
|
-
},
|
|
900
|
-
getPrimaryModel() { return getConfiguredPrimaryModel() },
|
|
901
|
-
getPrimaryModelRoute() { return getPrimaryModelRoute() },
|
|
902
|
-
getPortListeners(port = getConfiguredGatewayPort()) { return listPortListeners(port) },
|
|
903
|
-
getLaunchAgentDiagnosis,
|
|
904
|
-
get hint() {
|
|
905
|
-
return `Bridge + Gateway 已配置,默认模型为 ${getConfiguredPrimaryModel() || OPENCLAW_DEFAULT_MODEL}`
|
|
906
|
-
},
|
|
907
|
-
get launchSteps() {
|
|
908
|
-
const bridgePort = getConfiguredBridgePort()
|
|
909
|
-
const port = getConfiguredGatewayPort()
|
|
910
|
-
return [
|
|
911
|
-
{ cmd: getBridgeCommand(bridgePort), note: '先启动 HolySheep OpenClaw Bridge' },
|
|
912
|
-
{ cmd: getLaunchCommand(port), note: '再启动 OpenClaw Gateway' },
|
|
913
|
-
{ cmd: getDashboardCommand(), note: '再生成/打开可直接连接的 Dashboard 地址(推荐)' },
|
|
914
|
-
]
|
|
915
|
-
},
|
|
916
|
-
get launchNote() {
|
|
917
|
-
const runtime = module.exports._lastRuntimeVia === 'npx' ? '当前为 npx 模式,不安装常驻 daemon。' : ''
|
|
918
|
-
return `🌐 请先启动 Bridge,再启动 Gateway;最后运行 ${getDashboardCommand()} ${runtime}`.trim()
|
|
919
|
-
},
|
|
920
|
-
installCmd: 'npm install -g openclaw@latest',
|
|
921
|
-
docsUrl: 'https://docs.openclaw.ai',
|
|
922
|
-
// [HolySheep fork v2.1.38 / hs26] Test-only exports.
|
|
923
|
-
_atomicWriteJson: atomicWriteJson,
|
|
924
|
-
_pruneClobberedBackups: pruneClobberedBackups,
|
|
925
|
-
}
|