@simonyea/holysheep-cli 1.7.130 → 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/commands/claude.js +9 -10
- package/src/tools/claude-process-proxy.js +287 -49
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",
|
package/src/commands/claude.js
CHANGED
|
@@ -68,22 +68,19 @@ async function runClaude(args = []) {
|
|
|
68
68
|
claudeCodeTool.writeSettings(settings)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const { server, port, sessionId } = await startProcessProxy({})
|
|
72
|
-
const proxyUrl = getLocalProxyUrl(port)
|
|
73
71
|
const runtime = typeof claudeCodeTool.detectClaudeRuntime === 'function'
|
|
74
72
|
? claudeCodeTool.detectClaudeRuntime()
|
|
75
73
|
: { kind: 'unknown', launchMode: 'env-proxy' }
|
|
74
|
+
const { server, port, sessionId } = await startProcessProxy({})
|
|
75
|
+
const proxyUrl = getLocalProxyUrl(port)
|
|
76
76
|
const launchMode = runtime.launchMode === 'node-inject'
|
|
77
77
|
? 'local-api + connect-fallback + node-inject'
|
|
78
|
-
: '
|
|
78
|
+
: 'whole-process-proxy + local-api'
|
|
79
79
|
|
|
80
80
|
const env = {
|
|
81
81
|
...process.env,
|
|
82
82
|
ANTHROPIC_API_KEY: undefined,
|
|
83
83
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
84
|
-
// Route ALL Anthropic API traffic exclusively through ANTHROPIC_BASE_URL.
|
|
85
|
-
// HTTP(S)_PROXY must NOT be set: it causes Claude Code to open CONNECT
|
|
86
|
-
// tunnels to api.anthropic.com, bypassing CRS multi-account scheduling.
|
|
87
84
|
ANTHROPIC_BASE_URL: proxyUrl,
|
|
88
85
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
89
86
|
HOLYSHEEP_CLAUDE_PROCESS_PROXY: '1',
|
|
@@ -91,10 +88,12 @@ async function runClaude(args = []) {
|
|
|
91
88
|
HS_PROXY_URL: proxyUrl,
|
|
92
89
|
HOLYSHEEP_CLAUDE_RUNTIME_KIND: runtime.kind || 'unknown',
|
|
93
90
|
HOLYSHEEP_CLAUDE_LAUNCH_MODE: launchMode,
|
|
94
|
-
HTTP_PROXY:
|
|
95
|
-
HTTPS_PROXY:
|
|
96
|
-
ALL_PROXY:
|
|
97
|
-
NO_PROXY:
|
|
91
|
+
HTTP_PROXY: proxyUrl,
|
|
92
|
+
HTTPS_PROXY: proxyUrl,
|
|
93
|
+
ALL_PROXY: proxyUrl,
|
|
94
|
+
NO_PROXY: mergeNoProxy(process.env.NO_PROXY, ['127.0.0.1', 'localhost']),
|
|
95
|
+
ANTHROPIC_MAX_RETRIES: '0',
|
|
96
|
+
MAX_RETRIES: '0',
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
// 不再写 settings.json — 只用 env 变量,避免 Claude Code 从两个来源
|
|
@@ -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,14 +215,110 @@ 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) => {
|
|
164
|
-
const
|
|
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
|
+
})
|
|
231
|
+
let settled = false
|
|
232
|
+
let responseTimer = null
|
|
233
|
+
let stallTimer = null
|
|
234
|
+
let sawUpstreamResponse = false
|
|
235
|
+
let sawUpstreamData = false
|
|
236
|
+
let forwardReq = null
|
|
237
|
+
let upstreamAssignedAt = null
|
|
238
|
+
let firstByteAt = null
|
|
239
|
+
|
|
240
|
+
const clearResponseTimer = () => {
|
|
241
|
+
if (responseTimer) {
|
|
242
|
+
clearTimeout(responseTimer)
|
|
243
|
+
responseTimer = null
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const clearStallTimer = () => {
|
|
248
|
+
if (stallTimer) {
|
|
249
|
+
clearTimeout(stallTimer)
|
|
250
|
+
stallTimer = null
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const clearTimers = () => {
|
|
255
|
+
clearResponseTimer()
|
|
256
|
+
clearStallTimer()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const finish = () => {
|
|
260
|
+
if (settled) return
|
|
261
|
+
settled = true
|
|
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
|
+
}
|
|
275
|
+
resolve()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const failWithAnthropicError = (message) => {
|
|
279
|
+
if (settled) return
|
|
280
|
+
settled = true
|
|
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
|
+
})
|
|
291
|
+
if (forwardReq && !forwardReq.destroyed) {
|
|
292
|
+
forwardReq.destroy()
|
|
293
|
+
}
|
|
294
|
+
const error = createForwardError(400, createClientValidationErrorBody(message))
|
|
295
|
+
error.message = message
|
|
296
|
+
reject(error)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const fail = (error) => {
|
|
300
|
+
const err = error instanceof Error ? error : new Error(String(error || 'Proxy error'))
|
|
301
|
+
return failWithAnthropicError(err.message || 'Proxy error')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const armResponseTimer = () => {
|
|
305
|
+
if (settled || sawUpstreamResponse || responseTimer) return
|
|
306
|
+
responseTimer = setTimeout(() => failWithAnthropicError('upstream response timeout'), RESPONSE_TIMEOUT_MS)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const armStallTimer = () => {
|
|
310
|
+
if (settled) return
|
|
311
|
+
sawUpstreamData = true
|
|
312
|
+
clearStallTimer()
|
|
313
|
+
stallTimer = setTimeout(() => failWithAnthropicError('upstream stream stalled'), STALL_TIMEOUT_MS)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
forwardReq = http.request({
|
|
165
317
|
host: upstream.hostname,
|
|
166
318
|
port: Number(upstream.port || 80),
|
|
167
319
|
method: clientReq.method,
|
|
168
320
|
path: targetUrl.toString(),
|
|
321
|
+
agent: false,
|
|
169
322
|
headers: {
|
|
170
323
|
...clientReq.headers,
|
|
171
324
|
...extraHeaders,
|
|
@@ -173,22 +326,68 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
173
326
|
connection: 'close',
|
|
174
327
|
},
|
|
175
328
|
}, (forwardRes) => {
|
|
329
|
+
sawUpstreamResponse = true
|
|
330
|
+
clearResponseTimer()
|
|
331
|
+
|
|
176
332
|
const status = forwardRes.statusCode || 502
|
|
177
333
|
if (status === 403 || status === 502 || status === 503) {
|
|
178
|
-
// 收集响应体用于错误消息,不透传给客户端
|
|
179
334
|
const chunks = []
|
|
180
335
|
forwardRes.on('data', (c) => chunks.push(c))
|
|
181
336
|
forwardRes.on('end', () => {
|
|
337
|
+
if (settled) return
|
|
338
|
+
settled = true
|
|
339
|
+
clearTimers()
|
|
182
340
|
const body = Buffer.concat(chunks).toString('utf8')
|
|
183
|
-
|
|
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)
|
|
184
349
|
})
|
|
350
|
+
forwardRes.on('error', fail)
|
|
185
351
|
return
|
|
186
352
|
}
|
|
353
|
+
|
|
354
|
+
upstreamAssignedAt = upstreamAssignedAt || Date.now()
|
|
187
355
|
clientRes.writeHead(status, forwardRes.headers)
|
|
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
|
+
})
|
|
366
|
+
forwardRes.once('end', () => {
|
|
367
|
+
clearStallTimer()
|
|
368
|
+
finish()
|
|
369
|
+
})
|
|
370
|
+
forwardRes.once('close', () => {
|
|
371
|
+
clearStallTimer()
|
|
372
|
+
finish()
|
|
373
|
+
})
|
|
374
|
+
forwardRes.once('error', fail)
|
|
188
375
|
forwardRes.pipe(clientRes)
|
|
189
|
-
resolve()
|
|
190
376
|
})
|
|
191
|
-
|
|
377
|
+
|
|
378
|
+
forwardReq.on('socket', (socket) => {
|
|
379
|
+
upstreamAssignedAt = upstreamAssignedAt || Date.now()
|
|
380
|
+
socket.once('connect', () => {
|
|
381
|
+
upstreamAssignedAt = upstreamAssignedAt || Date.now()
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
forwardReq.setTimeout(RESPONSE_TIMEOUT_MS, () => {
|
|
385
|
+
if (!sawUpstreamResponse) failWithAnthropicError('upstream response timeout')
|
|
386
|
+
})
|
|
387
|
+
forwardReq.once('error', fail)
|
|
388
|
+
clientReq.once('aborted', () => finish())
|
|
389
|
+
|
|
390
|
+
armResponseTimer()
|
|
192
391
|
clientReq.pipe(forwardReq)
|
|
193
392
|
})
|
|
194
393
|
}
|
|
@@ -202,6 +401,7 @@ function createConnectTunnel(proxyUrl, target, headers) {
|
|
|
202
401
|
method: 'CONNECT',
|
|
203
402
|
path: target,
|
|
204
403
|
headers,
|
|
404
|
+
agent: false,
|
|
205
405
|
timeout: 15000, // CONNECT 握手 15 秒超时
|
|
206
406
|
})
|
|
207
407
|
|
|
@@ -221,11 +421,11 @@ function createConnectTunnel(proxyUrl, target, headers) {
|
|
|
221
421
|
})
|
|
222
422
|
}
|
|
223
423
|
|
|
224
|
-
// 等上游返回 first byte 的最大时间。
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) ||
|
|
228
|
-
// 上游 stream 已开始后,相邻两个 chunk
|
|
424
|
+
// 等上游返回 first byte 的最大时间。
|
|
425
|
+
// 这里不能默认等 5 分钟,否则上游/代理链路卡死时,用户会感觉每次下一条请求都被“挂住”。
|
|
426
|
+
// 默认快速失败为 30 秒,仍可通过环境变量覆盖。
|
|
427
|
+
const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 30000
|
|
428
|
+
// 上游 stream 已开始后,相邻两个 chunk 之间的最大间隔。
|
|
229
429
|
const STALL_TIMEOUT_MS = Number(process.env.HS_CLAUDE_STALL_TIMEOUT_MS) || 120000
|
|
230
430
|
|
|
231
431
|
function pipeWithCleanup(a, b) {
|
|
@@ -233,49 +433,61 @@ function pipeWithCleanup(a, b) {
|
|
|
233
433
|
if (typeof sock.setKeepAlive === 'function') sock.setKeepAlive(true, 10000)
|
|
234
434
|
}
|
|
235
435
|
|
|
236
|
-
// 双阶段超时:
|
|
237
|
-
// 1. 客户端发了数据,上游 RESPONSE_TIMEOUT_MS 没回第一个字节 → 断开(node proxy 挂了)
|
|
238
|
-
// 2. 上游在流数据突然停了 STALL_TIMEOUT_MS → 断开(stream 中断)
|
|
239
436
|
let timer = null
|
|
437
|
+
let closed = false
|
|
240
438
|
let streaming = false
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
a.on('data', () => {
|
|
247
|
-
// 客户端发了数据(请求),等上游响应
|
|
248
|
-
if (!streaming) {
|
|
249
|
-
if (timer) clearTimeout(timer)
|
|
250
|
-
timer = setTimeout(() => kill('upstream response timeout'), RESPONSE_TIMEOUT_MS)
|
|
439
|
+
|
|
440
|
+
const clearTimer = () => {
|
|
441
|
+
if (timer) {
|
|
442
|
+
clearTimeout(timer)
|
|
443
|
+
timer = null
|
|
251
444
|
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const destroySocket = (sock, err) => {
|
|
448
|
+
if (!sock || sock.destroyed) return
|
|
449
|
+
if (err) sock.destroy(err)
|
|
450
|
+
else sock.destroy()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const close = (reason) => {
|
|
454
|
+
if (closed) return
|
|
455
|
+
closed = true
|
|
456
|
+
clearTimer()
|
|
457
|
+
const err = reason ? new Error(reason) : null
|
|
458
|
+
destroySocket(a, err)
|
|
459
|
+
destroySocket(b, err)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const armResponseTimer = () => {
|
|
463
|
+
if (streaming || closed || timer) return
|
|
464
|
+
timer = setTimeout(() => close('upstream response timeout'), RESPONSE_TIMEOUT_MS)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const armStallTimer = () => {
|
|
468
|
+
if (closed) return
|
|
255
469
|
streaming = true
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
470
|
+
clearTimer()
|
|
471
|
+
timer = setTimeout(() => close('upstream stream stalled'), STALL_TIMEOUT_MS)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
b.on('data', armStallTimer)
|
|
260
475
|
|
|
476
|
+
armResponseTimer()
|
|
261
477
|
a.pipe(b)
|
|
262
478
|
b.pipe(a)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
a.once('error', close)
|
|
269
|
-
b.once('error', close)
|
|
270
|
-
a.once('close', close)
|
|
271
|
-
b.once('close', close)
|
|
479
|
+
|
|
480
|
+
a.once('error', () => close())
|
|
481
|
+
b.once('error', () => close())
|
|
482
|
+
a.once('close', () => close())
|
|
483
|
+
b.once('close', () => close())
|
|
272
484
|
}
|
|
273
485
|
|
|
274
|
-
function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
486
|
+
function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAnthropicConnect = false }) {
|
|
275
487
|
const server = http.createServer(async (clientReq, clientRes) => {
|
|
276
488
|
const isDirect = !clientReq.url.startsWith('http')
|
|
277
489
|
|
|
278
|
-
const doForward = async (lease) => {
|
|
490
|
+
const doForward = async (lease, attempt) => {
|
|
279
491
|
const config = readConfig(configPath)
|
|
280
492
|
const nodeProxyUrl = deriveNodeProxyUrl(lease)
|
|
281
493
|
|
|
@@ -288,19 +500,38 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
288
500
|
clientReq,
|
|
289
501
|
clientRes,
|
|
290
502
|
extraHeaders: buildAuthHeaders(config, lease),
|
|
503
|
+
trace: createForwardTrace({
|
|
504
|
+
clientReq,
|
|
505
|
+
targetUrl: target,
|
|
506
|
+
nodeProxyUrl,
|
|
507
|
+
sessionId,
|
|
508
|
+
lease,
|
|
509
|
+
attempt,
|
|
510
|
+
isDirect,
|
|
511
|
+
}),
|
|
291
512
|
})
|
|
292
513
|
}
|
|
293
514
|
|
|
515
|
+
const targetUrl = new URL(clientReq.url)
|
|
294
516
|
const headers = {
|
|
295
517
|
...buildAuthHeaders(config, lease),
|
|
296
|
-
host:
|
|
518
|
+
host: targetUrl.host,
|
|
297
519
|
}
|
|
298
520
|
return forwardViaNodeProxy({
|
|
299
521
|
nodeProxyUrl,
|
|
300
|
-
targetUrl
|
|
522
|
+
targetUrl,
|
|
301
523
|
clientReq,
|
|
302
524
|
clientRes,
|
|
303
525
|
extraHeaders: headers,
|
|
526
|
+
trace: createForwardTrace({
|
|
527
|
+
clientReq,
|
|
528
|
+
targetUrl,
|
|
529
|
+
nodeProxyUrl,
|
|
530
|
+
sessionId,
|
|
531
|
+
lease,
|
|
532
|
+
attempt,
|
|
533
|
+
isDirect,
|
|
534
|
+
}),
|
|
304
535
|
})
|
|
305
536
|
}
|
|
306
537
|
|
|
@@ -308,7 +539,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
308
539
|
for (let attempt = 0; attempt <= MAX_PROXY_RETRIES; attempt++) {
|
|
309
540
|
try {
|
|
310
541
|
if (attempt === 0) {
|
|
311
|
-
await doForward(getCachedLease(sessionId))
|
|
542
|
+
await doForward(getCachedLease(sessionId), attempt)
|
|
312
543
|
} else {
|
|
313
544
|
const config = readConfig(configPath)
|
|
314
545
|
leaseCache.delete(sessionId)
|
|
@@ -318,12 +549,19 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
318
549
|
const freshLease = await fetchFreshLease(config, sessionId, {
|
|
319
550
|
forceReassign: shouldRefreshLeaseAfterError(lastError),
|
|
320
551
|
})
|
|
321
|
-
await doForward(freshLease)
|
|
552
|
+
await doForward(freshLease, attempt)
|
|
322
553
|
}
|
|
323
554
|
lastError = null
|
|
324
555
|
break
|
|
325
556
|
} catch (err) {
|
|
326
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
|
+
})
|
|
327
565
|
if (clientRes.headersSent) return
|
|
328
566
|
if (!isRetryableNodeLeaseError(err) && attempt > 0) break
|
|
329
567
|
}
|
|
@@ -341,7 +579,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
341
579
|
|
|
342
580
|
// Hosts that must NOT be tunneled directly — they must go through CRS via
|
|
343
581
|
// the HTTP path (ANTHROPIC_BASE_URL), not a CONNECT tunnel that bypasses it.
|
|
344
|
-
const BLOCKED_CONNECT = new Set(['api.anthropic.com'])
|
|
582
|
+
const BLOCKED_CONNECT = allowAnthropicConnect ? new Set() : new Set(['api.anthropic.com'])
|
|
345
583
|
|
|
346
584
|
server.on('connect', async (req, clientSocket, head) => {
|
|
347
585
|
const target = String(req.url || '').trim()
|
|
@@ -408,7 +646,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
408
646
|
return server
|
|
409
647
|
}
|
|
410
648
|
|
|
411
|
-
async function startProcessProxy({ port = null, sessionId = null, configPath = CONFIG_PATH } = {}) {
|
|
649
|
+
async function startProcessProxy({ port = null, sessionId = null, configPath = CONFIG_PATH, allowAnthropicConnect = false } = {}) {
|
|
412
650
|
const config = readConfig(configPath)
|
|
413
651
|
const preferredPort = port || getProcessProxyPort(config)
|
|
414
652
|
const effectiveSessionId = sessionId || crypto.randomUUID()
|
|
@@ -416,7 +654,7 @@ async function startProcessProxy({ port = null, sessionId = null, configPath = C
|
|
|
416
654
|
// 启动时拿一次 lease,之后靠被动重试维持,不再主动续约
|
|
417
655
|
await fetchFreshLease(config, effectiveSessionId)
|
|
418
656
|
|
|
419
|
-
const server = createProcessProxyServer({ sessionId: effectiveSessionId, configPath })
|
|
657
|
+
const server = createProcessProxyServer({ sessionId: effectiveSessionId, configPath, allowAnthropicConnect })
|
|
420
658
|
|
|
421
659
|
return new Promise((resolve, reject) => {
|
|
422
660
|
const tryListen = (p) => {
|