@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.131",
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
- 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))
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
- reject(createForwardError(status, body))
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', armStallTimer)
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
- // 默认快速失败为 15 秒,仍可通过环境变量覆盖。
323
- const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 15000
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: new URL(clientReq.url).host,
520
+ host: targetUrl.host,
405
521
  }
406
522
  return forwardViaNodeProxy({
407
523
  nodeProxyUrl,
408
- targetUrl: new URL(clientReq.url),
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
  }