@simonyea/holysheep-cli 1.7.18 → 1.7.20
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/package.json +1 -1
- package/src/tools/openclaw-bridge.js +163 -2
- package/src/tools/openclaw.js +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.20",
|
|
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",
|
|
@@ -256,13 +256,13 @@ function convertToolChoice(toolChoice) {
|
|
|
256
256
|
return { type: 'auto' }
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
function buildAnthropicPayload(requestBody) {
|
|
259
|
+
function buildAnthropicPayload(requestBody, stream = false) {
|
|
260
260
|
const converted = convertOpenAIToAnthropicMessages(requestBody.messages)
|
|
261
261
|
const payload = {
|
|
262
262
|
model: requestBody.model,
|
|
263
263
|
max_tokens: requestBody.max_tokens || requestBody.max_completion_tokens || requestBody.max_output_tokens || 4096,
|
|
264
264
|
messages: converted.messages,
|
|
265
|
-
stream:
|
|
265
|
+
stream: Boolean(stream),
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
if (converted.system) payload.system = converted.system
|
|
@@ -526,6 +526,18 @@ async function relayOpenAIRequest(requestBody, config, res) {
|
|
|
526
526
|
body: JSON.stringify(upstreamBody),
|
|
527
527
|
})
|
|
528
528
|
|
|
529
|
+
// 流式请求:直接字节透传(已是 OpenAI SSE 格式,无需转换)
|
|
530
|
+
if (requestBody.stream === true && upstream.ok) {
|
|
531
|
+
res.writeHead(200, {
|
|
532
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
533
|
+
'cache-control': 'no-cache, no-transform',
|
|
534
|
+
connection: 'keep-alive',
|
|
535
|
+
})
|
|
536
|
+
try { await pipeStream(upstream.body, (chunk) => res.write(chunk)) } catch {}
|
|
537
|
+
if (!res.writableEnded) res.end()
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
529
541
|
const text = await upstream.text()
|
|
530
542
|
const parsed = parseOpenAIStreamText(text, requestBody.model)
|
|
531
543
|
if (upstream.ok && parsed) {
|
|
@@ -540,7 +552,156 @@ async function relayOpenAIRequest(requestBody, config, res) {
|
|
|
540
552
|
res.end(text)
|
|
541
553
|
}
|
|
542
554
|
|
|
555
|
+
// 兼容 native fetch (ReadableStream) 和 node-fetch v2 (Node.js stream) 的流读取
|
|
556
|
+
async function pipeStream(body, onChunk) {
|
|
557
|
+
if (body == null) return
|
|
558
|
+
if (typeof body.getReader === 'function') {
|
|
559
|
+
const reader = body.getReader()
|
|
560
|
+
const decoder = new TextDecoder()
|
|
561
|
+
try {
|
|
562
|
+
while (true) {
|
|
563
|
+
const { done, value } = await reader.read()
|
|
564
|
+
if (done) break
|
|
565
|
+
onChunk(decoder.decode(value, { stream: true }))
|
|
566
|
+
}
|
|
567
|
+
onChunk(decoder.decode())
|
|
568
|
+
} finally {
|
|
569
|
+
reader.releaseLock()
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
for await (const chunk of body) {
|
|
573
|
+
onChunk(typeof chunk === 'string' ? chunk : chunk.toString())
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Anthropic SSE → OpenAI SSE 实时透传(避免整包缓冲导致 OpenClaw timeout)
|
|
579
|
+
async function relayAnthropicStream(requestBody, config, route, res) {
|
|
580
|
+
const payload = buildAnthropicPayload(requestBody, true)
|
|
581
|
+
const baseUrl = route === 'minimax'
|
|
582
|
+
? `${config.baseUrlAnthropic.replace(/\/+$/, '')}/minimax/v1/messages`
|
|
583
|
+
: `${config.baseUrlAnthropic.replace(/\/+$/, '')}/v1/messages`
|
|
584
|
+
|
|
585
|
+
let upstream
|
|
586
|
+
try {
|
|
587
|
+
upstream = await fetch(baseUrl, {
|
|
588
|
+
method: 'POST',
|
|
589
|
+
headers: {
|
|
590
|
+
'content-type': 'application/json',
|
|
591
|
+
'x-api-key': config.apiKey,
|
|
592
|
+
'anthropic-version': '2023-06-01',
|
|
593
|
+
'user-agent': 'holysheep-openclaw-bridge/1.0',
|
|
594
|
+
},
|
|
595
|
+
body: JSON.stringify(payload),
|
|
596
|
+
})
|
|
597
|
+
} catch (err) {
|
|
598
|
+
return sendJson(res, 500, { error: { message: err.message || 'Bridge upstream error' } })
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!upstream.ok) {
|
|
602
|
+
let errBody
|
|
603
|
+
try { errBody = JSON.parse(await upstream.text()) } catch { errBody = { error: { message: 'Upstream error' } } }
|
|
604
|
+
return sendJson(res, upstream.status, errBody)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
res.writeHead(200, {
|
|
608
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
609
|
+
'cache-control': 'no-cache, no-transform',
|
|
610
|
+
connection: 'keep-alive',
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
const msgId = `chatcmpl_${Date.now()}`
|
|
614
|
+
const created = Math.floor(Date.now() / 1000)
|
|
615
|
+
const model = requestBody.model
|
|
616
|
+
let headerSent = false
|
|
617
|
+
let inputTokens = 0
|
|
618
|
+
// tool_use 流式:按 content block index 收集
|
|
619
|
+
const toolBlocks = {} // index → {id, name, argsBuf}
|
|
620
|
+
|
|
621
|
+
function writeChunk(delta, finishReason, usage) {
|
|
622
|
+
const chunk = {
|
|
623
|
+
id: msgId,
|
|
624
|
+
object: 'chat.completion.chunk',
|
|
625
|
+
created,
|
|
626
|
+
model,
|
|
627
|
+
choices: [{ index: 0, delta, finish_reason: finishReason || null }],
|
|
628
|
+
}
|
|
629
|
+
if (usage) chunk.usage = usage
|
|
630
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function handleEvent(event, data) {
|
|
634
|
+
if (data === '[DONE]') return
|
|
635
|
+
let obj
|
|
636
|
+
try { obj = JSON.parse(data) } catch { return }
|
|
637
|
+
|
|
638
|
+
if (event === 'message_start') {
|
|
639
|
+
inputTokens = obj.message?.usage?.input_tokens || 0
|
|
640
|
+
if (!headerSent) {
|
|
641
|
+
writeChunk({ role: 'assistant', content: '' }, null, null)
|
|
642
|
+
headerSent = true
|
|
643
|
+
}
|
|
644
|
+
} else if (event === 'content_block_start') {
|
|
645
|
+
if (!headerSent) { writeChunk({ role: 'assistant', content: '' }, null, null); headerSent = true }
|
|
646
|
+
const block = obj.content_block || {}
|
|
647
|
+
if (block.type === 'tool_use') {
|
|
648
|
+
toolBlocks[obj.index] = { id: block.id, name: block.name, argsBuf: '' }
|
|
649
|
+
writeChunk({
|
|
650
|
+
tool_calls: [{ index: obj.index, id: block.id, type: 'function', function: { name: block.name, arguments: '' } }],
|
|
651
|
+
}, null, null)
|
|
652
|
+
}
|
|
653
|
+
} else if (event === 'content_block_delta') {
|
|
654
|
+
if (!headerSent) { writeChunk({ role: 'assistant', content: '' }, null, null); headerSent = true }
|
|
655
|
+
const delta = obj.delta || {}
|
|
656
|
+
if (delta.type === 'text_delta' && typeof delta.text === 'string') {
|
|
657
|
+
writeChunk({ content: delta.text }, null, null)
|
|
658
|
+
} else if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
|
|
659
|
+
if (toolBlocks[obj.index]) toolBlocks[obj.index].argsBuf += delta.partial_json
|
|
660
|
+
writeChunk({ tool_calls: [{ index: obj.index, function: { arguments: delta.partial_json } }] }, null, null)
|
|
661
|
+
}
|
|
662
|
+
} else if (event === 'message_delta') {
|
|
663
|
+
const stopReason = obj.delta?.stop_reason
|
|
664
|
+
const finishReason = mapFinishReason(stopReason)
|
|
665
|
+
const outputTokens = obj.usage?.output_tokens || 0
|
|
666
|
+
const usage = {
|
|
667
|
+
prompt_tokens: inputTokens,
|
|
668
|
+
completion_tokens: outputTokens,
|
|
669
|
+
total_tokens: inputTokens + outputTokens,
|
|
670
|
+
}
|
|
671
|
+
if (!headerSent) { writeChunk({ role: 'assistant', content: '' }, null, null); headerSent = true }
|
|
672
|
+
writeChunk({}, finishReason, usage)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
let buf = ''
|
|
677
|
+
let curEvent = ''
|
|
678
|
+
|
|
679
|
+
function processBuffer(text) {
|
|
680
|
+
buf += text
|
|
681
|
+
const lines = buf.split('\n')
|
|
682
|
+
buf = lines.pop() ?? ''
|
|
683
|
+
for (const line of lines) {
|
|
684
|
+
const trimmed = line.trimEnd()
|
|
685
|
+
if (trimmed.startsWith('event:')) {
|
|
686
|
+
curEvent = trimmed.slice(6).trim()
|
|
687
|
+
} else if (trimmed.startsWith('data:')) {
|
|
688
|
+
handleEvent(curEvent, trimmed.slice(5).trim())
|
|
689
|
+
curEvent = ''
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
await pipeStream(upstream.body, processBuffer)
|
|
696
|
+
} catch {}
|
|
697
|
+
|
|
698
|
+
if (!res.writableEnded) res.end('data: [DONE]\n\n')
|
|
699
|
+
}
|
|
700
|
+
|
|
543
701
|
async function relayAnthropicRequest(requestBody, config, route, res) {
|
|
702
|
+
if (requestBody.stream === true) {
|
|
703
|
+
return relayAnthropicStream(requestBody, config, route, res)
|
|
704
|
+
}
|
|
544
705
|
const payload = buildAnthropicPayload(requestBody)
|
|
545
706
|
const baseUrl = route === 'minimax'
|
|
546
707
|
? `${config.baseUrlAnthropic.replace(/\/+$/, '')}/minimax/v1/messages`
|
package/src/tools/openclaw.js
CHANGED
|
@@ -203,10 +203,13 @@ function startBridge(port) {
|
|
|
203
203
|
if (waitForBridge(port)) return true
|
|
204
204
|
|
|
205
205
|
const scriptPath = path.join(__dirname, '..', 'index.js')
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
// Windows: use shell+node command to avoid ERROR_FILE_NOT_FOUND with process.execPath
|
|
207
|
+
// (Windows Store / nvm paths can be unresolvable when spawning detached)
|
|
208
|
+
const spawnCmd = isWin ? 'node' : process.execPath
|
|
209
|
+
const spawnOpts = isWin
|
|
210
|
+
? { shell: true, detached: true, stdio: 'ignore', windowsHide: true }
|
|
211
|
+
: { detached: true, stdio: 'ignore' }
|
|
212
|
+
const child = spawn(spawnCmd, [scriptPath, 'openclaw-bridge', '--port', String(port)], spawnOpts)
|
|
210
213
|
child.unref()
|
|
211
214
|
return waitForBridge(port)
|
|
212
215
|
}
|