feat(设置): 添加 API 格式选择功能
支持在 Claude 原生格式和 OpenAI 兼容格式之间切换: - 新增 api_format 数据库字段和迁移脚本 - 更新设置 Hook 类型定义 - 扩展设置 API 支持 apiFormat 读写 - 添加设置页面 API 格式选择 UI 组件
This commit is contained in:
parent
c341b0d67d
commit
99ca472dd2
@ -9,7 +9,8 @@ import { encryptApiKey } from '@/lib/crypto';
|
|||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
cchUrl: process.env.CCH_DEFAULT_URL || 'https://claude.leocoder.cn/',
|
cchUrl: process.env.CCH_DEFAULT_URL || 'https://claude.leocoder.cn/',
|
||||||
cchApiKeyConfigured: false,
|
cchApiKeyConfigured: false,
|
||||||
defaultModel: 'claude-sonnet-4-20250514',
|
apiFormat: 'claude' as 'claude' | 'openai', // API 格式:claude(原生)| openai(兼容)
|
||||||
|
defaultModel: 'claude-sonnet-4-5-20250929',
|
||||||
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
temperature: '0.7',
|
temperature: '0.7',
|
||||||
@ -29,6 +30,7 @@ function formatSettingsResponse(settings: typeof userSettings.$inferSelect | nul
|
|||||||
return {
|
return {
|
||||||
cchUrl: settings.cchUrl || DEFAULT_SETTINGS.cchUrl,
|
cchUrl: settings.cchUrl || DEFAULT_SETTINGS.cchUrl,
|
||||||
cchApiKeyConfigured: settings.cchApiKeyConfigured || false,
|
cchApiKeyConfigured: settings.cchApiKeyConfigured || false,
|
||||||
|
apiFormat: (settings.apiFormat as 'claude' | 'openai') || DEFAULT_SETTINGS.apiFormat,
|
||||||
defaultModel: settings.defaultModel || DEFAULT_SETTINGS.defaultModel,
|
defaultModel: settings.defaultModel || DEFAULT_SETTINGS.defaultModel,
|
||||||
defaultTools: settings.defaultTools || DEFAULT_SETTINGS.defaultTools,
|
defaultTools: settings.defaultTools || DEFAULT_SETTINGS.defaultTools,
|
||||||
systemPrompt: settings.systemPrompt || '',
|
systemPrompt: settings.systemPrompt || '',
|
||||||
@ -102,6 +104,7 @@ export async function PUT(request: Request) {
|
|||||||
const {
|
const {
|
||||||
cchUrl,
|
cchUrl,
|
||||||
cchApiKey,
|
cchApiKey,
|
||||||
|
apiFormat,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
defaultTools,
|
defaultTools,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
@ -135,6 +138,11 @@ export async function PUT(request: Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API 格式类型
|
||||||
|
if (apiFormat !== undefined) {
|
||||||
|
updateData.apiFormat = apiFormat;
|
||||||
|
}
|
||||||
|
|
||||||
if (defaultModel !== undefined) {
|
if (defaultModel !== undefined) {
|
||||||
updateData.defaultModel = defaultModel;
|
updateData.defaultModel = defaultModel;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export default function SettingsPage() {
|
|||||||
// CCH 配置状态
|
// CCH 配置状态
|
||||||
const [cchUrl, setCchUrl] = useState('');
|
const [cchUrl, setCchUrl] = useState('');
|
||||||
const [cchApiKey, setCchApiKey] = useState('');
|
const [cchApiKey, setCchApiKey] = useState('');
|
||||||
|
const [apiFormat, setApiFormat] = useState<'claude' | 'openai'>('claude');
|
||||||
const [showApiKey, setShowApiKey] = useState(false);
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ export default function SettingsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
setCchUrl(settings.cchUrl || '');
|
setCchUrl(settings.cchUrl || '');
|
||||||
|
setApiFormat((settings.apiFormat as 'claude' | 'openai') || 'claude');
|
||||||
setSystemPrompt(settings.systemPrompt || '');
|
setSystemPrompt(settings.systemPrompt || '');
|
||||||
setTemperature(settings.temperature || '0.7');
|
setTemperature(settings.temperature || '0.7');
|
||||||
}
|
}
|
||||||
@ -82,6 +84,7 @@ export default function SettingsPage() {
|
|||||||
const updates: Record<string, string> = {};
|
const updates: Record<string, string> = {};
|
||||||
if (cchUrl) updates.cchUrl = cchUrl;
|
if (cchUrl) updates.cchUrl = cchUrl;
|
||||||
if (cchApiKey) updates.cchApiKey = cchApiKey;
|
if (cchApiKey) updates.cchApiKey = cchApiKey;
|
||||||
|
updates.apiFormat = apiFormat;
|
||||||
|
|
||||||
if (Object.keys(updates).length > 0) {
|
if (Object.keys(updates).length > 0) {
|
||||||
await updateSettings(updates);
|
await updateSettings(updates);
|
||||||
@ -333,6 +336,83 @@ export default function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
{/* API 格式选择 */}
|
||||||
|
<div className="px-5 py-4 border-b border-[var(--color-border-light)]">
|
||||||
|
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-1">
|
||||||
|
API 格式
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-[var(--color-text-tertiary)] mb-4">
|
||||||
|
选择中转站的 API 接口格式
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{/* Claude 原生选项 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setApiFormat('claude')}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 p-4 rounded-lg border-2 text-left transition-all',
|
||||||
|
apiFormat === 'claude'
|
||||||
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
|
: 'border-[var(--color-border)] hover:border-[var(--color-border-dark)] bg-[var(--color-bg-primary)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className={cn(
|
||||||
|
'w-4 h-4 rounded-full border-2 flex items-center justify-center',
|
||||||
|
apiFormat === 'claude'
|
||||||
|
? 'border-[var(--color-primary)]'
|
||||||
|
: 'border-[var(--color-border)]'
|
||||||
|
)}>
|
||||||
|
{apiFormat === 'claude' && (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[var(--color-primary)]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className={cn(
|
||||||
|
'font-medium',
|
||||||
|
apiFormat === 'claude'
|
||||||
|
? 'text-[var(--color-primary)]'
|
||||||
|
: 'text-[var(--color-text-primary)]'
|
||||||
|
)}>
|
||||||
|
Claude 原生
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* OpenAI 兼容选项 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setApiFormat('openai')}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 p-4 rounded-lg border-2 text-left transition-all',
|
||||||
|
apiFormat === 'openai'
|
||||||
|
? 'border-[var(--color-primary)] bg-[var(--color-primary)]/5'
|
||||||
|
: 'border-[var(--color-border)] hover:border-[var(--color-border-dark)] bg-[var(--color-bg-primary)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className={cn(
|
||||||
|
'w-4 h-4 rounded-full border-2 flex items-center justify-center',
|
||||||
|
apiFormat === 'openai'
|
||||||
|
? 'border-[var(--color-primary)]'
|
||||||
|
: 'border-[var(--color-border)]'
|
||||||
|
)}>
|
||||||
|
{apiFormat === 'openai' && (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-[var(--color-primary)]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className={cn(
|
||||||
|
'font-medium',
|
||||||
|
apiFormat === 'openai'
|
||||||
|
? 'text-[var(--color-primary)]'
|
||||||
|
: 'text-[var(--color-text-primary)]'
|
||||||
|
)}>
|
||||||
|
OpenAI 兼容
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
label="API Key"
|
label="API Key"
|
||||||
description={
|
description={
|
||||||
|
|||||||
1
src/drizzle/migrations/0008_flat_star_brand.sql
Normal file
1
src/drizzle/migrations/0008_flat_star_brand.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_settings" ADD COLUMN "api_format" varchar(20) DEFAULT 'claude';
|
||||||
1128
src/drizzle/migrations/meta/0008_snapshot.json
Normal file
1128
src/drizzle/migrations/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -57,6 +57,13 @@
|
|||||||
"when": 1766299055211,
|
"when": 1766299055211,
|
||||||
"tag": "0007_fantastic_molten_man",
|
"tag": "0007_fantastic_molten_man",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1766314954003,
|
||||||
|
"tag": "0008_flat_star_brand",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -63,8 +63,10 @@ export const userSettings = pgTable('user_settings', {
|
|||||||
cchUrl: varchar('cch_url', { length: 512 }).notNull().default('http://localhost:13500'),
|
cchUrl: varchar('cch_url', { length: 512 }).notNull().default('http://localhost:13500'),
|
||||||
cchApiKey: varchar('cch_api_key', { length: 512 }),
|
cchApiKey: varchar('cch_api_key', { length: 512 }),
|
||||||
cchApiKeyConfigured: boolean('cch_api_key_configured').default(false),
|
cchApiKeyConfigured: boolean('cch_api_key_configured').default(false),
|
||||||
|
// API 格式类型:claude(原生)| openai(兼容)
|
||||||
|
apiFormat: varchar('api_format', { length: 20 }).default('claude'),
|
||||||
// 默认设置
|
// 默认设置
|
||||||
defaultModel: varchar('default_model', { length: 64 }).default('claude-sonnet-4-20250514'),
|
defaultModel: varchar('default_model', { length: 64 }).default('claude-sonnet-4-5-20250929'),
|
||||||
defaultTools: jsonb('default_tools').$type<string[]>().default(['web_search', 'code_execution', 'web_fetch']),
|
defaultTools: jsonb('default_tools').$type<string[]>().default(['web_search', 'code_execution', 'web_fetch']),
|
||||||
// AI 行为设置
|
// AI 行为设置
|
||||||
systemPrompt: text('system_prompt'), // 系统提示词
|
systemPrompt: text('system_prompt'), // 系统提示词
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
export interface Settings {
|
export interface Settings {
|
||||||
cchUrl: string;
|
cchUrl: string;
|
||||||
cchApiKeyConfigured: boolean;
|
cchApiKeyConfigured: boolean;
|
||||||
|
apiFormat: 'claude' | 'openai';
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
defaultTools: string[];
|
defaultTools: string[];
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
@ -50,7 +51,8 @@ export interface Model {
|
|||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
cchUrl: 'http://localhost:13500',
|
cchUrl: 'http://localhost:13500',
|
||||||
cchApiKeyConfigured: false,
|
cchApiKeyConfigured: false,
|
||||||
defaultModel: 'claude-sonnet-4-20250514',
|
apiFormat: 'claude',
|
||||||
|
defaultModel: 'claude-sonnet-4-5-20250929',
|
||||||
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
temperature: '0.7',
|
temperature: '0.7',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user