@simonyea/holysheep-cli 1.6.6 → 1.6.8

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
@@ -218,6 +218,8 @@ A: OpenClaw 需要 Node.js 20+,运行 `node --version` 确认版本后重试
218
218
 
219
219
  ## Changelog
220
220
 
221
+ - **v1.6.8** — 修复 Codex 重复写入 `config.toml` 导致的 duplicate key,并修复 OpenClaw 在 Windows 下的安装检测;针对 OpenClaw 2026.3.13 的模型路由回归,临时跳过 MiniMax 避免 `model not allowed`
222
+ - **v1.6.7** — OpenClaw 配置新增 `MiniMax-M2.7-highspeed`,并补齐节点迁移脚本中的 SSH 代理账号创建逻辑
221
223
  - **v1.6.6** — 修复 Droid CLI 的 GPT-5.4 配置残留问题,同时同步 `~/.factory/settings.json` 和 `~/.factory/config.json`,统一使用 `openai + https://api.holysheep.ai/v1`
222
224
  - **v1.6.5** — 修复 HolySheep 对 Droid Responses API 的兼容
223
225
  - **v1.6.4** — 修复 OpenClaw 的 npx 运行时检测,避免配置后页面仍卡在 Unauthorized / 未连接状态
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.6.6",
3
+ "version": "1.6.8",
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/index.js CHANGED
@@ -173,8 +173,3 @@ program
173
173
  })
174
174
 
175
175
  program.parse(process.argv)
176
-
177
- // 无参数时显示默认信息
178
- if (process.argv.length === 2) {
179
- program.help()
180
- }
@@ -25,6 +25,49 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml')
25
25
  // 保留 JSON 兼容性(老版本 TypeScript Codex 用)
26
26
  const CONFIG_FILE_JSON = path.join(CONFIG_DIR, 'config.json')
27
27
 
28
+ function normalizeToml(content) {
29
+ return String(content || '').replace(/\r\n/g, '\n')
30
+ }
31
+
32
+ function cleanupToml(content) {
33
+ return normalizeToml(content)
34
+ .replace(/\n{3,}/g, '\n\n')
35
+ .trim()
36
+ }
37
+
38
+ function stripManagedTomlConfig(content) {
39
+ const lines = normalizeToml(content).split('\n')
40
+ const output = []
41
+ let currentSection = null
42
+ let skipHolySheepBlock = false
43
+
44
+ for (const line of lines) {
45
+ const trimmed = line.trim()
46
+
47
+ if (/^\[[^\]]+\]$/.test(trimmed)) {
48
+ if (trimmed === '[model_providers.holysheep]') {
49
+ currentSection = trimmed
50
+ skipHolySheepBlock = true
51
+ continue
52
+ }
53
+
54
+ currentSection = trimmed
55
+ skipHolySheepBlock = false
56
+ }
57
+
58
+ if (skipHolySheepBlock) continue
59
+
60
+ if (!currentSection) {
61
+ if (/^model\s*=\s*"[^"]*"\s*$/.test(trimmed)) continue
62
+ if (/^model_provider\s*=\s*"holysheep"\s*$/.test(trimmed)) continue
63
+ }
64
+
65
+ output.push(line)
66
+ }
67
+
68
+ return cleanupToml(output.join('\n'))
69
+ }
70
+
28
71
  /**
29
72
  * 读取 TOML config(简单解析,不依赖 toml 库)
30
73
  */
@@ -55,14 +98,7 @@ function writeTomlConfig(apiKey, baseUrlOpenAI, model) {
55
98
  fs.mkdirSync(CONFIG_DIR, { recursive: true })
56
99
  }
57
100
 
