@simonyea/holysheep-cli 1.7.126 → 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.
|
|
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",
|
|
@@ -46,6 +46,26 @@ function getControlPlaneUrl(config) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const leaseCache = new Map()
|
|
49
|
+
const MAX_PROXY_RETRIES = 3
|
|
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
|
+
}
|
|
49
69
|
|
|
50
70
|
function isRetryableNodeLeaseError(err) {
|
|
51
71
|
const message = String(err?.message || '')
|
|
@@ -159,7 +179,7 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
159
179
|
forwardRes.on('data', (c) => chunks.push(c))
|
|
160
180
|
forwardRes.on('end', () => {
|
|
161
181
|
const body = Buffer.concat(chunks).toString('utf8')
|
|
162
|
-
reject(
|
|
182
|
+
reject(createForwardError(status, body))
|
|
163
183
|
})
|
|
164
184
|
return
|
|
165
185
|
}
|
|
@@ -200,15 +220,21 @@ function createConnectTunnel(proxyUrl, target, headers) {
|
|
|
200
220
|
})
|
|
201
221
|
}
|
|
202
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
|
+
|
|
203
230
|
function pipeWithCleanup(a, b) {
|
|
204
231
|
for (const sock of [a, b]) {
|
|
205
232
|
if (typeof sock.setKeepAlive === 'function') sock.setKeepAlive(true, 10000)
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
// 双阶段超时:
|
|
209
|
-
// 1. 客户端发了数据,上游
|
|
210
|
-
// 2. 上游在流数据突然停了
|
|
211
|
-
// 120 秒足够覆盖 extended thinking 的间歇(thinking 期间服务端仍会发心跳)
|
|
236
|
+
// 1. 客户端发了数据,上游 RESPONSE_TIMEOUT_MS 没回第一个字节 → 断开(node proxy 挂了)
|
|
237
|
+
// 2. 上游在流数据突然停了 STALL_TIMEOUT_MS → 断开(stream 中断)
|
|
212
238
|
let timer = null
|
|
213
239
|
let streaming = false
|
|
214
240
|
const kill = (reason) => {
|
|
@@ -220,15 +246,15 @@ function pipeWithCleanup(a, b) {
|
|
|
220
246
|
// 客户端发了数据(请求),等上游响应
|
|
221
247
|
if (!streaming) {
|
|
222
248
|
if (timer) clearTimeout(timer)
|
|
223
|
-
timer = setTimeout(() => kill('upstream response timeout'),
|
|
249
|
+
timer = setTimeout(() => kill('upstream response timeout'), RESPONSE_TIMEOUT_MS)
|
|
224
250
|
}
|
|
225
251
|
})
|
|
226
252
|
b.on('data', () => {
|
|
227
253
|
// 上游回数据了,切换到 stream 模式
|
|
228
254
|
streaming = true
|
|
229
|
-
// 每次收到数据重置
|
|
255
|
+
// 每次收到数据重置 stall 超时
|
|
230
256
|
if (timer) clearTimeout(timer)
|
|
231
|
-
timer = setTimeout(() => kill('upstream stream stalled'),
|
|
257
|
+
timer = setTimeout(() => kill('upstream stream stalled'), STALL_TIMEOUT_MS)
|
|
232
258
|
})
|
|
233
259
|
|
|
234
260
|
a.pipe(b)
|
|
@@ -277,29 +303,37 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
277
303
|
})
|
|
278
304
|
}
|
|
279
305
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (shouldRefreshLease) {
|
|
287
|
-
try {
|
|
306
|
+
let lastError = null
|
|
307
|
+
for (let attempt = 0; attempt <= MAX_PROXY_RETRIES; attempt++) {
|
|
308
|
+
try {
|
|
309
|
+
if (attempt === 0) {
|
|
310
|
+
await doForward(getCachedLease(sessionId))
|
|
311
|
+
} else {
|
|
288
312
|
const config = readConfig(configPath)
|
|
289
313
|
leaseCache.delete(sessionId)
|
|
314
|
+
if (shouldRefreshLeaseAfterError(lastError)) {
|
|
315
|
+
await closeSession(configPath, sessionId)
|
|
316
|
+
}
|
|
290
317
|
const freshLease = await fetchFreshLease(config, sessionId)
|
|
291
318
|
await doForward(freshLease)
|
|
292
|
-
} catch (retryError) {
|
|
293
|
-
if (!clientRes.headersSent) {
|
|
294
|
-
clientRes.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
|
|
295
|
-
clientRes.end(retryError.message || 'Proxy error')
|
|
296
|
-
}
|
|
297
319
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
320
|
+
lastError = null
|
|
321
|
+
break
|
|
322
|
+
} catch (err) {
|
|
323
|
+
lastError = err
|
|
324
|
+
if (clientRes.headersSent) return
|
|
325
|
+
if (!isRetryableNodeLeaseError(err) && attempt > 0) break
|
|
301
326
|
}
|
|
302
327
|
}
|
|
328
|
+
if (lastError && !clientRes.headersSent) {
|
|
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)
|
|
336
|
+
}
|
|
303
337
|
})
|
|
304
338
|
|
|
305
339
|
// Hosts that must NOT be tunneled directly — they must go through CRS via
|
|
@@ -333,26 +367,37 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
333
367
|
pipeWithCleanup(clientSocket, upstreamSocket)
|
|
334
368
|
}
|
|
335
369
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
try {
|
|
370
|
+
let lastError = null
|
|
371
|
+
for (let attempt = 0; attempt <= MAX_PROXY_RETRIES; attempt++) {
|
|
372
|
+
try {
|
|
373
|
+
if (attempt === 0) {
|
|
374
|
+
await doConnect(getCachedLease(sessionId))
|
|
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
|
-
} catch (retryError) {
|
|
348
|
-
clientSocket.write(`HTTP/1.1 502 Bad Gateway\r\ncontent-type: text/plain; charset=utf-8\r\n\r\n${retryError.message}`)
|
|
349
|
-
clientSocket.destroy()
|
|
350
383
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
384
|
+
lastError = null
|
|
385
|
+
break
|
|
386
|
+
} catch (err) {
|
|
387
|
+
lastError = err
|
|
388
|
+
if (!isRetryableNodeLeaseError(err) && attempt > 0) break
|
|
354
389
|
}
|
|
355
390
|
}
|
|
391
|
+
if (lastError) {
|
|
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}`)
|
|
399
|
+
clientSocket.destroy()
|
|
400
|
+
}
|
|
356
401
|
})
|
|
357
402
|
|
|
358
403
|
return server
|