'use strict'; const fs = require('fs'); const path = require('path'); const DEFAULT_CONFIG_NAME = 'models.json'; const DEFAULT_CONFIG_PATH = path.resolve(__dirname, '..', DEFAULT_CONFIG_NAME); function isNonEmptyString(value) { return typeof value === 'string' && value.trim().length > 0; } function parsePositiveInt(value) { if (value === null || value === undefined || value === '') return null; const num = Number(value); if (!Number.isFinite(num)) return null; const out = Math.floor(num); return out > 0 ? out : null; } function normalizeModes(value) { if (!value) return null; const text = String(value).trim().toLowerCase(); if (!text) return null; const parts = text.split(/[,,\s]+/).filter(Boolean); const hasFast = parts.some((p) => p.includes('fast') || p.includes('快速')); const hasThinking = parts.some((p) => p.includes('thinking') || p.includes('思考')); if (hasFast && hasThinking) return 'fast+thinking'; if (hasThinking) return 'thinking'; if (hasFast) return 'fast'; return null; } function normalizeMultimodal(value) { if (value === null || value === undefined) return 'none'; const text = String(value).trim().toLowerCase(); if (!text) return 'none'; if (text.includes('无') || text.includes('none') || text.includes('no')) return 'none'; const parts = text.split(/[,,\s]+/).filter(Boolean); const hasImage = parts.some((p) => p.includes('图片') || p.includes('image')); const hasVideo = parts.some((p) => p.includes('视频') || p.includes('video')); if (hasVideo && hasImage) return 'image+video'; if (hasVideo) return 'image+video'; if (hasImage) return 'image'; return null; } function normalizeModel(raw) { const name = String(raw.name || raw.model_name || raw.model || '').trim(); const url = String(raw.url || raw.base_url || '').trim(); const apiKey = String(raw.apikey || raw.api_key || '').trim(); const modes = normalizeModes(raw.modes || raw.mode || raw.supported_modes); const multimodal = normalizeMultimodal(raw.multimodal || raw.multi_modal || raw.multi); const maxOutput = parsePositiveInt(raw.max_output ?? raw.max_tokens ?? raw.max_output_tokens); const maxContext = parsePositiveInt(raw.max_context ?? raw.context_window ?? raw.max_context_tokens); const valid = Boolean( isNonEmptyString(name) && isNonEmptyString(url) && isNonEmptyString(apiKey) && modes && multimodal && maxOutput && maxContext ); return { key: name, name, model_id: name, base_url: url, api_key: apiKey, modes, multimodal, max_output: maxOutput, max_context: maxContext, valid, }; } function buildConfig(raw, filePath) { const modelsRaw = Array.isArray(raw.models) ? raw.models : []; const models = modelsRaw.map((item) => normalizeModel(item || {})); const modelMap = new Map(); models.forEach((model) => { if (model.key) modelMap.set(model.key, model); }); const validModels = models.filter((model) => model.valid); const defaultModelKey = String(raw.default_model || raw.default_model_key || '').trim(); const resolvedDefault = modelMap.get(defaultModelKey)?.valid ? defaultModelKey : (validModels[0] ? validModels[0].key : ''); return { path: filePath, tavily_api_key: String(raw.tavily_api_key || '').trim(), models, valid_models: validModels, model_map: modelMap, default_model_key: resolvedDefault, }; } function ensureConfig() { const file = DEFAULT_CONFIG_PATH; if (!fs.existsSync(file)) { const template = { tavily_api_key: '', default_model: '', models: [], }; fs.writeFileSync(file, JSON.stringify(template, null, 2), 'utf8'); } const content = fs.readFileSync(file, 'utf8'); const raw = JSON.parse(content); return buildConfig(raw, file); } function maskKey(key) { if (!key) return ''; if (key.length <= 8) return key; return `${key.slice(0, 3)}...${key.slice(-3)}`; } function getModelByKey(config, key) { if (!config || !key) return null; if (config.model_map && typeof config.model_map.get === 'function') { return config.model_map.get(key) || null; } if (Array.isArray(config.models)) { return config.models.find((m) => m && m.key === key) || null; } return null; } module.exports = { ensureConfig, maskKey, getModelByKey };