2026-04-19-02-48-25 重构AI接口配置:多服务商底座架构、OpenAI兼容协议、动态模型切换、旧数据自动迁移

This commit is contained in:
2026-04-19 02:53:26 +08:00
parent 221daf61a5
commit d5cbbf9137
8 changed files with 416 additions and 59 deletions

View File

@@ -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]

View File

@@ -849,10 +849,12 @@ export default function ReportEditor() {
setIsGenerating(true);
try {
const settings = storage.get<SystemSettings>('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 }

View File

@@ -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<ISystemSettings & { frameMode?: 'uniform' | 'keep' }>({
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<Template[]>([]);
const [isSaved, setIsSaved] = useState(false);
@@ -32,6 +30,20 @@ export default function SystemSettings() {
setCurrentUser(user);
const savedSettings = storage.get<ISystemSettings & { frameMode?: 'uniform' | 'keep' }>('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<Template[]>('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() {
</div>
<div className="space-y-6">
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> API (Endpoint)</label>
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">AI </label>
<select
value={settings.activeAiProvider}
onChange={(e) => setSettings({ ...settings, activeAiProvider: e.target.value })}
className="input-minimal bg-white"
>
<option value="kimi">Kimi (Moonshot)</option>
<option value="deepseek">DeepSeek</option>
<option value="openai">OpenAI</option>
<option value="custom"></option>
</select>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> (Base URL)</label>
<input
type="url"
value={settings.apiEndpoint}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">API (Secret Key)</label>
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">API </label>
<input
type="password"
value={settings.apiKey}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">Kimi API Endpoint</label>
<label className="block text-xs font-bold text-text-main uppercase tracking-wider"> (Model Name)</label>
<input
type="url"
value={settings.kimiApiEndpoint}
onChange={(e) => setSettings({ ...settings, kimiApiEndpoint: e.target.value })}
placeholder="https://api.moonshot.cn/v1"
className="input-minimal"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">Kimi API Key</label>
<input
type="password"
value={settings.kimiApiKey}
onChange={(e) => 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"
/>
<p className="text-[11px] text-text-muted">使</p>
</div>
</div>
</div>

View File

@@ -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<string, AiProviderConfig>;
}
export const DEFAULT_AI_PROVIDERS: Record<string, AiProviderConfig> = {
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;