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 = {
|
||||
cchUrl: process.env.CCH_DEFAULT_URL || 'https://claude.leocoder.cn/',
|
||||
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'],
|
||||
systemPrompt: '',
|
||||
temperature: '0.7',
|
||||
@ -29,6 +30,7 @@ function formatSettingsResponse(settings: typeof userSettings.$inferSelect | nul
|
||||
return {
|
||||
cchUrl: settings.cchUrl || DEFAULT_SETTINGS.cchUrl,
|
||||
cchApiKeyConfigured: settings.cchApiKeyConfigured || false,
|
||||
apiFormat: (settings.apiFormat as 'claude' | 'openai') || DEFAULT_SETTINGS.apiFormat,
|
||||
defaultModel: settings.defaultModel || DEFAULT_SETTINGS.defaultModel,
|
||||
defaultTools: settings.defaultTools || DEFAULT_SETTINGS.defaultTools,
|
||||
systemPrompt: settings.systemPrompt || '',
|
||||
@ -102,6 +104,7 @@ export async function PUT(request: Request) {
|
||||
const {
|
||||
cchUrl,
|
||||
cchApiKey,
|
||||
apiFormat,
|
||||
defaultModel,
|
||||
defaultTools,
|
||||
systemPrompt,
|
||||
@ -135,6 +138,11 @@ export async function PUT(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// API 格式类型
|
||||
if (apiFormat !== undefined) {
|
||||
updateData.apiFormat = apiFormat;
|
||||
}
|
||||
|
||||
if (defaultModel !== undefined) {
|
||||
updateData.defaultModel = defaultModel;
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ export default function SettingsPage() {
|
||||
// CCH 配置状态
|
||||
const [cchUrl, setCchUrl] = useState('');
|
||||
const [cchApiKey, setCchApiKey] = useState('');
|
||||
const [apiFormat, setApiFormat] = useState<'claude' | 'openai'>('claude');
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||
|
||||
@ -70,6 +71,7 @@ export default function SettingsPage() {
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setCchUrl(settings.cchUrl || '');
|
||||
setApiFormat((settings.apiFormat as 'claude' | 'openai') || 'claude');
|
||||
setSystemPrompt(settings.systemPrompt || '');
|
||||
setTemperature(settings.temperature || '0.7');
|
||||
}
|
||||
@ -82,6 +84,7 @@ export default function SettingsPage() {
|
||||
const updates: Record<string, string> = {};
|
||||
if (cchUrl) updates.cchUrl = cchUrl;
|
||||
if (cchApiKey) updates.cchApiKey = cchApiKey;
|
||||
updates.apiFormat = apiFormat;
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await updateSettings(updates);
|
||||
@ -333,6 +336,83 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</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
|
||||
label="API Key"
|
||||
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,
|
||||
"tag": "0007_fantastic_molten_man",
|
||||
"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'),
|
||||
cchApiKey: varchar('cch_api_key', { length: 512 }),
|
||||
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']),
|
||||
// AI 行为设置
|
||||
systemPrompt: text('system_prompt'), // 系统提示词
|
||||
|
||||
@ -5,6 +5,7 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
export interface Settings {
|
||||
cchUrl: string;
|
||||
cchApiKeyConfigured: boolean;
|
||||
apiFormat: 'claude' | 'openai';
|
||||
defaultModel: string;
|
||||
defaultTools: string[];
|
||||
systemPrompt: string;
|
||||
@ -50,7 +51,8 @@ export interface Model {
|
||||
const defaultSettings: Settings = {
|
||||
cchUrl: 'http://localhost:13500',
|
||||
cchApiKeyConfigured: false,
|
||||
defaultModel: 'claude-sonnet-4-20250514',
|
||||
apiFormat: 'claude',
|
||||
defaultModel: 'claude-sonnet-4-5-20250929',
|
||||
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
||||
systemPrompt: '',
|
||||
temperature: '0.7',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user