@simonyea/holysheep-cli 1.7.129 → 1.7.131
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 +150 -42
- package/src/tools/droid.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.131",
|
|
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 从两个来源
|
|
@@ -161,11 +161,83 @@ function deriveNodeProxyUrl(lease) {
|
|
|
161
161
|
function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, extraHeaders = {} }) {
|
|
162
162
|
const upstream = new URL(nodeProxyUrl)
|
|
163
163
|
return new Promise((resolve, reject) => {
|
|
164
|
-
|
|
164
|
+
let settled = false
|
|
165
|
+
let responseTimer = null
|
|
166
|
+
let stallTimer = null
|
|
167
|
+
let sawUpstreamResponse = false
|
|
168
|
+
let sawUpstreamData = false
|
|
169
|
+
let forwardReq = null
|
|
170
|
+
|
|
171
|
+
const clearResponseTimer = () => {
|
|
172
|
+
if (responseTimer) {
|
|
173
|
+
clearTimeout(responseTimer)
|
|
174
|
+
responseTimer = null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const clearStallTimer = () => {
|
|
179
|
+
if (stallTimer) {
|
|
180
|
+
clearTimeout(stallTimer)
|
|
181
|
+
stallTimer = null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const clearTimers = () => {
|
|
186
|
+
clearResponseTimer()
|
|
187
|
+
clearStallTimer()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const finish = () => {
|
|
191
|
+
if (settled) return
|
|
192
|
+
settled = true
|
|
193
|
+
clearTimers()
|
|
194
|
+
resolve()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const failWithAnthropicError = (message) => {
|
|
198
|
+
if (settled) return
|
|
199
|
+
settled = true
|
|
200
|
+
clearTimers()
|
|
201
|
+
if (forwardReq && !forwardReq.destroyed) {
|
|
202
|
+
forwardReq.destroy()
|
|
203
|
+
}
|
|
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))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const fail = (error) => {
|
|
219
|
+
const err = error instanceof Error ? error : new Error(String(error || 'Proxy error'))
|
|
220
|
+
return failWithAnthropicError(err.message || 'Proxy error')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const armResponseTimer = () => {
|
|
224
|
+
if (settled || sawUpstreamResponse || responseTimer) return
|
|
225
|
+
responseTimer = setTimeout(() => failWithAnthropicError('upstream response timeout'), RESPONSE_TIMEOUT_MS)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const armStallTimer = () => {
|
|
229
|
+
if (settled) return
|
|
230
|
+
sawUpstreamData = true
|
|
231
|
+
clearStallTimer()
|
|
232
|
+
stallTimer = setTimeout(() => failWithAnthropicError('upstream stream stalled'), STALL_TIMEOUT_MS)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
forwardReq = http.request({
|
|
165
236
|
host: upstream.hostname,
|
|
166
237
|
port: Number(upstream.port || 80),
|
|
167
238
|
method: clientReq.method,
|
|
168
239
|
path: targetUrl.toString(),
|
|
240
|
+
agent: false,
|
|
169
241
|
headers: {
|
|
170
242
|
...clientReq.headers,
|
|
171
243
|
...extraHeaders,
|
|
@@ -173,22 +245,45 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
173
245
|
connection: 'close',
|
|
174
246
|
},
|
|
175
247
|
}, (forwardRes) => {
|
|
248
|
+
sawUpstreamResponse = true
|
|
249
|
+
clearResponseTimer()
|
|
250
|
+
|
|
176
251
|
const status = forwardRes.statusCode || 502
|
|
177
252
|
if (status === 403 || status === 502 || status === 503) {
|
|
178
|
-
// 收集响应体用于错误消息,不透传给客户端
|
|
179
253
|
const chunks = []
|
|
180
254
|
forwardRes.on('data', (c) => chunks.push(c))
|
|
181
255
|
forwardRes.on('end', () => {
|
|
256
|
+
if (settled) return
|
|
257
|
+
settled = true
|
|
258
|
+
clearTimers()
|
|
182
259
|
const body = Buffer.concat(chunks).toString('utf8')
|
|
183
260
|
reject(createForwardError(status, body))
|
|
184
261
|
})
|
|
262
|
+
forwardRes.on('error', fail)
|
|
185
263
|
return
|
|
186
264
|
}
|
|
265
|
+
|
|
187
266
|
clientRes.writeHead(status, forwardRes.headers)
|
|
267
|
+
forwardRes.on('data', armStallTimer)
|
|
268
|
+
forwardRes.once('end', () => {
|
|
269
|
+
clearStallTimer()
|
|
270
|
+
finish()
|
|
271
|
+
})
|
|
272
|
+
forwardRes.once('close', () => {
|
|
273
|
+
clearStallTimer()
|
|
274
|
+
finish()
|
|
275
|
+
})
|
|
276
|
+
forwardRes.once('error', fail)
|
|
188
277
|
forwardRes.pipe(clientRes)
|
|
189
|
-
resolve()
|
|
190
278
|
})
|
|
191
|
-
|
|
279
|
+
|
|
280
|
+
forwardReq.setTimeout(RESPONSE_TIMEOUT_MS, () => {
|
|
281
|
+
if (!sawUpstreamResponse) failWithAnthropicError('upstream response timeout')
|
|
282
|
+
})
|
|
283
|
+
forwardReq.once('error', fail)
|
|
284
|
+
clientReq.once('aborted', () => finish())
|
|
285
|
+
|
|
286
|
+
armResponseTimer()
|
|
192
287
|
clientReq.pipe(forwardReq)
|
|
193
288
|
})
|
|
194
289
|
}
|
|
@@ -202,6 +297,7 @@ function createConnectTunnel(proxyUrl, target, headers) {
|
|
|
202
297
|
method: 'CONNECT',
|
|
203
298
|
path: target,
|
|
204
299
|
headers,
|
|
300
|
+
agent: false,
|
|
205
301
|
timeout: 15000, // CONNECT 握手 15 秒超时
|
|
206
302
|
})
|
|
207
303
|
|
|
@@ -221,11 +317,11 @@ function createConnectTunnel(proxyUrl, target, headers) {
|
|
|
221
317
|
})
|
|
222
318
|
}
|
|
223
319
|
|
|
224
|
-
// 等上游返回 first byte 的最大时间。
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) ||
|
|
228
|
-
// 上游 stream 已开始后,相邻两个 chunk
|
|
320
|
+
// 等上游返回 first byte 的最大时间。
|
|
321
|
+
// 这里不能默认等 5 分钟,否则上游/代理链路卡死时,用户会感觉每次下一条请求都被“挂住”。
|
|
322
|
+
// 默认快速失败为 15 秒,仍可通过环境变量覆盖。
|
|
323
|
+
const RESPONSE_TIMEOUT_MS = Number(process.env.HS_CLAUDE_RESPONSE_TIMEOUT_MS) || 15000
|
|
324
|
+
// 上游 stream 已开始后,相邻两个 chunk 之间的最大间隔。
|
|
229
325
|
const STALL_TIMEOUT_MS = Number(process.env.HS_CLAUDE_STALL_TIMEOUT_MS) || 120000
|
|
230
326
|
|
|
231
327
|
function pipeWithCleanup(a, b) {
|
|
@@ -233,45 +329,57 @@ function pipeWithCleanup(a, b) {
|
|
|
233
329
|
if (typeof sock.setKeepAlive === 'function') sock.setKeepAlive(true, 10000)
|
|
234
330
|
}
|
|
235
331
|
|
|
236
|
-
// 双阶段超时:
|
|
237
|
-
// 1. 客户端发了数据,上游 RESPONSE_TIMEOUT_MS 没回第一个字节 → 断开(node proxy 挂了)
|
|
238
|
-
// 2. 上游在流数据突然停了 STALL_TIMEOUT_MS → 断开(stream 中断)
|
|
239
332
|
let timer = null
|
|
333
|
+
let closed = false
|
|
240
334
|
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)
|
|
335
|
+
|
|
336
|
+
const clearTimer = () => {
|
|
337
|
+
if (timer) {
|
|
338
|
+
clearTimeout(timer)
|
|
339
|
+
timer = null
|
|
251
340
|
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const destroySocket = (sock, err) => {
|
|
344
|
+
if (!sock || sock.destroyed) return
|
|
345
|
+
if (err) sock.destroy(err)
|
|
346
|
+
else sock.destroy()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const close = (reason) => {
|
|
350
|
+
if (closed) return
|
|
351
|
+
closed = true
|
|
352
|
+
clearTimer()
|
|
353
|
+
const err = reason ? new Error(reason) : null
|
|
354
|
+
destroySocket(a, err)
|
|
355
|
+
destroySocket(b, err)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const armResponseTimer = () => {
|
|
359
|
+
if (streaming || closed || timer) return
|
|
360
|
+
timer = setTimeout(() => close('upstream response timeout'), RESPONSE_TIMEOUT_MS)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const armStallTimer = () => {
|
|
364
|
+
if (closed) return
|
|
255
365
|
streaming = true
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
366
|
+
clearTimer()
|
|
367
|
+
timer = setTimeout(() => close('upstream stream stalled'), STALL_TIMEOUT_MS)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
b.on('data', armStallTimer)
|
|
260
371
|
|
|
372
|
+
armResponseTimer()
|
|
261
373
|
a.pipe(b)
|
|
262
374
|
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)
|
|
375
|
+
|
|
376
|
+
a.once('error', () => close())
|
|
377
|
+
b.once('error', () => close())
|
|
378
|
+
a.once('close', () => close())
|
|
379
|
+
b.once('close', () => close())
|
|
272
380
|
}
|
|
273
381
|
|
|
274
|
-
function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
382
|
+
function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAnthropicConnect = false }) {
|
|
275
383
|
const server = http.createServer(async (clientReq, clientRes) => {
|
|
276
384
|
const isDirect = !clientReq.url.startsWith('http')
|
|
277
385
|
|
|
@@ -341,7 +449,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
341
449
|
|
|
342
450
|
// Hosts that must NOT be tunneled directly — they must go through CRS via
|
|
343
451
|
// the HTTP path (ANTHROPIC_BASE_URL), not a CONNECT tunnel that bypasses it.
|
|
344
|
-
const BLOCKED_CONNECT = new Set(['api.anthropic.com'])
|
|
452
|
+
const BLOCKED_CONNECT = allowAnthropicConnect ? new Set() : new Set(['api.anthropic.com'])
|
|
345
453
|
|
|
346
454
|
server.on('connect', async (req, clientSocket, head) => {
|
|
347
455
|
const target = String(req.url || '').trim()
|
|
@@ -408,7 +516,7 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH }) {
|
|
|
408
516
|
return server
|
|
409
517
|
}
|
|
410
518
|
|
|
411
|
-
async function startProcessProxy({ port = null, sessionId = null, configPath = CONFIG_PATH } = {}) {
|
|
519
|
+
async function startProcessProxy({ port = null, sessionId = null, configPath = CONFIG_PATH, allowAnthropicConnect = false } = {}) {
|
|
412
520
|
const config = readConfig(configPath)
|
|
413
521
|
const preferredPort = port || getProcessProxyPort(config)
|
|
414
522
|
const effectiveSessionId = sessionId || crypto.randomUUID()
|
|
@@ -416,7 +524,7 @@ async function startProcessProxy({ port = null, sessionId = null, configPath = C
|
|
|
416
524
|
// 启动时拿一次 lease,之后靠被动重试维持,不再主动续约
|
|
417
525
|
await fetchFreshLease(config, effectiveSessionId)
|
|
418
526
|
|
|
419
|
-
const server = createProcessProxyServer({ sessionId: effectiveSessionId, configPath })
|
|
527
|
+
const server = createProcessProxyServer({ sessionId: effectiveSessionId, configPath, allowAnthropicConnect })
|
|
420
528
|
|
|
421
529
|
return new Promise((resolve, reject) => {
|
|
422
530
|
const tryListen = (p) => {
|