feat(设置): 添加 API 格式选择功能

支持在 Claude 原生格式和 OpenAI 兼容格式之间切换:
- 新增 api_format 数据库字段和迁移脚本
- 更新设置 Hook 类型定义
- 扩展设置 API 支持 apiFormat 读写
- 添加设置页面 API 格式选择 UI 组件
This commit is contained in:
gaoziman 2025-12-21 21:14:41 +08:00
parent c341b0d67d
commit 99ca472dd2
7 changed files with 1231 additions and 3 deletions

View File

@ -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;
} }

View File

@ -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={

View File

@ -0,0 +1 @@
ALTER TABLE "user_settings" ADD COLUMN "api_format" varchar(20) DEFAULT 'claude';

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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'), // 系统提示词

View File

@ -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',