@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.
|
|
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(
|
|
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. 客户端发了数据,上游
|
|
211
|
-
// 2. 上游在流数据突然停了
|
|
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'),
|
|
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
|
-
// 每次收到数据重置
|
|
255
|
+
// 每次收到数据重置 stall 超时
|
|
231
256
|
if (timer) clearTimeout(timer)
|
|
232
|
-
timer = setTimeout(() => kill('upstream stream stalled'),
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
})
|