58
- let content = readTomlConfig()
59
-
60
- // 移除旧的 holysheep 相关配置
61
- content = content
62
- .replace(/\nmodel\s*=\s*"[^"]*"\n/g, '\n')
63
- .replace(/\nmodel_provider\s*=\s*"holysheep"\n/g, '\n')
64
- .replace(/\[model_providers\.holysheep\][^\[]*(\[|$)/gs, (m, end) => end === '[' ? '[' : '')
65
- .trim()
101
+ const content = stripManagedTomlConfig(readTomlConfig())
66
102
 
67
103
  // 在开头插入 holysheep 配置
68
104
  const newConfig = [
@@ -76,9 +112,9 @@ function writeTomlConfig(apiKey, baseUrlOpenAI, model) {
76
112
  `base_url = "${baseUrlOpenAI}"`,
77
113
  `env_key = "OPENAI_API_KEY"`,
78
114
  '',
79
- ].join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n'
115
+ ].join('\n')
80
116
 
81
- fs.writeFileSync(CONFIG_FILE, newConfig, 'utf8')
117
+ fs.writeFileSync(CONFIG_FILE, cleanupToml(newConfig) + '\n', 'utf8')
82
118
  }
83
119
 
84
120
  /**
@@ -91,8 +127,15 @@ function writeJsonConfigIfNeeded(apiKey, baseUrlOpenAI, model) {
91
127
  jsonConfig = JSON.parse(fs.readFileSync(CONFIG_FILE_JSON, 'utf8'))
92
128
  }
93
129
  jsonConfig.model = model || 'gpt-5.4'
130
+ jsonConfig.model_provider = 'holysheep'
94
131
  jsonConfig.provider = 'holysheep'
132
+ if (!jsonConfig.model_providers) jsonConfig.model_providers = {}
95
133
  if (!jsonConfig.providers) jsonConfig.providers = {}
134
+ jsonConfig.model_providers.holysheep = {
135
+ name: 'HolySheep',
136
+ base_url: baseUrlOpenAI,
137
+ env_key: 'OPENAI_API_KEY',
138
+ }
96
139
  jsonConfig.providers.holysheep = {
97
140
  name: 'HolySheep',
98
141
  baseURL: baseUrlOpenAI,
@@ -132,22 +175,23 @@ module.exports = {
132
175
  reset() {
133
176
  // 清理 TOML
134
177
  if (fs.existsSync(CONFIG_FILE)) {
135
- let content = readTomlConfig()
136
- content = content
137
- .replace(/^model\s*=\s*"[^"]*"\n/m, '')
138
- .replace(/^model_provider\s*=\s*"holysheep"\n/m, '')
139
- .replace(/\[model_providers\.holysheep\][^\[]*(\[|$)/gs, (m, end) => end === '[' ? '[' : '')
140
- .trim() + '\n'
141
- fs.writeFileSync(CONFIG_FILE, content, 'utf8')
178
+ const content = stripManagedTomlConfig(readTomlConfig())
179
+ fs.writeFileSync(CONFIG_FILE, (content ? content + '\n' : ''), 'utf8')
142
180
  }
143
181
  // 清理 JSON
144
182
  if (fs.existsSync(CONFIG_FILE_JSON)) {
145
183
  try {
146
184
  const c = JSON.parse(fs.readFileSync(CONFIG_FILE_JSON, 'utf8'))
185
+ if (c.model_provider === 'holysheep') {
186
+ delete c.model_provider
187
+ }
147
188
  if (c.provider === 'holysheep') {
148
189
  delete c.provider
149
- delete c.providers?.holysheep
150
190
  }
191
+ delete c.model_providers?.holysheep
192
+ delete c.providers?.holysheep
193
+ if (c.model_providers && Object.keys(c.model_providers).length === 0) delete c.model_providers
194
+ if (c.providers && Object.keys(c.providers).length === 0) delete c.providers
151
195
  fs.writeFileSync(CONFIG_FILE_JSON, JSON.stringify(c, null, 2), 'utf8')
152
196
  } catch {}
153
197
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * OpenClaw 适配器 (v2 — 基于实测的正确配置格式)
3
3
  *
4
- * 正确方案:写入 HolySheep 的 OpenAI + Anthropic provider,
5
- * 默认模型固定为 GPT-5.4,同时保留 Claude 模型供 /model 切换。
4
+ * 正确方案:写入 HolySheep 的 OpenAI + Anthropic + MiniMax provider,
5
+ * 默认模型固定为 GPT-5.4,同时保留 Claude / MiniMax 模型供 /model 切换。
6
6
  */
7
7
  const fs = require('fs')
8
8
  const path = require('path')
@@ -17,9 +17,21 @@ const DEFAULT_GATEWAY_PORT = 18789
17
17
  const MAX_PORT_SCAN = 20
18
18
  const OPENCLAW_DEFAULT_MODEL = 'gpt-5.4'
19
19
  const OPENCLAW_DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6'
20
+ const OPENCLAW_DEFAULT_MINIMAX_MODEL = 'MiniMax-M2.7-highspeed'
21
+ const OPENCLAW_ROUTING_REGRESSION_VERSION = /^2026\.3\.13(?:\D|$)/
22
+
23
+ function getOpenClawBinaryCandidates() {
24
+ return isWin ? ['openclaw.cmd', 'openclaw'] : ['openclaw']
25
+ }
26
+
27
+ function getBinaryRunner() {
28
+ return isWin
29
+ ? { cmd: 'openclaw.cmd', argsPrefix: [], shell: true, label: 'openclaw', via: 'binary' }
30
+ : { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
31
+ }
20
32
 
21
33
  function hasOpenClawBinary() {
22
- return commandExists('openclaw')
34
+ return getOpenClawBinaryCandidates().some((cmd) => commandExists(cmd))
23
35
  }
24
36
 
25
37
  function hasNpx() {
@@ -27,18 +39,41 @@ function hasNpx() {
27
39
  }
28
40
 
29
41
  function getRunner(preferNpx = false) {
42
+ const binaryRunner = hasOpenClawBinary() ? getBinaryRunner() : null
43
+
30
44
  if (!preferNpx && hasOpenClawBinary()) {
31
- return { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
45
+ return binaryRunner
32
46
  }
33
47
  if (hasNpx()) {
34
48
  return { cmd: 'npx', argsPrefix: ['openclaw'], shell: isWin, label: 'npx openclaw', via: 'npx' }
35
49
  }
36
- if (hasOpenClawBinary()) {
37
- return { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
50
+ if (binaryRunner) {
51
+ return binaryRunner
38
52
  }
39
53
  return null
40
54
  }
41
55
 
56
+ function runWithRunner(runner, args, opts = {}) {
57
+ return spawnSync(runner.cmd, [...runner.argsPrefix, ...args], {
58
+ shell: runner.shell,
59
+ timeout: opts.timeout || 30000,
60
+ stdio: opts.stdio || 'pipe',
61
+ encoding: 'utf8',
62
+ })
63
+ }
64
+
65
+ function normalizeVersionOutput(text) {
66
+ return firstLine(text).replace(/^openclaw\s+/i, '').trim()
67
+ }
68
+
69
+ function probeRunner(runner, timeout) {
70
+ const result = runWithRunner(runner, ['--version'], { timeout })
71
+ if (result.error || result.status !== 0) return null
72
+
73
+ const version = normalizeVersionOutput(result.stdout || result.stderr || '')
74
+ return version || null
75
+ }
76
+
42
77
  /** 运行 openclaw CLI(优先全局命令,可切换到 npx 回退) */
43
78
  function runOpenClaw(args, opts = {}) {
44
79
  const runner = getRunner(Boolean(opts.preferNpx))
@@ -46,12 +81,7 @@ function runOpenClaw(args, opts = {}) {
46
81
  return { status: 1, stdout: '', stderr: 'OpenClaw CLI not found' }
47
82
  }
48
83
 
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
- })
84
+ return runWithRunner(runner, args, opts)
55
85
  }
56
86
 
57
87
  function spawnOpenClaw(args, opts = {}) {
@@ -74,37 +104,68 @@ function firstLine(text) {
74
104
  }
75
105
 
76
106
  function getOpenClawVersion(preferNpx = false) {
77
- const result = runOpenClaw(['--version'], { preferNpx, timeout: preferNpx ? 60000 : 15000 })
78
- if (result.status !== 0) return null
79
- return firstLine(result.stdout)
107
+ const runner = getRunner(preferNpx)
108
+ if (!runner) return null
109
+ return probeRunner(runner, preferNpx ? 60000 : 15000)
80
110
  }
81
111
 
82
112
  function detectRuntime() {
83
113
  const preferNpx = getPreferredRuntime()
84
- const preferredRunner = getRunner(preferNpx)
85
-
86
- if (preferredRunner) {
87
- return {
88
- available: true,
89
- via: preferredRunner.via,
90
- command: preferredRunner.label,
91
- version: getOpenClawVersion(preferNpx),
114
+ const runnerOrder = preferNpx ? [getRunner(true), getRunner(false)] : [getRunner(false), getRunner(true)]
115
+ const seen = new Set()
116
+
117
+ for (const runner of runnerOrder) {
118
+ if (!runner) continue
119
+ const key = `${runner.via}:${runner.cmd}:${runner.argsPrefix.join(' ')}`
120
+ if (seen.has(key)) continue
121
+ seen.add(key)
122
+
123
+ const version = probeRunner(runner, runner.via === 'npx' ? 60000 : 15000)
124
+ if (version) {
125
+ return {
126
+ available: true,
127
+ via: runner.via,
128
+ command: runner.label,
129
+ version,
130
+ }
92
131
  }
93
132
  }
94
133
 
95
- const fallbackRunner = getRunner(true)
134
+ const fallbackRunner = getRunner(preferNpx)
96
135
  if (fallbackRunner) {
97
136
  return {
98
- available: true,
137
+ available: false,
99
138
  via: fallbackRunner.via,
100
139
  command: fallbackRunner.label,
101
- version: getOpenClawVersion(true),
140
+ version: null,
102
141
  }
103
142
  }
104
143
 
105
144
  return { available: false, via: null, command: null, version: null }
106
145
  }
107
146
 
147
+ function isRoutingRegressionVersion(version) {
148
+ return OPENCLAW_ROUTING_REGRESSION_VERSION.test(String(version || '').trim())
149
+ }
150
+
151
+ function sanitizeSelectedModelsForRuntime(selectedModels, runtimeVersion) {
152
+ if (!isRoutingRegressionVersion(runtimeVersion)) {
153
+ return { models: selectedModels, warning: '' }
154
+ }
155
+
156
+ const originalModels = Array.isArray(selectedModels) ? selectedModels : []
157
+ const filteredModels = originalModels.filter((model) => !model.startsWith('MiniMax-'))
158
+
159
+ if (filteredModels.length === originalModels.length) {
160
+ return { models: selectedModels, warning: '' }
161
+ }
162
+
163
+ return {
164
+ models: filteredModels,
165
+ warning: '当前 OpenClaw 2026.3.13 存在 provider 路由回归,已暂时跳过 MiniMax 模型,避免 /model 和网页切换时报 model not allowed。升级 OpenClaw 后可重新运行 hs setup 恢复。',
166
+ }
167
+ }
168
+
108
169
  function readConfig() {
109
170
  try {
110
171
  if (fs.existsSync(CONFIG_FILE)) {
@@ -213,7 +274,7 @@ function buildModelEntry(id) {
213
274
  function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModels) {
214
275
  const requestedModels = Array.isArray(selectedModels) && selectedModels.length > 0
215
276
  ? selectedModels
216
- : [OPENCLAW_DEFAULT_MODEL, OPENCLAW_DEFAULT_CLAUDE_MODEL]
277
+ : [OPENCLAW_DEFAULT_MODEL, OPENCLAW_DEFAULT_CLAUDE_MODEL, OPENCLAW_DEFAULT_MINIMAX_MODEL]
217
278
 
218
279
  const openaiModels = requestedModels.filter((model) => model.startsWith('gpt-'))
219
280
  if (!openaiModels.includes(OPENCLAW_DEFAULT_MODEL)) {
@@ -225,8 +286,14 @@ function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModel
225
286
  claudeModels.push(OPENCLAW_DEFAULT_CLAUDE_MODEL)
226
287
  }
227
288
 
289
+ const minimaxModels = requestedModels.filter((model) => model.startsWith('MiniMax-'))
290
+ if (requestedModels.includes(OPENCLAW_DEFAULT_MINIMAX_MODEL) && !minimaxModels.includes(OPENCLAW_DEFAULT_MINIMAX_MODEL)) {
291
+ minimaxModels.unshift(OPENCLAW_DEFAULT_MINIMAX_MODEL)
292
+ }
293
+
228
294
  const openaiProviderName = buildProviderName(baseUrlOpenAI, 'custom-openai')
229
295
  const anthropicProviderName = buildProviderName(baseUrlAnthropic, 'custom-anthropic')
296
+ const minimaxProviderName = buildProviderName(`${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`, 'custom-anthropic')
230
297
 
231
298
  const providers = {
232
299
  [openaiProviderName]: {
@@ -243,9 +310,19 @@ function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModel
243
310
  },
244
311
  }
245
312
 
313
+ if (minimaxModels.length > 0) {
314
+ providers[minimaxProviderName] = {
315
+ baseUrl: `${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`,
316
+ apiKey,
317
+ api: 'anthropic-messages',
318
+ models: minimaxModels.map(buildModelEntry),
319
+ }
320
+ }
321
+
246
322
  const managedModelRefs = [
247
323
  ...openaiModels.map((id) => `${openaiProviderName}/${id}`),
248
324
  ...claudeModels.map((id) => `${anthropicProviderName}/${id}`),
325
+ ...minimaxModels.map((id) => `${minimaxProviderName}/${id}`),
249
326
  ]
250
327
 
251
328
  return {
@@ -408,6 +485,11 @@ module.exports = {
408
485
  }
409
486
  this._lastRuntimeCommand = runtime.command
410
487
 
488
+ const sanitizedSelection = sanitizeSelectedModelsForRuntime(selectedModels, runtime.version)
489
+ if (sanitizedSelection.warning) {
490
+ console.log(chalk.yellow(` ⚠️ ${sanitizedSelection.warning}`))
491
+ }
492
+
411
493
  runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
412
494
 
413
495
  const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
@@ -453,7 +535,7 @@ module.exports = {
453
535
  apiKey,
454
536
  baseUrlAnthropic,
455
537
  baseUrlOpenAI,
456
- selectedModels,
538
+ sanitizedSelection.models,
457
539
  gatewayPort,
458
540
  )
459
541
 
@@ -4,18 +4,34 @@
4
4
  */
5
5
  const { execSync } = require('child_process')
6
6
 
7
- function commandExists(cmd) {
8
- const finder = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`
7
+ function canRun(command, options = {}) {
9
8
  try {
10
- execSync(finder, { stdio: 'ignore' })
9
+ execSync(command, { stdio: 'ignore', ...options })
11
10
  return true
12
11
  } catch {
13
- // 兜底:直接跑 --version
14
- try {
15
- execSync(`${cmd} --version`, { stdio: 'ignore', timeout: 3000 })
16
- return true
17
- } catch { return false }
12
+ return false
18
13
  }
19
14
  }
20
15
 
16
+ function commandExists(cmd) {
17
+ if (process.platform === 'win32') {
18
+ const variants = [cmd, `${cmd}.cmd`, `${cmd}.exe`, `${cmd}.bat`]
19
+ for (const variant of variants) {
20
+ if (canRun(`where ${variant}`)) return true
21
+ }
22
+
23
+ // Windows 上很多 npm 全局命令实际是 .cmd 包装器,需要交给 cmd.exe 执行。
24
+ for (const variant of variants) {
25
+ if (canRun(`cmd /d /s /c "${variant} --version"`, { timeout: 3000 })) return true
26
+ }
27
+
28
+ return false
29
+ }
30
+
31
+ if (canRun(`which ${cmd}`)) return true
32
+
33
+ // 兜底:直接跑 --version
34
+ return canRun(`${cmd} --version`, { timeout: 3000 })
35
+ }
36
+
21
37
  module.exports = { commandExists }