2026-04-19-02-48-25 重构AI接口配置:多服务商底座架构、OpenAI兼容协议、动态模型切换、旧数据自动迁移
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
|
||||
19
src/types.ts
19
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<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;
|
||||
|
||||
Reference in New Issue
Block a user