@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.131",
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
- if (!clientRes.headersSent && !clientRes.destroyed) {
205
- clientRes.writeHead(400, { 'content-type': 'application/json; charset=utf-8' })
206
- clientRes.end(JSON.stringify({
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
- reject(createForwardError(status, body))
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', armStallTimer)
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
- // 默认快速失败为 15 秒,仍可通过环境变量覆盖。
323
- const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 15000
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: new URL(clientReq.url).host,
518
+ host: targetUrl.host,
405
519
  }
406
520
  return forwardViaNodeProxy({
407
521
  nodeProxyUrl,
408
- targetUrl: new URL(clientReq.url),
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
  }