@simonyea/holysheep-cli 1.6.7 → 1.6.9
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 +2 -0
- package/package.json +1 -1
- package/src/index.js +0 -5
- package/src/tools/codex.js +62 -18
- package/src/tools/droid.js +17 -56
- package/src/tools/openclaw.js +82 -26
- package/src/utils/which.js +24 -8
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.9** — 保留 OpenClaw 的 MiniMax 配置,并为 MiniMax 使用独立 provider id,避免与 Claude provider 冲突;在 OpenClaw 2026.3.13 下改为提示精确 `/model` 切换命令,而不是停止配置 MiniMax
|
|
222
|
+
- **v1.6.8** — 修复 Codex 重复写入 `config.toml` 导致的 duplicate key,并修复 OpenClaw 在 Windows 下的安装检测;针对 OpenClaw 2026.3.13 的模型路由回归,临时跳过 MiniMax 避免 `model not allowed`
|
|
221
223
|
- **v1.6.7** — OpenClaw 配置新增 `MiniMax-M2.7-highspeed`,并补齐节点迁移脚本中的 SSH 代理账号创建逻辑
|
|
222
224
|
- **v1.6.6** — 修复 Droid CLI 的 GPT-5.4 配置残留问题,同时同步 `~/.factory/settings.json` 和 `~/.factory/config.json`,统一使用 `openai + https://api.holysheep.ai/v1`
|
|
223
225
|
- **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.9",
|
|
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/droid.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 配置文件: ~/.factory/settings.json
|
|
4
4
|
*
|
|
5
5
|
* 使用 Droid 原生 customModels 配置 HolySheep 的多个模型入口:
|
|
6
|
-
* - GPT 走 OpenAI 兼容入口: https://api.holysheep.ai/
|
|
6
|
+
* - GPT 走 OpenAI 兼容入口: https://api.holysheep.ai/openai
|
|
7
7
|
* - Claude 走 Anthropic 入口: https://api.holysheep.ai
|
|
8
8
|
* - MiniMax 走 Anthropic 入口: https://api.holysheep.ai/minimax
|
|
9
9
|
*/
|
|
@@ -13,13 +13,12 @@ const os = require('os')
|
|
|
13
13
|
|
|
14
14
|
const CONFIG_DIR = path.join(os.homedir(), '.factory')
|
|
15
15
|
const SETTINGS_FILE = path.join(CONFIG_DIR, 'settings.json')
|
|
16
|
-
const LEGACY_CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
17
16
|
|
|
18
17
|
const DEFAULT_MODELS = [
|
|
19
18
|
{
|
|
20
19
|
model: 'gpt-5.4',
|
|
21
20
|
id: 'custom:gpt-5.4-0',
|
|
22
|
-
baseUrlSuffix: '',
|
|
21
|
+
baseUrlSuffix: '/openai',
|
|
23
22
|
displayName: 'GPT-5.4',
|
|
24
23
|
provider: 'openai',
|
|
25
24
|
},
|
|
@@ -67,24 +66,6 @@ function writeSettings(data) {
|
|
|
67
66
|
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2), 'utf8')
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
function readLegacyConfig() {
|
|
71
|
-
try {
|
|
72
|
-
if (fs.existsSync(LEGACY_CONFIG_FILE)) {
|
|
73
|
-
return JSON.parse(fs.readFileSync(LEGACY_CONFIG_FILE, 'utf8'))
|
|
74
|
-
}
|
|
75
|
-
} catch {}
|
|
76
|
-
return {}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function writeLegacyConfig(data) {
|
|
80
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
81
|
-
fs.writeFileSync(LEGACY_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function isHolySheepModel(item) {
|
|
85
|
-
return typeof item?.baseUrl === 'string' && item.baseUrl.includes('api.holysheep.ai')
|
|
86
|
-
}
|
|
87
|
-
|
|
88
69
|
function normalizeSelectedModels(selectedModels) {
|
|
89
70
|
const selected = new Set(
|
|
90
71
|
Array.isArray(selectedModels) && selectedModels.length > 0
|
|
@@ -103,17 +84,13 @@ function normalizeSelectedModels(selectedModels) {
|
|
|
103
84
|
return models.length > 0 ? models : DEFAULT_MODELS.map((item, index) => ({ ...item, index }))
|
|
104
85
|
}
|
|
105
86
|
|
|
106
|
-
function buildCustomModels(apiKey, baseUrlAnthropic,
|
|
107
|
-
const
|
|
108
|
-
const openaiRootUrl = String(baseUrlOpenAI || '').replace(/\/+$/, '')
|
|
87
|
+
function buildCustomModels(apiKey, baseUrlAnthropic, selectedModels) {
|
|
88
|
+
const rootUrl = String(baseUrlAnthropic || '').replace(/\/+$/, '')
|
|
109
89
|
return normalizeSelectedModels(selectedModels).map((item) => ({
|
|
110
90
|
model: item.model,
|
|
111
91
|
id: item.id,
|
|
112
92
|
index: item.index,
|
|
113
|
-
baseUrl:
|
|
114
|
-
item.provider === 'openai'
|
|
115
|
-
? `${openaiRootUrl}${item.baseUrlSuffix}`
|
|
116
|
-
: `${anthropicRootUrl}${item.baseUrlSuffix}`,
|
|
93
|
+
baseUrl: `${rootUrl}${item.baseUrlSuffix}`,
|
|
117
94
|
apiKey,
|
|
118
95
|
displayName: item.displayName,
|
|
119
96
|
maxOutputTokens: 64000,
|
|
@@ -131,37 +108,25 @@ module.exports = {
|
|
|
131
108
|
isConfigured() {
|
|
132
109
|
const settings = readSettings()
|
|
133
110
|
const customModels = Array.isArray(settings.customModels) ? settings.customModels : []
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const legacyModels = Array.isArray(legacy.customModels) ? legacy.customModels : []
|
|
138
|
-
return legacyModels.some(isHolySheepModel)
|
|
111
|
+
return customModels.some((item) =>
|
|
112
|
+
typeof item.baseUrl === 'string' && item.baseUrl.includes('api.holysheep.ai')
|
|
113
|
+
)
|
|
139
114
|
},
|
|
140
|
-
configure(apiKey, baseUrlAnthropic,
|
|
141
|
-
const nextModels = buildCustomModels(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModels)
|
|
142
|
-
|
|
115
|
+
configure(apiKey, baseUrlAnthropic, _baseUrlOpenAI, _primaryModel, selectedModels) {
|
|
143
116
|
const settings = readSettings()
|
|
144
117
|
const preservedModels = Array.isArray(settings.customModels)
|
|
145
|
-
? settings.customModels.filter(
|
|
118
|
+
? settings.customModels.filter(
|
|
119
|
+
(item) => !(typeof item.baseUrl === 'string' && item.baseUrl.includes('api.holysheep.ai'))
|
|
120
|
+
)
|
|
146
121
|
: []
|
|
122
|
+
|
|
147
123
|
settings.customModels = [
|
|
148
|
-
...
|
|
124
|
+
...buildCustomModels(apiKey, baseUrlAnthropic, selectedModels),
|
|
149
125
|
...preservedModels,
|
|
150
126
|
]
|
|
151
127
|
settings.logoAnimation = 'off'
|
|
152
128
|
writeSettings(settings)
|
|
153
129
|
|
|
154
|
-
const legacy = readLegacyConfig()
|
|
155
|
-
const preservedLegacyModels = Array.isArray(legacy.customModels)
|
|
156
|
-
? legacy.customModels.filter((item) => !isHolySheepModel(item))
|
|
157
|
-
: []
|
|
158
|
-
legacy.customModels = [
|
|
159
|
-
...nextModels,
|
|
160
|
-
...preservedLegacyModels,
|
|
161
|
-
]
|
|
162
|
-
legacy.logoAnimation = 'off'
|
|
163
|
-
writeLegacyConfig(legacy)
|
|
164
|
-
|
|
165
130
|
return {
|
|
166
131
|
file: SETTINGS_FILE,
|
|
167
132
|
hot: true,
|
|
@@ -170,15 +135,11 @@ module.exports = {
|
|
|
170
135
|
reset() {
|
|
171
136
|
const settings = readSettings()
|
|
172
137
|
if (Array.isArray(settings.customModels)) {
|
|
173
|
-
settings.customModels = settings.customModels.filter(
|
|
138
|
+
settings.customModels = settings.customModels.filter(
|
|
139
|
+
(item) => !(typeof item.baseUrl === 'string' && item.baseUrl.includes('api.holysheep.ai'))
|
|
140
|
+
)
|
|
174
141
|
}
|
|
175
142
|
writeSettings(settings)
|
|
176
|
-
|
|
177
|
-
const legacy = readLegacyConfig()
|
|
178
|
-
if (Array.isArray(legacy.customModels)) {
|
|
179
|
-
legacy.customModels = legacy.customModels.filter((item) => !isHolySheepModel(item))
|
|
180
|
-
}
|
|
181
|
-
writeLegacyConfig(legacy)
|
|
182
143
|
},
|
|
183
144
|
getConfigPath() { return SETTINGS_FILE },
|
|
184
145
|
hint: '已写入 ~/.factory/settings.json;重启 Droid 后可见 HolySheep 模型列表',
|
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,58 @@ 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 getRoutingRegressionWarning(runtimeVersion, minimaxModelRef) {
|
|
152
|
+
if (!isRoutingRegressionVersion(runtimeVersion) || !minimaxModelRef) {
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return `当前 OpenClaw 2026.3.13 存在 provider 路由回归,但 HolySheep 仍会保留 MiniMax 配置。若网页模型切换失败,请直接输入 /model ${minimaxModelRef},或升级 OpenClaw 后再试。`
|
|
157
|
+
}
|
|
158
|
+
|
|
109
159
|
function readConfig() {
|
|
110
160
|
try {
|
|
111
161
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
@@ -233,7 +283,7 @@ function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModel
|
|
|
233
283
|
|
|
234
284
|
const openaiProviderName = buildProviderName(baseUrlOpenAI, 'custom-openai')
|
|
235
285
|
const anthropicProviderName = buildProviderName(baseUrlAnthropic, 'custom-anthropic')
|
|
236
|
-
const minimaxProviderName = buildProviderName(`${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`, 'custom-
|
|
286
|
+
const minimaxProviderName = buildProviderName(`${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`, 'custom-minimax')
|
|
237
287
|
|
|
238
288
|
const providers = {
|
|
239
289
|
[openaiProviderName]: {
|
|
@@ -269,6 +319,7 @@ function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModel
|
|
|
269
319
|
providers,
|
|
270
320
|
managedModelRefs,
|
|
271
321
|
primaryRef: `${openaiProviderName}/${OPENCLAW_DEFAULT_MODEL}`,
|
|
322
|
+
minimaxRef: minimaxModels[0] ? `${minimaxProviderName}/${minimaxModels[0]}` : '',
|
|
272
323
|
}
|
|
273
324
|
}
|
|
274
325
|
|
|
@@ -465,7 +516,7 @@ module.exports = {
|
|
|
465
516
|
console.log(chalk.yellow(' ⚠️ onboard 失败,使用备用配置...'))
|
|
466
517
|
}
|
|
467
518
|
|
|
468
|
-
writeManagedConfig(
|
|
519
|
+
const plan = writeManagedConfig(
|
|
469
520
|
result.status === 0 ? readConfig() : {},
|
|
470
521
|
apiKey,
|
|
471
522
|
baseUrlAnthropic,
|
|
@@ -474,6 +525,11 @@ module.exports = {
|
|
|
474
525
|
gatewayPort,
|
|
475
526
|
)
|
|
476
527
|
|
|
528
|
+
const routingRegressionWarning = getRoutingRegressionWarning(runtime.version, plan.minimaxRef)
|
|
529
|
+
if (routingRegressionWarning) {
|
|
530
|
+
console.log(chalk.yellow(` ⚠️ ${routingRegressionWarning}`))
|
|
531
|
+
}
|
|
532
|
+
|
|
477
533
|
_disableGatewayAuth(runtime.via === 'npx')
|
|
478
534
|
const serviceReady = _installGatewayService(gatewayPort, runtime.via === 'npx')
|
|
479
535
|
|
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 }
|