@simonyea/holysheep-cli 1.6.10 → 1.6.12
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 +13 -0
- package/src/tools/openclaw-bridge.js +546 -0
- package/src/tools/openclaw.js +130 -87
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.12** — 修复 OpenClaw Bridge 对 GPT-5.4 的流式响应转换,避免 `holysheep/gpt-5.4` 在 OpenClaw 中报错;同时增强 Dashboard URL 解析,减少安装后浏览器打开黑屏/空白页
|
|
222
|
+
- **v1.6.11** — OpenClaw 新增本地 HolySheep Bridge,统一暴露单一 `holysheep` provider 以支持自由切换 GPT / Claude / MiniMax;同时保留用户所选默认模型,不再强制 GPT-5.4 作为 primary
|
|
221
223
|
- **v1.6.10** — 将可运行的 OpenClaw runtime(含 npx 回退)视为已安装,避免 Windows/Node 环境下重复提示安装;同时修复 Droid CLI 的 GPT `/v1` 接入地址并同步写入 `~/.factory/config.json`
|
|
222
224
|
- **v1.6.9** — 保留 OpenClaw 的 MiniMax 配置,并为 MiniMax 使用独立 provider id,避免与 Claude provider 冲突;在 OpenClaw 2026.3.13 下改为提示精确 `/model` 切换命令,而不是停止配置 MiniMax
|
|
223
225
|
- **v1.6.8** — 修复 Codex 重复写入 `config.toml` 导致的 duplicate key,并修复 OpenClaw 在 Windows 下的安装检测;针对 OpenClaw 2026.3.13 的模型路由回归,临时跳过 MiniMax 避免 `model not allowed`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.12",
|
|
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
|
@@ -154,6 +154,19 @@ program
|
|
|
154
154
|
})
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
+
// ── openclaw-bridge ──────────────────────────────────────────────────────────
|
|
158
|
+
program
|
|
159
|
+
.command('openclaw-bridge')
|
|
160
|
+
.description('启动 HolySheep 的 OpenClaw 本地桥接服务')
|
|
161
|
+
.option('--port <port>', '指定桥接服务端口')
|
|
162
|
+
.action((opts) => {
|
|
163
|
+
const { startBridge } = require('./tools/openclaw-bridge')
|
|
164
|
+
startBridge({
|
|
165
|
+
port: opts.port ? Number(opts.port) : null,
|
|
166
|
+
host: '127.0.0.1',
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
157
170
|
// 默认:无命令时显示帮助 + 提示 setup
|
|
158
171
|
program
|
|
159
172
|
.action(() => {
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const http = require('http')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const os = require('os')
|
|
8
|
+
const fetch = global.fetch || require('node-fetch')
|
|
9
|
+
|
|
10
|
+
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw')
|
|
11
|
+
const BRIDGE_CONFIG_FILE = path.join(OPENCLAW_DIR, 'holysheep-bridge.json')
|
|
12
|
+
|
|
13
|
+
function readBridgeConfig(configPath = BRIDGE_CONFIG_FILE) {
|
|
14
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const args = { port: null, host: '127.0.0.1', config: BRIDGE_CONFIG_FILE }
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const value = argv[i]
|
|
21
|
+
if (value === '--port') args.port = Number(argv[++i])
|
|
22
|
+
else if (value === '--host') args.host = argv[++i]
|
|
23
|
+
else if (value === '--config') args.config = argv[++i]
|
|
24
|
+
}
|
|
25
|
+
return args
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJsonBody(req) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let raw = ''
|
|
31
|
+
req.on('data', (chunk) => {
|
|
32
|
+
raw += chunk
|
|
33
|
+
if (raw.length > 5 * 1024 * 1024) {
|
|
34
|
+
reject(new Error('Request body too large'))
|
|
35
|
+
req.destroy()
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
req.on('end', () => {
|
|
39
|
+
if (!raw) return resolve({})
|
|
40
|
+
try {
|
|
41
|
+
resolve(JSON.parse(raw))
|
|
42
|
+
} catch (error) {
|
|
43
|
+
reject(error)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
req.on('error', reject)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sendJson(res, statusCode, payload) {
|
|
51
|
+
res.writeHead(statusCode, {
|
|
52
|
+
'content-type': 'application/json; charset=utf-8',
|
|
53
|
+
'cache-control': 'no-store',
|
|
54
|
+
})
|
|
55
|
+
res.end(JSON.stringify(payload))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sendOpenAIStream(res, payload) {
|
|
59
|
+
const choice = payload.choices?.[0] || {}
|
|
60
|
+
const message = choice.message || {}
|
|
61
|
+
const created = payload.created || Math.floor(Date.now() / 1000)
|
|
62
|
+
|
|
63
|
+
res.writeHead(200, {
|
|
64
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
65
|
+
'cache-control': 'no-cache, no-transform',
|
|
66
|
+
connection: 'keep-alive',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const firstChunk = {
|
|
70
|
+
id: payload.id,
|
|
71
|
+
object: 'chat.completion.chunk',
|
|
72
|
+
created,
|
|
73
|
+
model: payload.model,
|
|
74
|
+
choices: [{
|
|
75
|
+
index: 0,
|
|
76
|
+
delta: {
|
|
77
|
+
role: 'assistant',
|
|
78
|
+
...(message.content ? { content: message.content } : {}),
|
|
79
|
+
...(message.tool_calls ? { tool_calls: message.tool_calls } : {}),
|
|
80
|
+
},
|
|
81
|
+
finish_reason: null,
|
|
82
|
+
}],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const finalChunk = {
|
|
86
|
+
id: payload.id,
|
|
87
|
+
object: 'chat.completion.chunk',
|
|
88
|
+
created,
|
|
89
|
+
model: payload.model,
|
|
90
|
+
choices: [{ index: 0, delta: {}, finish_reason: choice.finish_reason || 'stop' }],
|
|
91
|
+
usage: payload.usage,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
res.write(`data: ${JSON.stringify(firstChunk)}\n\n`)
|
|
95
|
+
res.write(`data: ${JSON.stringify(finalChunk)}\n\n`)
|
|
96
|
+
res.end('data: [DONE]\n\n')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeText(value) {
|
|
100
|
+
if (typeof value === 'string') return value
|
|
101
|
+
if (Array.isArray(value)) return value.map(normalizeText).filter(Boolean).join('\n')
|
|
102
|
+
if (value && typeof value === 'object') {
|
|
103
|
+
if (typeof value.text === 'string') return value.text
|
|
104
|
+
if (typeof value.content === 'string') return value.content
|
|
105
|
+
}
|
|
106
|
+
return value == null ? '' : String(value)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseDataUrl(url) {
|
|
110
|
+
const match = String(url || '').match(/^data:([^;]+);base64,(.+)$/)
|
|
111
|
+
if (!match) return null
|
|
112
|
+
return { mediaType: match[1], data: match[2] }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function openAIContentToAnthropicBlocks(content) {
|
|
116
|
+
if (typeof content === 'string') return [{ type: 'text', text: content }]
|
|
117
|
+
if (!Array.isArray(content)) return []
|
|
118
|
+
|
|
119
|
+
const blocks = []
|
|
120
|
+
for (const part of content) {
|
|
121
|
+
if (!part) continue
|
|
122
|
+
if (part.type === 'text' && typeof part.text === 'string') {
|
|
123
|
+
blocks.push({ type: 'text', text: part.text })
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (part.type === 'image_url' && part.image_url?.url) {
|
|
127
|
+
const dataUrl = parseDataUrl(part.image_url.url)
|
|
128
|
+
if (dataUrl) {
|
|
129
|
+
blocks.push({
|
|
130
|
+
type: 'image',
|
|
131
|
+
source: { type: 'base64', media_type: dataUrl.mediaType, data: dataUrl.data },
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return blocks
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pushAnthropicMessage(messages, role, blocks) {
|
|
140
|
+
if (!blocks.length) return
|
|
141
|
+
const previous = messages[messages.length - 1]
|
|
142
|
+
if (previous && previous.role === role) {
|
|
143
|
+
previous.content = previous.content.concat(blocks)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
messages.push({ role, content: blocks })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function convertOpenAIToAnthropicMessages(messages) {
|
|
150
|
+
const anthropicMessages = []
|
|
151
|
+
const systemParts = []
|
|
152
|
+
|
|
153
|
+
for (const message of messages || []) {
|
|
154
|
+
if (!message) continue
|
|
155
|
+
|
|
156
|
+
if (message.role === 'system') {
|
|
157
|
+
const blocks = openAIContentToAnthropicBlocks(message.content)
|
|
158
|
+
if (blocks.length === 0) {
|
|
159
|
+
const text = normalizeText(message.content)
|
|
160
|
+
if (text) systemParts.push(text)
|
|
161
|
+
} else {
|
|
162
|
+
for (const block of blocks) {
|
|
163
|
+
if (block.type === 'text') systemParts.push(block.text)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (message.role === 'tool') {
|
|
170
|
+
pushAnthropicMessage(anthropicMessages, 'user', [{
|
|
171
|
+
type: 'tool_result',
|
|
172
|
+
tool_use_id: message.tool_call_id,
|
|
173
|
+
content: normalizeText(message.content),
|
|
174
|
+
}])
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (message.role === 'assistant') {
|
|
179
|
+
const blocks = []
|
|
180
|
+
const textBlocks = openAIContentToAnthropicBlocks(message.content)
|
|
181
|
+
if (textBlocks.length) blocks.push(...textBlocks)
|
|
182
|
+
else if (typeof message.content === 'string' && message.content) blocks.push({ type: 'text', text: message.content })
|
|
183
|
+
|
|
184
|
+
for (const toolCall of message.tool_calls || []) {
|
|
185
|
+
let input = {}
|
|
186
|
+
try {
|
|
187
|
+
input = JSON.parse(toolCall.function?.arguments || '{}')
|
|
188
|
+
} catch {}
|
|
189
|
+
blocks.push({
|
|
190
|
+
type: 'tool_use',
|
|
191
|
+
id: toolCall.id,
|
|
192
|
+
name: toolCall.function?.name || 'tool',
|
|
193
|
+
input,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
pushAnthropicMessage(anthropicMessages, 'assistant', blocks)
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const blocks = openAIContentToAnthropicBlocks(message.content)
|
|
202
|
+
if (blocks.length) pushAnthropicMessage(anthropicMessages, 'user', blocks)
|
|
203
|
+
else {
|
|
204
|
+
const text = normalizeText(message.content)
|
|
205
|
+
if (text) pushAnthropicMessage(anthropicMessages, 'user', [{ type: 'text', text }])
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
system: systemParts.join('\n\n').trim() || undefined,
|
|
211
|
+
messages: anthropicMessages,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function convertOpenAIToolsToAnthropic(tools) {
|
|
216
|
+
return (tools || [])
|
|
217
|
+
.filter((tool) => tool?.type === 'function' && tool.function?.name)
|
|
218
|
+
.map((tool) => ({
|
|
219
|
+
name: tool.function.name,
|
|
220
|
+
description: tool.function.description || '',
|
|
221
|
+
input_schema: tool.function.parameters || { type: 'object', properties: {} },
|
|
222
|
+
}))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function convertToolChoice(toolChoice) {
|
|
226
|
+
if (!toolChoice || toolChoice === 'auto') return { type: 'auto' }
|
|
227
|
+
if (toolChoice === 'none') return { type: 'auto', disable_parallel_tool_use: true }
|
|
228
|
+
if (toolChoice === 'required') return { type: 'any' }
|
|
229
|
+
if (toolChoice.type === 'function' && toolChoice.function?.name) {
|
|
230
|
+
return { type: 'tool', name: toolChoice.function.name }
|
|
231
|
+
}
|
|
232
|
+
return { type: 'auto' }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildAnthropicPayload(requestBody) {
|
|
236
|
+
const converted = convertOpenAIToAnthropicMessages(requestBody.messages)
|
|
237
|
+
const payload = {
|
|
238
|
+
model: requestBody.model,
|
|
239
|
+
max_tokens: requestBody.max_tokens || requestBody.max_completion_tokens || requestBody.max_output_tokens || 4096,
|
|
240
|
+
messages: converted.messages,
|
|
241
|
+
stream: false,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (converted.system) payload.system = converted.system
|
|
245
|
+
if (requestBody.temperature != null) payload.temperature = requestBody.temperature
|
|
246
|
+
if (requestBody.top_p != null) payload.top_p = requestBody.top_p
|
|
247
|
+
if (Array.isArray(requestBody.stop) && requestBody.stop.length) payload.stop_sequences = requestBody.stop
|
|
248
|
+
if (typeof requestBody.stop === 'string') payload.stop_sequences = [requestBody.stop]
|
|
249
|
+
|
|
250
|
+
const tools = convertOpenAIToolsToAnthropic(requestBody.tools)
|
|
251
|
+
if (tools.length) payload.tools = tools
|
|
252
|
+
if (requestBody.tool_choice) payload.tool_choice = convertToolChoice(requestBody.tool_choice)
|
|
253
|
+
|
|
254
|
+
return payload
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function mapFinishReason(stopReason) {
|
|
258
|
+
if (stopReason === 'tool_use') return 'tool_calls'
|
|
259
|
+
if (stopReason === 'max_tokens') return 'length'
|
|
260
|
+
return 'stop'
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildToolCalls(content) {
|
|
264
|
+
const calls = []
|
|
265
|
+
for (const block of content || []) {
|
|
266
|
+
if (block?.type !== 'tool_use') continue
|
|
267
|
+
calls.push({
|
|
268
|
+
id: block.id,
|
|
269
|
+
type: 'function',
|
|
270
|
+
function: {
|
|
271
|
+
name: block.name,
|
|
272
|
+
arguments: JSON.stringify(block.input || {}),
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
return calls
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function anthropicToOpenAIResponse(responseBody, requestedModel) {
|
|
280
|
+
const text = (responseBody.content || [])
|
|
281
|
+
.filter((block) => block?.type === 'text')
|
|
282
|
+
.map((block) => block.text)
|
|
283
|
+
.join('')
|
|
284
|
+
const toolCalls = buildToolCalls(responseBody.content)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
id: responseBody.id || `chatcmpl_${Date.now()}`,
|
|
288
|
+
object: 'chat.completion',
|
|
289
|
+
created: Math.floor(Date.now() / 1000),
|
|
290
|
+
model: requestedModel,
|
|
291
|
+
choices: [{
|
|
292
|
+
index: 0,
|
|
293
|
+
message: {
|
|
294
|
+
role: 'assistant',
|
|
295
|
+
content: text || null,
|
|
296
|
+
...(toolCalls.length ? { tool_calls: toolCalls } : {}),
|
|
297
|
+
},
|
|
298
|
+
finish_reason: mapFinishReason(responseBody.stop_reason),
|
|
299
|
+
}],
|
|
300
|
+
usage: responseBody.usage
|
|
301
|
+
? {
|
|
302
|
+
prompt_tokens: responseBody.usage.input_tokens || 0,
|
|
303
|
+
completion_tokens: responseBody.usage.output_tokens || 0,
|
|
304
|
+
total_tokens: (responseBody.usage.input_tokens || 0) + (responseBody.usage.output_tokens || 0),
|
|
305
|
+
}
|
|
306
|
+
: undefined,
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function pickRoute(model) {
|
|
311
|
+
if (String(model).startsWith('gpt-')) return 'openai'
|
|
312
|
+
if (String(model).startsWith('claude-')) return 'anthropic'
|
|
313
|
+
if (String(model).startsWith('MiniMax-')) return 'minimax'
|
|
314
|
+
return 'openai'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseOpenAIStreamText(text) {
|
|
318
|
+
try {
|
|
319
|
+
const parsed = JSON.parse(String(text || ''))
|
|
320
|
+
if (parsed && typeof parsed === 'object') return parsed
|
|
321
|
+
} catch {}
|
|
322
|
+
|
|
323
|
+
const blocks = String(text || '').split(/\r?\n\r?\n+/).filter(Boolean)
|
|
324
|
+
let responseCompleted = null
|
|
325
|
+
let finalChunk = null
|
|
326
|
+
let content = ''
|
|
327
|
+
let sawOutputTextDelta = false
|
|
328
|
+
|
|
329
|
+
for (const block of blocks) {
|
|
330
|
+
const eventMatch = block.match(/^event:\s*(.+)$/m)
|
|
331
|
+
const dataMatch = block.match(/^data:\s*(.+)$/m)
|
|
332
|
+
if (!dataMatch) continue
|
|
333
|
+
|
|
334
|
+
const eventName = eventMatch ? eventMatch[1].trim() : ''
|
|
335
|
+
const payload = dataMatch[1].trim()
|
|
336
|
+
if (!payload || payload === '[DONE]') continue
|
|
337
|
+
|
|
338
|
+
let chunk
|
|
339
|
+
try {
|
|
340
|
+
chunk = JSON.parse(payload)
|
|
341
|
+
} catch {
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (eventName === 'response.output_text.delta' && typeof chunk.delta === 'string') {
|
|
346
|
+
sawOutputTextDelta = true
|
|
347
|
+
content += chunk.delta
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (eventName === 'response.content_part.done' && chunk.part?.type === 'output_text' && typeof chunk.part.text === 'string') {
|
|
352
|
+
if (!sawOutputTextDelta) content += chunk.part.text
|
|
353
|
+
continue
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (eventName === 'response.completed' && chunk.response) {
|
|
357
|
+
responseCompleted = chunk.response
|
|
358
|
+
if (!content) {
|
|
359
|
+
const outputText = (chunk.response.output || [])
|
|
360
|
+
.flatMap((item) => item?.content || [])
|
|
361
|
+
.filter((item) => item?.type === 'output_text' && typeof item.text === 'string')
|
|
362
|
+
.map((item) => item.text)
|
|
363
|
+
.join('')
|
|
364
|
+
if (outputText) content = outputText
|
|
365
|
+
}
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
finalChunk = chunk
|
|
370
|
+
const choice = chunk.choices?.[0] || {}
|
|
371
|
+
const delta = choice.delta || {}
|
|
372
|
+
if (delta.content) content += delta.content
|
|
373
|
+
else if (choice.message?.content) content += choice.message.content
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (responseCompleted) {
|
|
377
|
+
return {
|
|
378
|
+
id: responseCompleted.id || `chatcmpl_${Date.now()}`,
|
|
379
|
+
object: 'chat.completion',
|
|
380
|
+
created: responseCompleted.created_at || Math.floor(Date.now() / 1000),
|
|
381
|
+
model: responseCompleted.model,
|
|
382
|
+
choices: [{
|
|
383
|
+
index: 0,
|
|
384
|
+
message: { role: 'assistant', content: content || null },
|
|
385
|
+
finish_reason: responseCompleted.status === 'completed' ? 'stop' : 'length',
|
|
386
|
+
}],
|
|
387
|
+
usage: responseCompleted.usage,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!finalChunk) return null
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
id: finalChunk.id || `chatcmpl_${Date.now()}`,
|
|
395
|
+
object: 'chat.completion',
|
|
396
|
+
created: finalChunk.created || Math.floor(Date.now() / 1000),
|
|
397
|
+
model: finalChunk.model,
|
|
398
|
+
choices: [{
|
|
399
|
+
index: 0,
|
|
400
|
+
message: { role: 'assistant', content: content || null },
|
|
401
|
+
finish_reason: finalChunk.choices?.[0]?.finish_reason || 'stop',
|
|
402
|
+
}],
|
|
403
|
+
usage: finalChunk.usage,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function relayOpenAIRequest(requestBody, config, res) {
|
|
408
|
+
const upstreamBody = {
|
|
409
|
+
...requestBody,
|
|
410
|
+
stream: requestBody.stream === true,
|
|
411
|
+
}
|
|
412
|
+
const upstream = await fetch(`${config.baseUrlOpenAI.replace(/\/+$/, '')}/chat/completions`, {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
headers: {
|
|
415
|
+
'content-type': 'application/json',
|
|
416
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
417
|
+
'user-agent': 'holysheep-openclaw-bridge/1.0',
|
|
418
|
+
},
|
|
419
|
+
body: JSON.stringify(upstreamBody),
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const text = await upstream.text()
|
|
423
|
+
const parsed = parseOpenAIStreamText(text)
|
|
424
|
+
if (upstream.ok && parsed) {
|
|
425
|
+
if (requestBody.stream) return sendOpenAIStream(res, parsed)
|
|
426
|
+
return sendJson(res, upstream.status, parsed)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
res.writeHead(upstream.status, {
|
|
430
|
+
'content-type': upstream.headers.get('content-type') || 'application/json; charset=utf-8',
|
|
431
|
+
'cache-control': upstream.headers.get('cache-control') || 'no-store',
|
|
432
|
+
})
|
|
433
|
+
res.end(text)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function relayAnthropicRequest(requestBody, config, route, res) {
|
|
437
|
+
const payload = buildAnthropicPayload(requestBody)
|
|
438
|
+
const baseUrl = route === 'minimax'
|
|
439
|
+
? `${config.baseUrlAnthropic.replace(/\/+$/, '')}/minimax/v1/messages`
|
|
440
|
+
: `${config.baseUrlAnthropic.replace(/\/+$/, '')}/v1/messages`
|
|
441
|
+
|
|
442
|
+
const upstream = await fetch(baseUrl, {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: {
|
|
445
|
+
'content-type': 'application/json',
|
|
446
|
+
'x-api-key': config.apiKey,
|
|
447
|
+
'anthropic-version': '2023-06-01',
|
|
448
|
+
'user-agent': 'holysheep-openclaw-bridge/1.0',
|
|
449
|
+
},
|
|
450
|
+
body: JSON.stringify(payload),
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const text = await upstream.text()
|
|
454
|
+
let body
|
|
455
|
+
try {
|
|
456
|
+
body = JSON.parse(text)
|
|
457
|
+
} catch {
|
|
458
|
+
body = { error: { message: text || 'Invalid upstream response' } }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!upstream.ok) {
|
|
462
|
+
return sendJson(res, upstream.status, body)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const openaiBody = anthropicToOpenAIResponse(body, requestBody.model)
|
|
466
|
+
if (requestBody.stream) return sendOpenAIStream(res, openaiBody)
|
|
467
|
+
return sendJson(res, 200, openaiBody)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function buildModelsResponse(config) {
|
|
471
|
+
return {
|
|
472
|
+
object: 'list',
|
|
473
|
+
data: (config.models || []).map((model) => ({
|
|
474
|
+
id: model,
|
|
475
|
+
object: 'model',
|
|
476
|
+
owned_by: 'holysheep',
|
|
477
|
+
})),
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function createBridgeServer(configPath = BRIDGE_CONFIG_FILE) {
|
|
482
|
+
return http.createServer(async (req, res) => {
|
|
483
|
+
if (req.method === 'OPTIONS') {
|
|
484
|
+
res.writeHead(204, {
|
|
485
|
+
'access-control-allow-origin': '*',
|
|
486
|
+
'access-control-allow-methods': 'GET,POST,OPTIONS',
|
|
487
|
+
'access-control-allow-headers': 'content-type,authorization,x-api-key,anthropic-version',
|
|
488
|
+
})
|
|
489
|
+
return res.end()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const config = readBridgeConfig(configPath)
|
|
494
|
+
const url = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`)
|
|
495
|
+
|
|
496
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
497
|
+
return sendJson(res, 200, { ok: true, port: config.port, models: config.models || [] })
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (req.method === 'GET' && url.pathname === '/v1/models') {
|
|
501
|
+
return sendJson(res, 200, buildModelsResponse(config))
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (req.method === 'POST' && url.pathname === '/v1/chat/completions') {
|
|
505
|
+
const requestBody = await readJsonBody(req)
|
|
506
|
+
const route = pickRoute(requestBody.model)
|
|
507
|
+
if (route === 'openai') return relayOpenAIRequest(requestBody, config, res)
|
|
508
|
+
return relayAnthropicRequest(requestBody, config, route, res)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return sendJson(res, 404, { error: { message: 'Not found' } })
|
|
512
|
+
} catch (error) {
|
|
513
|
+
return sendJson(res, 500, { error: { message: error.message || 'Bridge error' } })
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function startBridge(args = parseArgs(process.argv.slice(2))) {
|
|
519
|
+
const config = readBridgeConfig(args.config)
|
|
520
|
+
const port = args.port || config.port
|
|
521
|
+
const host = args.host || '127.0.0.1'
|
|
522
|
+
const server = createBridgeServer(args.config)
|
|
523
|
+
|
|
524
|
+
server.listen(port, host, () => {
|
|
525
|
+
process.stdout.write(`HolySheep OpenClaw bridge listening on http://${host}:${port}\n`)
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
return server
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (require.main === module) {
|
|
532
|
+
startBridge()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
module.exports = {
|
|
536
|
+
BRIDGE_CONFIG_FILE,
|
|
537
|
+
buildAnthropicPayload,
|
|
538
|
+
anthropicToOpenAIResponse,
|
|
539
|
+
buildModelsResponse,
|
|
540
|
+
createBridgeServer,
|
|
541
|
+
parseArgs,
|
|
542
|
+
parseOpenAIStreamText,
|
|
543
|
+
pickRoute,
|
|
544
|
+
readBridgeConfig,
|
|
545
|
+
startBridge,
|
|
546
|
+
}
|
package/src/tools/openclaw.js
CHANGED
|
@@ -9,16 +9,18 @@ const path = require('path')
|
|
|
9
9
|
const os = require('os')
|
|
10
10
|
const { spawnSync, spawn, execSync } = require('child_process')
|
|
11
11
|
const { commandExists } = require('../utils/which')
|
|
12
|
+
const { BRIDGE_CONFIG_FILE } = require('./openclaw-bridge')
|
|
12
13
|
|
|
13
14
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw')
|
|
14
15
|
const CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json')
|
|
15
16
|
const isWin = process.platform === 'win32'
|
|
17
|
+
const DEFAULT_BRIDGE_PORT = 18788
|
|
16
18
|
const DEFAULT_GATEWAY_PORT = 18789
|
|
17
|
-
const MAX_PORT_SCAN =
|
|
19
|
+
const MAX_PORT_SCAN = 40
|
|
18
20
|
const OPENCLAW_DEFAULT_MODEL = 'gpt-5.4'
|
|
19
21
|
const OPENCLAW_DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6'
|
|
20
22
|
const OPENCLAW_DEFAULT_MINIMAX_MODEL = 'MiniMax-M2.7-highspeed'
|
|
21
|
-
const
|
|
23
|
+
const OPENCLAW_PROVIDER_NAME = 'holysheep'
|
|
22
24
|
|
|
23
25
|
function getOpenClawBinaryCandidates() {
|
|
24
26
|
return isWin ? ['openclaw.cmd', 'openclaw'] : ['openclaw']
|
|
@@ -144,16 +146,67 @@ function detectRuntime() {
|
|
|
144
146
|
return { available: false, via: null, command: null, version: null }
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
+
function readBridgeConfig() {
|
|
150
|
+
try {
|
|
151
|
+
if (fs.existsSync(BRIDGE_CONFIG_FILE)) {
|
|
152
|
+
return JSON.parse(fs.readFileSync(BRIDGE_CONFIG_FILE, 'utf8'))
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
return {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writeBridgeConfig(data) {
|
|
159
|
+
fs.mkdirSync(OPENCLAW_DIR, { recursive: true })
|
|
160
|
+
fs.writeFileSync(BRIDGE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getConfiguredBridgePort(config = readBridgeConfig()) {
|
|
164
|
+
const port = Number(config?.port)
|
|
165
|
+
return Number.isInteger(port) && port > 0 ? port : DEFAULT_BRIDGE_PORT
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getBridgeBaseUrl(port = getConfiguredBridgePort()) {
|
|
169
|
+
return `http://127.0.0.1:${port}/v1`
|
|
149
170
|
}
|
|
150
171
|
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
function waitForBridge(port) {
|
|
173
|
+
for (let i = 0; i < 10; i++) {
|
|
174
|
+
const t0 = Date.now()
|
|
175
|
+
while (Date.now() - t0 < 500) {}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
execSync(
|
|
179
|
+
isWin
|
|
180
|
+
? `powershell -NonInteractive -Command "try{(Invoke-WebRequest -Uri http://127.0.0.1:${port}/health -TimeoutSec 1 -UseBasicParsing).StatusCode}catch{exit 1}"`
|
|
181
|
+
: `curl -sf http://127.0.0.1:${port}/health -o /dev/null --max-time 1`,
|
|
182
|
+
{ stdio: 'ignore', timeout: 3000 }
|
|
183
|
+
)
|
|
184
|
+
return true
|
|
185
|
+
} catch {}
|
|
154
186
|
}
|
|
155
187
|
|
|
156
|
-
return
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function startBridge(port) {
|
|
192
|
+
if (waitForBridge(port)) return true
|
|
193
|
+
|
|
194
|
+
const scriptPath = path.join(__dirname, '..', 'index.js')
|
|
195
|
+
const child = spawn(process.execPath, [scriptPath, 'openclaw-bridge', '--port', String(port)], {
|
|
196
|
+
detached: true,
|
|
197
|
+
stdio: 'ignore',
|
|
198
|
+
})
|
|
199
|
+
child.unref()
|
|
200
|
+
return waitForBridge(port)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getBridgeCommand(port = getConfiguredBridgePort()) {
|
|
204
|
+
return `hs openclaw-bridge --port ${port}`
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function pickPrimaryModel(primaryModel, selectedModels) {
|
|
208
|
+
const models = Array.isArray(selectedModels) ? selectedModels : []
|
|
209
|
+
return primaryModel || models[0] || OPENCLAW_DEFAULT_MODEL
|
|
157
210
|
}
|
|
158
211
|
|
|
159
212
|
function readConfig() {
|
|
@@ -245,11 +298,6 @@ function getDashboardCommand() {
|
|
|
245
298
|
return `${runtime} dashboard --no-open`
|
|
246
299
|
}
|
|
247
300
|
|
|
248
|
-
function buildProviderName(baseUrl, prefix) {
|
|
249
|
-
const hostname = new URL(baseUrl).hostname.replace(/\./g, '-')
|
|
250
|
-
return `${prefix}-${hostname}`
|
|
251
|
-
}
|
|
252
|
-
|
|
253
301
|
function buildModelEntry(id) {
|
|
254
302
|
return {
|
|
255
303
|
id,
|
|
@@ -261,79 +309,51 @@ function buildModelEntry(id) {
|
|
|
261
309
|
}
|
|
262
310
|
}
|
|
263
311
|
|
|
264
|
-
function
|
|
312
|
+
function normalizeRequestedModels(selectedModels) {
|
|
265
313
|
const requestedModels = Array.isArray(selectedModels) && selectedModels.length > 0
|
|
266
|
-
? selectedModels
|
|
314
|
+
? [...selectedModels]
|
|
267
315
|
: [OPENCLAW_DEFAULT_MODEL, OPENCLAW_DEFAULT_CLAUDE_MODEL, OPENCLAW_DEFAULT_MINIMAX_MODEL]
|
|
268
316
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const claudeModels = requestedModels.filter((model) => model.startsWith('claude-'))
|
|
275
|
-
if (claudeModels.length === 0) {
|
|
276
|
-
claudeModels.push(OPENCLAW_DEFAULT_CLAUDE_MODEL)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const minimaxModels = requestedModels.filter((model) => model.startsWith('MiniMax-'))
|
|
280
|
-
if (requestedModels.includes(OPENCLAW_DEFAULT_MINIMAX_MODEL) && !minimaxModels.includes(OPENCLAW_DEFAULT_MINIMAX_MODEL)) {
|
|
281
|
-
minimaxModels.unshift(OPENCLAW_DEFAULT_MINIMAX_MODEL)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const openaiProviderName = buildProviderName(baseUrlOpenAI, 'custom-openai')
|
|
285
|
-
const anthropicProviderName = buildProviderName(baseUrlAnthropic, 'custom-anthropic')
|
|
286
|
-
const minimaxProviderName = buildProviderName(`${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`, 'custom-minimax')
|
|
287
|
-
|
|
288
|
-
const providers = {
|
|
289
|
-
[openaiProviderName]: {
|
|
290
|
-
baseUrl: baseUrlOpenAI,
|
|
291
|
-
apiKey,
|
|
292
|
-
api: 'openai-completions',
|
|
293
|
-
models: openaiModels.map(buildModelEntry),
|
|
294
|
-
},
|
|
295
|
-
[anthropicProviderName]: {
|
|
296
|
-
baseUrl: baseUrlAnthropic,
|
|
297
|
-
apiKey,
|
|
298
|
-
api: 'anthropic-messages',
|
|
299
|
-
models: claudeModels.map(buildModelEntry),
|
|
300
|
-
},
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (minimaxModels.length > 0) {
|
|
304
|
-
providers[minimaxProviderName] = {
|
|
305
|
-
baseUrl: `${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`,
|
|
306
|
-
apiKey,
|
|
307
|
-
api: 'anthropic-messages',
|
|
308
|
-
models: minimaxModels.map(buildModelEntry),
|
|
309
|
-
}
|
|
310
|
-
}
|
|
317
|
+
if (!requestedModels.includes(OPENCLAW_DEFAULT_MODEL)) requestedModels.unshift(OPENCLAW_DEFAULT_MODEL)
|
|
318
|
+
return Array.from(new Set(requestedModels))
|
|
319
|
+
}
|
|
311
320
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
321
|
+
function buildManagedPlan(baseUrlBridge, primaryModel, selectedModels) {
|
|
322
|
+
const requestedModels = normalizeRequestedModels(selectedModels)
|
|
323
|
+
const managedModelRefs = requestedModels.map((model) => `${OPENCLAW_PROVIDER_NAME}/${model}`)
|
|
324
|
+
const fallbackPrimaryModel = pickPrimaryModel(primaryModel, requestedModels)
|
|
325
|
+
const primaryRef = managedModelRefs.includes(`${OPENCLAW_PROVIDER_NAME}/${fallbackPrimaryModel}`)
|
|
326
|
+
? `${OPENCLAW_PROVIDER_NAME}/${fallbackPrimaryModel}`
|
|
327
|
+
: managedModelRefs[0] || `${OPENCLAW_PROVIDER_NAME}/${OPENCLAW_DEFAULT_MODEL}`
|
|
317
328
|
|
|
318
329
|
return {
|
|
319
|
-
providers
|
|
330
|
+
providers: {
|
|
331
|
+
[OPENCLAW_PROVIDER_NAME]: {
|
|
332
|
+
baseUrl: baseUrlBridge,
|
|
333
|
+
api: 'openai-completions',
|
|
334
|
+
models: requestedModels.map(buildModelEntry),
|
|
335
|
+
},
|
|
336
|
+
},
|
|
320
337
|
managedModelRefs,
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
models: requestedModels,
|
|
339
|
+
primaryRef,
|
|
323
340
|
}
|
|
324
341
|
}
|
|
325
342
|
|
|
326
343
|
function isHolySheepProvider(provider) {
|
|
327
|
-
return typeof provider?.baseUrl === 'string' &&
|
|
344
|
+
return typeof provider?.baseUrl === 'string' && (
|
|
345
|
+
provider.baseUrl.includes('api.holysheep.ai') ||
|
|
346
|
+
provider.baseUrl.includes('127.0.0.1')
|
|
347
|
+
)
|
|
328
348
|
}
|
|
329
349
|
|
|
330
|
-
function writeManagedConfig(baseConfig,
|
|
350
|
+
function writeManagedConfig(baseConfig, bridgeBaseUrl, primaryModel, selectedModels, gatewayPort) {
|
|
331
351
|
fs.mkdirSync(OPENCLAW_DIR, { recursive: true })
|
|
332
352
|
|
|
333
|
-
const plan = buildManagedPlan(
|
|
353
|
+
const plan = buildManagedPlan(bridgeBaseUrl, primaryModel, selectedModels)
|
|
334
354
|
const existingProviders = baseConfig?.models?.providers || {}
|
|
335
355
|
const managedProviderIds = Object.entries(existingProviders)
|
|
336
|
-
.filter(([, provider]) => isHolySheepProvider(provider))
|
|
356
|
+
.filter(([providerId, provider]) => providerId === OPENCLAW_PROVIDER_NAME || isHolySheepProvider(provider))
|
|
337
357
|
.map(([providerId]) => providerId)
|
|
338
358
|
|
|
339
359
|
const preservedProviders = Object.fromEntries(
|
|
@@ -441,7 +461,8 @@ function getDashboardUrl(port, preferNpx = false) {
|
|
|
441
461
|
timeout: preferNpx ? 60000 : 20000,
|
|
442
462
|
})
|
|
443
463
|
if (result.status === 0) {
|
|
444
|
-
const
|
|
464
|
+
const output = String(result.stdout || '')
|
|
465
|
+
const match = output.match(/Dashboard URL:\s*(\S+)/) || output.match(/(https?:\/\/\S+)/)
|
|
445
466
|
if (match) return match[1]
|
|
446
467
|
}
|
|
447
468
|
return `http://127.0.0.1:${port}/`
|
|
@@ -462,11 +483,13 @@ module.exports = {
|
|
|
462
483
|
},
|
|
463
484
|
|
|
464
485
|
isConfigured() {
|
|
465
|
-
const cfg =
|
|
466
|
-
|
|
486
|
+
const cfg = readConfig()
|
|
487
|
+
const hasProvider = cfg?.models?.providers?.[OPENCLAW_PROVIDER_NAME]?.baseUrl?.includes('127.0.0.1')
|
|
488
|
+
const bridge = readBridgeConfig()
|
|
489
|
+
return Boolean(hasProvider && bridge?.apiKey)
|
|
467
490
|
},
|
|
468
491
|
|
|
469
|
-
configure(apiKey, baseUrlAnthropic, baseUrlOpenAI,
|
|
492
|
+
configure(apiKey, baseUrlAnthropic, baseUrlOpenAI, primaryModel, selectedModels) {
|
|
470
493
|
const chalk = require('chalk')
|
|
471
494
|
console.log(chalk.gray('\n ⚙️ 正在配置 OpenClaw...'))
|
|
472
495
|
|
|
@@ -476,6 +499,27 @@ module.exports = {
|
|
|
476
499
|
}
|
|
477
500
|
this._lastRuntimeCommand = runtime.command
|
|
478
501
|
|
|
502
|
+
const resolvedPrimaryModel = pickPrimaryModel(primaryModel, selectedModels)
|
|
503
|
+
const bridgePort = findAvailableGatewayPort(DEFAULT_BRIDGE_PORT)
|
|
504
|
+
if (!bridgePort) {
|
|
505
|
+
throw new Error(`找不到可用桥接端口(已检查 ${DEFAULT_BRIDGE_PORT}-${DEFAULT_BRIDGE_PORT + MAX_PORT_SCAN - 1})`)
|
|
506
|
+
}
|
|
507
|
+
this._lastBridgePort = bridgePort
|
|
508
|
+
|
|
509
|
+
writeBridgeConfig({
|
|
510
|
+
port: bridgePort,
|
|
511
|
+
apiKey,
|
|
512
|
+
baseUrlAnthropic,
|
|
513
|
+
baseUrlOpenAI,
|
|
514
|
+
models: normalizeRequestedModels(selectedModels),
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
console.log(chalk.gray(' → 正在启动 HolySheep Bridge...'))
|
|
518
|
+
if (!startBridge(bridgePort)) {
|
|
519
|
+
throw new Error('HolySheep OpenClaw Bridge 启动失败')
|
|
520
|
+
}
|
|
521
|
+
const bridgeBaseUrl = getBridgeBaseUrl(bridgePort)
|
|
522
|
+
|
|
479
523
|
runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
|
|
480
524
|
|
|
481
525
|
const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
|
|
@@ -504,9 +548,9 @@ module.exports = {
|
|
|
504
548
|
'--non-interactive',
|
|
505
549
|
'--accept-risk',
|
|
506
550
|
'--auth-choice', 'custom-api-key',
|
|
507
|
-
'--custom-base-url',
|
|
551
|
+
'--custom-base-url', bridgeBaseUrl,
|
|
508
552
|
'--custom-api-key', apiKey,
|
|
509
|
-
'--custom-model-id',
|
|
553
|
+
'--custom-model-id', resolvedPrimaryModel,
|
|
510
554
|
'--custom-compatibility', 'openai',
|
|
511
555
|
'--gateway-port', String(gatewayPort),
|
|
512
556
|
'--install-daemon',
|
|
@@ -518,18 +562,12 @@ module.exports = {
|
|
|
518
562
|
|
|
519
563
|
const plan = writeManagedConfig(
|
|
520
564
|
result.status === 0 ? readConfig() : {},
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
baseUrlOpenAI,
|
|
565
|
+
bridgeBaseUrl,
|
|
566
|
+
resolvedPrimaryModel,
|
|
524
567
|
selectedModels,
|
|
525
568
|
gatewayPort,
|
|
526
569
|
)
|
|
527
570
|
|
|
528
|
-
const routingRegressionWarning = getRoutingRegressionWarning(runtime.version, plan.minimaxRef)
|
|
529
|
-
if (routingRegressionWarning) {
|
|
530
|
-
console.log(chalk.yellow(` ⚠️ ${routingRegressionWarning}`))
|
|
531
|
-
}
|
|
532
|
-
|
|
533
571
|
_disableGatewayAuth(runtime.via === 'npx')
|
|
534
572
|
const serviceReady = _installGatewayService(gatewayPort, runtime.via === 'npx')
|
|
535
573
|
|
|
@@ -545,7 +583,8 @@ module.exports = {
|
|
|
545
583
|
const dashUrl = getDashboardUrl(gatewayPort, runtime.via === 'npx')
|
|
546
584
|
console.log(chalk.cyan('\n → 浏览器打开(推荐使用此地址):'))
|
|
547
585
|
console.log(chalk.bold.cyan(` ${dashUrl}`))
|
|
548
|
-
console.log(chalk.gray(`
|
|
586
|
+
console.log(chalk.gray(` Bridge 地址: ${bridgeBaseUrl}`))
|
|
587
|
+
console.log(chalk.gray(` 默认模型: ${plan.primaryRef || OPENCLAW_DEFAULT_MODEL}`))
|
|
549
588
|
console.log(chalk.gray(' 如在 Windows 上打开裸 http://127.0.0.1:PORT/ 仍报 Unauthorized,请使用上面的 dashboard 地址'))
|
|
550
589
|
|
|
551
590
|
return {
|
|
@@ -559,24 +598,28 @@ module.exports = {
|
|
|
559
598
|
|
|
560
599
|
reset() {
|
|
561
600
|
try { fs.unlinkSync(CONFIG_FILE) } catch {}
|
|
601
|
+
try { fs.unlinkSync(BRIDGE_CONFIG_FILE) } catch {}
|
|
562
602
|
},
|
|
563
603
|
|
|
564
604
|
getConfigPath() { return CONFIG_FILE },
|
|
605
|
+
getBridgePort() { return getConfiguredBridgePort() },
|
|
565
606
|
getGatewayPort() { return getConfiguredGatewayPort() },
|
|
566
607
|
getPrimaryModel() { return getConfiguredPrimaryModel() },
|
|
567
608
|
getPortListeners(port = getConfiguredGatewayPort()) { return listPortListeners(port) },
|
|
568
609
|
get hint() {
|
|
569
|
-
return `Gateway
|
|
610
|
+
return `Bridge + Gateway 已配置,默认模型为 ${getConfiguredPrimaryModel() || OPENCLAW_DEFAULT_MODEL}`
|
|
570
611
|
},
|
|
571
612
|
get launchSteps() {
|
|
613
|
+
const bridgePort = getConfiguredBridgePort()
|
|
572
614
|
const port = getConfiguredGatewayPort()
|
|
573
615
|
return [
|
|
574
|
-
{ cmd:
|
|
616
|
+
{ cmd: getBridgeCommand(bridgePort), note: '先启动 HolySheep OpenClaw Bridge' },
|
|
617
|
+
{ cmd: getLaunchCommand(port), note: '再启动 OpenClaw Gateway' },
|
|
575
618
|
{ cmd: getDashboardCommand(), note: '再生成/打开可直接连接的 Dashboard 地址(推荐)' },
|
|
576
619
|
]
|
|
577
620
|
},
|
|
578
621
|
get launchNote() {
|
|
579
|
-
return `🌐
|
|
622
|
+
return `🌐 请先启动 Bridge,再启动 Gateway;最后运行 ${getDashboardCommand()}`
|
|
580
623
|
},
|
|
581
624
|
installCmd: 'npm install -g openclaw@latest',
|
|
582
625
|
docsUrl: 'https://docs.openclaw.ai',
|