mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
refactor: standardize code formatting and linting configuration
- Replace .eslintrc.js with .eslintrc.cjs for better ES module compatibility - Add .prettierrc configuration for consistent code formatting - Update package.json with new lint and format scripts - Add nodemon.json for development hot reloading configuration - Standardize code formatting across all JavaScript and Vue files - Update web admin SPA with improved linting rules and formatting - Add prettier configuration to web admin SPA 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,64 +1,63 @@
|
||||
const pricingService = require('../services/pricingService');
|
||||
const pricingService = require('../services/pricingService')
|
||||
|
||||
// Claude模型价格配置 (USD per 1M tokens) - 备用定价
|
||||
const MODEL_PRICING = {
|
||||
// Claude 3.5 Sonnet
|
||||
'claude-3-5-sonnet-20241022': {
|
||||
input: 3.00,
|
||||
output: 15.00,
|
||||
input: 3.0,
|
||||
output: 15.0,
|
||||
cacheWrite: 3.75,
|
||||
cacheRead: 0.30
|
||||
cacheRead: 0.3
|
||||
},
|
||||
'claude-sonnet-4-20250514': {
|
||||
input: 3.00,
|
||||
output: 15.00,
|
||||
input: 3.0,
|
||||
output: 15.0,
|
||||
cacheWrite: 3.75,
|
||||
cacheRead: 0.30
|
||||
cacheRead: 0.3
|
||||
},
|
||||
|
||||
|
||||
// Claude 3.5 Haiku
|
||||
'claude-3-5-haiku-20241022': {
|
||||
input: 0.25,
|
||||
output: 1.25,
|
||||
cacheWrite: 0.30,
|
||||
cacheWrite: 0.3,
|
||||
cacheRead: 0.03
|
||||
},
|
||||
|
||||
|
||||
// Claude 3 Opus
|
||||
'claude-3-opus-20240229': {
|
||||
input: 15.00,
|
||||
output: 75.00,
|
||||
input: 15.0,
|
||||
output: 75.0,
|
||||
cacheWrite: 18.75,
|
||||
cacheRead: 1.50
|
||||
cacheRead: 1.5
|
||||
},
|
||||
|
||||
|
||||
// Claude 3 Sonnet
|
||||
'claude-3-sonnet-20240229': {
|
||||
input: 3.00,
|
||||
output: 15.00,
|
||||
input: 3.0,
|
||||
output: 15.0,
|
||||
cacheWrite: 3.75,
|
||||
cacheRead: 0.30
|
||||
cacheRead: 0.3
|
||||
},
|
||||
|
||||
|
||||
// Claude 3 Haiku
|
||||
'claude-3-haiku-20240307': {
|
||||
input: 0.25,
|
||||
output: 1.25,
|
||||
cacheWrite: 0.30,
|
||||
cacheWrite: 0.3,
|
||||
cacheRead: 0.03
|
||||
},
|
||||
|
||||
|
||||
// 默认定价(用于未知模型)
|
||||
'unknown': {
|
||||
input: 3.00,
|
||||
output: 15.00,
|
||||
unknown: {
|
||||
input: 3.0,
|
||||
output: 15.0,
|
||||
cacheWrite: 3.75,
|
||||
cacheRead: 0.30
|
||||
cacheRead: 0.3
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class CostCalculator {
|
||||
|
||||
/**
|
||||
* 计算单次请求的费用
|
||||
* @param {Object} usage - 使用量数据
|
||||
@@ -70,16 +69,16 @@ class CostCalculator {
|
||||
* @returns {Object} 费用详情
|
||||
*/
|
||||
static calculateCost(usage, model = 'unknown') {
|
||||
const inputTokens = usage.input_tokens || 0;
|
||||
const outputTokens = usage.output_tokens || 0;
|
||||
const cacheCreateTokens = usage.cache_creation_input_tokens || 0;
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
||||
|
||||
const inputTokens = usage.input_tokens || 0
|
||||
const outputTokens = usage.output_tokens || 0
|
||||
const cacheCreateTokens = usage.cache_creation_input_tokens || 0
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || 0
|
||||
|
||||
// 优先使用动态价格服务
|
||||
const pricingData = pricingService.getModelPricing(model);
|
||||
let pricing;
|
||||
let usingDynamicPricing = false;
|
||||
|
||||
const pricingData = pricingService.getModelPricing(model)
|
||||
let pricing
|
||||
let usingDynamicPricing = false
|
||||
|
||||
if (pricingData) {
|
||||
// 转换动态价格格式为内部格式
|
||||
pricing = {
|
||||
@@ -87,21 +86,21 @@ class CostCalculator {
|
||||
output: (pricingData.output_cost_per_token || 0) * 1000000,
|
||||
cacheWrite: (pricingData.cache_creation_input_token_cost || 0) * 1000000,
|
||||
cacheRead: (pricingData.cache_read_input_token_cost || 0) * 1000000
|
||||
};
|
||||
usingDynamicPricing = true;
|
||||
}
|
||||
usingDynamicPricing = true
|
||||
} else {
|
||||
// 回退到静态价格
|
||||
pricing = MODEL_PRICING[model] || MODEL_PRICING['unknown'];
|
||||
pricing = MODEL_PRICING[model] || MODEL_PRICING['unknown']
|
||||
}
|
||||
|
||||
|
||||
// 计算各类型token的费用 (USD)
|
||||
const inputCost = (inputTokens / 1000000) * pricing.input;
|
||||
const outputCost = (outputTokens / 1000000) * pricing.output;
|
||||
const cacheWriteCost = (cacheCreateTokens / 1000000) * pricing.cacheWrite;
|
||||
const cacheReadCost = (cacheReadTokens / 1000000) * pricing.cacheRead;
|
||||
|
||||
const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost;
|
||||
|
||||
const inputCost = (inputTokens / 1000000) * pricing.input
|
||||
const outputCost = (outputTokens / 1000000) * pricing.output
|
||||
const cacheWriteCost = (cacheCreateTokens / 1000000) * pricing.cacheWrite
|
||||
const cacheReadCost = (cacheReadTokens / 1000000) * pricing.cacheRead
|
||||
|
||||
const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost
|
||||
|
||||
return {
|
||||
model,
|
||||
pricing,
|
||||
@@ -128,9 +127,9 @@ class CostCalculator {
|
||||
cacheRead: this.formatCost(cacheReadCost),
|
||||
total: this.formatCost(totalCost)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算聚合使用量的费用
|
||||
* @param {Object} aggregatedUsage - 聚合使用量数据
|
||||
@@ -141,39 +140,41 @@ class CostCalculator {
|
||||
const usage = {
|
||||
input_tokens: aggregatedUsage.inputTokens || aggregatedUsage.totalInputTokens || 0,
|
||||
output_tokens: aggregatedUsage.outputTokens || aggregatedUsage.totalOutputTokens || 0,
|
||||
cache_creation_input_tokens: aggregatedUsage.cacheCreateTokens || aggregatedUsage.totalCacheCreateTokens || 0,
|
||||
cache_read_input_tokens: aggregatedUsage.cacheReadTokens || aggregatedUsage.totalCacheReadTokens || 0
|
||||
};
|
||||
|
||||
return this.calculateCost(usage, model);
|
||||
cache_creation_input_tokens:
|
||||
aggregatedUsage.cacheCreateTokens || aggregatedUsage.totalCacheCreateTokens || 0,
|
||||
cache_read_input_tokens:
|
||||
aggregatedUsage.cacheReadTokens || aggregatedUsage.totalCacheReadTokens || 0
|
||||
}
|
||||
|
||||
return this.calculateCost(usage, model)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取模型定价信息
|
||||
* @param {string} model - 模型名称
|
||||
* @returns {Object} 定价信息
|
||||
*/
|
||||
static getModelPricing(model = 'unknown') {
|
||||
return MODEL_PRICING[model] || MODEL_PRICING['unknown'];
|
||||
return MODEL_PRICING[model] || MODEL_PRICING['unknown']
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有支持的模型和定价
|
||||
* @returns {Object} 所有模型定价
|
||||
*/
|
||||
static getAllModelPricing() {
|
||||
return { ...MODEL_PRICING };
|
||||
return { ...MODEL_PRICING }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证模型是否支持
|
||||
* @param {string} model - 模型名称
|
||||
* @returns {boolean} 是否支持
|
||||
*/
|
||||
static isModelSupported(model) {
|
||||
return !!MODEL_PRICING[model];
|
||||
return !!MODEL_PRICING[model]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化费用显示
|
||||
* @param {number} cost - 费用金额
|
||||
@@ -182,14 +183,14 @@ class CostCalculator {
|
||||
*/
|
||||
static formatCost(cost, decimals = 6) {
|
||||
if (cost >= 1) {
|
||||
return `$${cost.toFixed(2)}`;
|
||||
return `$${cost.toFixed(2)}`
|
||||
} else if (cost >= 0.001) {
|
||||
return `$${cost.toFixed(4)}`;
|
||||
return `$${cost.toFixed(4)}`
|
||||
} else {
|
||||
return `$${cost.toFixed(decimals)}`;
|
||||
return `$${cost.toFixed(decimals)}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算费用节省(使用缓存的节省)
|
||||
* @param {Object} usage - 使用量数据
|
||||
@@ -197,15 +198,15 @@ class CostCalculator {
|
||||
* @returns {Object} 节省信息
|
||||
*/
|
||||
static calculateCacheSavings(usage, model = 'unknown') {
|
||||
const pricing = this.getModelPricing(model);
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
||||
|
||||
const pricing = this.getModelPricing(model)
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || 0
|
||||
|
||||
// 如果这些token不使用缓存,需要按正常input价格计费
|
||||
const normalCost = (cacheReadTokens / 1000000) * pricing.input;
|
||||
const cacheCost = (cacheReadTokens / 1000000) * pricing.cacheRead;
|
||||
const savings = normalCost - cacheCost;
|
||||
const savingsPercentage = normalCost > 0 ? (savings / normalCost) * 100 : 0;
|
||||
|
||||
const normalCost = (cacheReadTokens / 1000000) * pricing.input
|
||||
const cacheCost = (cacheReadTokens / 1000000) * pricing.cacheRead
|
||||
const savings = normalCost - cacheCost
|
||||
const savingsPercentage = normalCost > 0 ? (savings / normalCost) * 100 : 0
|
||||
|
||||
return {
|
||||
normalCost,
|
||||
cacheCost,
|
||||
@@ -217,8 +218,8 @@ class CostCalculator {
|
||||
savings: this.formatCost(savings),
|
||||
savingsPercentage: `${savingsPercentage.toFixed(1)}%`
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CostCalculator;
|
||||
module.exports = CostCalculator
|
||||
|
||||
@@ -1,52 +1,58 @@
|
||||
const winston = require('winston');
|
||||
const DailyRotateFile = require('winston-daily-rotate-file');
|
||||
const config = require('../../config/config');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const winston = require('winston')
|
||||
const DailyRotateFile = require('winston-daily-rotate-file')
|
||||
const config = require('../../config/config')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const os = require('os')
|
||||
|
||||
// 安全的 JSON 序列化函数,处理循环引用
|
||||
const safeStringify = (obj, maxDepth = 3) => {
|
||||
const seen = new WeakSet();
|
||||
|
||||
const seen = new WeakSet()
|
||||
|
||||
const replacer = (key, value, depth = 0) => {
|
||||
if (depth > maxDepth) return '[Max Depth Reached]';
|
||||
|
||||
if (depth > maxDepth) {
|
||||
return '[Max Depth Reached]'
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === 'object') {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular Reference]';
|
||||
return '[Circular Reference]'
|
||||
}
|
||||
seen.add(value);
|
||||
|
||||
seen.add(value)
|
||||
|
||||
// 过滤掉常见的循环引用对象
|
||||
if (value.constructor) {
|
||||
const constructorName = value.constructor.name;
|
||||
if (['Socket', 'TLSSocket', 'HTTPParser', 'IncomingMessage', 'ServerResponse'].includes(constructorName)) {
|
||||
return `[${constructorName} Object]`;
|
||||
const constructorName = value.constructor.name
|
||||
if (
|
||||
['Socket', 'TLSSocket', 'HTTPParser', 'IncomingMessage', 'ServerResponse'].includes(
|
||||
constructorName
|
||||
)
|
||||
) {
|
||||
return `[${constructorName} Object]`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 递归处理对象属性
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item, index) => replacer(index, item, depth + 1));
|
||||
return value.map((item, index) => replacer(index, item, depth + 1))
|
||||
} else {
|
||||
const result = {};
|
||||
const result = {}
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
result[k] = replacer(k, v, depth + 1);
|
||||
result[k] = replacer(k, v, depth + 1)
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(replacer('', obj));
|
||||
} catch (error) {
|
||||
return JSON.stringify({ error: 'Failed to serialize object', message: error.message });
|
||||
|
||||
return value
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(replacer('', obj))
|
||||
} catch (error) {
|
||||
return JSON.stringify({ error: 'Failed to serialize object', message: error.message })
|
||||
}
|
||||
}
|
||||
|
||||
// 📝 增强的日志格式
|
||||
const createLogFormat = (colorize = false) => {
|
||||
@@ -54,12 +60,12 @@ const createLogFormat = (colorize = false) => {
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'stack'] })
|
||||
];
|
||||
|
||||
]
|
||||
|
||||
if (colorize) {
|
||||
formats.push(winston.format.colorize());
|
||||
formats.push(winston.format.colorize())
|
||||
}
|
||||
|
||||
|
||||
formats.push(
|
||||
winston.format.printf(({ level, message, timestamp, stack, metadata, ...rest }) => {
|
||||
const emoji = {
|
||||
@@ -68,39 +74,39 @@ const createLogFormat = (colorize = false) => {
|
||||
info: 'ℹ️ ',
|
||||
debug: '🐛',
|
||||
verbose: '📝'
|
||||
};
|
||||
|
||||
let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}`;
|
||||
|
||||
}
|
||||
|
||||
let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}`
|
||||
|
||||
// 添加元数据
|
||||
if (metadata && Object.keys(metadata).length > 0) {
|
||||
logMessage += ` | ${safeStringify(metadata)}`;
|
||||
logMessage += ` | ${safeStringify(metadata)}`
|
||||
}
|
||||
|
||||
// 添加其他属性
|
||||
const additionalData = { ...rest };
|
||||
delete additionalData.level;
|
||||
delete additionalData.message;
|
||||
delete additionalData.timestamp;
|
||||
delete additionalData.stack;
|
||||
|
||||
if (Object.keys(additionalData).length > 0) {
|
||||
logMessage += ` | ${safeStringify(additionalData)}`;
|
||||
}
|
||||
|
||||
return stack ? `${logMessage}\n${stack}` : logMessage;
|
||||
})
|
||||
);
|
||||
|
||||
return winston.format.combine(...formats);
|
||||
};
|
||||
|
||||
const logFormat = createLogFormat(false);
|
||||
const consoleFormat = createLogFormat(true);
|
||||
// 添加其他属性
|
||||
const additionalData = { ...rest }
|
||||
delete additionalData.level
|
||||
delete additionalData.message
|
||||
delete additionalData.timestamp
|
||||
delete additionalData.stack
|
||||
|
||||
if (Object.keys(additionalData).length > 0) {
|
||||
logMessage += ` | ${safeStringify(additionalData)}`
|
||||
}
|
||||
|
||||
return stack ? `${logMessage}\n${stack}` : logMessage
|
||||
})
|
||||
)
|
||||
|
||||
return winston.format.combine(...formats)
|
||||
}
|
||||
|
||||
const logFormat = createLogFormat(false)
|
||||
const consoleFormat = createLogFormat(true)
|
||||
|
||||
// 📁 确保日志目录存在并设置权限
|
||||
if (!fs.existsSync(config.logging.dirname)) {
|
||||
fs.mkdirSync(config.logging.dirname, { recursive: true, mode: 0o755 });
|
||||
fs.mkdirSync(config.logging.dirname, { recursive: true, mode: 0o755 })
|
||||
}
|
||||
|
||||
// 🔄 增强的日志轮转配置
|
||||
@@ -113,40 +119,38 @@ const createRotateTransport = (filename, level = null) => {
|
||||
maxFiles: config.logging.maxFiles,
|
||||
auditFile: path.join(config.logging.dirname, `.${filename.replace('%DATE%', 'audit')}.json`),
|
||||
format: logFormat
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
if (level) {
|
||||
transport.level = level;
|
||||
transport.level = level
|
||||
}
|
||||
|
||||
|
||||
// 监听轮转事件
|
||||
transport.on('rotate', (oldFilename, newFilename) => {
|
||||
console.log(`📦 Log rotated: ${oldFilename} -> ${newFilename}`);
|
||||
});
|
||||
|
||||
transport.on('new', (newFilename) => {
|
||||
console.log(`📄 New log file created: ${newFilename}`);
|
||||
});
|
||||
|
||||
transport.on('archive', (zipFilename) => {
|
||||
console.log(`🗜️ Log archived: ${zipFilename}`);
|
||||
});
|
||||
|
||||
return transport;
|
||||
};
|
||||
console.log(`📦 Log rotated: ${oldFilename} -> ${newFilename}`)
|
||||
})
|
||||
|
||||
const dailyRotateFileTransport = createRotateTransport('claude-relay-%DATE%.log');
|
||||
const errorFileTransport = createRotateTransport('claude-relay-error-%DATE%.log', 'error');
|
||||
transport.on('new', (newFilename) => {
|
||||
console.log(`📄 New log file created: ${newFilename}`)
|
||||
})
|
||||
|
||||
transport.on('archive', (zipFilename) => {
|
||||
console.log(`🗜️ Log archived: ${zipFilename}`)
|
||||
})
|
||||
|
||||
return transport
|
||||
}
|
||||
|
||||
const dailyRotateFileTransport = createRotateTransport('claude-relay-%DATE%.log')
|
||||
const errorFileTransport = createRotateTransport('claude-relay-error-%DATE%.log', 'error')
|
||||
|
||||
// 🔒 创建专门的安全日志记录器
|
||||
const securityLogger = winston.createLogger({
|
||||
level: 'warn',
|
||||
format: logFormat,
|
||||
transports: [
|
||||
createRotateTransport('claude-relay-security-%DATE%.log', 'warn')
|
||||
],
|
||||
transports: [createRotateTransport('claude-relay-security-%DATE%.log', 'warn')],
|
||||
silent: false
|
||||
});
|
||||
})
|
||||
|
||||
// 🌟 增强的 Winston logger
|
||||
const logger = winston.createLogger({
|
||||
@@ -156,7 +160,7 @@ const logger = winston.createLogger({
|
||||
// 📄 文件输出
|
||||
dailyRotateFileTransport,
|
||||
errorFileTransport,
|
||||
|
||||
|
||||
// 🖥️ 控制台输出
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat,
|
||||
@@ -164,10 +168,10 @@ const logger = winston.createLogger({
|
||||
handleRejections: false
|
||||
})
|
||||
],
|
||||
|
||||
|
||||
// 🚨 异常处理
|
||||
exceptionHandlers: [
|
||||
new winston.transports.File({
|
||||
new winston.transports.File({
|
||||
filename: path.join(config.logging.dirname, 'exceptions.log'),
|
||||
format: logFormat,
|
||||
maxsize: 10485760, // 10MB
|
||||
@@ -177,10 +181,10 @@ const logger = winston.createLogger({
|
||||
format: consoleFormat
|
||||
})
|
||||
],
|
||||
|
||||
|
||||
// 🔄 未捕获异常处理
|
||||
rejectionHandlers: [
|
||||
new winston.transports.File({
|
||||
new winston.transports.File({
|
||||
filename: path.join(config.logging.dirname, 'rejections.log'),
|
||||
format: logFormat,
|
||||
maxsize: 10485760, // 10MB
|
||||
@@ -190,24 +194,24 @@ const logger = winston.createLogger({
|
||||
format: consoleFormat
|
||||
})
|
||||
],
|
||||
|
||||
|
||||
// 防止进程退出
|
||||
exitOnError: false
|
||||
});
|
||||
})
|
||||
|
||||
// 🎯 增强的自定义方法
|
||||
logger.success = (message, metadata = {}) => {
|
||||
logger.info(`✅ ${message}`, { type: 'success', ...metadata });
|
||||
};
|
||||
logger.info(`✅ ${message}`, { type: 'success', ...metadata })
|
||||
}
|
||||
|
||||
logger.start = (message, metadata = {}) => {
|
||||
logger.info(`🚀 ${message}`, { type: 'startup', ...metadata });
|
||||
};
|
||||
logger.info(`🚀 ${message}`, { type: 'startup', ...metadata })
|
||||
}
|
||||
|
||||
logger.request = (method, url, status, duration, metadata = {}) => {
|
||||
const emoji = status >= 400 ? '🔴' : status >= 300 ? '🟡' : '🟢';
|
||||
const level = status >= 400 ? 'error' : status >= 300 ? 'warn' : 'info';
|
||||
|
||||
const emoji = status >= 400 ? '🔴' : status >= 300 ? '🟡' : '🟢'
|
||||
const level = status >= 400 ? 'error' : status >= 300 ? 'warn' : 'info'
|
||||
|
||||
logger[level](`${emoji} ${method} ${url} - ${status} (${duration}ms)`, {
|
||||
type: 'request',
|
||||
method,
|
||||
@@ -215,12 +219,12 @@ logger.request = (method, url, status, duration, metadata = {}) => {
|
||||
status,
|
||||
duration,
|
||||
...metadata
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
logger.api = (message, metadata = {}) => {
|
||||
logger.info(`🔗 ${message}`, { type: 'api', ...metadata });
|
||||
};
|
||||
logger.info(`🔗 ${message}`, { type: 'api', ...metadata })
|
||||
}
|
||||
|
||||
logger.security = (message, metadata = {}) => {
|
||||
const securityData = {
|
||||
@@ -229,99 +233,99 @@ logger.security = (message, metadata = {}) => {
|
||||
pid: process.pid,
|
||||
hostname: os.hostname(),
|
||||
...metadata
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// 记录到主日志
|
||||
logger.warn(`🔒 ${message}`, securityData);
|
||||
|
||||
logger.warn(`🔒 ${message}`, securityData)
|
||||
|
||||
// 记录到专门的安全日志文件
|
||||
try {
|
||||
securityLogger.warn(`🔒 ${message}`, securityData);
|
||||
securityLogger.warn(`🔒 ${message}`, securityData)
|
||||
} catch (error) {
|
||||
// 如果安全日志文件不可用,只记录到主日志
|
||||
console.warn('Security logger not available:', error.message);
|
||||
console.warn('Security logger not available:', error.message)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
logger.database = (message, metadata = {}) => {
|
||||
logger.debug(`💾 ${message}`, { type: 'database', ...metadata });
|
||||
};
|
||||
logger.debug(`💾 ${message}`, { type: 'database', ...metadata })
|
||||
}
|
||||
|
||||
logger.performance = (message, metadata = {}) => {
|
||||
logger.info(`⚡ ${message}`, { type: 'performance', ...metadata });
|
||||
};
|
||||
logger.info(`⚡ ${message}`, { type: 'performance', ...metadata })
|
||||
}
|
||||
|
||||
logger.audit = (message, metadata = {}) => {
|
||||
logger.info(`📋 ${message}`, {
|
||||
logger.info(`📋 ${message}`, {
|
||||
type: 'audit',
|
||||
timestamp: new Date().toISOString(),
|
||||
pid: process.pid,
|
||||
...metadata
|
||||
});
|
||||
};
|
||||
...metadata
|
||||
})
|
||||
}
|
||||
|
||||
// 🔧 性能监控方法
|
||||
logger.timer = (label) => {
|
||||
const start = Date.now();
|
||||
const start = Date.now()
|
||||
return {
|
||||
end: (message = '', metadata = {}) => {
|
||||
const duration = Date.now() - start;
|
||||
logger.performance(`${label} ${message}`, { duration, ...metadata });
|
||||
return duration;
|
||||
const duration = Date.now() - start
|
||||
logger.performance(`${label} ${message}`, { duration, ...metadata })
|
||||
return duration
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 📊 日志统计
|
||||
logger.stats = {
|
||||
requests: 0,
|
||||
errors: 0,
|
||||
warnings: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 重写原始方法以统计
|
||||
const originalError = logger.error;
|
||||
const originalWarn = logger.warn;
|
||||
const originalInfo = logger.info;
|
||||
const originalError = logger.error
|
||||
const originalWarn = logger.warn
|
||||
const originalInfo = logger.info
|
||||
|
||||
logger.error = function(message, ...args) {
|
||||
logger.stats.errors++;
|
||||
return originalError.call(this, message, ...args);
|
||||
};
|
||||
logger.error = function (message, ...args) {
|
||||
logger.stats.errors++
|
||||
return originalError.call(this, message, ...args)
|
||||
}
|
||||
|
||||
logger.warn = function(message, ...args) {
|
||||
logger.stats.warnings++;
|
||||
return originalWarn.call(this, message, ...args);
|
||||
};
|
||||
logger.warn = function (message, ...args) {
|
||||
logger.stats.warnings++
|
||||
return originalWarn.call(this, message, ...args)
|
||||
}
|
||||
|
||||
logger.info = function(message, ...args) {
|
||||
logger.info = function (message, ...args) {
|
||||
// 检查是否是请求类型的日志
|
||||
if (args.length > 0 && typeof args[0] === 'object' && args[0].type === 'request') {
|
||||
logger.stats.requests++;
|
||||
logger.stats.requests++
|
||||
}
|
||||
return originalInfo.call(this, message, ...args);
|
||||
};
|
||||
return originalInfo.call(this, message, ...args)
|
||||
}
|
||||
|
||||
// 📈 获取日志统计
|
||||
logger.getStats = () => ({ ...logger.stats });
|
||||
logger.getStats = () => ({ ...logger.stats })
|
||||
|
||||
// 🧹 清理统计
|
||||
logger.resetStats = () => {
|
||||
logger.stats.requests = 0;
|
||||
logger.stats.errors = 0;
|
||||
logger.stats.warnings = 0;
|
||||
};
|
||||
logger.stats.requests = 0
|
||||
logger.stats.errors = 0
|
||||
logger.stats.warnings = 0
|
||||
}
|
||||
|
||||
// 📡 健康检查
|
||||
logger.healthCheck = () => {
|
||||
try {
|
||||
const testMessage = 'Logger health check';
|
||||
logger.debug(testMessage);
|
||||
return { healthy: true, timestamp: new Date().toISOString() };
|
||||
const testMessage = 'Logger health check'
|
||||
logger.debug(testMessage)
|
||||
return { healthy: true, timestamp: new Date().toISOString() }
|
||||
} catch (error) {
|
||||
return { healthy: false, error: error.message, timestamp: new Date().toISOString() };
|
||||
return { healthy: false, error: error.message, timestamp: new Date().toISOString() }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 🎬 启动日志记录系统
|
||||
logger.start('Logger initialized', {
|
||||
@@ -330,6 +334,6 @@ logger.start('Logger initialized', {
|
||||
maxSize: config.logging.maxSize,
|
||||
maxFiles: config.logging.maxFiles,
|
||||
envOverride: process.env.LOG_LEVEL ? true : false
|
||||
});
|
||||
})
|
||||
|
||||
module.exports = logger;
|
||||
module.exports = logger
|
||||
|
||||
@@ -3,27 +3,27 @@
|
||||
* 基于claude-code-login.js中的OAuth流程实现
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const axios = require('axios');
|
||||
const logger = require('./logger');
|
||||
const crypto = require('crypto')
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent')
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent')
|
||||
const axios = require('axios')
|
||||
const logger = require('./logger')
|
||||
|
||||
// OAuth 配置常量 - 从claude-code-login.js提取
|
||||
const OAUTH_CONFIG = {
|
||||
AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
|
||||
TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
|
||||
CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
||||
REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback',
|
||||
SCOPES: 'org:create_api_key user:profile user:inference'
|
||||
};
|
||||
AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
|
||||
TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
|
||||
CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
||||
REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback',
|
||||
SCOPES: 'org:create_api_key user:profile user:inference'
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机的 state 参数
|
||||
* @returns {string} 随机生成的 state (64字符hex)
|
||||
*/
|
||||
function generateState() {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
return crypto.randomBytes(32).toString('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ function generateState() {
|
||||
* @returns {string} base64url 编码的随机字符串
|
||||
*/
|
||||
function generateCodeVerifier() {
|
||||
return crypto.randomBytes(32).toString('base64url');
|
||||
return crypto.randomBytes(32).toString('base64url')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,9 +40,7 @@ function generateCodeVerifier() {
|
||||
* @returns {string} SHA256 哈希后的 base64url 编码字符串
|
||||
*/
|
||||
function generateCodeChallenge(codeVerifier) {
|
||||
return crypto.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest('base64url');
|
||||
return crypto.createHash('sha256').update(codeVerifier).digest('base64url')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,18 +50,18 @@ function generateCodeChallenge(codeVerifier) {
|
||||
* @returns {string} 完整的授权 URL
|
||||
*/
|
||||
function generateAuthUrl(codeChallenge, state) {
|
||||
const params = new URLSearchParams({
|
||||
code: 'true',
|
||||
client_id: OAUTH_CONFIG.CLIENT_ID,
|
||||
response_type: 'code',
|
||||
redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
|
||||
scope: OAUTH_CONFIG.SCOPES,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
state: state
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
code: 'true',
|
||||
client_id: OAUTH_CONFIG.CLIENT_ID,
|
||||
response_type: 'code',
|
||||
redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
|
||||
scope: OAUTH_CONFIG.SCOPES,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
state
|
||||
})
|
||||
|
||||
return `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`;
|
||||
return `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,18 +69,18 @@ function generateAuthUrl(codeChallenge, state) {
|
||||
* @returns {{authUrl: string, codeVerifier: string, state: string, codeChallenge: string}}
|
||||
*/
|
||||
function generateOAuthParams() {
|
||||
const state = generateState();
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
const authUrl = generateAuthUrl(codeChallenge, state);
|
||||
|
||||
return {
|
||||
authUrl,
|
||||
codeVerifier,
|
||||
state,
|
||||
codeChallenge
|
||||
};
|
||||
const state = generateState()
|
||||
const codeVerifier = generateCodeVerifier()
|
||||
const codeChallenge = generateCodeChallenge(codeVerifier)
|
||||
|
||||
const authUrl = generateAuthUrl(codeChallenge, state)
|
||||
|
||||
return {
|
||||
authUrl,
|
||||
codeVerifier,
|
||||
state,
|
||||
codeChallenge
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,25 +89,31 @@ function generateOAuthParams() {
|
||||
* @returns {object|null} 代理agent或null
|
||||
*/
|
||||
function createProxyAgent(proxyConfig) {
|
||||
if (!proxyConfig) {
|
||||
return null;
|
||||
}
|
||||
if (!proxyConfig) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
if (proxyConfig.type === 'socks5') {
|
||||
const auth = proxyConfig.username && proxyConfig.password ? `${proxyConfig.username}:${proxyConfig.password}@` : '';
|
||||
const socksUrl = `socks5://${auth}${proxyConfig.host}:${proxyConfig.port}`;
|
||||
return new SocksProxyAgent(socksUrl);
|
||||
} else if (proxyConfig.type === 'http' || proxyConfig.type === 'https') {
|
||||
const auth = proxyConfig.username && proxyConfig.password ? `${proxyConfig.username}:${proxyConfig.password}@` : '';
|
||||
const httpUrl = `${proxyConfig.type}://${auth}${proxyConfig.host}:${proxyConfig.port}`;
|
||||
return new HttpsProxyAgent(httpUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Invalid proxy configuration:', error);
|
||||
try {
|
||||
if (proxyConfig.type === 'socks5') {
|
||||
const auth =
|
||||
proxyConfig.username && proxyConfig.password
|
||||
? `${proxyConfig.username}:${proxyConfig.password}@`
|
||||
: ''
|
||||
const socksUrl = `socks5://${auth}${proxyConfig.host}:${proxyConfig.port}`
|
||||
return new SocksProxyAgent(socksUrl)
|
||||
} else if (proxyConfig.type === 'http' || proxyConfig.type === 'https') {
|
||||
const auth =
|
||||
proxyConfig.username && proxyConfig.password
|
||||
? `${proxyConfig.username}:${proxyConfig.password}@`
|
||||
: ''
|
||||
const httpUrl = `${proxyConfig.type}://${auth}${proxyConfig.host}:${proxyConfig.port}`
|
||||
return new HttpsProxyAgent(httpUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Invalid proxy configuration:', error)
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,110 +125,110 @@ function createProxyAgent(proxyConfig) {
|
||||
* @returns {Promise<object>} Claude格式的token响应
|
||||
*/
|
||||
async function exchangeCodeForTokens(authorizationCode, codeVerifier, state, proxyConfig = null) {
|
||||
// 清理授权码,移除URL片段
|
||||
const cleanedCode = authorizationCode.split('#')[0]?.split('&')[0] ?? authorizationCode;
|
||||
|
||||
const params = {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: OAUTH_CONFIG.CLIENT_ID,
|
||||
code: cleanedCode,
|
||||
redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
|
||||
code_verifier: codeVerifier,
|
||||
state: state
|
||||
};
|
||||
// 清理授权码,移除URL片段
|
||||
const cleanedCode = authorizationCode.split('#')[0]?.split('&')[0] ?? authorizationCode
|
||||
|
||||
// 创建代理agent
|
||||
const agent = createProxyAgent(proxyConfig);
|
||||
const params = {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: OAUTH_CONFIG.CLIENT_ID,
|
||||
code: cleanedCode,
|
||||
redirect_uri: OAUTH_CONFIG.REDIRECT_URI,
|
||||
code_verifier: codeVerifier,
|
||||
state
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug('🔄 Attempting OAuth token exchange', {
|
||||
url: OAUTH_CONFIG.TOKEN_URL,
|
||||
codeLength: cleanedCode.length,
|
||||
codePrefix: cleanedCode.substring(0, 10) + '...',
|
||||
hasProxy: !!proxyConfig,
|
||||
proxyType: proxyConfig?.type || 'none'
|
||||
});
|
||||
// 创建代理agent
|
||||
const agent = createProxyAgent(proxyConfig)
|
||||
|
||||
const response = await axios.post(OAUTH_CONFIG.TOKEN_URL, params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'claude-cli/1.0.56 (external, cli)',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Referer': 'https://claude.ai/',
|
||||
'Origin': 'https://claude.ai'
|
||||
},
|
||||
httpsAgent: agent,
|
||||
timeout: 30000
|
||||
});
|
||||
try {
|
||||
logger.debug('🔄 Attempting OAuth token exchange', {
|
||||
url: OAUTH_CONFIG.TOKEN_URL,
|
||||
codeLength: cleanedCode.length,
|
||||
codePrefix: `${cleanedCode.substring(0, 10)}...`,
|
||||
hasProxy: !!proxyConfig,
|
||||
proxyType: proxyConfig?.type || 'none'
|
||||
})
|
||||
|
||||
logger.success('✅ OAuth token exchange successful', {
|
||||
status: response.status,
|
||||
hasAccessToken: !!response.data?.access_token,
|
||||
hasRefreshToken: !!response.data?.refresh_token,
|
||||
scopes: response.data?.scope
|
||||
});
|
||||
const response = await axios.post(OAUTH_CONFIG.TOKEN_URL, params, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'claude-cli/1.0.56 (external, cli)',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Referer: 'https://claude.ai/',
|
||||
Origin: 'https://claude.ai'
|
||||
},
|
||||
httpsAgent: agent,
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
const data = response.data;
|
||||
|
||||
// 返回Claude格式的token数据
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000,
|
||||
scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'],
|
||||
isMax: true
|
||||
};
|
||||
} catch (error) {
|
||||
// 处理axios错误响应
|
||||
if (error.response) {
|
||||
// 服务器返回了错误状态码
|
||||
const status = error.response.status;
|
||||
const errorData = error.response.data;
|
||||
|
||||
logger.error('❌ OAuth token exchange failed with server error', {
|
||||
status: status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: errorData,
|
||||
codeLength: cleanedCode.length,
|
||||
codePrefix: cleanedCode.substring(0, 10) + '...'
|
||||
});
|
||||
|
||||
// 尝试从错误响应中提取有用信息
|
||||
let errorMessage = `HTTP ${status}`;
|
||||
|
||||
if (errorData) {
|
||||
if (typeof errorData === 'string') {
|
||||
errorMessage += `: ${errorData}`;
|
||||
} else if (errorData.error) {
|
||||
errorMessage += `: ${errorData.error}`;
|
||||
if (errorData.error_description) {
|
||||
errorMessage += ` - ${errorData.error_description}`;
|
||||
}
|
||||
} else {
|
||||
errorMessage += `: ${JSON.stringify(errorData)}`;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Token exchange failed: ${errorMessage}`);
|
||||
} else if (error.request) {
|
||||
// 请求被发送但没有收到响应
|
||||
logger.error('❌ OAuth token exchange failed with network error', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
hasProxy: !!proxyConfig
|
||||
});
|
||||
throw new Error('Token exchange failed: No response from server (network error or timeout)');
|
||||
} else {
|
||||
// 其他错误
|
||||
logger.error('❌ OAuth token exchange failed with unknown error', {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw new Error(`Token exchange failed: ${error.message}`);
|
||||
}
|
||||
logger.success('✅ OAuth token exchange successful', {
|
||||
status: response.status,
|
||||
hasAccessToken: !!response.data?.access_token,
|
||||
hasRefreshToken: !!response.data?.refresh_token,
|
||||
scopes: response.data?.scope
|
||||
})
|
||||
|
||||
const { data } = response
|
||||
|
||||
// 返回Claude格式的token数据
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt: (Math.floor(Date.now() / 1000) + data.expires_in) * 1000,
|
||||
scopes: data.scope ? data.scope.split(' ') : ['user:inference', 'user:profile'],
|
||||
isMax: true
|
||||
}
|
||||
} catch (error) {
|
||||
// 处理axios错误响应
|
||||
if (error.response) {
|
||||
// 服务器返回了错误状态码
|
||||
const { status } = error.response
|
||||
const errorData = error.response.data
|
||||
|
||||
logger.error('❌ OAuth token exchange failed with server error', {
|
||||
status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: errorData,
|
||||
codeLength: cleanedCode.length,
|
||||
codePrefix: `${cleanedCode.substring(0, 10)}...`
|
||||
})
|
||||
|
||||
// 尝试从错误响应中提取有用信息
|
||||
let errorMessage = `HTTP ${status}`
|
||||
|
||||
if (errorData) {
|
||||
if (typeof errorData === 'string') {
|
||||
errorMessage += `: ${errorData}`
|
||||
} else if (errorData.error) {
|
||||
errorMessage += `: ${errorData.error}`
|
||||
if (errorData.error_description) {
|
||||
errorMessage += ` - ${errorData.error_description}`
|
||||
}
|
||||
} else {
|
||||
errorMessage += `: ${JSON.stringify(errorData)}`
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Token exchange failed: ${errorMessage}`)
|
||||
} else if (error.request) {
|
||||
// 请求被发送但没有收到响应
|
||||
logger.error('❌ OAuth token exchange failed with network error', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
hasProxy: !!proxyConfig
|
||||
})
|
||||
throw new Error('Token exchange failed: No response from server (network error or timeout)')
|
||||
} else {
|
||||
// 其他错误
|
||||
logger.error('❌ OAuth token exchange failed with unknown error', {
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
})
|
||||
throw new Error(`Token exchange failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,47 +237,47 @@ async function exchangeCodeForTokens(authorizationCode, codeVerifier, state, pro
|
||||
* @returns {string} 授权码
|
||||
*/
|
||||
function parseCallbackUrl(input) {
|
||||
if (!input || typeof input !== 'string') {
|
||||
throw new Error('请提供有效的授权码或回调 URL');
|
||||
}
|
||||
if (!input || typeof input !== 'string') {
|
||||
throw new Error('请提供有效的授权码或回调 URL')
|
||||
}
|
||||
|
||||
const trimmedInput = input.trim();
|
||||
|
||||
// 情况1: 尝试作为完整URL解析
|
||||
if (trimmedInput.startsWith('http://') || trimmedInput.startsWith('https://')) {
|
||||
try {
|
||||
const urlObj = new URL(trimmedInput);
|
||||
const authorizationCode = urlObj.searchParams.get('code');
|
||||
const trimmedInput = input.trim()
|
||||
|
||||
if (!authorizationCode) {
|
||||
throw new Error('回调 URL 中未找到授权码 (code 参数)');
|
||||
}
|
||||
// 情况1: 尝试作为完整URL解析
|
||||
if (trimmedInput.startsWith('http://') || trimmedInput.startsWith('https://')) {
|
||||
try {
|
||||
const urlObj = new URL(trimmedInput)
|
||||
const authorizationCode = urlObj.searchParams.get('code')
|
||||
|
||||
return authorizationCode;
|
||||
} catch (error) {
|
||||
if (error.message.includes('回调 URL 中未找到授权码')) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('无效的 URL 格式,请检查回调 URL 是否正确');
|
||||
}
|
||||
if (!authorizationCode) {
|
||||
throw new Error('回调 URL 中未找到授权码 (code 参数)')
|
||||
}
|
||||
|
||||
return authorizationCode
|
||||
} catch (error) {
|
||||
if (error.message.includes('回调 URL 中未找到授权码')) {
|
||||
throw error
|
||||
}
|
||||
throw new Error('无效的 URL 格式,请检查回调 URL 是否正确')
|
||||
}
|
||||
|
||||
// 情况2: 直接的授权码(可能包含URL fragments)
|
||||
// 参考claude-code-login.js的处理方式:移除URL fragments和参数
|
||||
const cleanedCode = trimmedInput.split('#')[0]?.split('&')[0] ?? trimmedInput;
|
||||
|
||||
// 验证授权码格式(Claude的授权码通常是base64url格式)
|
||||
if (!cleanedCode || cleanedCode.length < 10) {
|
||||
throw new Error('授权码格式无效,请确保复制了完整的 Authorization Code');
|
||||
}
|
||||
|
||||
// 基本格式验证:授权码应该只包含字母、数字、下划线、连字符
|
||||
const validCodePattern = /^[A-Za-z0-9_-]+$/;
|
||||
if (!validCodePattern.test(cleanedCode)) {
|
||||
throw new Error('授权码包含无效字符,请检查是否复制了正确的 Authorization Code');
|
||||
}
|
||||
|
||||
return cleanedCode;
|
||||
}
|
||||
|
||||
// 情况2: 直接的授权码(可能包含URL fragments)
|
||||
// 参考claude-code-login.js的处理方式:移除URL fragments和参数
|
||||
const cleanedCode = trimmedInput.split('#')[0]?.split('&')[0] ?? trimmedInput
|
||||
|
||||
// 验证授权码格式(Claude的授权码通常是base64url格式)
|
||||
if (!cleanedCode || cleanedCode.length < 10) {
|
||||
throw new Error('授权码格式无效,请确保复制了完整的 Authorization Code')
|
||||
}
|
||||
|
||||
// 基本格式验证:授权码应该只包含字母、数字、下划线、连字符
|
||||
const validCodePattern = /^[A-Za-z0-9_-]+$/
|
||||
if (!validCodePattern.test(cleanedCode)) {
|
||||
throw new Error('授权码包含无效字符,请检查是否复制了正确的 Authorization Code')
|
||||
}
|
||||
|
||||
return cleanedCode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,26 +286,26 @@ function parseCallbackUrl(input) {
|
||||
* @returns {object} claudeAiOauth格式的数据
|
||||
*/
|
||||
function formatClaudeCredentials(tokenData) {
|
||||
return {
|
||||
claudeAiOauth: {
|
||||
accessToken: tokenData.accessToken,
|
||||
refreshToken: tokenData.refreshToken,
|
||||
expiresAt: tokenData.expiresAt,
|
||||
scopes: tokenData.scopes,
|
||||
isMax: tokenData.isMax
|
||||
}
|
||||
};
|
||||
return {
|
||||
claudeAiOauth: {
|
||||
accessToken: tokenData.accessToken,
|
||||
refreshToken: tokenData.refreshToken,
|
||||
expiresAt: tokenData.expiresAt,
|
||||
scopes: tokenData.scopes,
|
||||
isMax: tokenData.isMax
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OAUTH_CONFIG,
|
||||
generateOAuthParams,
|
||||
exchangeCodeForTokens,
|
||||
parseCallbackUrl,
|
||||
formatClaudeCredentials,
|
||||
generateState,
|
||||
generateCodeVerifier,
|
||||
generateCodeChallenge,
|
||||
generateAuthUrl,
|
||||
createProxyAgent
|
||||
};
|
||||
OAUTH_CONFIG,
|
||||
generateOAuthParams,
|
||||
exchangeCodeForTokens,
|
||||
parseCallbackUrl,
|
||||
formatClaudeCredentials,
|
||||
generateState,
|
||||
generateCodeVerifier,
|
||||
generateCodeChallenge,
|
||||
generateAuthUrl,
|
||||
createProxyAgent
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const crypto = require('crypto');
|
||||
const logger = require('./logger');
|
||||
const crypto = require('crypto')
|
||||
const logger = require('./logger')
|
||||
|
||||
class SessionHelper {
|
||||
/**
|
||||
@@ -10,92 +10,104 @@ class SessionHelper {
|
||||
*/
|
||||
generateSessionHash(requestBody) {
|
||||
if (!requestBody || typeof requestBody !== 'object') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
let cacheableContent = '';
|
||||
const system = requestBody.system || '';
|
||||
const messages = requestBody.messages || [];
|
||||
let cacheableContent = ''
|
||||
const system = requestBody.system || ''
|
||||
const messages = requestBody.messages || []
|
||||
|
||||
// 1. 优先提取带有cache_control: {"type": "ephemeral"}的内容
|
||||
// 检查system中的cacheable内容
|
||||
if (Array.isArray(system)) {
|
||||
for (const part of system) {
|
||||
if (part && part.cache_control && part.cache_control.type === 'ephemeral') {
|
||||
cacheableContent += part.text || '';
|
||||
cacheableContent += part.text || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查messages中的cacheable内容
|
||||
for (const msg of messages) {
|
||||
const content = msg.content || '';
|
||||
const content = msg.content || ''
|
||||
if (Array.isArray(content)) {
|
||||
for (const part of content) {
|
||||
if (part && part.cache_control && part.cache_control.type === 'ephemeral') {
|
||||
if (part.type === 'text') {
|
||||
cacheableContent += part.text || '';
|
||||
cacheableContent += part.text || ''
|
||||
}
|
||||
// 其他类型(如image)不参与hash计算
|
||||
}
|
||||
}
|
||||
} else if (typeof content === 'string' && msg.cache_control && msg.cache_control.type === 'ephemeral') {
|
||||
} else if (
|
||||
typeof content === 'string' &&
|
||||
msg.cache_control &&
|
||||
msg.cache_control.type === 'ephemeral'
|
||||
) {
|
||||
// 罕见情况,但需要检查
|
||||
cacheableContent += content;
|
||||
cacheableContent += content
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果有cacheable内容,直接使用
|
||||
if (cacheableContent) {
|
||||
const hash = crypto.createHash('sha256').update(cacheableContent).digest('hex').substring(0, 32);
|
||||
logger.debug(`📋 Session hash generated from cacheable content: ${hash}`);
|
||||
return hash;
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(cacheableContent)
|
||||
.digest('hex')
|
||||
.substring(0, 32)
|
||||
logger.debug(`📋 Session hash generated from cacheable content: ${hash}`)
|
||||
return hash
|
||||
}
|
||||
|
||||
// 3. Fallback: 使用system内容
|
||||
if (system) {
|
||||
let systemText = '';
|
||||
let systemText = ''
|
||||
if (typeof system === 'string') {
|
||||
systemText = system;
|
||||
systemText = system
|
||||
} else if (Array.isArray(system)) {
|
||||
systemText = system.map(part => part.text || '').join('');
|
||||
systemText = system.map((part) => part.text || '').join('')
|
||||
}
|
||||
|
||||
|
||||
if (systemText) {
|
||||
const hash = crypto.createHash('sha256').update(systemText).digest('hex').substring(0, 32);
|
||||
logger.debug(`📋 Session hash generated from system content: ${hash}`);
|
||||
return hash;
|
||||
const hash = crypto.createHash('sha256').update(systemText).digest('hex').substring(0, 32)
|
||||
logger.debug(`📋 Session hash generated from system content: ${hash}`)
|
||||
return hash
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 最后fallback: 使用第一条消息内容
|
||||
if (messages.length > 0) {
|
||||
const firstMessage = messages[0];
|
||||
let firstMessageText = '';
|
||||
|
||||
const firstMessage = messages[0]
|
||||
let firstMessageText = ''
|
||||
|
||||
if (typeof firstMessage.content === 'string') {
|
||||
firstMessageText = firstMessage.content;
|
||||
firstMessageText = firstMessage.content
|
||||
} else if (Array.isArray(firstMessage.content)) {
|
||||
if (!firstMessage.content) {
|
||||
logger.error('📋 Session hash generated from first message failed: ', firstMessage);
|
||||
logger.error('📋 Session hash generated from first message failed: ', firstMessage)
|
||||
}
|
||||
|
||||
firstMessageText = firstMessage.content
|
||||
.filter(part => part.type === 'text')
|
||||
.map(part => part.text || '')
|
||||
.join('');
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => part.text || '')
|
||||
.join('')
|
||||
}
|
||||
|
||||
|
||||
if (firstMessageText) {
|
||||
const hash = crypto.createHash('sha256').update(firstMessageText).digest('hex').substring(0, 32);
|
||||
logger.debug(`📋 Session hash generated from first message: ${hash}`);
|
||||
return hash;
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(firstMessageText)
|
||||
.digest('hex')
|
||||
.substring(0, 32)
|
||||
logger.debug(`📋 Session hash generated from first message: ${hash}`)
|
||||
return hash
|
||||
}
|
||||
}
|
||||
|
||||
// 无法生成会话哈希
|
||||
logger.debug('📋 Unable to generate session hash - no suitable content found');
|
||||
return null;
|
||||
logger.debug('📋 Unable to generate session hash - no suitable content found')
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +116,7 @@ class SessionHelper {
|
||||
* @returns {string} - Redis键名
|
||||
*/
|
||||
getSessionRedisKey(sessionHash) {
|
||||
return `sticky_session:${sessionHash}`;
|
||||
return `sticky_session:${sessionHash}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,10 +125,12 @@ class SessionHelper {
|
||||
* @returns {boolean} - 是否有效
|
||||
*/
|
||||
isValidSessionHash(sessionHash) {
|
||||
return typeof sessionHash === 'string' &&
|
||||
sessionHash.length === 32 &&
|
||||
/^[a-f0-9]{32}$/.test(sessionHash);
|
||||
return (
|
||||
typeof sessionHash === 'string' &&
|
||||
sessionHash.length === 32 &&
|
||||
/^[a-f0-9]{32}$/.test(sessionHash)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SessionHelper();
|
||||
module.exports = new SessionHelper()
|
||||
|
||||
@@ -11,29 +11,29 @@
|
||||
*/
|
||||
function maskToken(token, visiblePercent = 70) {
|
||||
if (!token || typeof token !== 'string') {
|
||||
return '[EMPTY]';
|
||||
return '[EMPTY]'
|
||||
}
|
||||
|
||||
const length = token.length;
|
||||
|
||||
const { length } = token
|
||||
|
||||
// 对于非常短的 token,至少隐藏一部分
|
||||
if (length <= 10) {
|
||||
return token.slice(0, 5) + '*'.repeat(length - 5);
|
||||
return token.slice(0, 5) + '*'.repeat(length - 5)
|
||||
}
|
||||
|
||||
// 计算可见字符数量
|
||||
const visibleLength = Math.floor(length * (visiblePercent / 100));
|
||||
|
||||
const visibleLength = Math.floor(length * (visiblePercent / 100))
|
||||
|
||||
// 在前部和尾部分配可见字符
|
||||
const frontLength = Math.ceil(visibleLength * 0.6);
|
||||
const backLength = visibleLength - frontLength;
|
||||
|
||||
const frontLength = Math.ceil(visibleLength * 0.6)
|
||||
const backLength = visibleLength - frontLength
|
||||
|
||||
// 构建脱敏后的 token
|
||||
const front = token.slice(0, frontLength);
|
||||
const back = token.slice(-backLength);
|
||||
const middle = '*'.repeat(length - visibleLength);
|
||||
|
||||
return `${front}${middle}${back}`;
|
||||
const front = token.slice(0, frontLength)
|
||||
const back = token.slice(-backLength)
|
||||
const middle = '*'.repeat(length - visibleLength)
|
||||
|
||||
return `${front}${middle}${back}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,20 +42,23 @@ function maskToken(token, visiblePercent = 70) {
|
||||
* @param {Array<string>} tokenFields - 需要脱敏的字段名列表
|
||||
* @returns {Object} 脱敏后的对象副本
|
||||
*/
|
||||
function maskTokensInObject(obj, tokenFields = ['accessToken', 'refreshToken', 'access_token', 'refresh_token']) {
|
||||
function maskTokensInObject(
|
||||
obj,
|
||||
tokenFields = ['accessToken', 'refreshToken', 'access_token', 'refresh_token']
|
||||
) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
return obj
|
||||
}
|
||||
|
||||
const masked = { ...obj };
|
||||
|
||||
tokenFields.forEach(field => {
|
||||
const masked = { ...obj }
|
||||
|
||||
tokenFields.forEach((field) => {
|
||||
if (masked[field]) {
|
||||
masked[field] = maskToken(masked[field]);
|
||||
masked[field] = maskToken(masked[field])
|
||||
}
|
||||
});
|
||||
|
||||
return masked;
|
||||
})
|
||||
|
||||
return masked
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,21 +78,21 @@ function formatTokenRefreshLog(accountId, accountName, tokens, status, message =
|
||||
accountName,
|
||||
status,
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
if (tokens) {
|
||||
log.tokens = {
|
||||
accessToken: tokens.accessToken ? maskToken(tokens.accessToken) : '[NOT_PROVIDED]',
|
||||
refreshToken: tokens.refreshToken ? maskToken(tokens.refreshToken) : '[NOT_PROVIDED]',
|
||||
expiresAt: tokens.expiresAt || '[NOT_PROVIDED]'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return log;
|
||||
return log
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
maskToken,
|
||||
maskTokensInObject,
|
||||
formatTokenRefreshLog
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { maskToken } = require('./tokenMask');
|
||||
const winston = require('winston')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { maskToken } = require('./tokenMask')
|
||||
|
||||
// 确保日志目录存在
|
||||
const logDir = path.join(process.cwd(), 'logs');
|
||||
const logDir = path.join(process.cwd(), 'logs')
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 创建专用的 token 刷新日志记录器
|
||||
@@ -17,9 +17,7 @@ const tokenRefreshLogger = winston.createLogger({
|
||||
format: 'YYYY-MM-DD HH:mm:ss.SSS'
|
||||
}),
|
||||
winston.format.json(),
|
||||
winston.format.printf(info => {
|
||||
return JSON.stringify(info, null, 2);
|
||||
})
|
||||
winston.format.printf((info) => JSON.stringify(info, null, 2))
|
||||
),
|
||||
transports: [
|
||||
// 文件传输 - 每日轮转
|
||||
@@ -39,16 +37,15 @@ const tokenRefreshLogger = winston.createLogger({
|
||||
],
|
||||
// 错误处理
|
||||
exitOnError: false
|
||||
});
|
||||
})
|
||||
|
||||
// 在开发环境添加控制台输出
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
tokenRefreshLogger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
tokenRefreshLogger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(winston.format.colorize(), winston.format.simple())
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +59,7 @@ function logRefreshStart(accountId, accountName, platform = 'claude', reason = '
|
||||
platform,
|
||||
reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +71,7 @@ function logRefreshSuccess(accountId, accountName, platform = 'claude', tokenDat
|
||||
refreshToken: tokenData.refreshToken ? maskToken(tokenData.refreshToken) : '[NOT_PROVIDED]',
|
||||
expiresAt: tokenData.expiresAt || tokenData.expiry_date || '[NOT_PROVIDED]',
|
||||
scopes: tokenData.scopes || tokenData.scope || '[NOT_PROVIDED]'
|
||||
};
|
||||
}
|
||||
|
||||
tokenRefreshLogger.info({
|
||||
event: 'token_refresh_success',
|
||||
@@ -83,7 +80,7 @@ function logRefreshSuccess(accountId, accountName, platform = 'claude', tokenDat
|
||||
platform,
|
||||
tokenData: maskedTokenData,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +92,7 @@ function logRefreshError(accountId, accountName, platform = 'claude', error, att
|
||||
code: error.code || 'UNKNOWN',
|
||||
statusCode: error.response?.status || 'N/A',
|
||||
responseData: error.response?.data || 'N/A'
|
||||
};
|
||||
}
|
||||
|
||||
tokenRefreshLogger.error({
|
||||
event: 'token_refresh_error',
|
||||
@@ -105,7 +102,7 @@ function logRefreshError(accountId, accountName, platform = 'claude', error, att
|
||||
error: errorInfo,
|
||||
attemptNumber,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +116,7 @@ function logRefreshSkipped(accountId, accountName, platform = 'claude', reason =
|
||||
platform,
|
||||
reason,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +132,7 @@ function logTokenUsage(accountId, accountName, platform = 'claude', expiresAt, i
|
||||
isExpired,
|
||||
remainingMinutes: expiresAt ? Math.floor((new Date(expiresAt) - Date.now()) / 60000) : 'N/A',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,7 +144,7 @@ function logBatchRefreshStart(totalAccounts, platform = 'all') {
|
||||
totalAccounts,
|
||||
platform,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,7 +160,7 @@ function logBatchRefreshComplete(results) {
|
||||
skipped: results.skipped || 0
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@@ -175,4 +172,4 @@ module.exports = {
|
||||
logTokenUsage,
|
||||
logBatchRefreshStart,
|
||||
logBatchRefreshComplete
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user