@simonyea/holysheep-cli 1.6.1 → 1.6.3
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 +34 -11
- package/package.json +1 -1
- package/src/commands/doctor.js +116 -21
- package/src/commands/setup.js +1 -1
- package/src/tools/openclaw.js +414 -144
package/README.md
CHANGED
|
@@ -67,17 +67,26 @@ You'll be prompted for your API Key (`cr_xxx`), then select the tools to configu
|
|
|
67
67
|
|
|
68
68
|
[OpenClaw](https://openclaw.ai) is a powerful AI agent gateway with a web dashboard. After running `hs setup`:
|
|
69
69
|
|
|
70
|
-
1.
|
|
71
|
-
2.
|
|
72
|
-
3.
|
|
70
|
+
1. HolySheep configures OpenClaw to use HolySheep API
|
|
71
|
+
2. The OpenClaw Gateway starts on **`http://127.0.0.1:18789/` by default**
|
|
72
|
+
3. If `18789` is occupied, `hs setup` automatically picks the next available local port
|
|
73
|
+
4. Open the exact browser URL shown in the terminal and start chatting — no token required
|
|
74
|
+
|
|
75
|
+
**Default OpenClaw model:** `gpt-5.4`
|
|
73
76
|
|
|
74
77
|
> **Keep the gateway window open** while using OpenClaw. The gateway must be running for the browser UI to work.
|
|
75
78
|
|
|
79
|
+
> **OpenClaw itself requires Node.js 20+**. If setup fails, first check `node --version`.
|
|
80
|
+
|
|
76
81
|
To restart the gateway later:
|
|
77
82
|
```bash
|
|
78
|
-
|
|
83
|
+
openclaw gateway --port <shown-port>
|
|
84
|
+
# or
|
|
85
|
+
npx openclaw gateway --port <shown-port>
|
|
79
86
|
```
|
|
80
87
|
|
|
88
|
+
If you forget the port, check `~/.openclaw/openclaw.json` (`gateway.port`) or run `hs doctor`.
|
|
89
|
+
|
|
81
90
|
### Commands
|
|
82
91
|
|
|
83
92
|
| Command | Description |
|
|
@@ -145,18 +154,27 @@ hs setup
|
|
|
145
154
|
|
|
146
155
|
**`hs setup` 配置完成后:**
|
|
147
156
|
|
|
148
|
-
1.
|
|
149
|
-
2.
|
|
150
|
-
3.
|
|
157
|
+
1. HolySheep 会自动把 OpenClaw 接到 HolySheep API
|
|
158
|
+
2. 默认启动在 **`http://127.0.0.1:18789/`**
|
|
159
|
+
3. 如果 `18789` 被占用,`hs setup` 会自动切换到下一个可用本地端口
|
|
160
|
+
4. 按终端里显示的准确地址打开浏览器,直接开始聊天,无需填写 token
|
|
161
|
+
|
|
162
|
+
**OpenClaw 默认模型:** `gpt-5.4`
|
|
151
163
|
|
|
152
164
|
> ⚠️ **保持 Gateway 窗口开启**,关闭后 Gateway 停止,浏览器界面无法使用。
|
|
153
165
|
|
|
166
|
+
> ⚠️ **OpenClaw 自身要求 Node.js 20+**。如果配置失败,请先运行 `node --version` 检查版本。
|
|
167
|
+
|
|
154
168
|
**下次启动 Gateway:**
|
|
155
169
|
```bash
|
|
156
|
-
|
|
170
|
+
openclaw gateway --port <显示的端口>
|
|
171
|
+
# 或
|
|
172
|
+
npx openclaw gateway --port <显示的端口>
|
|
157
173
|
```
|
|
158
174
|
|
|
159
|
-
|
|
175
|
+
如果忘了端口,可以查看 `~/.openclaw/openclaw.json` 里的 `gateway.port`,或直接运行 `hs doctor`。
|
|
176
|
+
|
|
177
|
+
**默认模型:** `gpt-5.4`(可在 OpenClaw 内切换到 Claude 模型)
|
|
160
178
|
|
|
161
179
|
### 命令说明
|
|
162
180
|
|
|
@@ -185,18 +203,23 @@ A: 在 [holysheep.ai](https://holysheep.ai) 注册后,在「API 密钥」页
|
|
|
185
203
|
A: 支持,需要 Node.js 16+。如果 `hs` 命令找不到,请重启终端,或直接用 `npx @simonyea/holysheep-cli@latest setup`。
|
|
186
204
|
|
|
187
205
|
**Q: OpenClaw Gateway 窗口可以最小化吗?**
|
|
188
|
-
A: 可以最小化,但不能关闭。关闭后 Gateway
|
|
206
|
+
A: 可以最小化,但不能关闭。关闭后 Gateway 停止,需要按 `hs setup` / `hs doctor` 显示的端口重新运行 `openclaw gateway --port <端口>` 或 `npx openclaw gateway --port <端口>`。
|
|
207
|
+
|
|
208
|
+
**Q: 18789 端口被占用怎么办?**
|
|
209
|
+
A: `hs setup` 会自动切换到下一个可用本地端口,并把准确访问地址打印出来;也可以运行 `hs doctor` 查看当前 `gateway.port` 和端口占用情况。
|
|
189
210
|
|
|
190
211
|
**Q: 如何恢复原来的配置?**
|
|
191
212
|
A: 运行 `hs reset` 清除所有 HolySheep 相关配置。
|
|
192
213
|
|
|
193
214
|
**Q: OpenClaw 安装失败?**
|
|
194
|
-
A: OpenClaw 需要 Node.js 20+,运行 `node --version`
|
|
215
|
+
A: OpenClaw 需要 Node.js 20+,运行 `node --version` 确认版本后重试;如果全局安装失败,`hs setup` 也会尽量回退到 `npx openclaw` 继续配置。
|
|
195
216
|
|
|
196
217
|
---
|
|
197
218
|
|
|
198
219
|
## Changelog
|
|
199
220
|
|
|
221
|
+
- **v1.6.3** — OpenClaw 默认模型改为 GPT-5.4,并继续保留 Claude 模型切换能力
|
|
222
|
+
- **v1.6.2** — 修复 OpenClaw 配置误判与 npx 回退,端口冲突时自动切换空闲端口,并补充 Doctor 诊断
|
|
200
223
|
- **v1.6.0** — 新增 Droid CLI 一键配置,默认写入 GPT-5.4 / Sonnet 4.6 / Opus 4.6 / MiniMax 2.7 Highspeed / Haiku 4.5
|
|
201
224
|
- **v1.5.2** — OpenClaw 安装失败(无 git 环境)时自动降级为 npx 模式继续配置
|
|
202
225
|
- **v1.5.0** — OpenClaw gateway 无需 token,直接浏览器打开 http://127.0.0.1:18789/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
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
|
@@ -12,9 +12,11 @@ async function doctor() {
|
|
|
12
12
|
console.log(chalk.gray('━'.repeat(50)))
|
|
13
13
|
console.log()
|
|
14
14
|
|
|
15
|
+
const nodeMajor = parseInt(process.version.slice(1), 10)
|
|
16
|
+
|
|
15
17
|
// Node.js 版本
|
|
16
18
|
const nodeVer = process.version
|
|
17
|
-
const nodeOk =
|
|
19
|
+
const nodeOk = nodeMajor >= 16
|
|
18
20
|
printCheck(nodeOk, `Node.js ${nodeVer}`, nodeOk ? '' : '需要 >= 16')
|
|
19
21
|
|
|
20
22
|
// API Key
|
|
@@ -23,32 +25,38 @@ async function doctor() {
|
|
|
23
25
|
|
|
24
26
|
// 环境变量
|
|
25
27
|
const envAnthropicKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
|
|
26
|
-
const envOpenAIKey
|
|
28
|
+
const envOpenAIKey = process.env.OPENAI_API_KEY
|
|
27
29
|
const envAnthropicUrl = process.env.ANTHROPIC_BASE_URL
|
|
28
|
-
const envOpenAIUrl
|
|
30
|
+
const envOpenAIUrl = process.env.OPENAI_BASE_URL
|
|
29
31
|
|
|
30
32
|
console.log()
|
|
31
33
|
console.log(chalk.bold('环境变量:'))
|
|
32
34
|
printCheck(!!envAnthropicKey, 'ANTHROPIC_API_KEY / AUTH_TOKEN', envAnthropicKey ? maskKey(envAnthropicKey) : '未设置')
|
|
33
35
|
printCheck(!!envAnthropicUrl, 'ANTHROPIC_BASE_URL', envAnthropicUrl || '未设置')
|
|
34
|
-
printCheck(!!envOpenAIKey,
|
|
35
|
-
printCheck(!!envOpenAIUrl,
|
|
36
|
+
printCheck(!!envOpenAIKey, 'OPENAI_API_KEY', envOpenAIKey ? maskKey(envOpenAIKey) : '未设置')
|
|
37
|
+
printCheck(!!envOpenAIUrl, 'OPENAI_BASE_URL', envOpenAIUrl || '未设置')
|
|
36
38
|
|
|
37
39
|
// 各工具检查
|
|
38
40
|
console.log()
|
|
39
41
|
console.log(chalk.bold('工具状态:'))
|
|
40
42
|
|
|
41
43
|
for (const tool of TOOLS) {
|
|
42
|
-
const
|
|
44
|
+
const installState = getInstallState(tool)
|
|
45
|
+
const installed = installState.installed
|
|
43
46
|
const configured = installed ? tool.isConfigured() : null
|
|
44
|
-
const version
|
|
47
|
+
const version = installState.version
|
|
48
|
+
const suffix = installState.detail ? chalk.gray(` (${installState.detail})`) : ''
|
|
45
49
|
|
|
46
50
|
if (!installed) {
|
|
47
51
|
console.log(` ${chalk.gray('○')} ${chalk.gray(tool.name.padEnd(20))} ${chalk.gray('未安装')} ${chalk.gray(`— ${tool.installCmd}`)}`)
|
|
48
52
|
} else if (configured) {
|
|
49
|
-
console.log(` ${chalk.green('✓')} ${chalk.green(tool.name.padEnd(20))} ${chalk.gray(version || '已安装')} ${chalk.green('已配置 HolySheep')}`)
|
|
53
|
+
console.log(` ${chalk.green('✓')} ${chalk.green(tool.name.padEnd(20))} ${chalk.gray(version || '已安装')}${suffix} ${chalk.green('已配置 HolySheep')}`)
|
|
50
54
|
} else {
|
|
51
|
-
console.log(` ${chalk.yellow('!')} ${chalk.yellow(tool.name.padEnd(20))} ${chalk.gray(version || '已安装')} ${chalk.yellow('未配置')} ${chalk.gray(
|
|
55
|
+
console.log(` ${chalk.yellow('!')} ${chalk.yellow(tool.name.padEnd(20))} ${chalk.gray(version || '已安装')}${suffix} ${chalk.yellow('未配置')} ${chalk.gray('— 运行 hs setup')}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (tool.id === 'openclaw' && installed) {
|
|
59
|
+
printOpenClawDetails(tool, installState, nodeMajor)
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -79,32 +87,119 @@ async function doctor() {
|
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
function printCheck(ok, label, detail = '') {
|
|
82
|
-
const icon
|
|
83
|
-
const lbl
|
|
84
|
-
const det
|
|
90
|
+
const icon = ok ? chalk.green('✓') : chalk.red('✗')
|
|
91
|
+
const lbl = ok ? chalk.green(label.padEnd(35)) : chalk.red(label.padEnd(35))
|
|
92
|
+
const det = detail ? chalk.gray(detail) : ''
|
|
85
93
|
console.log(` ${icon} ${lbl} ${det}`)
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
function printOpenClawDetails(tool, installState, nodeMajor) {
|
|
97
|
+
const details = []
|
|
98
|
+
const gatewayPort = typeof tool.getGatewayPort === 'function' ? tool.getGatewayPort() : 18789
|
|
99
|
+
const primaryModel = typeof tool.getPrimaryModel === 'function' ? tool.getPrimaryModel() : ''
|
|
100
|
+
const listeners = typeof tool.getPortListeners === 'function' ? tool.getPortListeners(gatewayPort) : []
|
|
101
|
+
const foreignListeners = listeners.filter((item) => !String(item.command || '').toLowerCase().includes('openclaw'))
|
|
102
|
+
|
|
103
|
+
if (installState.detail === 'npx fallback') {
|
|
104
|
+
details.push({
|
|
105
|
+
level: 'info',
|
|
106
|
+
text: '未检测到全局 openclaw,当前将通过 npx 运行',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
details.push(
|
|
111
|
+
nodeMajor >= 20
|
|
112
|
+
? { level: 'ok', text: `OpenClaw Node 版本要求满足(当前 ${process.version})` }
|
|
113
|
+
: { level: 'warn', text: `OpenClaw 建议 Node.js >= 20(当前 ${process.version})` }
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (primaryModel) {
|
|
117
|
+
details.push({
|
|
118
|
+
level: 'info',
|
|
119
|
+
text: `当前默认模型:${primaryModel}`,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (foreignListeners.length) {
|
|
124
|
+
const occupiedBy = foreignListeners
|
|
125
|
+
.slice(0, 2)
|
|
126
|
+
.map((item) => `${item.command}(${item.pid})`)
|
|
127
|
+
.join(', ')
|
|
128
|
+
details.push({
|
|
129
|
+
level: 'warn',
|
|
130
|
+
text: `Gateway 端口 ${gatewayPort} 被其他进程占用:${occupiedBy}`,
|
|
131
|
+
})
|
|
132
|
+
} else if (listeners.length) {
|
|
133
|
+
details.push({
|
|
134
|
+
level: 'ok',
|
|
135
|
+
text: `Gateway 端口 ${gatewayPort} 当前由 OpenClaw 占用`,
|
|
136
|
+
})
|
|
137
|
+
} else {
|
|
138
|
+
details.push({
|
|
139
|
+
level: 'info',
|
|
140
|
+
text: `Gateway 端口 ${gatewayPort} 当前空闲;如刚完成配置,可运行 ${tool.launchCmd}`,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
details.forEach((detail) => {
|
|
145
|
+
const icon = detail.level === 'ok'
|
|
146
|
+
? chalk.green('↳')
|
|
147
|
+
: detail.level === 'warn'
|
|
148
|
+
? chalk.yellow('↳')
|
|
149
|
+
: chalk.gray('↳')
|
|
150
|
+
const text = detail.level === 'ok'
|
|
151
|
+
? chalk.green(detail.text)
|
|
152
|
+
: detail.level === 'warn'
|
|
153
|
+
? chalk.yellow(detail.text)
|
|
154
|
+
: chalk.gray(detail.text)
|
|
155
|
+
console.log(` ${icon} ${text}`)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
88
159
|
function maskKey(key) {
|
|
89
160
|
if (!key || key.length < 8) return '****'
|
|
90
161
|
return key.slice(0, 6) + '...' + key.slice(-4)
|
|
91
162
|
}
|
|
92
163
|
|
|
93
|
-
function
|
|
164
|
+
function getInstallState(tool) {
|
|
165
|
+
if (tool.id === 'openclaw' && typeof tool.detectRuntime === 'function') {
|
|
166
|
+
const runtime = tool.detectRuntime()
|
|
167
|
+
return {
|
|
168
|
+
installed: runtime.available,
|
|
169
|
+
version: runtime.version,
|
|
170
|
+
detail: runtime.via === 'npx' ? 'npx fallback' : '',
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const installed = tool.checkInstalled()
|
|
175
|
+
return {
|
|
176
|
+
installed,
|
|
177
|
+
version: installed ? getVersion(tool) : null,
|
|
178
|
+
detail: '',
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getVersion(tool) {
|
|
183
|
+
if (typeof tool.getVersion === 'function') {
|
|
184
|
+
return tool.getVersion()
|
|
185
|
+
}
|
|
186
|
+
|
|
94
187
|
const cmds = {
|
|
95
188
|
'claude-code': 'claude --version',
|
|
96
|
-
'codex':
|
|
97
|
-
'droid':
|
|
98
|
-
'gemini-cli':
|
|
99
|
-
'opencode':
|
|
100
|
-
'openclaw':
|
|
101
|
-
'aider':
|
|
189
|
+
'codex': 'codex --version',
|
|
190
|
+
'droid': 'droid --version',
|
|
191
|
+
'gemini-cli': 'gemini --version',
|
|
192
|
+
'opencode': 'opencode --version',
|
|
193
|
+
'openclaw': 'openclaw --version',
|
|
194
|
+
'aider': 'aider --version',
|
|
102
195
|
}
|
|
103
|
-
const cmd = cmds[
|
|
196
|
+
const cmd = cmds[tool.id]
|
|
104
197
|
if (!cmd) return null
|
|
105
198
|
try {
|
|
106
199
|
return execSync(cmd, { stdio: 'pipe' }).toString().trim().split('\n')[0].slice(0, 30)
|
|
107
|
-
} catch {
|
|
200
|
+
} catch {
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
108
203
|
}
|
|
109
204
|
|
|
110
205
|
module.exports = doctor
|
package/src/commands/setup.js
CHANGED
|
@@ -195,7 +195,7 @@ async function setup(options) {
|
|
|
195
195
|
justInstalled.add(tool.id)
|
|
196
196
|
} else if (tool.id === 'openclaw') {
|
|
197
197
|
// openclaw 安装失败时(如无 git),改用 npx 模式继续配置
|
|
198
|
-
//
|
|
198
|
+
// 这里直接标记为本次可配置,具体执行阶段再走 npx fallback
|
|
199
199
|
console.log(chalk.yellow(` ⚠️ 全局安装失败,将使用 npx openclaw 代替`))
|
|
200
200
|
tool._useNpx = true
|
|
201
201
|
justInstalled.add(tool.id)
|
package/src/tools/openclaw.js
CHANGED
|
@@ -1,221 +1,491 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenClaw 适配器 (v2 — 基于实测的正确配置格式)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 模型引用格式: "custom-api-holysheep-ai/claude-sonnet-4-6"
|
|
7
|
-
*
|
|
8
|
-
* 必须的 onboard 参数:
|
|
9
|
-
* --accept-risk --auth-choice custom-api-key
|
|
10
|
-
* --custom-base-url --custom-api-key --custom-model-id --custom-compatibility anthropic
|
|
11
|
-
* --install-daemon
|
|
4
|
+
* 正确方案:写入 HolySheep 的 OpenAI + Anthropic 双 provider,
|
|
5
|
+
* 默认模型固定为 GPT-5.4,同时保留 Claude 模型供 /model 切换。
|
|
12
6
|
*/
|
|
13
|
-
const fs
|
|
7
|
+
const fs = require('fs')
|
|
14
8
|
const path = require('path')
|
|
15
|
-
const os
|
|
9
|
+
const os = require('os')
|
|
16
10
|
const { spawnSync, spawn, execSync } = require('child_process')
|
|
11
|
+
const { commandExists } = require('../utils/which')
|
|
17
12
|
|
|
18
13
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw')
|
|
19
|
-
const CONFIG_FILE
|
|
20
|
-
const isWin
|
|
14
|
+
const CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json')
|
|
15
|
+
const isWin = process.platform === 'win32'
|
|
16
|
+
const DEFAULT_GATEWAY_PORT = 18789
|
|
17
|
+
const MAX_PORT_SCAN = 20
|
|
18
|
+
const OPENCLAW_DEFAULT_MODEL = 'gpt-5.4'
|
|
19
|
+
const OPENCLAW_DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6'
|
|
20
|
+
|
|
21
|
+
function hasOpenClawBinary() {
|
|
22
|
+
return commandExists('openclaw')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasNpx() {
|
|
26
|
+
return commandExists('npx')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getRunner(preferNpx = false) {
|
|
30
|
+
if (!preferNpx && hasOpenClawBinary()) {
|
|
31
|
+
return { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
|
|
32
|
+
}
|
|
33
|
+
if (hasNpx()) {
|
|
34
|
+
return { cmd: 'npx', argsPrefix: ['openclaw'], shell: isWin, label: 'npx openclaw', via: 'npx' }
|
|
35
|
+
}
|
|
36
|
+
if (hasOpenClawBinary()) {
|
|
37
|
+
return { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 运行 openclaw CLI(优先全局命令,可切换到 npx 回退) */
|
|
43
|
+
function runOpenClaw(args, opts = {}) {
|
|
44
|
+
const runner = getRunner(Boolean(opts.preferNpx))
|
|
45
|
+
if (!runner) {
|
|
46
|
+
return { status: 1, stdout: '', stderr: 'OpenClaw CLI not found' }
|
|
47
|
+
}
|
|
21
48
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:
|
|
49
|
+
return spawnSync(runner.cmd, [...runner.argsPrefix, ...args], {
|
|
50
|
+
shell: runner.shell,
|
|
51
|
+
timeout: opts.timeout || 30000,
|
|
52
|
+
stdio: opts.stdio || 'pipe',
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function spawnOpenClaw(args, opts = {}) {
|
|
58
|
+
const runner = getRunner(Boolean(opts.preferNpx))
|
|
59
|
+
if (!runner) throw new Error('OpenClaw CLI not found')
|
|
60
|
+
|
|
61
|
+
const { preferNpx: _preferNpx, ...spawnOpts } = opts
|
|
62
|
+
return spawn(runner.cmd, [...runner.argsPrefix, ...args], {
|
|
63
|
+
shell: runner.shell,
|
|
64
|
+
...spawnOpts,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getPreferredRuntime() {
|
|
69
|
+
return module.exports._useNpx || !hasOpenClawBinary()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function firstLine(text) {
|
|
73
|
+
return String(text || '').trim().split('\n')[0] || ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getOpenClawVersion(preferNpx = false) {
|
|
77
|
+
const result = runOpenClaw(['--version'], { preferNpx, timeout: 15000 })
|
|
78
|
+
if (result.status !== 0) return null
|
|
79
|
+
return firstLine(result.stdout)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function detectRuntime() {
|
|
83
|
+
const preferNpx = getPreferredRuntime()
|
|
84
|
+
const version = getOpenClawVersion(preferNpx)
|
|
85
|
+
|
|
86
|
+
if (version) {
|
|
87
|
+
const runner = getRunner(preferNpx)
|
|
88
|
+
return {
|
|
89
|
+
available: true,
|
|
90
|
+
via: runner?.via || (preferNpx ? 'npx' : 'binary'),
|
|
91
|
+
command: runner?.label || (preferNpx ? 'npx openclaw' : 'openclaw'),
|
|
92
|
+
version,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!preferNpx && hasNpx()) {
|
|
97
|
+
const fallbackVersion = getOpenClawVersion(true)
|
|
98
|
+
if (fallbackVersion) {
|
|
99
|
+
return {
|
|
100
|
+
available: true,
|
|
101
|
+
via: 'npx',
|
|
102
|
+
command: 'npx openclaw',
|
|
103
|
+
version: fallbackVersion,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { available: false, via: null, command: null, version: null }
|
|
27
109
|
}
|
|
28
110
|
|
|
29
111
|
function readConfig() {
|
|
30
112
|
try {
|
|
31
113
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
32
114
|
const raw = fs.readFileSync(CONFIG_FILE, 'utf8')
|
|
33
|
-
|
|
34
|
-
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(raw)
|
|
117
|
+
} catch {
|
|
118
|
+
// 兼容极少数带注释的配置,但不要误伤 https:// 之类的 URL
|
|
119
|
+
return JSON.parse(raw.replace(/^\s*\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, ''))
|
|
120
|
+
}
|
|
35
121
|
}
|
|
36
122
|
} catch {}
|
|
37
123
|
return {}
|
|
38
124
|
}
|
|
39
125
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
126
|
+
function getConfiguredGatewayPort(config = readConfig()) {
|
|
127
|
+
const port = Number(config?.gateway?.port)
|
|
128
|
+
return Number.isInteger(port) && port > 0 ? port : DEFAULT_GATEWAY_PORT
|
|
129
|
+
}
|
|
43
130
|
|
|
44
|
-
|
|
45
|
-
|
|
131
|
+
function getConfiguredPrimaryModel(config = readConfig()) {
|
|
132
|
+
return config?.agents?.defaults?.model?.primary || ''
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isPortInUse(port) {
|
|
136
|
+
try {
|
|
46
137
|
if (isWin) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return true
|
|
50
|
-
} catch {}
|
|
138
|
+
const out = execSync(`netstat -ano | findstr :${port}`, { shell: true, stdio: 'pipe', encoding: 'utf8' })
|
|
139
|
+
return out.trim().length > 0
|
|
51
140
|
}
|
|
141
|
+
|
|
142
|
+
execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN`, { shell: true, stdio: 'ignore' })
|
|
143
|
+
return true
|
|
144
|
+
} catch {
|
|
52
145
|
return false
|
|
53
|
-
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
54
148
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
149
|
+
function listPortListeners(port) {
|
|
150
|
+
try {
|
|
151
|
+
if (isWin) {
|
|
152
|
+
const out = execSync(`netstat -ano | findstr :${port}`, { shell: true, stdio: 'pipe', encoding: 'utf8' })
|
|
153
|
+
return out
|
|
154
|
+
.trim()
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((line) => {
|
|
158
|
+
const parts = line.trim().split(/\s+/)
|
|
159
|
+
return { pid: parts[parts.length - 1], command: 'pid', detail: parts[1] || '' }
|
|
160
|
+
})
|
|
161
|
+
}
|
|
59
162
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
163
|
+
const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN`, { shell: true, stdio: 'pipe', encoding: 'utf8' })
|
|
164
|
+
return out
|
|
165
|
+
.trim()
|
|
166
|
+
.split('\n')
|
|
167
|
+
.slice(1)
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.map((line) => {
|
|
170
|
+
const parts = line.trim().split(/\s+/)
|
|
171
|
+
return {
|
|
172
|
+
command: parts[0] || 'unknown',
|
|
173
|
+
pid: parts[1] || '?',
|
|
174
|
+
detail: parts[parts.length - 1] || '',
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
} catch {
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
}
|
|
63
181
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const match = out.match(/\s(\d+)\s*$/)
|
|
72
|
-
if (match) execSync(`taskkill /F /PID ${match[1]}`, { shell: true, stdio: 'ignore' })
|
|
73
|
-
} else {
|
|
74
|
-
execSync('lsof -ti:18789 | xargs kill -9 2>/dev/null || true', { shell: true, stdio: 'ignore' })
|
|
75
|
-
}
|
|
76
|
-
} catch {}
|
|
77
|
-
// 等 1s 让端口释放
|
|
78
|
-
const t0 = Date.now(); while (Date.now() - t0 < 1000) {}
|
|
182
|
+
function findAvailableGatewayPort(startPort = DEFAULT_GATEWAY_PORT) {
|
|
183
|
+
for (let offset = 0; offset < MAX_PORT_SCAN; offset++) {
|
|
184
|
+
const port = startPort + offset
|
|
185
|
+
if (!isPortInUse(port)) return port
|
|
186
|
+
}
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
79
189
|
|
|
80
|
-
|
|
81
|
-
|
|
190
|
+
function getLaunchCommand(port = getConfiguredGatewayPort()) {
|
|
191
|
+
const runtime = module.exports._lastRuntimeCommand || (hasOpenClawBinary() ? 'openclaw' : 'npx openclaw')
|
|
192
|
+
return `${runtime} gateway --port ${port}`
|
|
193
|
+
}
|
|
82
194
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
'onboard',
|
|
88
|
-
'--non-interactive',
|
|
89
|
-
'--accept-risk',
|
|
90
|
-
'--auth-choice', 'custom-api-key',
|
|
91
|
-
'--custom-base-url', baseUrl,
|
|
92
|
-
'--custom-api-key', apiKey,
|
|
93
|
-
'--custom-model-id', primaryModel || 'claude-sonnet-4-6',
|
|
94
|
-
'--custom-compatibility', 'anthropic',
|
|
95
|
-
'--install-daemon',
|
|
96
|
-
)
|
|
195
|
+
function buildProviderName(baseUrl, prefix) {
|
|
196
|
+
const hostname = new URL(baseUrl).hostname.replace(/\./g, '-')
|
|
197
|
+
return `${prefix}-${hostname}`
|
|
198
|
+
}
|
|
97
199
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
200
|
+
function buildModelEntry(id) {
|
|
201
|
+
return {
|
|
202
|
+
id,
|
|
203
|
+
name: `${id} (HolySheep)`,
|
|
204
|
+
reasoning: false,
|
|
205
|
+
input: ['text'],
|
|
206
|
+
contextWindow: 200000,
|
|
207
|
+
maxTokens: id.startsWith('gpt-') ? 8192 : 16000,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
103
210
|
|
|
104
|
-
|
|
105
|
-
|
|
211
|
+
function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModels) {
|
|
212
|
+
const requestedModels = Array.isArray(selectedModels) && selectedModels.length > 0
|
|
213
|
+
? selectedModels
|
|
214
|
+
: [OPENCLAW_DEFAULT_MODEL, OPENCLAW_DEFAULT_CLAUDE_MODEL]
|
|
106
215
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
216
|
+
const openaiModels = requestedModels.filter((model) => model.startsWith('gpt-'))
|
|
217
|
+
if (!openaiModels.includes(OPENCLAW_DEFAULT_MODEL)) {
|
|
218
|
+
openaiModels.unshift(OPENCLAW_DEFAULT_MODEL)
|
|
219
|
+
}
|
|
110
220
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
221
|
+
const claudeModels = requestedModels.filter((model) => model.startsWith('claude-'))
|
|
222
|
+
if (claudeModels.length === 0) {
|
|
223
|
+
claudeModels.push(OPENCLAW_DEFAULT_CLAUDE_MODEL)
|
|
224
|
+
}
|
|
116
225
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log(chalk.bold.cyan(` ${dashUrl}`))
|
|
226
|
+
const openaiProviderName = buildProviderName(baseUrlOpenAI, 'custom-openai')
|
|
227
|
+
const anthropicProviderName = buildProviderName(baseUrlAnthropic, 'custom-anthropic')
|
|
120
228
|
|
|
121
|
-
|
|
122
|
-
|
|
229
|
+
const providers = {
|
|
230
|
+
[openaiProviderName]: {
|
|
231
|
+
baseUrl: baseUrlOpenAI,
|
|
232
|
+
apiKey,
|
|
233
|
+
api: 'openai-completions',
|
|
234
|
+
models: openaiModels.map(buildModelEntry),
|
|
235
|
+
},
|
|
236
|
+
[anthropicProviderName]: {
|
|
237
|
+
baseUrl: baseUrlAnthropic,
|
|
238
|
+
apiKey,
|
|
239
|
+
api: 'anthropic-messages',
|
|
240
|
+
models: claudeModels.map(buildModelEntry),
|
|
241
|
+
},
|
|
242
|
+
}
|
|
123
243
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
244
|
+
const managedModelRefs = [
|
|
245
|
+
...openaiModels.map((id) => `${openaiProviderName}/${id}`),
|
|
246
|
+
...claudeModels.map((id) => `${anthropicProviderName}/${id}`),
|
|
247
|
+
]
|
|
127
248
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
249
|
+
return {
|
|
250
|
+
providers,
|
|
251
|
+
managedModelRefs,
|
|
252
|
+
primaryRef: `${openaiProviderName}/${OPENCLAW_DEFAULT_MODEL}`,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isHolySheepProvider(provider) {
|
|
257
|
+
return typeof provider?.baseUrl === 'string' && provider.baseUrl.includes('api.holysheep.ai')
|
|
136
258
|
}
|
|
137
259
|
|
|
138
|
-
|
|
139
|
-
function _writeFallbackConfig(apiKey, baseUrl, selectedModels, primaryModel) {
|
|
260
|
+
function writeManagedConfig(baseConfig, apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModels, gatewayPort) {
|
|
140
261
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true })
|
|
141
262
|
|
|
142
|
-
const
|
|
143
|
-
const
|
|
263
|
+
const plan = buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModels)
|
|
264
|
+
const existingProviders = baseConfig?.models?.providers || {}
|
|
265
|
+
const managedProviderIds = Object.entries(existingProviders)
|
|
266
|
+
.filter(([, provider]) => isHolySheepProvider(provider))
|
|
267
|
+
.map(([providerId]) => providerId)
|
|
144
268
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (claudeModels.length === 0) claudeModels.push('claude-sonnet-4-6')
|
|
269
|
+
const preservedProviders = Object.fromEntries(
|
|
270
|
+
Object.entries(existingProviders).filter(([, provider]) => !isHolySheepProvider(provider))
|
|
271
|
+
)
|
|
149
272
|
|
|
150
|
-
const
|
|
273
|
+
const existingModelMap = baseConfig?.agents?.defaults?.models || {}
|
|
274
|
+
const preservedModelMap = Object.fromEntries(
|
|
275
|
+
Object.entries(existingModelMap).filter(([ref]) => {
|
|
276
|
+
return !managedProviderIds.some((providerId) => ref.startsWith(`${providerId}/`))
|
|
277
|
+
})
|
|
278
|
+
)
|
|
151
279
|
|
|
152
|
-
const
|
|
280
|
+
const managedModelMap = Object.fromEntries(plan.managedModelRefs.map((ref) => [ref, {}]))
|
|
281
|
+
|
|
282
|
+
const nextConfig = {
|
|
283
|
+
...baseConfig,
|
|
153
284
|
models: {
|
|
285
|
+
...(baseConfig.models || {}),
|
|
154
286
|
mode: 'merge',
|
|
155
287
|
providers: {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
api: 'anthropic-messages',
|
|
160
|
-
models: claudeModels.map(id => ({
|
|
161
|
-
id,
|
|
162
|
-
name: `${id} (HolySheep)`,
|
|
163
|
-
reasoning: false,
|
|
164
|
-
input: ['text'],
|
|
165
|
-
contextWindow: 200000,
|
|
166
|
-
maxTokens: 16000,
|
|
167
|
-
})),
|
|
168
|
-
}
|
|
169
|
-
}
|
|
288
|
+
...preservedProviders,
|
|
289
|
+
...plan.providers,
|
|
290
|
+
},
|
|
170
291
|
},
|
|
171
292
|
agents: {
|
|
293
|
+
...(baseConfig.agents || {}),
|
|
172
294
|
defaults: {
|
|
173
|
-
|
|
174
|
-
|
|
295
|
+
...(baseConfig.agents?.defaults || {}),
|
|
296
|
+
model: {
|
|
297
|
+
...(baseConfig.agents?.defaults?.model || {}),
|
|
298
|
+
primary: plan.primaryRef,
|
|
299
|
+
},
|
|
300
|
+
models: {
|
|
301
|
+
...preservedModelMap,
|
|
302
|
+
...managedModelMap,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
175
305
|
},
|
|
176
306
|
gateway: {
|
|
307
|
+
...(baseConfig.gateway || {}),
|
|
177
308
|
mode: 'local',
|
|
178
|
-
port:
|
|
309
|
+
port: gatewayPort,
|
|
179
310
|
bind: 'loopback',
|
|
180
|
-
auth: {
|
|
181
|
-
|
|
311
|
+
auth: {
|
|
312
|
+
...(baseConfig.gateway?.auth || {}),
|
|
313
|
+
mode: 'none',
|
|
314
|
+
},
|
|
315
|
+
},
|
|
182
316
|
}
|
|
183
317
|
|
|
184
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(
|
|
318
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), 'utf8')
|
|
319
|
+
return plan
|
|
185
320
|
}
|
|
186
321
|
|
|
187
|
-
|
|
188
|
-
function _disableGatewayAuth() {
|
|
322
|
+
function _disableGatewayAuth(preferNpx = false) {
|
|
189
323
|
try {
|
|
190
|
-
|
|
324
|
+
runOpenClaw(['config', 'set', 'gateway.auth.mode', 'none'], { preferNpx })
|
|
191
325
|
} catch {}
|
|
192
326
|
}
|
|
193
327
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
328
|
+
function _installGatewayService(port, preferNpx = false) {
|
|
329
|
+
const result = runOpenClaw(['gateway', 'install', '--force', '--port', String(port)], {
|
|
330
|
+
preferNpx,
|
|
331
|
+
timeout: 60000,
|
|
332
|
+
})
|
|
333
|
+
return result.status === 0
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _startGateway(port, preferNpx = false, preferService = true) {
|
|
337
|
+
const serviceResult = preferService
|
|
338
|
+
? runOpenClaw(['gateway', 'start'], { preferNpx, timeout: 20000 })
|
|
339
|
+
: { status: 1 }
|
|
340
|
+
|
|
341
|
+
if (serviceResult.status !== 0) {
|
|
342
|
+
const child = spawnOpenClaw(['gateway', '--port', String(port)], {
|
|
343
|
+
preferNpx,
|
|
344
|
+
detached: true,
|
|
345
|
+
stdio: 'ignore',
|
|
203
346
|
})
|
|
204
347
|
child.unref()
|
|
205
348
|
}
|
|
206
349
|
|
|
207
|
-
// 等待最多 8 秒
|
|
208
350
|
for (let i = 0; i < 8; i++) {
|
|
209
|
-
const
|
|
351
|
+
const t0 = Date.now()
|
|
352
|
+
while (Date.now() - t0 < 1000) {}
|
|
353
|
+
|
|
210
354
|
try {
|
|
211
355
|
execSync(
|
|
212
356
|
isWin
|
|
213
|
-
?
|
|
214
|
-
:
|
|
357
|
+
? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/ -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
|
|
358
|
+
: `curl -sf http://127.0.0.1:${port}/ -o /dev/null --max-time 1`,
|
|
215
359
|
{ stdio: 'ignore', timeout: 3000 }
|
|
216
360
|
)
|
|
217
361
|
return true
|
|
218
362
|
} catch {}
|
|
219
363
|
}
|
|
364
|
+
|
|
220
365
|
return false
|
|
221
366
|
}
|
|
367
|
+
|
|
368
|
+
module.exports = {
|
|
369
|
+
name: 'OpenClaw',
|
|
370
|
+
id: 'openclaw',
|
|
371
|
+
|
|
372
|
+
checkInstalled() {
|
|
373
|
+
return hasOpenClawBinary()
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
detectRuntime,
|
|
377
|
+
|
|
378
|
+
getVersion() {
|
|
379
|
+
return detectRuntime().version
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
isConfigured() {
|
|
383
|
+
const cfg = JSON.stringify(readConfig())
|
|
384
|
+
return cfg.includes('holysheep.ai')
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
configure(apiKey, baseUrlAnthropic, baseUrlOpenAI, _primaryModel, selectedModels) {
|
|
388
|
+
const chalk = require('chalk')
|
|
389
|
+
console.log(chalk.gray('\n ⚙️ 正在配置 OpenClaw...'))
|
|
390
|
+
|
|
391
|
+
const runtime = detectRuntime()
|
|
392
|
+
if (!runtime.available) {
|
|
393
|
+
throw new Error('未检测到 OpenClaw;请先全局安装,或确保 npx 可用')
|
|
394
|
+
}
|
|
395
|
+
this._lastRuntimeCommand = runtime.command
|
|
396
|
+
|
|
397
|
+
runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
|
|
398
|
+
|
|
399
|
+
const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
|
|
400
|
+
if (!gatewayPort) {
|
|
401
|
+
throw new Error(`找不到可用端口(已检查 ${DEFAULT_GATEWAY_PORT}-${DEFAULT_GATEWAY_PORT + MAX_PORT_SCAN - 1})`)
|
|
402
|
+
}
|
|
403
|
+
this._lastGatewayPort = gatewayPort
|
|
404
|
+
|
|
405
|
+
if (gatewayPort !== DEFAULT_GATEWAY_PORT) {
|
|
406
|
+
console.log(chalk.yellow(` ⚠️ 端口 ${DEFAULT_GATEWAY_PORT} 已占用,自动切换到 ${gatewayPort}`))
|
|
407
|
+
const listeners = listPortListeners(DEFAULT_GATEWAY_PORT)
|
|
408
|
+
if (listeners.length) {
|
|
409
|
+
const summary = listeners
|
|
410
|
+
.slice(0, 2)
|
|
411
|
+
.map((item) => `${item.command}(${item.pid})`)
|
|
412
|
+
.join(', ')
|
|
413
|
+
console.log(chalk.gray(` 占用进程: ${summary}`))
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try { fs.unlinkSync(CONFIG_FILE) } catch {}
|
|
418
|
+
|
|
419
|
+
console.log(chalk.gray(' → 写入配置...'))
|
|
420
|
+
const result = runOpenClaw([
|
|
421
|
+
'onboard',
|
|
422
|
+
'--non-interactive',
|
|
423
|
+
'--accept-risk',
|
|
424
|
+
'--auth-choice', 'custom-api-key',
|
|
425
|
+
'--custom-base-url', baseUrlOpenAI,
|
|
426
|
+
'--custom-api-key', apiKey,
|
|
427
|
+
'--custom-model-id', OPENCLAW_DEFAULT_MODEL,
|
|
428
|
+
'--custom-compatibility', 'openai',
|
|
429
|
+
'--gateway-port', String(gatewayPort),
|
|
430
|
+
'--install-daemon',
|
|
431
|
+
], { preferNpx: runtime.via === 'npx' })
|
|
432
|
+
|
|
433
|
+
if (result.status !== 0) {
|
|
434
|
+
console.log(chalk.yellow(' ⚠️ onboard 失败,使用备用配置...'))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
writeManagedConfig(
|
|
438
|
+
result.status === 0 ? readConfig() : {},
|
|
439
|
+
apiKey,
|
|
440
|
+
baseUrlAnthropic,
|
|
441
|
+
baseUrlOpenAI,
|
|
442
|
+
selectedModels,
|
|
443
|
+
gatewayPort,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
_disableGatewayAuth(runtime.via === 'npx')
|
|
447
|
+
const serviceReady = _installGatewayService(gatewayPort, runtime.via === 'npx')
|
|
448
|
+
|
|
449
|
+
console.log(chalk.gray(' → 正在启动 Gateway...'))
|
|
450
|
+
const ok = _startGateway(gatewayPort, runtime.via === 'npx', serviceReady)
|
|
451
|
+
|
|
452
|
+
if (ok) {
|
|
453
|
+
console.log(chalk.green(' ✓ OpenClaw Gateway 已启动'))
|
|
454
|
+
} else {
|
|
455
|
+
console.log(chalk.yellow(' ⚠️ Gateway 启动中,稍等几秒后刷新浏览器'))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const dashUrl = `http://127.0.0.1:${gatewayPort}/`
|
|
459
|
+
console.log(chalk.cyan('\n → 浏览器打开(无需 token):'))
|
|
460
|
+
console.log(chalk.bold.cyan(` ${dashUrl}`))
|
|
461
|
+
console.log(chalk.gray(` 默认模型: ${OPENCLAW_DEFAULT_MODEL}`))
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
file: CONFIG_FILE,
|
|
465
|
+
hot: false,
|
|
466
|
+
dashboardUrl: dashUrl,
|
|
467
|
+
gatewayPort,
|
|
468
|
+
launchCmd: getLaunchCommand(gatewayPort),
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
reset() {
|
|
473
|
+
try { fs.unlinkSync(CONFIG_FILE) } catch {}
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
getConfigPath() { return CONFIG_FILE },
|
|
477
|
+
getGatewayPort() { return getConfiguredGatewayPort() },
|
|
478
|
+
getPrimaryModel() { return getConfiguredPrimaryModel() },
|
|
479
|
+
getPortListeners(port = getConfiguredGatewayPort()) { return listPortListeners(port) },
|
|
480
|
+
get hint() {
|
|
481
|
+
return `Gateway 已启动,默认模型为 ${getConfiguredPrimaryModel() || OPENCLAW_DEFAULT_MODEL}`
|
|
482
|
+
},
|
|
483
|
+
get launchCmd() {
|
|
484
|
+
return getLaunchCommand(getConfiguredGatewayPort())
|
|
485
|
+
},
|
|
486
|
+
get launchNote() {
|
|
487
|
+
return `🌐 打开浏览器: http://127.0.0.1:${getConfiguredGatewayPort()}/`
|
|
488
|
+
},
|
|
489
|
+
installCmd: 'npm install -g openclaw@latest',
|
|
490
|
+
docsUrl: 'https://docs.openclaw.ai',
|
|
491
|
+
}
|