@simonyea/holysheep-cli 1.6.0 → 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 CHANGED
@@ -38,7 +38,7 @@ Instead of manually editing config files for each tool, run one command and you'
38
38
  |------|-------------|--------|
39
39
  | [Claude Code](https://docs.anthropic.com/claude-code) | `~/.claude/settings.json` | ✅ Auto |
40
40
  | [Codex CLI](https://github.com/openai/codex) | `~/.codex/config.toml` | ✅ Auto |
41
- | Droid CLI | `~/.factory/config.json` | ✅ Auto |
41
+ | Droid CLI | `~/.factory/settings.json` | ✅ Auto |
42
42
  | [Aider](https://aider.chat) | `~/.aider.conf.yml` | ✅ Auto |
43
43
  | [Continue.dev](https://continue.dev) | `~/.continue/config.yaml` | ✅ Auto |
44
44
  | [OpenCode](https://github.com/anomalyco/opencode) | `~/.config/opencode/opencode.json` | ✅ Auto |
@@ -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. A new terminal window opens running the OpenClaw Gateway
71
- 2. Open your browser: **http://127.0.0.1:18789/**
72
- 3. Start chatting no token required
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
- npx openclaw gateway --port 18789
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. 自动弹出一个新终端窗口,运行 OpenClaw Gateway
149
- 2. 打开浏览器访问:**http://127.0.0.1:18789/**
150
- 3. 直接开始聊天,无需填写 token
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
- npx openclaw gateway --port 18789
170
+ openclaw gateway --port <显示的端口>
171
+ # 或
172
+ npx openclaw gateway --port <显示的端口>
157
173
  ```
158
174
 
159
- **使用的模型:** `claude-sonnet-4-6`(通过 HolySheep 中转)
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 停止,需重新运行 `npx openclaw gateway --port 18789`。
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.0",
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",
@@ -42,7 +42,7 @@
42
42
  "homepage": "https://holysheep.ai",
43
43
  "repository": {
44
44
  "type": "git",
45
- "url": "https://github.com/holysheep123/holysheep-cli"
45
+ "url": "git+https://github.com/holysheep123/holysheep-cli.git"
46
46
  },
47
47
  "license": "MIT",
48
48
  "bin": {
@@ -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 = parseInt(nodeVer.slice(1)) >= 16
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 = process.env.OPENAI_API_KEY
28
+ const envOpenAIKey = process.env.OPENAI_API_KEY
27
29
  const envAnthropicUrl = process.env.ANTHROPIC_BASE_URL
28
- const envOpenAIUrl = process.env.OPENAI_BASE_URL
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, 'OPENAI_API_KEY', envOpenAIKey ? maskKey(envOpenAIKey) : '未设置')
35
- printCheck(!!envOpenAIUrl, 'OPENAI_BASE_URL', 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 installed = tool.checkInstalled()
44
+ const installState = getInstallState(tool)
45
+ const installed = installState.installed
43
46
  const configured = installed ? tool.isConfigured() : null
44
- const version = installed ? getVersion(tool.id) : null
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(`— 运行 hs setup`)}`)
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 = ok ? chalk.green('✓') : chalk.red('✗')
83
- const lbl = ok ? chalk.green(label.padEnd(35)) : chalk.red(label.padEnd(35))
84
- const det = detail ? chalk.gray(detail) : ''
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 getVersion(toolId) {
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': 'codex --version',
97
- 'droid': 'droid --version',
98
- 'gemini-cli': 'gemini --version',
99
- 'opencode': 'opencode --version',
100
- 'openclaw': 'openclaw --version',
101
- 'aider': 'aider --version',
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[toolId]
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 { return null }
200
+ } catch {
201
+ return null
202
+ }
108
203
  }
109
204
 
110
205
  module.exports = doctor
@@ -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
- // checkInstalled() 里已有 npx fallback,标记为已安装
198
+ // 这里直接标记为本次可配置,具体执行阶段再走 npx fallback
199
199
  console.log(chalk.yellow(` ⚠️ 全局安装失败,将使用 npx openclaw 代替`))
200
200
  tool._useNpx = true
201
201
  justInstalled.add(tool.id)
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Droid CLI 适配器
3
- * 配置文件: ~/.factory/config.json
3
+ * 配置文件: ~/.factory/settings.json
4
4
  *
5
5
  * 使用 Droid 原生 customModels 配置 HolySheep 的多个模型入口:
6
6
  * - GPT 走 OpenAI 兼容入口: https://api.holysheep.ai/openai
@@ -12,7 +12,7 @@ const path = require('path')
12
12
  const os = require('os')
13
13
 
14
14
  const CONFIG_DIR = path.join(os.homedir(), '.factory')
15
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
15
+ const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.json')
16
16
 
17
17
  const DEFAULT_MODELS = [
18
18
  {
@@ -52,18 +52,18 @@ const DEFAULT_MODELS = [
52
52
  },
53
53
  ]
54
54
 
55
- function readConfig() {
55
+ function readSettings() {
56
56
  try {
57
- if (fs.existsSync(CONFIG_FILE)) {
58
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'))
57
+ if (fs.existsSync(SETTINGS_FILE)) {
58
+ return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'))
59
59
  }
60
60
  } catch {}
61
61
  return {}
62
62
  }
63
63
 
64
- function writeConfig(data) {
64
+ function writeSettings(data) {
65
65
  fs.mkdirSync(CONFIG_DIR, { recursive: true })
66
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8')
66
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2), 'utf8')
67
67
  }
68
68
 
69
69
  function normalizeSelectedModels(selectedModels) {
@@ -106,43 +106,43 @@ module.exports = {
106
106
  return require('../utils/which').commandExists('droid')
107
107
  },
108
108
  isConfigured() {
109
- const config = readConfig()
110
- const customModels = Array.isArray(config.customModels) ? config.customModels : []
109
+ const settings = readSettings()
110
+ const customModels = Array.isArray(settings.customModels) ? settings.customModels : []
111
111
  return customModels.some((item) =>
112
112
  typeof item.baseUrl === 'string' && item.baseUrl.includes('api.holysheep.ai')
113
113
  )
114
114
  },
115
115
  configure(apiKey, baseUrlAnthropic, _baseUrlOpenAI, _primaryModel, selectedModels) {
116
- const config = readConfig()
117
- const preservedModels = Array.isArray(config.customModels)
118
- ? config.customModels.filter(
116
+ const settings = readSettings()
117
+ const preservedModels = Array.isArray(settings.customModels)
118
+ ? settings.customModels.filter(
119
119
  (item) => !(typeof item.baseUrl === 'string' && item.baseUrl.includes('api.holysheep.ai'))
120
120
  )
121
121
  : []
122
122
 
123
- config.customModels = [
123
+ settings.customModels = [
124
124
  ...buildCustomModels(apiKey, baseUrlAnthropic, selectedModels),
125
125
  ...preservedModels,
126
126
  ]
127
- config.logoAnimation = 'off'
128
- writeConfig(config)
127
+ settings.logoAnimation = 'off'
128
+ writeSettings(settings)
129
129
 
130
130
  return {
131
- file: CONFIG_FILE,
131
+ file: SETTINGS_FILE,
132
132
  hot: true,
133
133
  }
134
134
  },
135
135
  reset() {
136
- const config = readConfig()
137
- if (Array.isArray(config.customModels)) {
138
- config.customModels = config.customModels.filter(
136
+ const settings = readSettings()
137
+ if (Array.isArray(settings.customModels)) {
138
+ settings.customModels = settings.customModels.filter(
139
139
  (item) => !(typeof item.baseUrl === 'string' && item.baseUrl.includes('api.holysheep.ai'))
140
140
  )
141
141
  }
142
- writeConfig(config)
142
+ writeSettings(settings)
143
143
  },
144
- getConfigPath() { return CONFIG_FILE },
145
- hint: '已写入 ~/.factory/config.json;重启 Droid 后可见 HolySheep 模型列表',
144
+ getConfigPath() { return SETTINGS_FILE },
145
+ hint: '已写入 ~/.factory/settings.json;重启 Droid 后可见 HolySheep 模型列表',
146
146
  launchCmd: 'droid',
147
147
  installCmd: 'brew install --cask droid',
148
148
  docsUrl: 'https://docs.factory.ai/cli/getting-started/overview',
@@ -1,221 +1,491 @@
1
1
  /**
2
2
  * OpenClaw 适配器 (v2 — 基于实测的正确配置格式)
3
3
  *
4
- * 正确方案:custom-api-key provider,配置在 models.providers
5
- * provider name 自动生成为 "custom-api-{hostname}"
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 = require('fs')
7
+ const fs = require('fs')
14
8
  const path = require('path')
15
- const os = require('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 = path.join(OPENCLAW_DIR, 'openclaw.json')
20
- const isWin = process.platform === 'win32'
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
- /** 运行 openclaw CLI */
23
- function npx(...args) {
24
- return isWin
25
- ? spawnSync('npx', ['openclaw', ...args], { shell: true, timeout: 30000, stdio: 'pipe' })
26
- : spawnSync('openclaw', args, { shell: false, timeout: 30000, stdio: 'pipe' })
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
- // 去掉 JSON5 注释再解析
34
- return JSON.parse(raw.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, ''))
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
- module.exports = {
41
- name: 'OpenClaw',
42
- id: 'openclaw',
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
- checkInstalled() {
45
- if (require('../utils/which').commandExists('openclaw')) return true
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
- try {
48
- execSync('npx openclaw --version', { stdio: 'ignore', timeout: 15000, shell: true })
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
- isConfigured() {
56
- const cfg = JSON.stringify(readConfig())
57
- return cfg.includes('holysheep.ai')
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
- configure(apiKey, baseUrl, _baseUrlOpenAI, primaryModel, selectedModels) {
61
- const chalk = require('chalk')
62
- console.log(chalk.gray('\n ⚙️ 正在配置 OpenClaw...'))
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
- // 1. 先停旧 gateway(必须在 onboard 之前,否则 hot-reload 会覆盖新 token)
65
- npx('gateway', 'stop')
66
- // 强制 kill 占用 18789 端口的进程(gateway stop 停不掉手动启动的进程)
67
- try {
68
- if (isWin) {
69
- // netstat 找 pid,然后 taskkill
70
- const out = execSync('netstat -ano | findstr :18789', { shell: true, encoding: 'utf8', stdio: 'pipe' })
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
- // 2. 删除旧配置,确保 onboard 会重新写入
81
- try { fs.unlinkSync(CONFIG_FILE) } catch {}
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
- // 3. openclaw 官方 onboard 命令写入正确配置
84
- // 这会生成完整的 models.providers.custom-api-holysheep-ai 配置
85
- console.log(chalk.gray(' 写入配置...'))
86
- const result = npx(
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
- if (result.status !== 0) {
99
- // onboard 失败时 fallback:手写最小化配置
100
- console.log(chalk.yellow(' ⚠️ onboard 失败,使用备用配置...'))
101
- _writeFallbackConfig(apiKey, baseUrl, selectedModels, primaryModel)
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
- // 4. 关闭 gateway token 认证(直接打开浏览器无需 token)
105
- _disableGatewayAuth()
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
- // 5. 启动 Gateway
108
- console.log(chalk.gray(' → 正在启动 Gateway...'))
109
- const ok = _startGateway()
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
- if (ok) {
112
- console.log(chalk.green(' ✓ OpenClaw Gateway 已启动'))
113
- } else {
114
- console.log(chalk.yellow(' ⚠️ Gateway 启动中,稍等几秒后刷新浏览器'))
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
- const dashUrl = 'http://127.0.0.1:18789/'
118
- console.log(chalk.cyan('\n 浏览器打开(无需 token):'))
119
- console.log(chalk.bold.cyan(` ${dashUrl}`))
226
+ const openaiProviderName = buildProviderName(baseUrlOpenAI, 'custom-openai')
227
+ const anthropicProviderName = buildProviderName(baseUrlAnthropic, 'custom-anthropic')
120
228
 
121
- return { file: CONFIG_FILE, hot: false }
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
- reset() {
125
- try { fs.unlinkSync(CONFIG_FILE) } catch {}
126
- },
244
+ const managedModelRefs = [
245
+ ...openaiModels.map((id) => `${openaiProviderName}/${id}`),
246
+ ...claudeModels.map((id) => `${anthropicProviderName}/${id}`),
247
+ ]
127
248
 
128
- getConfigPath() { return CONFIG_FILE },
129
- hint: 'Gateway 已启动,打开浏览器即可使用',
130
- launchCmd: null,
131
- get launchNote() {
132
- return '🌐 打开浏览器: http://127.0.0.1:18789/'
133
- },
134
- installCmd: 'npm install -g openclaw@latest',
135
- docsUrl: 'https://docs.openclaw.ai',
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
- /** onboard 失败时的备用配置(基于实测的正确格式) */
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 hostname = new URL(baseUrl).hostname.replace(/\./g, '-')
143
- const providerName = `custom-api-${hostname}`
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
- // 默认配置的 Claude 模型列表(只注册 Claude 系列,MiniMax 用独立 provider)
146
- const claudeModels = (selectedModels || ['claude-sonnet-4-6'])
147
- .filter(m => m.startsWith('claude-'))
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 primary = primaryModel || claudeModels[0]
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 config = {
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
- [providerName]: {
157
- baseUrl,
158
- apiKey,
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
- model: { primary: `${providerName}/${primary}` }
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: 18789,
309
+ port: gatewayPort,
179
310
  bind: 'loopback',
180
- auth: { mode: 'none' }, // 无需 token,本地访问直接打开
181
- }
311
+ auth: {
312
+ ...(baseConfig.gateway?.auth || {}),
313
+ mode: 'none',
314
+ },
315
+ },
182
316
  }
183
317
 
184
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8')
318
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), 'utf8')
319
+ return plan
185
320
  }
186
321
 
187
- /** openclaw config set 把 gateway auth 改成 none */
188
- function _disableGatewayAuth() {
322
+ function _disableGatewayAuth(preferNpx = false) {
189
323
  try {
190
- npx('config', 'set', 'gateway.auth.mode', 'none')
324
+ runOpenClaw(['config', 'set', 'gateway.auth.mode', 'none'], { preferNpx })
191
325
  } catch {}
192
326
  }
193
327
 
194
- /** 启动 Gateway 后台进程 */
195
- function _startGateway() {
196
- if (isWin) {
197
- spawnSync('cmd /c start cmd /k "npx openclaw gateway --port 18789"', [], {
198
- shell: true, timeout: 5000, stdio: 'ignore',
199
- })
200
- } else {
201
- const child = spawn('openclaw', ['gateway', '--port', '18789'], {
202
- detached: true, stdio: 'ignore',
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 t = Date.now(); while (Date.now() - t < 1000) {}
351
+ const t0 = Date.now()
352
+ while (Date.now() - t0 < 1000) {}
353
+
210
354
  try {
211
355
  execSync(
212
356
  isWin
213
- ? 'powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:18789/ -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"'
214
- : 'curl -sf http://127.0.0.1:18789/ -o /dev/null --max-time 1',
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
+ }