@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 +1 -0
- package/package.json +1 -1
- package/src/index.js +0 -5
- package/src/tools/codex.js +62 -18
- package/src/tools/openclaw.js +90 -25
- package/src/utils/which.js +24 -8
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.
|
|
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
package/src/tools/codex.js
CHANGED
|
@@ -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
|
-
|
|
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')
|
|
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
|
-
|
|
136
|
-
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
|
}
|
package/src/tools/openclaw.js
CHANGED
|
@@ -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(
|
|
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
|
|
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 (
|
|
38
|
-
return
|
|
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
|
|
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
|
|
79
|
-
if (
|
|
80
|
-
return
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
134
|
+
const fallbackRunner = getRunner(preferNpx)
|
|
97
135
|
if (fallbackRunner) {
|
|
98
136
|
return {
|
|
99
|
-
available:
|
|
137
|
+
available: false,
|
|
100
138
|
via: fallbackRunner.via,
|
|
101
139
|
command: fallbackRunner.label,
|
|
102
|
-
version:
|
|
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
|
-
|
|
538
|
+
sanitizedSelection.models,
|
|
474
539
|
gatewayPort,
|
|
475
540
|
)
|
|
476
541
|
|
package/src/utils/which.js
CHANGED
|
@@ -4,18 +4,34 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const { execSync } = require('child_process')
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
const finder = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`
|
|
7
|
+
function canRun(command, options = {}) {
|
|
9
8
|
try {
|
|
10
|
-
execSync(
|
|
9
|
+
execSync(command, { stdio: 'ignore', ...options })
|
|
11
10
|
return true
|
|
12
11
|
} catch {
|
|
13
|
-
|
|
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 }
|