- 新增日志系统配置选项,支持日志级别、输出模式、文件大小等设置 - 添加当日日志文件下载功能,可通过Web界面直接下载 - 将console.log/error替换为结构化logger,提升日志可管理性 - 在日志页面添加自动滚动到底部功能 - 更新配置示例文件,包含完整的日志配置参数
434 lines
No EOL
15 KiB
JavaScript
434 lines
No EOL
15 KiB
JavaScript
/**
|
||
* 转换器公共工具函数模块
|
||
* 提供各种协议转换所需的通用辅助函数
|
||
*/
|
||
|
||
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(); |