@simonyea/holysheep-cli 1.6.7 → 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,7 @@ 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`
221
222
  - **v1.6.7** — OpenClaw 配置新增 `MiniMax-M2.7-highspeed`,并补齐节点迁移脚本中的 SSH 代理账号创建逻辑
222
223
  - **v1.6.6** — 修复 Droid CLI 的 GPT-5.4 配置残留问题,同时同步 `~/.factory/settings.json` 和 `~/.factory/config.json`,统一使用 `openai + https://api.holysheep.ai/v1`
223
224
  - **v1.6.5** — 修复 HolySheep 对 Droid Responses API 的兼容
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.6.7",
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
  }
@@ -18,9 +18,20 @@ 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
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
+ }
21
32
 
22
33
  function hasOpenClawBinary() {
23
- return commandExists('openclaw')
34
+ return getOpenClawBinaryCandidates().some((cmd) => commandExists(cmd))
24
35
  }
25
36
 
26
37
  function hasNpx() {
@@ -28,18 +39,41 @@ function hasNpx() {
28
39
  }
29
40
 
30
41
  function getRunner(preferNpx = false) {
42
+ const binaryRunner = hasOpenClawBinary() ? getBinaryRunner() : null
43
+
31
44
  if (!preferNpx && hasOpenClawBinary()) {
32
- return { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
45
+ return binaryRunner
33
46
  }
34
47
  if (hasNpx()) {
35
48
  return { cmd: 'npx', argsPrefix: ['openclaw'], shell: isWin, label: 'npx openclaw', via: 'npx' }
36
49
  }
37
- if (hasOpenClawBinary()) {
38
- return { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
50
+ if (binaryRunner) {
51
+ return binaryRunner
39
52
  }
40
53
  return null
41
54
  }
42
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
+
43
77
  /** 运行 openclaw CLI(优先全局命令,可切换到 npx 回退) */
44
78
  function runOpenClaw(args, opts = {}) {
45
79
  const runner = getRunner(Boolean(opts.preferNpx))
@@ -47,12 +81,7 @@ function runOpenClaw(args, opts = {}) {
47
81
  return { status: 1, stdout: '', stderr: 'OpenClaw CLI not found' }
48
82
  }
49
83
 
50
- return spawnSync(runner.cmd, [...runner.argsPrefix, ...args], {
51
- shell: runner.shell,
52
- timeout: opts.timeout || 30000,
53
- stdio: opts.stdio || 'pipe',
54
- encoding: 'utf8',
55
- })
84
+ return runWithRunner(runner, args, opts)
56
85
  }
57
86
 
58
87
  function spawnOpenClaw(args, opts = {}) {
@@ -75,37 +104,68 @@ function firstLine(text) {
75
104
  }
76
105
 
77
106
  function getOpenClawVersion(preferNpx = false) {
78
- const result = runOpenClaw(['--version'], { preferNpx, timeout: preferNpx ? 60000 : 15000 })
79
- if (result.status !== 0) return null
80
- return firstLine(result.stdout)
107
+ const runner = getRunner(preferNpx)
108
+ if (!runner) return null
109
+ return probeRunner(runner, preferNpx ? 60000 : 15000)
81
110
  }
82
111
 
83
112
  function detectRuntime() {
84
113
  const preferNpx = getPreferredRuntime()
85
- const preferredRunner = getRunner(preferNpx)
86
-
87
- if (preferredRunner) {
88
- return {
89
- available: true,
90
- via: preferredRunner.via,
91
- command: preferredRunner.label,
92
- 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
+ }
93
131
  }
94
132
  }
95
133
 
96
- const fallbackRunner = getRunner(true)
134
+ const fallbackRunner = getRunner(preferNpx)
97
135
  if (fallbackRunner) {
98
136
  return {
99
- available: true,
137
+ available: false,
100
138
  via: fallbackRunner.via,
101
139
  command: fallbackRunner.label,
102
- version: getOpenClawVersion(true),
140
+ version: null,
103
141
  }
104
142
  }
105
143
 
106
144
  return { available: false, via: null, command: null, version: null }
107
145
  }
108
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
+
109
169
  function readConfig() {
110
170
  try {
111
171
  if (fs.existsSync(CONFIG_FILE)) {
@@ -425,6 +485,11 @@ module.exports = {
425
485
  }
426
486
  this._lastRuntimeCommand = runtime.command
427
487
 
488
+ const sanitizedSelection = sanitizeSelectedModelsForRuntime(selectedModels, runtime.version)
489
+ if (sanitizedSelection.warning) {
490
+ console.log(chalk.yellow(` ⚠️ ${sanitizedSelection.warning}`))
491
+ }
492
+
428
493
  runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
429
494
 
430
495
  const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
@@ -470,7 +535,7 @@ module.exports = {
470
535
  apiKey,
471
536
  baseUrlAnthropic,
472
537
  baseUrlOpenAI,
473
- selectedModels,
538
+ sanitizedSelection.models,
474
539
  gatewayPort,
475
540
  )
476
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 }