/** * 模型配置 Pack API * * 与 Cloudflare Workers Pack 服务交互 */ import { fetchWithAuth } from './fetch-with-auth' // ============ 类型定义 ============ /** * 提供商配置(分享时不含 api_key) */ export interface PackProvider { name: string base_url: string client_type: 'openai' | 'gemini' max_retry?: number timeout?: number retry_interval?: number } /** * 模型配置 */ export interface PackModel { model_identifier: string name: string api_provider: string price_in: number price_out: number temperature?: number max_tokens?: number force_stream_mode?: boolean extra_params?: Record } /** * 单个任务配置 */ export interface PackTaskConfig { model_list: string[] temperature?: number max_tokens?: number slow_threshold?: number } /** * 所有任务配置 */ export interface PackTaskConfigs { utils?: PackTaskConfig utils_small?: PackTaskConfig tool_use?: PackTaskConfig replyer?: PackTaskConfig planner?: PackTaskConfig vlm?: PackTaskConfig voice?: PackTaskConfig embedding?: PackTaskConfig lpmm_entity_extract?: PackTaskConfig lpmm_rdf_build?: PackTaskConfig lpmm_qa?: PackTaskConfig } /** * Pack 列表项 */ export interface PackListItem { id: string name: string description: string author: string version: string created_at: string updated_at: string status: 'pending' | 'approved' | 'rejected' reject_reason?: string downloads: number likes: number tags?: string[] provider_count: number model_count: number task_count: number } /** * 完整的 Pack 数据 */ export interface ModelPack extends Omit { providers: PackProvider[] models: PackModel[] task_config: PackTaskConfigs } /** * Pack 列表响应 */ export interface ListPacksResponse { packs: PackListItem[] total: number page: number page_size: number total_pages: number } /** * 应用 Pack 时的选项 */ export interface ApplyPackOptions { apply_providers: boolean apply_models: boolean apply_task_config: boolean task_mode: 'replace' | 'append' selected_providers?: string[] selected_models?: string[] selected_tasks?: string[] } /** * 应用 Pack 时的冲突检测结果 */ export interface ApplyPackConflicts { existing_providers: Array<{ pack_provider: PackProvider local_providers: Array<{ // 改为数组,支持多个匹配 name: string base_url: string }> }> new_providers: PackProvider[] conflicting_models: Array<{ pack_model: string local_model: string }> } // ============ API 配置 ============ // Pack 服务基础 URL(Cloudflare Workers) const PACK_SERVICE_URL = 'https://maibot-plugin-stats.maibot-webui.workers.dev' // ============ API 函数 ============ /** * 获取 Pack 列表 */ export async function listPacks(params?: { status?: 'pending' | 'approved' | 'rejected' | 'all' page?: number page_size?: number search?: string sort_by?: 'created_at' | 'downloads' | 'likes' sort_order?: 'asc' | 'desc' }): Promise { const searchParams = new URLSearchParams() if (params?.status) searchParams.set('status', params.status) if (params?.page) searchParams.set('page', params.page.toString()) if (params?.page_size) searchParams.set('page_size', params.page_size.toString()) if (params?.search) searchParams.set('search', params.search) if (params?.sort_by) searchParams.set('sort_by', params.sort_by) if (params?.sort_order) searchParams.set('sort_order', params.sort_order) const response = await fetch(`${PACK_SERVICE_URL}/pack?${searchParams.toString()}`) if (!response.ok) { throw new Error(`获取 Pack 列表失败: ${response.status}`) } return response.json() } /** * 获取单个 Pack 详情 */ export async function getPack(packId: string): Promise { const response = await fetch(`${PACK_SERVICE_URL}/pack/${packId}`) if (!response.ok) { throw new Error(`获取 Pack 失败: ${response.status}`) } const data = await response.json() if (!data.success) { throw new Error(data.error || '获取 Pack 失败') } return data.pack } /** * 创建新 Pack */ export async function createPack(pack: { name: string description: string author: string tags?: string[] providers: PackProvider[] models: PackModel[] task_config: PackTaskConfigs }): Promise<{ pack_id: string; message: string }> { const response = await fetch(`${PACK_SERVICE_URL}/pack`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(pack), }) const data = await response.json() if (!data.success) { throw new Error(data.error || '创建 Pack 失败') } return data } /** * 记录 Pack 下载 */ export async function recordPackDownload(packId: string, userId?: string): Promise { await fetch(`${PACK_SERVICE_URL}/pack/download`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pack_id: packId, user_id: userId }), }) } /** * 点赞/取消点赞 Pack */ export async function togglePackLike(packId: string, userId: string): Promise<{ likes: number; liked: boolean }> { const response = await fetch(`${PACK_SERVICE_URL}/pack/like`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pack_id: packId, user_id: userId }), }) const data = await response.json() if (!data.success) { throw new Error(data.error || '点赞失败') } return { likes: data.likes, liked: data.liked } } /** * 检查是否已点赞 */ export async function checkPackLike(packId: string, userId: string): Promise { const response = await fetch( `${PACK_SERVICE_URL}/pack/like/check?pack_id=${packId}&user_id=${userId}` ) const data = await response.json() return data.liked || false } // ============ 本地应用 Pack 相关 ============ /** * 检测应用 Pack 时的冲突 */ export async function detectPackConflicts( pack: ModelPack ): Promise { // 获取当前配置 const response = await fetchWithAuth('/api/webui/config/model') if (!response.ok) { throw new Error('获取当前模型配置失败') } const responseData = await response.json() const currentConfig = responseData.config || responseData console.log('=== Pack Conflict Detection ===') console.log('Pack providers:', pack.providers) console.log('Local providers:', currentConfig.api_providers) const conflicts: ApplyPackConflicts = { existing_providers: [], new_providers: [], conflicting_models: [], } // 检测提供商冲突 const localProviders = currentConfig.api_providers || [] for (const packProvider of pack.providers) { console.log(`\nChecking pack provider: ${packProvider.name}`) console.log(` Pack URL: ${packProvider.base_url}`) console.log(` Normalized: ${normalizeUrl(packProvider.base_url)}`) // 按 URL 匹配 - 找出所有匹配的本地提供商 const matchedProviders = localProviders.filter( (p: { base_url: string; name: string }) => { const localNormalized = normalizeUrl(p.base_url) const packNormalized = normalizeUrl(packProvider.base_url) console.log(` Comparing with local "${p.name}": ${p.base_url}`) console.log(` Local normalized: ${localNormalized}`) console.log(` Match: ${localNormalized === packNormalized}`) return localNormalized === packNormalized } ) if (matchedProviders.length > 0) { console.log(` ✓ Matched with ${matchedProviders.length} local provider(s):`, matchedProviders.map((p: {name: string}) => p.name).join(', ')) conflicts.existing_providers.push({ pack_provider: packProvider, local_providers: matchedProviders.map((p: { name: string; base_url: string }) => ({ name: p.name, base_url: p.base_url, })), }) } else { console.log(` ✗ No match found - will need API key`) conflicts.new_providers.push(packProvider) } } // 检测模型名称冲突 const localModels = currentConfig.models || [] console.log('\n=== Model Conflict Detection ===') for (const packModel of pack.models) { const conflictModel = localModels.find( (m: { name: string }) => m.name === packModel.name ) if (conflictModel) { console.log(`Model conflict: ${packModel.name}`) conflicts.conflicting_models.push({ pack_model: packModel.name, local_model: conflictModel.name, }) } } console.log('\n=== Detection Summary ===') console.log(`Existing providers: ${conflicts.existing_providers.length}`) console.log(`New providers: ${conflicts.new_providers.length}`) console.log(`Conflicting models: ${conflicts.conflicting_models.length}`) console.log('===========================\n') return conflicts } /** * 应用 Pack 到本地配置 */ export async function applyPack( pack: ModelPack, options: ApplyPackOptions, providerMapping: Record, // pack_provider_name -> local_provider_name newProviderApiKeys: Record, // provider_name -> api_key ): Promise { // 获取当前配置 const response = await fetchWithAuth('/api/webui/config/model') if (!response.ok) { throw new Error('获取当前模型配置失败') } const responseData = await response.json() const currentConfig = responseData.config || responseData // 1. 处理提供商 if (options.apply_providers) { const providersToApply = options.selected_providers ? pack.providers.filter(p => options.selected_providers!.includes(p.name)) : pack.providers for (const packProvider of providersToApply) { // 检查是否映射到已有提供商 if (providerMapping[packProvider.name]) { // 使用已有提供商,不需要添加 continue } // 添加新提供商 const apiKey = newProviderApiKeys[packProvider.name] if (!apiKey) { throw new Error(`提供商 "${packProvider.name}" 缺少 API Key`) } const newProvider = { ...packProvider, api_key: apiKey, } // 检查是否已存在同名提供商 const existingIndex = currentConfig.api_providers.findIndex( (p: { name: string }) => p.name === packProvider.name ) if (existingIndex >= 0) { // 覆盖 currentConfig.api_providers[existingIndex] = newProvider } else { // 添加 currentConfig.api_providers.push(newProvider) } } } // 2. 处理模型 if (options.apply_models) { const modelsToApply = options.selected_models ? pack.models.filter(m => options.selected_models!.includes(m.name)) : pack.models for (const packModel of modelsToApply) { // 映射提供商名称 const actualProvider = providerMapping[packModel.api_provider] || packModel.api_provider const newModel = { ...packModel, api_provider: actualProvider, } // 检查是否已存在同名模型 const existingIndex = currentConfig.models.findIndex( (m: { name: string }) => m.name === packModel.name ) if (existingIndex >= 0) { // 覆盖 currentConfig.models[existingIndex] = newModel } else { // 添加 currentConfig.models.push(newModel) } } } // 3. 处理任务配置 if (options.apply_task_config) { const taskKeys = options.selected_tasks || Object.keys(pack.task_config) for (const taskKey of taskKeys) { const packTaskConfig = pack.task_config[taskKey as keyof PackTaskConfigs] if (!packTaskConfig) continue // 映射模型名称(如果模型名称被跳过,则从任务列表中移除) const appliedModelNames = new Set( options.selected_models || pack.models.map(m => m.name) ) const filteredModelList = packTaskConfig.model_list.filter( name => appliedModelNames.has(name) ) if (filteredModelList.length === 0) continue const newTaskConfig = { ...packTaskConfig, model_list: filteredModelList, } if (options.task_mode === 'replace') { // 替换模式 currentConfig.model_task_config[taskKey] = newTaskConfig } else { // 追加模式 const existingConfig = currentConfig.model_task_config[taskKey] if (existingConfig) { // 合并模型列表(去重) const mergedList = [...new Set([ ...existingConfig.model_list, ...filteredModelList, ])] currentConfig.model_task_config[taskKey] = { ...existingConfig, model_list: mergedList, } } else { currentConfig.model_task_config[taskKey] = newTaskConfig } } } } // 保存配置 const saveResponse = await fetchWithAuth('/api/webui/config/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(currentConfig), }) if (!saveResponse.ok) { throw new Error('保存配置失败') } } /** * 从当前配置导出 Pack */ export async function exportCurrentConfigAsPack(params: { name: string description: string author: string tags?: string[] selectedProviders?: string[] selectedModels?: string[] selectedTasks?: string[] }): Promise<{ providers: PackProvider[] models: PackModel[] task_config: PackTaskConfigs }> { // 获取当前配置 const response = await fetchWithAuth('/api/webui/config/model') if (!response.ok) { throw new Error('获取当前模型配置失败') } const responseData = await response.json() // API 返回的格式是 { success: true, config: {...} } if (!responseData.success || !responseData.config) { throw new Error('获取配置失败') } const currentConfig = responseData.config // 过滤提供商(移除 api_key) let providers: PackProvider[] = (currentConfig.api_providers || []).map( (p: { name: string; base_url: string; client_type: string; max_retry?: number; timeout?: number; retry_interval?: number }) => ({ name: p.name, base_url: p.base_url, client_type: p.client_type, max_retry: p.max_retry, timeout: p.timeout, retry_interval: p.retry_interval, }) ) if (params.selectedProviders) { providers = providers.filter(p => params.selectedProviders!.includes(p.name)) } // 过滤模型 let models: PackModel[] = currentConfig.models || [] if (params.selectedModels) { models = models.filter(m => params.selectedModels!.includes(m.name)) } // 过滤任务配置 const task_config: PackTaskConfigs = {} const allTasks = currentConfig.model_task_config || {} const taskKeys = params.selectedTasks || Object.keys(allTasks) for (const key of taskKeys) { if (allTasks[key]) { task_config[key as keyof PackTaskConfigs] = allTasks[key] } } return { providers, models, task_config } } // ============ 辅助函数 ============ /** * 标准化 URL 用于比较 */ function normalizeUrl(url: string): string { try { const parsed = new URL(url) // 移除末尾斜杠,统一小写 return `${parsed.protocol}//${parsed.host}${parsed.pathname}`.replace(/\/$/, '').toLowerCase() } catch { return url.toLowerCase().replace(/\/$/, '') } } /** * 生成用户 ID(用于统计) */ export function getPackUserId(): string { const storageKey = 'maibot_pack_user_id' let userId = localStorage.getItem(storageKey) if (!userId) { userId = 'pack_user_' + Math.random().toString(36).substring(2, 15) localStorage.setItem(storageKey, userId) } return userId }