@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.18",
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: false,
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`
@@ -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
- const child = spawn(process.execPath, [scriptPath, 'openclaw-bridge', '--port', String(port)], {
207
- detached: true,
208
- stdio: 'ignore',
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
  }