first commit

This commit is contained in:
shaw
2025-07-14 18:14:13 +08:00
parent a96a372011
commit b1ca3f307e
31 changed files with 20046 additions and 21 deletions

224
src/utils/costCalculator.js Normal file
View File

@@ -0,0 +1,224 @@
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,
cacheWrite: 3.75,
cacheRead: 0.30
},
'claude-sonnet-4-20250514': {
input: 3.00,
output: 15.00,
cacheWrite: 3.75,
cacheRead: 0.30
},
// Claude 3.5 Haiku
'claude-3-5-haiku-20241022': {
input: 0.25,
output: 1.25,
cacheWrite: 0.30,
cacheRead: 0.03
},
// Claude 3 Opus
'claude-3-opus-20240229': {
input: 15.00,
output: 75.00,
cacheWrite: 18.75,
cacheRead: 1.50
},
// Claude 3 Sonnet
'claude-3-sonnet-20240229': {
input: 3.00,
output: 15.00,
cacheWrite: 3.75,
cacheRead: 0.30
},
// Claude 3 Haiku
'claude-3-haiku-20240307': {
input: 0.25,
output: 1.25,
cacheWrite: 0.30,
cacheRead: 0.03
},
// 默认定价(用于未知模型)
'unknown': {
input: 3.00,
output: 15.00,
cacheWrite: 3.75,
cacheRead: 0.30
}
};
class CostCalculator {
/**
* 计算单次请求的费用
* @param {Object} usage - 使用量数据
* @param {number} usage.input_tokens - 输入token数量
* @param {number} usage.output_tokens - 输出token数量
* @param {number} usage.cache_creation_input_tokens - 缓存创建token数量
* @param {number} usage.cache_read_input_tokens - 缓存读取token数量
* @param {string} model - 模型名称
* @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 pricingData = pricingService.getModelPricing(model);
let pricing;
let usingDynamicPricing = false;
if (pricingData) {
// 转换动态价格格式为内部格式
pricing = {
input: (pricingData.input_cost_per_token || 0) * 1000000, // 转换为per 1M tokens
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;
} else {
// 回退到静态价格
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;
return {
model,
pricing,
usingDynamicPricing,
usage: {
inputTokens,
outputTokens,
cacheCreateTokens,
cacheReadTokens,
totalTokens: inputTokens + outputTokens + cacheCreateTokens + cacheReadTokens
},
costs: {
input: inputCost,
output: outputCost,
cacheWrite: cacheWriteCost,
cacheRead: cacheReadCost,
total: totalCost
},
// 格式化的费用字符串
formatted: {
input: this.formatCost(inputCost),
output: this.formatCost(outputCost),
cacheWrite: this.formatCost(cacheWriteCost),
cacheRead: this.formatCost(cacheReadCost),
total: this.formatCost(totalCost)
}
};
}
/**
* 计算聚合使用量的费用
* @param {Object} aggregatedUsage - 聚合使用量数据
* @param {string} model - 模型名称
* @returns {Object} 费用详情
*/
static calculateAggregatedCost(aggregatedUsage, model = 'unknown') {
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);
}
/**
* 获取模型定价信息
* @param {string} model - 模型名称
* @returns {Object} 定价信息
*/
static getModelPricing(model = 'unknown') {
return MODEL_PRICING[model] || MODEL_PRICING['unknown'];
}
/**
* 获取所有支持的模型和定价
* @returns {Object} 所有模型定价
*/
static getAllModelPricing() {
return { ...MODEL_PRICING };
}
/**
* 验证模型是否支持
* @param {string} model - 模型名称
* @returns {boolean} 是否支持
*/
static isModelSupported(model) {
return !!MODEL_PRICING[model];
}
/**
* 格式化费用显示
* @param {number} cost - 费用金额
* @param {number} decimals - 小数位数
* @returns {string} 格式化的费用字符串
*/
static formatCost(cost, decimals = 6) {
if (cost >= 1) {
return `$${cost.toFixed(2)}`;
} else if (cost >= 0.001) {
return `$${cost.toFixed(4)}`;
} else {
return `$${cost.toFixed(decimals)}`;
}
}
/**
* 计算费用节省(使用缓存的节省)
* @param {Object} usage - 使用量数据
* @param {string} model - 模型名称
* @returns {Object} 节省信息
*/
static calculateCacheSavings(usage, model = 'unknown') {
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;
return {
normalCost,
cacheCost,
savings,
savingsPercentage,
formatted: {
normalCost: this.formatCost(normalCost),
cacheCost: this.formatCost(cacheCost),
savings: this.formatCost(savings),
savingsPercentage: `${savingsPercentage.toFixed(1)}%`
}
};
}
}
module.exports = CostCalculator;

290
src/utils/logger.js Normal file
View File

