diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index a1eef08..fff657b 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { User, Template, SystemSettings, FormField, DEFAULT_FORM_FIELDS } from '../types'; +import { User, Template, SystemSettings, FormField, DEFAULT_FORM_FIELDS, DEFAULT_AI_PROVIDERS } from '../types'; import { defaultReportContent } from '../utils/defaultContent'; import { storage } from '../utils/storage'; import { User as UserIcon, Lock } from 'lucide-react'; @@ -71,10 +71,10 @@ export default function Login() { const defaultSettings = { frameCount: 12, framePositions: positions, - apiEndpoint: '', - apiKey: '', defaultTemplate: savedTemplates[0]?.id || '', frameMode: 'uniform', + activeAiProvider: 'kimi', + aiProviders: { ...DEFAULT_AI_PROVIDERS }, autoInsertFrames: true, autoInsertDelay: 1, autoInsertFrameIndices: [0, 2, 4, 6, 8, 10] diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index c1cf1c3..3b3d725 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -849,10 +849,12 @@ export default function ReportEditor() { setIsGenerating(true); try { const settings = storage.get('systemSettings', {} as SystemSettings); - const apiKey = settings.kimiApiKey || ''; - const apiEndpoint = settings.kimiApiEndpoint || 'https://api.moonshot.cn/v1'; + const provider = settings.aiProviders?.[settings.activeAiProvider || 'kimi']; + const apiKey = provider?.apiKey || ''; + const apiEndpoint = provider?.endpoint || 'https://api.moonshot.cn/v1'; + const modelName = provider?.modelName || 'kimi-k2-5'; if (!apiKey) { - setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: '【系统提示】尚未配置 Kimi API Key,请前往系统设置填写。' }]); + setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: '【系统提示】尚未配置 AI API Key,请前往系统设置填写。' }]); setIsGenerating(false); return; } @@ -879,7 +881,7 @@ export default function ReportEditor() { 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ - model: 'kimi-k2-5', + model: modelName, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: messageContent } diff --git a/src/pages/SystemSettings.tsx b/src/pages/SystemSettings.tsx index 80750f6..abeac70 100644 --- a/src/pages/SystemSettings.tsx +++ b/src/pages/SystemSettings.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import Sidebar from '../components/Sidebar'; import { Video, Globe, Layout, Check, Plus, X } from 'lucide-react'; -import { User, SystemSettings as ISystemSettings, Template } from '../types'; +import { User, SystemSettings as ISystemSettings, Template, DEFAULT_AI_PROVIDERS, AiProviderConfig } from '../types'; import { storage } from '../utils/storage'; export default function SystemSettings() { @@ -11,12 +11,10 @@ export default function SystemSettings() { const [settings, setSettings] = useState({ frameCount: 12, framePositions: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60], - apiEndpoint: '', - apiKey: '', - kimiApiKey: '', - kimiApiEndpoint: '', defaultTemplate: '', - frameMode: 'uniform' + frameMode: 'uniform', + activeAiProvider: 'kimi', + aiProviders: { ...DEFAULT_AI_PROVIDERS } }); const [templates, setTemplates] = useState([]); const [isSaved, setIsSaved] = useState(false); @@ -32,6 +30,20 @@ export default function SystemSettings() { setCurrentUser(user); const savedSettings = storage.get('systemSettings', {} as ISystemSettings & { frameMode?: 'uniform' | 'keep' }); + // Migrate old flat fields to new structured format + if (!savedSettings.aiProviders) { + const providers = { ...DEFAULT_AI_PROVIDERS }; + if ((savedSettings as any).kimiApiKey || (savedSettings as any).kimiApiEndpoint) { + providers.kimi = { + endpoint: (savedSettings as any).kimiApiEndpoint || providers.kimi.endpoint, + apiKey: (savedSettings as any).kimiApiKey || '', + modelName: 'kimi-k2-5' + }; + } + savedSettings.aiProviders = providers; + savedSettings.activeAiProvider = 'kimi'; + storage.set('systemSettings', savedSettings); + } const savedTemplates = storage.get('templates', []); if (savedSettings.frameCount) { if (!savedSettings.defaultTemplate && savedTemplates.length > 0) { @@ -81,30 +93,24 @@ export default function SystemSettings() { }; const testApi = async () => { - const endpoint = settings.kimiApiEndpoint || 'https://api.moonshot.cn/v1'; - const apiKey = settings.kimiApiKey; - if (!apiKey) { - alert('请先输入 Kimi API Key'); + const provider = settings.aiProviders[settings.activeAiProvider]; + if (!provider?.apiKey) { + alert('请先输入 API 密钥'); return; } try { - const res = await fetch(`${endpoint.replace(/\/$/, '')}/models`, { + const res = await fetch(`${provider.endpoint}/models`, { method: 'GET', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - } + headers: { 'Authorization': `Bearer ${provider.apiKey}`, 'Content-Type': 'application/json' } }); if (res.ok) { const data = await res.json(); - const modelCount = data?.data?.length || 0; - alert(`Kimi API 连接成功!\nEndpoint: ${endpoint}\n可用模型数: ${modelCount}`); + alert(`连接成功!可用模型数: ${data.data?.length || '未知'}`); } else { - const text = await res.text(); - alert(`Kimi API 连接失败 (HTTP ${res.status})\n${text}`); + alert(`连接失败: ${res.status} ${res.statusText}`); } - } catch (err: any) { - alert(`Kimi API 连接异常: ${err?.message || String(err)}`); + } catch (e: any) { + alert(`连接失败: ${e.message}`); } }; @@ -113,12 +119,10 @@ export default function SystemSettings() { const defaultSettings: ISystemSettings & { frameMode?: 'uniform' | 'keep' } = { frameCount: 12, framePositions: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60], - apiEndpoint: '', - apiKey: '', - kimiApiKey: '', - kimiApiEndpoint: '', defaultTemplate: templates[0]?.id || '', frameMode: 'uniform', + activeAiProvider: 'kimi', + aiProviders: { ...DEFAULT_AI_PROVIDERS }, autoInsertFrames: true, autoInsertDelay: 1, autoInsertFrameIndices: [0, 2, 4, 6, 8, 10] @@ -312,47 +316,63 @@ export default function SystemSettings() {
- + + +
+ +
+ setSettings({ ...settings, apiEndpoint: e.target.value })} - placeholder="https://api.example.com/v1/generate" + value={settings.aiProviders[settings.activeAiProvider]?.endpoint || ''} + onChange={(e) => { + const next = { ...settings.aiProviders }; + next[settings.activeAiProvider] = { ...next[settings.activeAiProvider], endpoint: e.target.value }; + setSettings({ ...settings, aiProviders: next }); + }} + placeholder="https://api.example.com/v1" className="input-minimal" />
- + setSettings({ ...settings, apiKey: e.target.value })} + value={settings.aiProviders[settings.activeAiProvider]?.apiKey || ''} + onChange={(e) => { + const next = { ...settings.aiProviders }; + next[settings.activeAiProvider] = { ...next[settings.activeAiProvider], apiKey: e.target.value }; + setSettings({ ...settings, aiProviders: next }); + }} placeholder="sk-xxxxxxxxxxxxxxxx" className="input-minimal" />
- + setSettings({ ...settings, kimiApiEndpoint: e.target.value })} - placeholder="https://api.moonshot.cn/v1" - className="input-minimal" - /> -
- -
- - setSettings({ ...settings, kimiApiKey: e.target.value })} - placeholder="sk-..." + type="text" + value={settings.aiProviders[settings.activeAiProvider]?.modelName || ''} + onChange={(e) => { + const next = { ...settings.aiProviders }; + next[settings.activeAiProvider] = { ...next[settings.activeAiProvider], modelName: e.target.value }; + setSettings({ ...settings, aiProviders: next }); + }} + placeholder="kimi-k2-5" className="input-minimal" /> +

