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