@simonyea/holysheep-cli 1.7.127 → 1.7.128

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.127",
3
+ "version": "1.7.128",
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",
@@ -48,6 +48,25 @@ function getControlPlaneUrl(config) {
48
48
  const leaseCache = new Map()
49
49
  const MAX_PROXY_RETRIES = 3
50
50
 
51
+ function createForwardError(statusCode, body) {
52
+ const error = new Error(`HTTP ${statusCode}: ${String(body || '').slice(0, 200)}`)
53
+ error.statusCode = Number(statusCode) || 502
54
+ error.body = String(body || '')
55
+ return error
56
+ }
57
+
58
+ function shouldRefreshLeaseAfterError(err) {
59
+ const statusCode = Number(err?.statusCode || 0)
60
+ const body = String(err?.body || err?.message || '')
61
+ if (statusCode === 403 || statusCode === 503) return true
62
+ return (
63
+ body.includes('client_validation_error') ||
64
+ body.includes('Claude Code 必须使用 hs claude 指令启动') ||
65
+ body.includes('当前代理节点') ||
66
+ body.includes('No active Claude relay nodes are available')
67
+ )
68
+ }
69
+
51
70
  function isRetryableNodeLeaseError(err) {
52
71
  const message = String(err?.message || '')
53
72
  return [
@@ -160,7 +179,7 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
160
179
  forwardRes.on('data', (c) => chunks.push(c))
161
180
  forwardRes.on('end', () => {
162
181
  const body = Buffer.concat(chunks).toString('utf8')
163
- reject(new Error(`HTTP ${status}: ${body.slice(0, 200)}`))
182
+ reject(createForwardError(status, body))
164
183
  })
165
184
  return
166
185
  }
@@ -201,15 +220,21 @@ function createConnectTunnel(proxyUrl, target, headers) {
201
220
  })
202
221
  }
203
222
 
223
+ // 等上游返回 first byte 的最大时间。Claude 长上下文(>200K tokens)的 prefill 时间
224
+ // 可以达到 30-90 秒,30 秒太严会误杀正常长 prompt。默认 300 秒,可通过环境变量
225
+ // HS_CLAUDE_RESPONSE_TIMEOUT_MS 覆盖。
226
+ const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 300000
227
+ // 上游 stream 已开始后,相邻两个 chunk 之间的最大间隔(覆盖 extended thinking 间歇)。
228
+ const STALL_TIMEOUT_MS = Number(process.env.HS_CLAUDE_STALL_TIMEOUT_MS) || 120000
229
+
204
230
  function pipeWithCleanup(a, b) {
205
231
  for (const sock of [a, b]) {
206
232
  if (typeof sock.setKeepAlive === 'function') sock.setKeepAlive(true, 10000)
207
233
  }
208
234
 
209
235
  // 双阶段超时:
210
- // 1. 客户端发了数据,上游 30 秒没回第一个字节 → 断开(node proxy 挂了)
211
- // 2. 上游在流数据突然停了 120 → 断开(stream 中断)
212
- // 120 秒足够覆盖 extended thinking 的间歇(thinking 期间服务端仍会发心跳)
236
+ // 1. 客户端发了数据,上游 RESPONSE_TIMEOUT_MS 没回第一个字节 → 断开(node proxy 挂了)
237
+ // 2. 上游在流数据突然停了 STALL_TIMEOUT_MS → 断开(stream 中断)
213
238
  let timer = null
214
239
  let streaming = false
215
240
  const kill = (reason) => {
@@ -221,15 +246,15 @@ function pipeWithCleanup(a, b) {
221
246
  // 客户端发了数据(请求),等上游响应
222
247
  if (!streaming) {
223
248
  if (timer) clearTimeout(timer)
224
- timer = setTimeout(() => kill('upstream response timeout'), 30000)
249
+ timer = setTimeout(() => kill('upstream response timeout'), RESPONSE_TIMEOUT_MS)
225
250
  }
226
251
  })
227
252
  b.on('data', () => {
228
253
  // 上游回数据了,切换到 stream 模式
229
254
  streaming = true
230
- // 每次收到数据重置 120 秒超时
255
+ // 每次收到数据重置 stall 超时
231
256
  if (timer) clearTimeout(timer)
232
- timer = setTimeout(() => kill('upstream stream stalled'), 120000)
257
+ timer = setTimeout(() => kill('upstream stream stalled'), STALL_TIMEOUT_MS)
233
258
  })
234
259
 
235
260
  a.pipe(b)
@@ -286,6 +311,9 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
286
311
  } else {
287
312
  const config = readConfig(configPath)
288
313
  leaseCache.delete(sessionId)
314
+ if (shouldRefreshLeaseAfterError(lastError)) {
315
+ await closeSession(configPath, sessionId)
316
+ }
289
317
  const freshLease = await fetchFreshLease(config, sessionId)
290
318
  await doForward(freshLease)
291
319
  }
@@ -298,8 +326,13 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
298
326
  }
299
327
  }
300
328
  if (lastError && !clientRes.headersSent) {
301
- clientRes.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
302
- clientRes.end(lastError.message || 'Proxy error')
329
+ const status = Number(lastError.statusCode || 502)
330
+ const body = String(lastError.body || lastError.message || 'Proxy error')
331
+ const isJson = body.trim().startsWith('{')
332
+ clientRes.writeHead(status, {
333
+ 'content-type': isJson ? 'application/json; charset=utf-8' : 'text/plain; charset=utf-8'
334
+ })
335
+ clientRes.end(body)
303
336
  }
304
337
  })
305
338
 
@@ -342,6 +375,9 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
342
375
  } else {
343
376
  const config = readConfig(configPath)
344
377
  leaseCache.delete(sessionId)
378
+ if (shouldRefreshLeaseAfterError(lastError)) {
379
+ await closeSession(configPath, sessionId)
380
+ }
345
381
  const freshLease = await fetchFreshLease(config, sessionId)
346
382
  await doConnect(freshLease)
347
383
  }
@@ -353,7 +389,13 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
353
389
  }
354
390
  }
355
391
  if (lastError) {
356
- clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\ncontent-type: text/plain; charset=utf-8\r\n\r\n${lastError.message || 'Proxy error'}`)
392
+ const status = Number(lastError.statusCode || 502)
393
+ const body = String(lastError.body || lastError.message || 'Proxy error')
394
+ const statusText = status === 403 ? 'Forbidden' : status === 503 ? 'Service Unavailable' : 'Bad Gateway'
395
+ const contentType = body.trim().startsWith('{')
396
+ ? 'application/json; charset=utf-8'
397
+ : 'text/plain; charset=utf-8'
398
+ clientSocket.write(`HTTP/1.1 ${status} ${statusText}\r\ncontent-type: ${contentType}\r\n\r\n${body}`)
357
399
  clientSocket.destroy()
358
400
  }
359
401
  })