@simonyea/holysheep-cli 1.7.131 → 1.7.132
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 +1 -1
- package/src/tools/claude-process-proxy.js +152 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.132",
|
|
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",
|
|
@@ -47,6 +47,39 @@ function getControlPlaneUrl(config) {
|
|
|
47
47
|
|
|
48
48
|
const leaseCache = new Map()
|
|
49
49
|
const MAX_PROXY_RETRIES = 3
|
|
50
|
+
const SLOW_PATH_LOG_MS = Number(process.env.HS_CLAUDE_SLOW_PATH_LOG_MS) || 5000
|
|
51
|
+
|
|
52
|
+
function sanitizeUrl(value) {
|
|
53
|
+
if (!value) return ''
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(String(value))
|
|
56
|
+
return `${url.protocol}//${url.host}${url.pathname}`
|
|
57
|
+
} catch {
|
|
58
|
+
return String(value)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function logProxyTiming(event, details = {}) {
|
|
63
|
+
const payload = Object.fromEntries(
|
|
64
|
+
Object.entries(details).filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
65
|
+
)
|
|
66
|
+
console.error(`[hs-claude-proxy] ${event} ${JSON.stringify(payload)}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createForwardTrace({ clientReq, targetUrl, nodeProxyUrl, sessionId, lease, attempt, isDirect }) {
|
|
70
|
+
return {
|
|
71
|
+
requestId: crypto.randomUUID().slice(0, 8),
|
|
72
|
+
sessionId,
|
|
73
|
+
nodeId: lease?.nodeId || '',
|
|
74
|
+
attempt,
|
|
75
|
+
isDirect,
|
|
76
|
+
method: clientReq?.method || '',
|
|
77
|
+
target: sanitizeUrl(targetUrl),
|
|
78
|
+
nodeProxy: sanitizeUrl(nodeProxyUrl),
|
|
79
|
+
leaseOpenMs: lease?._hsLeaseOpenMs,
|
|
80
|
+
leaseAgeMs: lease?._hsLeaseOpenedAt ? Date.now() - lease._hsLeaseOpenedAt : undefined,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
50
83
|
|
|
51
84
|
function createForwardError(statusCode, body) {
|
|
52
85
|
const error = new Error(`HTTP ${statusCode}: ${String(body || '').slice(0, 200)}`)
|
|
@@ -55,6 +88,16 @@ function createForwardError(statusCode, body) {
|
|
|
55
88
|
return error
|
|
56
89
|
}
|
|
57
90
|
|
|
91
|
+
function createClientValidationErrorBody(message) {
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
type: 'error',
|
|
94
|
+
error: {
|
|
95
|
+
type: 'client_validation_error',
|
|
96
|
+
message,
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
58
101
|
function shouldRefreshLeaseAfterError(err) {
|
|
59
102
|
const statusCode = Number(err?.statusCode || 0)
|
|
60
103
|
const body = String(err?.body || err?.message || '')
|
|
@@ -87,6 +130,8 @@ function isRetryableNodeLeaseError(err) {
|
|
|
87
130
|
'HTTP 503',
|
|
88
131
|
'HTTP 504',
|
|
89
132
|
'client_validation_error',
|
|
133
|
+
'upstream response timeout',
|
|
134
|
+
'upstream stream stalled',
|
|
90
135
|
'不可用'
|
|
91
136
|
].some((token) => message.includes(token))
|
|
92
137
|
}
|
|
@@ -107,6 +152,7 @@ async function fetchFreshLease(config, sessionId, options = {}) {
|
|
|
107
152
|
const controlPlaneUrl = getControlPlaneUrl(config)
|
|
108
153
|
if (!controlPlaneUrl) throw new Error('Claude relay control plane is not configured')
|
|
109
154
|
|
|
155
|
+
const startedAt = Date.now()
|
|
110
156
|
const response = await fetch(`${controlPlaneUrl}/session/open`, {
|
|
111
157
|
method: 'POST',
|
|
112
158
|
headers: { 'content-type': 'application/json' },
|
|
@@ -124,6 +170,17 @@ async function fetchFreshLease(config, sessionId, options = {}) {
|
|
|
124
170
|
if (!response.ok || !payload?.success || !payload?.data?.ticket) {
|
|
125
171
|
throw new Error(payload?.error?.message || `Failed to open Claude session (HTTP ${response.status})`)
|
|
126
172
|
}
|
|
173
|
+
const openedAt = Date.now()
|
|
174
|
+
payload.data._hsLeaseOpenMs = openedAt - startedAt
|
|
175
|
+
payload.data._hsLeaseOpenedAt = openedAt
|
|
176
|
+
if (payload.data._hsLeaseOpenMs >= SLOW_PATH_LOG_MS) {
|
|
177
|
+
logProxyTiming('lease.open', {
|
|
178
|
+
sessionId,
|
|
179
|
+
nodeId: payload.data.nodeId || '',
|
|
180
|
+
leaseOpenMs: payload.data._hsLeaseOpenMs,
|
|
181
|
+
forceReassign: options.forceReassign === true,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
127
184
|
leaseCache.set(sessionId, payload.data)
|
|
128
185
|
return payload.data
|
|
129
186
|
}
|
|
@@ -158,15 +215,27 @@ function deriveNodeProxyUrl(lease) {
|
|
|
158
215
|
return upstream.toString().replace(/\/+$/, '')
|
|
159
216
|
}
|
|
160
217
|
|
|
161
|
-
function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, extraHeaders = {} }) {
|
|
218
|
+
function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, extraHeaders = {}, trace = null }) {
|
|
162
219
|
const upstream = new URL(nodeProxyUrl)
|
|
163
220
|
return new Promise((resolve, reject) => {
|
|
221
|
+
const requestStartedAt = Date.now()
|
|
222
|
+
const resolvedTrace = trace || createForwardTrace({
|
|
223
|
+
clientReq,
|
|
224
|
+
targetUrl,
|
|
225
|
+
nodeProxyUrl,
|
|
226
|
+
sessionId: clientReq.headers['x-hs-session-id'] || '',
|
|
227
|
+
lease: null,
|
|
228
|
+
attempt: 0,
|
|
229
|
+
isDirect: !String(clientReq.url || '').startsWith('http'),
|
|
230
|
+
})
|
|
164
231
|
let settled = false
|
|
165
232
|
let responseTimer = null
|
|
166
233
|
let stallTimer = null
|
|
167
234
|
let sawUpstreamResponse = false
|
|
168
235
|
let sawUpstreamData = false
|
|
169
236
|
let forwardReq = null
|
|
237
|
+
let upstreamAssignedAt = null
|
|
238
|
+
let firstByteAt = null
|
|
170
239
|
|
|
171
240
|
const clearResponseTimer = () => {
|
|
172
241
|
if (responseTimer) {
|
|
@@ -191,6 +260,18 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
191
260
|
if (settled) return
|
|
192
261
|
settled = true
|
|
193
262
|
clearTimers()
|
|
263
|
+
const finishedAt = Date.now()
|
|
264
|
+
const totalMs = finishedAt - requestStartedAt
|
|
265
|
+
if (totalMs >= SLOW_PATH_LOG_MS) {
|
|
266
|
+
logProxyTiming('request.complete', {
|
|
267
|
+
...resolvedTrace,
|
|
268
|
+
totalMs,
|
|
269
|
+
connectMs: upstreamAssignedAt ? upstreamAssignedAt - requestStartedAt : undefined,
|
|
270
|
+
firstByteMs: firstByteAt ? firstByteAt - requestStartedAt : undefined,
|
|
271
|
+
streamMs: firstByteAt ? finishedAt - firstByteAt : undefined,
|
|
272
|
+
bytesStarted: sawUpstreamData,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
194
275
|
resolve()
|
|
195
276
|
}
|
|
196
277
|
|
|
@@ -198,21 +279,21 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
198
279
|
if (settled) return
|
|
199
280
|
settled = true
|
|
200
281
|
clearTimers()
|
|
282
|
+
const failedAt = Date.now()
|
|
283
|
+
logProxyTiming('request.fail', {
|
|
284
|
+
...resolvedTrace,
|
|
285
|
+
totalMs: failedAt - requestStartedAt,
|
|
286
|
+
connectMs: upstreamAssignedAt ? upstreamAssignedAt - requestStartedAt : undefined,
|
|
287
|
+
firstByteMs: firstByteAt ? firstByteAt - requestStartedAt : undefined,
|
|
288
|
+
bytesStarted: sawUpstreamData,
|
|
289
|
+
error: message,
|
|
290
|
+
})
|
|
201
291
|
if (forwardReq && !forwardReq.destroyed) {
|
|
202
292
|
forwardReq.destroy()
|
|
203
293
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
type: 'error',
|
|
208
|
-
error: {
|
|
209
|
-
type: 'client_validation_error',
|
|
210
|
-
message,
|
|
211
|
-
},
|
|
212
|
-
}))
|
|
213
|
-
return resolve()
|
|
214
|
-
}
|
|
215
|
-
reject(new Error(message))
|
|
294
|
+
const error = createForwardError(400, createClientValidationErrorBody(message))
|
|
295
|
+
error.message = message
|
|
296
|
+
reject(error)
|
|
216
297
|
}
|
|
217
298
|
|
|
218
299
|
const fail = (error) => {
|
|
@@ -257,14 +338,31 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
257
338
|
settled = true
|
|
258
339
|
clearTimers()
|
|
259
340
|
const body = Buffer.concat(chunks).toString('utf8')
|
|
260
|
-
|
|
341
|
+
const error = createForwardError(status, body)
|
|
342
|
+
logProxyTiming('request.upstream_error', {
|
|
343
|
+
...resolvedTrace,
|
|
344
|
+
status,
|
|
345
|
+
totalMs: Date.now() - requestStartedAt,
|
|
346
|
+
body: String(body || '').slice(0, 200),
|
|
347
|
+
})
|
|
348
|
+
reject(error)
|
|
261
349
|
})
|
|
262
350
|
forwardRes.on('error', fail)
|
|
263
351
|
return
|
|
264
352
|
}
|
|
265
353
|
|
|
354
|
+
upstreamAssignedAt = upstreamAssignedAt || Date.now()
|
|
266
355
|
clientRes.writeHead(status, forwardRes.headers)
|
|
267
|
-
forwardRes.on('data',
|
|
356
|
+
forwardRes.on('data', () => {
|
|
357
|
+
if (!firstByteAt) {
|
|
358
|
+
firstByteAt = Date.now()
|
|
359
|
+
logProxyTiming('request.first_byte', {
|
|
360
|
+
...resolvedTrace,
|
|
361
|
+
firstByteMs: firstByteAt - requestStartedAt,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
armStallTimer()
|
|
365
|
+
})
|
|
268
366
|
forwardRes.once('end', () => {
|
|
269
367
|
clearStallTimer()
|
|
270
368
|
finish()
|
|
@@ -277,6 +375,12 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
277
375
|
forwardRes.pipe(clientRes)
|
|
278
376
|
})
|
|
279
377
|
|
|
378
|
+
forwardReq.on('socket', (socket) => {
|
|
379
|
+
upstreamAssignedAt = upstreamAssignedAt || Date.now()
|
|
380
|
+
socket.once('connect', () => {
|
|
381
|
+
upstreamAssignedAt = upstreamAssignedAt || Date.now()
|
|
382
|
+
})
|
|
383
|
+
})
|
|
280
384
|
forwardReq.setTimeout(RESPONSE_TIMEOUT_MS, () => {
|
|
281
385
|
if (!sawUpstreamResponse) failWithAnthropicError('upstream response timeout')
|
|
282
386
|
})
|
|
@@ -319,8 +423,8 @@ function createConnectTunnel(proxyUrl, target, headers) {
|
|
|
319
423
|
|
|
320
424
|
// 等上游返回 first byte 的最大时间。
|
|
321
425
|
// 这里不能默认等 5 分钟,否则上游/代理链路卡死时,用户会感觉每次下一条请求都被“挂住”。
|
|
322
|
-
// 默认快速失败为
|
|
323
|
-
const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) ||
|
|
426
|
+
// 默认快速失败为 30 秒,仍可通过环境变量覆盖。
|
|
427
|
+
const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 30000
|
|
324
428
|
// 上游 stream 已开始后,相邻两个 chunk 之间的最大间隔。
|
|
325
429
|
const STALL_TIMEOUT_MS = Number(process.env.HS_CLAUDE_STALL_TIMEOUT_MS) || 120000
|
|
326
430
|
|
|
@@ -383,7 +487,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAn
|
|
|
383
487
|
const server = http.createServer(async (clientReq, clientRes) => {
|
|
384
488
|
const isDirect = !clientReq.url.startsWith('http')
|
|
385
489
|
|
|
386
|
-
const doForward = async (lease) => {
|
|
490
|
+
const doForward = async (lease, attempt) => {
|
|
387
491
|
const config = readConfig(configPath)
|
|
388
492
|
const nodeProxyUrl = deriveNodeProxyUrl(lease)
|
|
389
493
|
|
|
@@ -396,19 +500,38 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAn
|
|
|
396
500
|
clientReq,
|
|
397
501
|
clientRes,
|
|
398
502
|
extraHeaders: buildAuthHeaders(config, lease),
|
|
503
|
+
trace: createForwardTrace({
|
|
504
|
+
clientReq,
|
|
505
|
+
targetUrl: target,
|
|
506
|
+
nodeProxyUrl,
|
|
507
|
+
sessionId,
|
|
508
|
+
lease,
|
|
509
|
+
attempt,
|
|
510
|
+
isDirect,
|
|
511
|
+
}),
|
|
399
512
|
})
|
|
400
513
|
}
|
|
401
514
|
|
|
515
|
+
const targetUrl = new URL(clientReq.url)
|
|
402
516
|
const headers = {
|
|
403
517
|
...buildAuthHeaders(config, lease),
|
|
404
|
-
host:
|
|
518
|
+
host: targetUrl.host,
|
|
405
519
|
}
|
|
406
520
|
return forwardViaNodeProxy({
|
|
407
521
|
nodeProxyUrl,
|
|
408
|
-
targetUrl
|
|
522
|
+
targetUrl,
|
|
409
523
|
clientReq,
|
|
410
524
|
clientRes,
|
|
411
525
|
extraHeaders: headers,
|
|
526
|
+
trace: createForwardTrace({
|
|
527
|
+
clientReq,
|
|
528
|
+
targetUrl,
|
|
529
|
+
nodeProxyUrl,
|
|
530
|
+
sessionId,
|
|
531
|
+
lease,
|
|
532
|
+
attempt,
|
|
533
|
+
isDirect,
|
|
534
|
+
}),
|
|
412
535
|
})
|
|
413
536
|
}
|
|
414
537
|
|
|
@@ -416,7 +539,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAn
|
|
|
416
539
|
for (let attempt = 0; attempt <= MAX_PROXY_RETRIES; attempt++) {
|
|
417
540
|
try {
|
|
418
541
|
if (attempt === 0) {
|
|
419
|
-
await doForward(getCachedLease(sessionId))
|
|
542
|
+
await doForward(getCachedLease(sessionId), attempt)
|
|
420
543
|
} else {
|
|
421
544
|
const config = readConfig(configPath)
|
|
422
545
|
leaseCache.delete(sessionId)
|
|
@@ -426,12 +549,19 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAn
|
|
|
426
549
|
const freshLease = await fetchFreshLease(config, sessionId, {
|
|
427
550
|
forceReassign: shouldRefreshLeaseAfterError(lastError),
|
|
428
551
|
})
|
|
429
|
-
await doForward(freshLease)
|
|
552
|
+
await doForward(freshLease, attempt)
|
|
430
553
|
}
|
|
431
554
|
lastError = null
|
|
432
555
|
break
|
|
433
556
|
} catch (err) {
|
|
434
557
|
lastError = err
|
|
558
|
+
logProxyTiming('request.retry', {
|
|
559
|
+
sessionId,
|
|
560
|
+
attempt,
|
|
561
|
+
error: String(err?.message || err),
|
|
562
|
+
retryable: isRetryableNodeLeaseError(err),
|
|
563
|
+
forceReassign: shouldRefreshLeaseAfterError(err),
|
|
564
|
+
})
|
|
435
565
|
if (clientRes.headersSent) return
|
|
436
566
|
if (!isRetryableNodeLeaseError(err) && attempt > 0) break
|
|
437
567
|
}
|