当前服务商使用的具体模型版本,可随时修改以切换模型。

diff --git a/src/types.ts b/src/types.ts index 49f90d7..c2655fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,20 +70,31 @@ export interface Template { fields?: FormField[]; } +export interface AiProviderConfig { + endpoint: string; + apiKey: string; + modelName: string; +} + export interface SystemSettings { frameCount: number; framePositions: number[]; - apiEndpoint: string; - apiKey: string; defaultTemplate?: string; frameMode?: 'uniform' | 'keep'; autoInsertFrames?: boolean; autoInsertFrameIndices?: number[]; autoInsertDelay?: number; - kimiApiKey?: string; - kimiApiEndpoint?: string; + activeAiProvider: string; + aiProviders: Record; } +export const DEFAULT_AI_PROVIDERS: Record = { + kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: '', modelName: 'kimi-k2-5' }, + deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: '', modelName: 'deepseek-chat' }, + openai: { endpoint: 'https://api.openai.com/v1', apiKey: '', modelName: 'gpt-4o' }, + custom: { endpoint: '', apiKey: '', modelName: '' } +}; + export interface BindableField { key: string; label: string; diff --git a/工程分析/实现方案-2026-04-19-02-48-25.md b/工程分析/实现方案-2026-04-19-02-48-25.md new file mode 100644 index 0000000..81ed4cf --- /dev/null +++ b/工程分析/实现方案-2026-04-19-02-48-25.md @@ -0,0 +1,117 @@ +# 实现方案 — 2026-04-19-02-48-25 + +## 1. 方案概述 +基于 OpenAI 兼容协议进行标准化抽象。将 4 个散装字段收敛为 `activeAiProvider` + `aiProviders` 字典结构。UI 改为"选择器 + 动态表单",调用端完全配置驱动。通过初始化时的数据迁移保证旧用户无损升级。 + +## 2. 详细步骤 + +### 步骤 1:`src/types.ts` — 数据结构重构 +**目标文件**:`src/types.ts` +**修改内容**: +1. 新增 `AiProviderConfig` 接口: + ```ts + export interface AiProviderConfig { + endpoint: string; + apiKey: string; + modelName: string; + } + ``` +2. 重构 `SystemSettings`: + - 删除:`apiEndpoint`, `apiKey`, `kimiApiKey`, `kimiApiEndpoint` + - 新增:`activeAiProvider: string` + - 新增:`aiProviders: Record` +3. 新增默认预设常量: + ```ts + export const DEFAULT_AI_PROVIDERS: Record = { + kimi: { endpoint: 'https://api.moonshot.cn/v1', apiKey: '', modelName: 'kimi-k2-5' }, + deepseek: { endpoint: 'https://api.deepseek.com/v1', apiKey: '', modelName: 'deepseek-chat' }, + openai: { endpoint: 'https://api.openai.com/v1', apiKey: '', modelName: 'gpt-4o' }, + custom: { endpoint: '', apiKey: '', modelName: '' } + }; + ``` + +### 步骤 2:`src/pages/SystemSettings.tsx` — UI 重构 + 数据迁移 +**目标文件**:`src/pages/SystemSettings.tsx` +**修改内容**: +1. **初始化数据迁移**(在 `useEffect` 读取 settings 后): + ```ts + const migrateOldAiSettings = (saved: any) => { + if (!saved.aiProviders) { + const providers = { ...DEFAULT_AI_PROVIDERS }; + if (saved.kimiApiKey || saved.kimiApiEndpoint) { + providers.kimi = { + endpoint: saved.kimiApiEndpoint || providers.kimi.endpoint, + apiKey: saved.kimiApiKey || '', + modelName: 'kimi-k2-5' + }; + } + saved.aiProviders = providers; + saved.activeAiProvider = 'kimi'; + // 清理旧字段(可选,保留 localStorage 中不删除,但代码不再读取) + storage.set('systemSettings', saved); + } + return saved; + }; + ``` +2. **状态初始化**: + ```ts + const [settings, setSettings] = useState({ + frameCount: 12, + framePositions: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60], + defaultTemplate: '', + frameMode: 'uniform', + activeAiProvider: 'kimi', + aiProviders: { ...DEFAULT_AI_PROVIDERS } + }); + ``` +3. **UI 改造**: + - 用一个 `