AIClient-2-API/src/converters/utils.js
hex2077 245583b96a feat(logging): 添加日志系统配置和下载功能
- 新增日志系统配置选项,支持日志级别、输出模式、文件大小等设置
- 添加当日日志文件下载功能,可通过Web界面直接下载
- 将console.log/error替换为结构化logger,提升日志可管理性
- 在日志页面添加自动滚动到底部功能
- 更新配置示例文件,包含完整的日志配置参数
2026-01-25 17:24:39 +08:00

434 lines
No EOL
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 转换器公共工具函数模块
* 提供各种协议转换所需的通用辅助函数
*/
import { v4 as uuidv4 } from 'uuid';
import logger from '../utils/logger.js';
// =============================================================================
// 常量定义
// =============================================================================
// 通用默认值
export const DEFAULT_MAX_TOKENS = 8192;
export const DEFAULT_TEMPERATURE = 1;
export const DEFAULT_TOP_P = 0.95;
// =============================================================================
// OpenAI 相关常量
// =============================================================================
export const OPENAI_DEFAULT_MAX_TOKENS = 128000;
export const OPENAI_DEFAULT_TEMPERATURE = 1;
export const OPENAI_DEFAULT_TOP_P = 0.95;
export const OPENAI_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
export const OPENAI_DEFAULT_OUTPUT_TOKEN_LIMIT = 128000;
// =============================================================================
// Claude 相关常量
// =============================================================================
export const CLAUDE_DEFAULT_MAX_TOKENS = 200000;
export const CLAUDE_DEFAULT_TEMPERATURE = 1;
export const CLAUDE_DEFAULT_TOP_P = 0.95;
// =============================================================================
// Gemini 相关常量
// =============================================================================
export const GEMINI_DEFAULT_MAX_TOKENS = 65534;
export const GEMINI_DEFAULT_TEMPERATURE = 1;
export const GEMINI_DEFAULT_TOP_P = 0.95;
export const GEMINI_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
export const GEMINI_DEFAULT_OUTPUT_TOKEN_LIMIT = 65534;
// =============================================================================
// OpenAI Responses 相关常量
// =============================================================================
export const OPENAI_RESPONSES_DEFAULT_MAX_TOKENS = 128000;
export const OPENAI_RESPONSES_DEFAULT_TEMPERATURE = 1;
export const OPENAI_RESPONSES_DEFAULT_TOP_P = 0.95;
export const OPENAI_RESPONSES_DEFAULT_INPUT_TOKEN_LIMIT = 32768;
export const OPENAI_RESPONSES_DEFAULT_OUTPUT_TOKEN_LIMIT = 128000;
// =============================================================================
// Ollama 相关常量
// =============================================================================
export const OLLAMA_DEFAULT_CONTEXT_LENGTH = 65534;
export const OLLAMA_DEFAULT_MAX_OUTPUT_TOKENS = 8192;
// Claude 模型上下文长度
export const OLLAMA_CLAUDE_DEFAULT_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_SONNET_45_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_SONNET_45_MAX_OUTPUT_TOKENS = 200000;
export const OLLAMA_CLAUDE_HAIKU_45_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_HAIKU_45_MAX_OUTPUT_TOKENS = 200000;
export const OLLAMA_CLAUDE_OPUS_41_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_OPUS_41_MAX_OUTPUT_TOKENS = 32000;
export const OLLAMA_CLAUDE_SONNET_40_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_SONNET_40_MAX_OUTPUT_TOKENS = 200000;
export const OLLAMA_CLAUDE_SONNET_37_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_SONNET_37_MAX_OUTPUT_TOKENS = 200000;
export const OLLAMA_CLAUDE_OPUS_40_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_OPUS_40_MAX_OUTPUT_TOKENS = 32000;
export const OLLAMA_CLAUDE_HAIKU_35_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_HAIKU_35_MAX_OUTPUT_TOKENS = 200000;
export const OLLAMA_CLAUDE_HAIKU_30_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_HAIKU_30_MAX_OUTPUT_TOKENS = 8192;
export const OLLAMA_CLAUDE_SONNET_35_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_SONNET_35_MAX_OUTPUT_TOKENS = 200000;
export const OLLAMA_CLAUDE_OPUS_30_CONTEXT_LENGTH = 200000;
export const OLLAMA_CLAUDE_OPUS_30_MAX_OUTPUT_TOKENS = 8192;
// Gemini 模型上下文长度
export const OLLAMA_GEMINI_25_PRO_CONTEXT_LENGTH = 1048576;
export const OLLAMA_GEMINI_25_PRO_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_GEMINI_25_FLASH_CONTEXT_LENGTH = 1048576;
export const OLLAMA_GEMINI_25_FLASH_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_GEMINI_25_IMAGE_CONTEXT_LENGTH = 65534;
export const OLLAMA_GEMINI_25_IMAGE_MAX_OUTPUT_TOKENS = 32768;
export const OLLAMA_GEMINI_25_LIVE_CONTEXT_LENGTH = 131072;
export const OLLAMA_GEMINI_25_LIVE_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_GEMINI_25_TTS_CONTEXT_LENGTH = 65534;
export const OLLAMA_GEMINI_25_TTS_MAX_OUTPUT_TOKENS = 16384;
export const OLLAMA_GEMINI_20_FLASH_CONTEXT_LENGTH = 1048576;
export const OLLAMA_GEMINI_20_FLASH_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_GEMINI_20_IMAGE_CONTEXT_LENGTH = 32768;
export const OLLAMA_GEMINI_20_IMAGE_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_GEMINI_15_PRO_CONTEXT_LENGTH = 2097152;
export const OLLAMA_GEMINI_15_PRO_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_GEMINI_15_FLASH_CONTEXT_LENGTH = 1048576;
export const OLLAMA_GEMINI_15_FLASH_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_GEMINI_DEFAULT_CONTEXT_LENGTH = 1048576;
export const OLLAMA_GEMINI_DEFAULT_MAX_OUTPUT_TOKENS = 65534;
// GPT 模型上下文长度
export const OLLAMA_GPT4_TURBO_CONTEXT_LENGTH = 128000;
export const OLLAMA_GPT4_TURBO_MAX_OUTPUT_TOKENS = 8192;
export const OLLAMA_GPT4_32K_CONTEXT_LENGTH = 32768;
export const OLLAMA_GPT4_32K_MAX_OUTPUT_TOKENS = 8192;
export const OLLAMA_GPT4_BASE_CONTEXT_LENGTH = 200000;
export const OLLAMA_GPT4_BASE_MAX_OUTPUT_TOKENS = 8192;
export const OLLAMA_GPT35_16K_CONTEXT_LENGTH = 16385;
export const OLLAMA_GPT35_16K_MAX_OUTPUT_TOKENS = 8192;
export const OLLAMA_GPT35_BASE_CONTEXT_LENGTH = 8192;
export const OLLAMA_GPT35_BASE_MAX_OUTPUT_TOKENS = 8192;
// Qwen 模型上下文长度
export const OLLAMA_QWEN_CODER_PLUS_CONTEXT_LENGTH = 128000;
export const OLLAMA_QWEN_CODER_PLUS_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_QWEN_VL_PLUS_CONTEXT_LENGTH = 262144;
export const OLLAMA_QWEN_VL_PLUS_MAX_OUTPUT_TOKENS = 32768;
export const OLLAMA_QWEN_CODER_FLASH_CONTEXT_LENGTH = 128000;
export const OLLAMA_QWEN_CODER_FLASH_MAX_OUTPUT_TOKENS = 65534;
export const OLLAMA_QWEN_DEFAULT_CONTEXT_LENGTH = 32768;
export const OLLAMA_QWEN_DEFAULT_MAX_OUTPUT_TOKENS = 200000;
export const OLLAMA_DEFAULT_FILE_TYPE = 2;
export const OLLAMA_DEFAULT_QUANTIZATION_VERSION = 2;
export const OLLAMA_DEFAULT_ROPE_FREQ_BASE = 10000.0;
export const OLLAMA_DEFAULT_TEMPERATURE = 0.7;
export const OLLAMA_DEFAULT_TOP_P = 0.9;
export const OLLAMA_DEFAULT_QUANTIZATION_LEVEL = 'Q4_0';
export const OLLAMA_SHOW_QUANTIZATION_LEVEL = 'Q4_K_M';
// =============================================================================
// 通用辅助函数
// =============================================================================
/**
* 判断值是否为 undefined 或 0并返回默认值
* @param {*} value - 要检查的值
* @param {*} defaultValue - 默认值
* @returns {*} 处理后的值
*/
export function checkAndAssignOrDefault(value, defaultValue) {
if (value !== undefined && value !== 0) {
return value;
}
return defaultValue;
}
/**
* 生成唯一ID
* @param {string} prefix - ID前缀
* @returns {string} 生成的ID
*/
export function generateId(prefix = '') {
return prefix ? `${prefix}_${uuidv4()}` : uuidv4();
}
/**
* 安全解析JSON字符串
* @param {string} str - JSON字符串
* @returns {*} 解析后的对象或原始字符串
*/
export function safeParseJSON(str) {
if (!str) {
return str;
}
let cleanedStr = str;
// 处理可能被截断的转义序列
if (cleanedStr.endsWith('\\') && !cleanedStr.endsWith('\\\\')) {
cleanedStr = cleanedStr.substring(0, cleanedStr.length - 1);
} else if (cleanedStr.endsWith('\\u') || cleanedStr.endsWith('\\u0') || cleanedStr.endsWith('\\u00')) {
const idx = cleanedStr.lastIndexOf('\\u');
cleanedStr = cleanedStr.substring(0, idx);
}
try {
return JSON.parse(cleanedStr || '{}');
} catch (e) {
return str;
}
}
/**
* 提取消息内容中的文本
* @param {string|Array} content - 消息内容
* @returns {string} 提取的文本
*/
export function extractTextFromMessageContent(content) {
if (typeof content === 'string') {
return content;
}
if (Array.isArray(content)) {
return content
.filter(part => part.type === 'text' && part.text)
.map(part => part.text)
.join('\n');
}
return '';
}
/**
* 提取并处理系统消息
* @param {Array} messages - 消息数组
* @returns {{systemInstruction: Object|null, nonSystemMessages: Array}}
*/
export function extractAndProcessSystemMessages(messages) {
const systemContents = [];
const nonSystemMessages = [];
for (const message of messages) {
if (message.role === 'system') {
systemContents.push(extractTextFromMessageContent(message.content));
} else {
nonSystemMessages.push(message);
}
}
let systemInstruction = null;
if (systemContents.length > 0) {
systemInstruction = {
parts: [{
text: systemContents.join('\n')
}]
};
}
return { systemInstruction, nonSystemMessages };
}
/**
* 清理JSON Schema属性移除Gemini不支持的属性
* Google Gemini API 只支持有限的 JSON Schema 属性,不支持以下属性:
* - exclusiveMinimum, exclusiveMaximum, minimum, maximum
* - minLength, maxLength, minItems, maxItems
* - pattern, format, default, const
* - additionalProperties, $schema, $ref, $id
* - allOf, anyOf, oneOf, not
* @param {Object} schema - JSON Schema
* @returns {Object} 清理后的JSON Schema
*/
export function cleanJsonSchemaProperties(schema) {
if (!schema || typeof schema !== 'object') {
return schema;
}
// 如果是数组,递归处理每个元素
if (Array.isArray(schema)) {
return schema.map(item => cleanJsonSchemaProperties(item));
}
// Gemini 支持的 JSON Schema 属性白名单
const allowedKeys = [
"type",
"description",
"properties",
"required",
"enum",
"items",
"nullable"
];
const sanitized = {};
for (const [key, value] of Object.entries(schema)) {
if (allowedKeys.includes(key)) {
// 对于需要递归处理的属性
if (key === 'properties' && typeof value === 'object' && value !== null) {
const cleanProperties = {};
for (const [propName, propSchema] of Object.entries(value)) {
cleanProperties[propName] = cleanJsonSchemaProperties(propSchema);
}
sanitized[key] = cleanProperties;
} else if (key === 'items') {
sanitized[key] = cleanJsonSchemaProperties(value);
} else {
sanitized[key] = value;
}
}
// 其他属性(如 exclusiveMinimum, minimum, maximum, pattern 等)被忽略
}
return sanitized;
}
/**
* 映射结束原因
* @param {string} reason - 结束原因
* @param {string} sourceFormat - 源格式
* @param {string} targetFormat - 目标格式
* @returns {string} 映射后的结束原因
*/
export function mapFinishReason(reason, sourceFormat, targetFormat) {
const reasonMappings = {
openai: {
anthropic: {
stop: "end_turn",
length: "max_tokens",
content_filter: "stop_sequence",
tool_calls: "tool_use"
}
},
gemini: {
anthropic: {
STOP: "end_turn",
MAX_TOKENS: "max_tokens",
SAFETY: "stop_sequence",
RECITATION: "stop_sequence",
stop: "end_turn",
length: "max_tokens",
safety: "stop_sequence",
recitation: "stop_sequence",
other: "end_turn"
}
}
};
try {
return reasonMappings[sourceFormat][targetFormat][reason] || "end_turn";
} catch (e) {
return "end_turn";
}
}
/**
* 根据budget_tokens智能判断OpenAI reasoning_effort等级
* @param {number|null} budgetTokens - Anthropic thinking的budget_tokens值
* @returns {string} OpenAI reasoning_effort等级
*/
export function determineReasoningEffortFromBudget(budgetTokens) {
if (budgetTokens === null || budgetTokens === undefined) {
logger.info("No budget_tokens provided, defaulting to reasoning_effort='high'");
return "high";
}
const LOW_THRESHOLD = 50;
const HIGH_THRESHOLD = 200;
logger.debug(`Threshold configuration: low <= ${LOW_THRESHOLD}, medium <= ${HIGH_THRESHOLD}, high > ${HIGH_THRESHOLD}`);
let effort;
if (budgetTokens <= LOW_THRESHOLD) {
effort = "low";
} else if (budgetTokens <= HIGH_THRESHOLD) {
effort = "medium";
} else {
effort = "high";
}
logger.info(`🎯 Budget tokens ${budgetTokens} -> reasoning_effort '${effort}' (thresholds: low<=${LOW_THRESHOLD}, high<=${HIGH_THRESHOLD})`);
return effort;
}
/**
* 从OpenAI文本中提取thinking内容
* @param {string} text - 文本内容
* @returns {string|Array} 提取后的内容
*/
export function extractThinkingFromOpenAIText(text) {
const thinkingPattern = /<thinking>\s*(.*?)\s*<\/thinking>/gs;
const matches = [...text.matchAll(thinkingPattern)];
const contentBlocks = [];
let lastEnd = 0;
for (const match of matches) {
const beforeText = text.substring(lastEnd, match.index).trim();
if (beforeText) {
contentBlocks.push({
type: "text",
text: beforeText
});
}
const thinkingText = match[1].trim();
if (thinkingText) {
contentBlocks.push({
type: "thinking",
thinking: thinkingText
});
}
lastEnd = match.index + match[0].length;
}
const afterText = text.substring(lastEnd).trim();
if (afterText) {
contentBlocks.push({
type: "text",
text: afterText
});
}
if (contentBlocks.length === 0) {
return text;
}
if (contentBlocks.length === 1 && contentBlocks[0].type === "text") {
return contentBlocks[0].text;
}
return contentBlocks;
}
// =============================================================================
// 工具状态管理器(单例模式)
// =============================================================================
/**
* 全局工具状态管理器
*/
class ToolStateManager {
constructor() {
if (ToolStateManager.instance) {
return ToolStateManager.instance;
}
ToolStateManager.instance = this;
this._toolMappings = {};
return this;
}
storeToolMapping(funcName, toolId) {
this._toolMappings[funcName] = toolId;
}
getToolId(funcName) {
return this._toolMappings[funcName] || null;
}
clearMappings() {
this._toolMappings = {};
}
}
export const toolStateManager = new ToolStateManager();