mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
主要变更: 1. **僵尸流看门狗 (Zombie Stream Watchdog)**: - 新增 resetActivityTimeout 机制,45秒无数据强制断开连接,防止服务假死。 2. **智能重试机制**: - 针对 Antigravity 429 (Resource Exhausted) 错误,自动清理会话并切换账号重试。 - 涵盖流式 (Stream) 和非流式 (Non-stream) 请求。 3. **Thought Signature 增强**: - 新增签名缓存与恢复机制 (signatureCache)。 - 增加 skip_thought_signature_validator 兜底签名策略。 - 强制补充 thought: true 标记以满足上游校验。 4. **系统稳定性与调试**: - 使用 util.inspect 替代 JSON.stringify 打印错误日志,彻底修复循环引用导致的服务崩溃。 - 新增针对 Antigravity 参数错误 (400) 的详细请求结构分析日志。 - 优化日志写入为轮转模式 (safeRotatingAppend)。 5. **其他优化**: - antigravityClient 数据处理安全增强 (safeDataToString)。
127 lines
2.9 KiB
JavaScript
127 lines
2.9 KiB
JavaScript
const path = require('path')
|
|
const logger = require('./logger')
|
|
const { getProjectRoot } = require('./projectPaths')
|
|
const { safeRotatingAppend } = require('./safeRotatingAppend')
|
|
|
|
const REQUEST_DUMP_ENV = 'ANTHROPIC_DEBUG_REQUEST_DUMP'
|
|
const REQUEST_DUMP_MAX_BYTES_ENV = 'ANTHROPIC_DEBUG_REQUEST_DUMP_MAX_BYTES'
|
|
const REQUEST_DUMP_FILENAME = 'anthropic-requests-dump.jsonl'
|
|
|
|
function isEnabled() {
|
|
const raw = process.env[REQUEST_DUMP_ENV]
|
|
if (!raw) {
|
|
return false
|
|
}
|
|
return raw === '1' || raw.toLowerCase() === 'true'
|
|
}
|
|
|
|
function getMaxBytes() {
|
|
const raw = process.env[REQUEST_DUMP_MAX_BYTES_ENV]
|
|
if (!raw) {
|
|
return 2 * 1024 * 1024
|
|
}
|
|
const parsed = Number.parseInt(raw, 10)
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
return 2 * 1024 * 1024
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
function maskSecret(value) {
|
|
if (value === null || value === undefined) {
|
|
return value
|
|
}
|
|
const str = String(value)
|
|
if (str.length <= 8) {
|
|
return '***'
|
|
}
|
|
return `${str.slice(0, 4)}...${str.slice(-4)}`
|
|
}
|
|
|
|
function sanitizeHeaders(headers) {
|
|
const sensitive = new Set([
|
|
'authorization',
|
|
'proxy-authorization',
|
|
'x-api-key',
|
|
'cookie',
|
|
'set-cookie',
|
|
'x-forwarded-for',
|
|
'x-real-ip'
|
|
])
|
|
|
|
const out = {}
|
|
for (const [k, v] of Object.entries(headers || {})) {
|
|
const key = k.toLowerCase()
|
|
if (sensitive.has(key)) {
|
|
out[key] = maskSecret(v)
|
|
continue
|
|
}
|
|
out[key] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
function safeJsonStringify(payload, maxBytes) {
|
|
let json = ''
|
|
try {
|
|
json = JSON.stringify(payload)
|
|
} catch (e) {
|
|
return JSON.stringify({
|
|
type: 'anthropic_request_dump_error',
|
|
error: 'JSON.stringify_failed',
|
|
message: e?.message || String(e)
|
|
})
|
|
}
|
|
|
|
if (Buffer.byteLength(json, 'utf8') <= maxBytes) {
|
|
return json
|
|
}
|
|
|
|
const truncated = Buffer.from(json, 'utf8').subarray(0, maxBytes).toString('utf8')
|
|
return JSON.stringify({
|
|
type: 'anthropic_request_dump_truncated',
|
|
maxBytes,
|
|
originalBytes: Buffer.byteLength(json, 'utf8'),
|
|
partialJson: truncated
|
|
})
|
|
}
|
|
|
|
async function dumpAnthropicMessagesRequest(req, meta = {}) {
|
|
if (!isEnabled()) {
|
|
return
|
|
}
|
|
|
|
const maxBytes = getMaxBytes()
|
|
const filename = path.join(getProjectRoot(), REQUEST_DUMP_FILENAME)
|
|
|
|
const record = {
|
|
ts: new Date().toISOString(),
|
|
requestId: req?.requestId || null,
|
|
method: req?.method || null,
|
|
url: req?.originalUrl || req?.url || null,
|
|
ip: req?.ip || null,
|
|
meta,
|
|
headers: sanitizeHeaders(req?.headers || {}),
|
|
body: req?.body || null
|
|
}
|
|
|
|
const line = `${safeJsonStringify(record, maxBytes)}\n`
|
|
|
|
try {
|
|
await safeRotatingAppend(filename, line)
|
|
} catch (e) {
|
|
logger.warn('Failed to dump Anthropic request', {
|
|
filename,
|
|
requestId: req?.requestId || null,
|
|
error: e?.message || String(e)
|
|
})
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
dumpAnthropicMessagesRequest,
|
|
REQUEST_DUMP_ENV,
|
|
REQUEST_DUMP_MAX_BYTES_ENV,
|
|
REQUEST_DUMP_FILENAME
|
|
}
|