@simonyea/holysheep-cli 1.7.127 → 1.7.129

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.129",
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 [
@@ -84,7 +103,7 @@ async function readJsonResponse(response) {
84
103
  }
85
104
 
86
105
  // 向 relay 申请新 lease(启动时 + CONNECT 失败时被动重试)
87
- async function fetchFreshLease(config, sessionId) {
106
+ async function fetchFreshLease(config, sessionId, options = {}) {
88
107
  const controlPlaneUrl = getControlPlaneUrl(config)
89
108
  if (!controlPlaneUrl) throw new Error('Claude relay control plane is not configured')
90
109
 
@@ -97,6 +116,7 @@ async function fetchFreshLease(config, sessionId) {
97
116
  deviceId: config.deviceId || '',
98
117
  installSource: config.installSource || 'holysheep-cli',
99
118
  proxyMode: 'claude-process',
119
+ forceReassign: options.forceReassign === true,
100
120
  }),
101
121
  })
102
122
 
@@ -160,7 +180,7 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
160
180
  forwardRes.on('data', (c) => chunks.push(c))
161
181
  forwardRes.on('end', () => {
162
182
  const body = Buffer.concat(chunks).toString('utf8')
163
- reject(new Error(`HTTP ${status}: ${body.slice(0, 200)}`))
183
+ reject(createForwardError(status, body))
164
184
  })
165
185
  return
166
186
  }
@@ -201,15 +221,21 @@ function createConnectTunnel(proxyUrl, target, headers) {
201
221
  })
202
222
  }
203
223
 
224
+ // 等上游返回 first byte 的最大时间。Claude 长上下文(>200K tokens)的 prefill 时间
225
+ // 可以达到 30-90 秒,30 秒太严会误杀正常长 prompt。默认 300 秒,可通过环境变量
226
+ // HS_CLAUDE_RESPONSE_TIMEOUT_MS 覆盖。
227
+ const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 300000
228
+ // 上游 stream 已开始后,相邻两个 chunk 之间的最大间隔(覆盖 extended thinking 间歇)。
229
+ const STALL_TIMEOUT_MS = Number(process.env.HS_CLAUDE_STALL_TIMEOUT_MS) || 120000
230
+
204
231
  function pipeWithCleanup(a, b) {
205
232
  for (const sock of [a, b]) {
206
233
  if (typeof sock.setKeepAlive === 'function') sock.setKeepAlive(true, 10000)
207
234
  }
208
235
 
209
236
  // 双阶段超时:
210
- // 1. 客户端发了数据,上游 30 秒没回第一个字节 → 断开(node proxy 挂了)
211
- // 2. 上游在流数据突然停了 120 → 断开(stream 中断)
212
- // 120 秒足够覆盖 extended thinking 的间歇(thinking 期间服务端仍会发心跳)
237
+ // 1. 客户端发了数据,上游 RESPONSE_TIMEOUT_MS 没回第一个字节 → 断开(node proxy 挂了)
238
+ // 2. 上游在流数据突然停了 STALL_TIMEOUT_MS → 断开(stream 中断)
213
239
  let timer = null
214
240
  let streaming = false
215
241
  const kill = (reason) => {
@@ -221,15 +247,15 @@ function pipeWithCleanup(a, b) {
221
247
  // 客户端发了数据(请求),等上游响应
222
248
  if (!streaming) {
223
249
  if (timer) clearTimeout(timer)
224
- timer = setTimeout(() => kill('upstream response timeout'), 30000)
250
+ timer = setTimeout(() => kill('upstream response timeout'), RESPONSE_TIMEOUT_MS)
225
251
  }
226
252
  })
227
253
  b.on('data', () => {
228
254
  // 上游回数据了,切换到 stream 模式
229
255
  streaming = true
230
- // 每次收到数据重置 120 秒超时
256
+ // 每次收到数据重置 stall 超时
231
257
  if (timer) clearTimeout(timer)
232
- timer = setTimeout(() => kill('upstream stream stalled'), 120000)
258
+ timer = setTimeout(() => kill('upstream stream stalled'), STALL_TIMEOUT_MS)
233
259
  })
234
260
 
235
261
  a.pipe(b)
@@ -286,7 +312,12 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
286
312
  } else {
287
313
  const config = readConfig(configPath)
288
314
  leaseCache.delete(sessionId)
289
- const freshLease = await fetchFreshLease(config, sessionId)
315
+ if (shouldRefreshLeaseAfterError(lastError)) {
316
+ await closeSession(configPath, sessionId)
317
+ }
318
+ const freshLease = await fetchFreshLease(config, sessionId, {
319
+ forceReassign: shouldRefreshLeaseAfterError(lastError),
320
+ })
290
321
  await doForward(freshLease)
291
322
  }
292
323
  lastError = null
@@ -298,8 +329,13 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
298
329
  }
299
330
  }
300
331
  if (lastError && !clientRes.headersSent) {
301
- clientRes.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
302
- clientRes.end(lastError.message || 'Proxy error')
332
+ const status = Number(lastError.statusCode || 502)
333
+ const body = String(lastError.body || lastError.message || 'Proxy error')
334
+ const isJson = body.trim().startsWith('{')
335
+ clientRes.writeHead(status, {
336
+ 'content-type': isJson ? 'application/json; charset=utf-8' : 'text/plain; charset=utf-8'
337
+ })
338
+ clientRes.end(body)
303
339
  }
304
340
  })
305
341
 
@@ -342,7 +378,12 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
342
378
  } else {
343
379
  const config = readConfig(configPath)
344
380
  leaseCache.delete(sessionId)
345
- const freshLease = await fetchFreshLease(config, sessionId)
381
+ if (shouldRefreshLeaseAfterError(lastError)) {
382
+ await closeSession(configPath, sessionId)
383
+ }
384
+ const freshLease = await fetchFreshLease(config, sessionId, {
385
+ forceReassign: shouldRefreshLeaseAfterError(lastError),
386
+ })
346
387
  await doConnect(freshLease)
347
388
  }
348
389
  lastError = null
@@ -353,7 +394,13 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
353
394
  }
354
395
  }
355
396
  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'}`)
397
+ const status = Number(lastError.statusCode || 502)
398
+ const body = String(lastError.body || lastError.message || 'Proxy error')
399
+ const statusText = status === 403 ? 'Forbidden' : status === 503 ? 'Service Unavailable' : 'Bad Gateway'
400
+ const contentType = body.trim().startsWith('{')
401
+ ? 'application/json; charset=utf-8'
402
+ : 'text/plain; charset=utf-8'
403
+ clientSocket.write(`HTTP/1.1 ${status} ${statusText}\r\ncontent-type: ${contentType}\r\n\r\n${body}`)
357
404
  clientSocket.destroy()
358
405
  }
359
406
  })