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