@@ -0,0 +1,290 @@
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 createLogFormat = (colorize = false) => {
const formats = [
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.printf(({ level, message, timestamp, stack, metadata, ...rest }) => {
const emoji = {
error: '❌',
warn: '⚠️ ',
info: ' ',
debug: '🐛',
verbose: '📝'
};
let logMessage = `${emoji[level] || '📝'} [${timestamp}] ${level.toUpperCase()}: ${message}`;
// 添加元数据
if (metadata && Object.keys(metadata).length > 0) {
logMessage += ` | ${JSON.stringify(metadata)}`;
}
// 添加其他属性
const additionalData = { ...rest };
delete additionalData.level;
delete additionalData.message;
delete additionalData.timestamp;
delete additionalData.stack;
if (Object.keys(additionalData).length > 0) {
logMessage += ` | ${JSON.stringify(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 });
}
// 🔄 增强的日志轮转配置
const createRotateTransport = (filename, level = null) => {
const transport = new DailyRotateFile({
filename: path.join(config.logging.dirname, filename),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: config.logging.maxSize,
maxFiles: config.logging.maxFiles,
auditFile: path.join(config.logging.dirname, `.${filename.replace('%DATE%', 'audit')}.json`),
format: logFormat
});
if (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;
};
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')
],
silent: false
});
// 🌟 增强的 Winston logger
const logger = winston.createLogger({
level: config.logging.level,
format: logFormat,
transports: [
// 📄 文件输出
dailyRotateFileTransport,
errorFileTransport,
// 🖥️ 控制台输出
new winston.transports.Console({
format: consoleFormat,
handleExceptions: false,
handleRejections: false
})
],
// 🚨 异常处理
exceptionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'exceptions.log'),
format: logFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.Console({
format: consoleFormat
})
],
// 🔄 未捕获异常处理
rejectionHandlers: [
new winston.transports.File({
filename: path.join(config.logging.dirname, 'rejections.log'),
format: logFormat,
maxsize: 10485760, // 10MB
maxFiles: 5
}),
new winston.transports.Console({
format: consoleFormat
})
],
// 防止进程退出
exitOnError: false
});
// 🎯 增强的自定义方法
logger.success = (message, metadata = {}) => {
logger.info(`${message}`, { type: 'success', ...metadata });
};
logger.start = (message, 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';
logger[level](`${emoji} ${method} ${url} - ${status} (${duration}ms)`, {
type: 'request',
method,
url,
status,
duration,
...metadata
});
};
logger.api = (message, metadata = {}) => {
logger.info(`🔗 ${message}`, { type: 'api', ...metadata });
};
logger.security = (message, metadata = {}) => {
const securityData = {
type: 'security',
timestamp: new Date().toISOString(),
pid: process.pid,
hostname: os.hostname(),
...metadata
};
// 记录到主日志
logger.warn(`🔒 ${message}`, securityData);
// 记录到专门的安全日志文件
try {
securityLogger.warn(`🔒 ${message}`, securityData);
} catch (error) {
// 如果安全日志文件不可用,只记录到主日志
console.warn('Security logger not available:', error.message);
}
};
logger.database = (message, metadata = {}) => {
logger.debug(`💾 ${message}`, { type: 'database', ...metadata });
};
logger.performance = (message, metadata = {}) => {
logger.info(`${message}`, { type: 'performance', ...metadata });
};
logger.audit = (message, metadata = {}) => {
logger.info(`📋 ${message}`, {
type: 'audit',
timestamp: new Date().toISOString(),
pid: process.pid,
...metadata
});
};
// 🔧 性能监控方法
logger.timer = (label) => {
const start = Date.now();
return {
end: (message = '', metadata = {}) => {
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;
logger.error = function(message, metadata = {}) {
logger.stats.errors++;
return originalError.call(this, message, metadata);
};
logger.warn = function(message, metadata = {}) {
logger.stats.warnings++;
return originalWarn.call(this, message, metadata);
};
logger.info = function(message, metadata = {}) {
if (metadata.type === 'request') {
logger.stats.requests++;
}
return originalInfo.call(this, message, metadata);
};
// 📈 获取日志统计
logger.getStats = () => ({ ...logger.stats });
// 🧹 清理统计
logger.resetStats = () => {
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() };
} catch (error) {
return { healthy: false, error: error.message, timestamp: new Date().toISOString() };
}
};
// 🎬 启动日志记录系统
logger.start('Logger initialized', {
level: config.logging.level,
directory: config.logging.dirname,
maxSize: config.logging.maxSize,
maxFiles: config.logging.maxFiles
});
module.exports = logger;

307
src/utils/oauthHelper.js Normal file
View File

@@ -0,0 +1,307 @@
/**
* OAuth助手工具
* 基于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');
// 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'
};
/**
* 生成随机的 state 参数
* @returns {string} 随机生成的 state (64字符hex)
*/
function generateState() {
return crypto.randomBytes(32).toString('hex');
}
/**
* 生成随机的 code verifierPKCE
* @returns {string} base64url 编码的随机字符串
*/
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
/**
* 生成 code challengePKCE
* @param {string} codeVerifier - code verifier 字符串
* @returns {string} SHA256 哈希后的 base64url 编码字符串
*/
function generateCodeChallenge(codeVerifier) {
return crypto.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
}
/**
* 生成授权 URL
* @param {string} codeChallenge - PKCE code challenge
* @param {string} state - state 参数
* @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
});
return `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}`;
}
/**
* 生成OAuth授权URL和相关参数
* @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
};
}
/**
* 创建代理agent
* @param {object|null} proxyConfig - 代理配置对象
* @returns {object|null} 代理agent或null
*/
function createProxyAgent(proxyConfig) {
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);
}
return null;
}
/**
* 使用授权码交换访问令牌
* @param {string} authorizationCode - 授权码
* @param {string} codeVerifier - PKCE code verifier
* @param {string} state - state 参数
* @param {object|null} 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
};
// 创建代理agent
const agent = createProxyAgent(proxyConfig);
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'
});
const response = await axios.post(OAUTH_CONFIG.TOKEN_URL, params, {
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'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
});
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.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}`);
}
}
}
/**
* 解析回调 URL 或授权码
* @param {string} input - 完整的回调 URL 或直接的授权码
* @returns {string} 授权码
*/
function parseCallbackUrl(input) {
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');
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;
}
/**
* 格式化为Claude标准格式
* @param {object} tokenData - token数据
* @returns {object} claudeAiOauth格式的数据
*/
function formatClaudeCredentials(tokenData) {
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
};