Compare commits
4 Commits
269fc798aa
...
3b0683faf9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b0683faf9 | ||
|
|
fd6c93cb30 | ||
|
|
058ea85daa | ||
|
|
2e8033a8ae |
@ -122,9 +122,19 @@ export async function POST(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// 创建默认用户设置
|
// 创建默认用户设置(API Key 为空,需要用户自行配置)
|
||||||
await db.insert(userSettings).values({
|
await db.insert(userSettings).values({
|
||||||
userId,
|
userId,
|
||||||
|
cchUrl: process.env.CCH_DEFAULT_URL || 'https://claude.leocoder.cn/',
|
||||||
|
cchApiKey: null,
|
||||||
|
cchApiKeyConfigured: false,
|
||||||
|
defaultModel: 'claude-sonnet-4-20250514',
|
||||||
|
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
||||||
|
theme: 'light',
|
||||||
|
language: 'zh-CN',
|
||||||
|
fontSize: 15,
|
||||||
|
enableThinking: false,
|
||||||
|
saveChatHistory: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成 JWT Token
|
// 生成 JWT Token
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { conversations, messages, userSettings } from '@/drizzle/schema';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { executeTool } from '@/services/tools';
|
import { executeTool } from '@/services/tools';
|
||||||
|
import { getCurrentUser } from '@/lib/auth';
|
||||||
|
import { decryptApiKey } from '@/lib/crypto';
|
||||||
|
|
||||||
interface ChatRequest {
|
interface ChatRequest {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
@ -212,18 +214,30 @@ export async function POST(request: Request) {
|
|||||||
})) : undefined,
|
})) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取用户设置
|
// 获取当前登录用户
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '请先登录后再使用聊天功能' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取该用户的设置
|
||||||
const settings = await db.query.userSettings.findFirst({
|
const settings = await db.query.userSettings.findFirst({
|
||||||
where: eq(userSettings.id, 1),
|
where: eq(userSettings.userId, user.userId),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings?.cchApiKey) {
|
if (!settings?.cchApiKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Please configure your CCH API key in settings' },
|
{ error: '请先在设置中配置您的 API Key 才能使用聊天功能', code: 'API_KEY_NOT_CONFIGURED' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解密 API Key
|
||||||
|
const decryptedApiKey = decryptApiKey(settings.cchApiKey);
|
||||||
|
|
||||||
// 获取对话信息
|
// 获取对话信息
|
||||||
const conversation = await db.query.conversations.findFirst({
|
const conversation = await db.query.conversations.findFirst({
|
||||||
where: eq(conversations.conversationId, conversationId),
|
where: eq(conversations.conversationId, conversationId),
|
||||||
@ -267,7 +281,7 @@ export async function POST(request: Request) {
|
|||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
try {
|
try {
|
||||||
const cchUrl = settings.cchUrl || 'http://localhost:13500';
|
const cchUrl = settings.cchUrl || process.env.CCH_DEFAULT_URL || 'https://claude.leocoder.cn/';
|
||||||
|
|
||||||
// 获取系统提示词(叠加模式)
|
// 获取系统提示词(叠加模式)
|
||||||
// 1. 始终使用 DEFAULT_SYSTEM_PROMPT 作为基础
|
// 1. 始终使用 DEFAULT_SYSTEM_PROMPT 作为基础
|
||||||
@ -302,7 +316,7 @@ export async function POST(request: Request) {
|
|||||||
// ==================== Codex 模型处理(OpenAI 格式) ====================
|
// ==================== Codex 模型处理(OpenAI 格式) ====================
|
||||||
const result = await handleCodexChat({
|
const result = await handleCodexChat({
|
||||||
cchUrl,
|
cchUrl,
|
||||||
apiKey: settings.cchApiKey!,
|
apiKey: decryptedApiKey,
|
||||||
model: useModel,
|
model: useModel,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
temperature,
|
temperature,
|
||||||
@ -321,7 +335,7 @@ export async function POST(request: Request) {
|
|||||||
// ==================== Claude 模型处理(原有逻辑) ====================
|
// ==================== Claude 模型处理(原有逻辑) ====================
|
||||||
const result = await handleClaudeChat({
|
const result = await handleClaudeChat({
|
||||||
cchUrl,
|
cchUrl,
|
||||||
apiKey: settings.cchApiKey!,
|
apiKey: decryptedApiKey,
|
||||||
model: useModel,
|
model: useModel,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
temperature,
|
temperature,
|
||||||
|
|||||||
@ -2,18 +2,12 @@ import { NextResponse } from 'next/server';
|
|||||||
import { db } from '@/drizzle/db';
|
import { db } from '@/drizzle/db';
|
||||||
import { userSettings } from '@/drizzle/schema';
|
import { userSettings } from '@/drizzle/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { getCurrentUser } from '@/lib/auth';
|
||||||
|
import { encryptApiKey } from '@/lib/crypto';
|
||||||
|
|
||||||
// GET /api/settings - 获取用户设置
|
// 默认设置值
|
||||||
export async function GET() {
|
const DEFAULT_SETTINGS = {
|
||||||
try {
|
cchUrl: process.env.CCH_DEFAULT_URL || 'https://claude.leocoder.cn/',
|
||||||
const settings = await db.query.userSettings.findFirst({
|
|
||||||
where: eq(userSettings.id, 1),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
// 如果没有设置,返回默认值
|
|
||||||
return NextResponse.json({
|
|
||||||
cchUrl: process.env.CCH_DEFAULT_URL || 'http://localhost:13500',
|
|
||||||
cchApiKeyConfigured: false,
|
cchApiKeyConfigured: false,
|
||||||
defaultModel: 'claude-sonnet-4-20250514',
|
defaultModel: 'claude-sonnet-4-20250514',
|
||||||
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
defaultTools: ['web_search', 'code_execution', 'web_fetch'],
|
||||||
@ -24,23 +18,65 @@ export async function GET() {
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
enableThinking: false,
|
enableThinking: false,
|
||||||
saveChatHistory: true,
|
saveChatHistory: true,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// 格式化设置响应(不返回 API Key 本身)
|
||||||
|
function formatSettingsResponse(settings: typeof userSettings.$inferSelect | null) {
|
||||||
|
if (!settings) {
|
||||||
|
return DEFAULT_SETTINGS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不返回 API Key 本身,只返回是否已配置
|
return {
|
||||||
return NextResponse.json({
|
cchUrl: settings.cchUrl || DEFAULT_SETTINGS.cchUrl,
|
||||||
cchUrl: settings.cchUrl,
|
cchApiKeyConfigured: settings.cchApiKeyConfigured || false,
|
||||||
cchApiKeyConfigured: settings.cchApiKeyConfigured,
|
defaultModel: settings.defaultModel || DEFAULT_SETTINGS.defaultModel,
|
||||||
defaultModel: settings.defaultModel,
|
defaultTools: settings.defaultTools || DEFAULT_SETTINGS.defaultTools,
|
||||||
defaultTools: settings.defaultTools,
|
|
||||||
systemPrompt: settings.systemPrompt || '',
|
systemPrompt: settings.systemPrompt || '',
|
||||||
temperature: settings.temperature || '0.7',
|
temperature: settings.temperature || '0.7',
|
||||||
theme: settings.theme,
|
theme: settings.theme || DEFAULT_SETTINGS.theme,
|
||||||
language: settings.language,
|
language: settings.language || DEFAULT_SETTINGS.language,
|
||||||
fontSize: settings.fontSize || 15,
|
fontSize: settings.fontSize || 15,
|
||||||
enableThinking: settings.enableThinking,
|
enableThinking: settings.enableThinking || false,
|
||||||
saveChatHistory: settings.saveChatHistory,
|
saveChatHistory: settings.saveChatHistory ?? true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/settings - 获取当前用户的设置
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// 1. 获取当前登录用户
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
// 未登录用户返回默认设置(允许查看但无法保存)
|
||||||
|
return NextResponse.json(DEFAULT_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询该用户的设置
|
||||||
|
const settings = await db.query.userSettings.findFirst({
|
||||||
|
where: eq(userSettings.userId, user.userId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3. 如果没有设置记录,创建默认设置
|
||||||
|
if (!settings) {
|
||||||
|
const newSettings = await db
|
||||||
|
.insert(userSettings)
|
||||||
|
.values({
|
||||||
|
userId: user.userId,
|
||||||
|
cchUrl: DEFAULT_SETTINGS.cchUrl,
|
||||||
|
defaultModel: DEFAULT_SETTINGS.defaultModel,
|
||||||
|
defaultTools: DEFAULT_SETTINGS.defaultTools,
|
||||||
|
theme: DEFAULT_SETTINGS.theme,
|
||||||
|
language: DEFAULT_SETTINGS.language,
|
||||||
|
fontSize: DEFAULT_SETTINGS.fontSize,
|
||||||
|
enableThinking: DEFAULT_SETTINGS.enableThinking,
|
||||||
|
saveChatHistory: DEFAULT_SETTINGS.saveChatHistory,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return NextResponse.json(formatSettingsResponse(newSettings[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(formatSettingsResponse(settings));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get settings:', error);
|
console.error('Failed to get settings:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -50,9 +86,18 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT /api/settings - 更新用户设置
|
// PUT /api/settings - 更新当前用户的设置
|
||||||
export async function PUT(request: Request) {
|
export async function PUT(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
// 1. 获取当前登录用户
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '请先登录后再修改设置' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const {
|
const {
|
||||||
cchUrl,
|
cchUrl,
|
||||||
@ -68,7 +113,7 @@ export async function PUT(request: Request) {
|
|||||||
saveChatHistory,
|
saveChatHistory,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
// 构建更新对象
|
// 2. 构建更新对象
|
||||||
const updateData: Record<string, unknown> = {
|
const updateData: Record<string, unknown> = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -77,14 +122,15 @@ export async function PUT(request: Request) {
|
|||||||
updateData.cchUrl = cchUrl;
|
updateData.cchUrl = cchUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果提供了 API Key,更新它
|
// 如果提供了 API Key,加密后存储
|
||||||
if (cchApiKey !== undefined) {
|
if (cchApiKey !== undefined) {
|
||||||
if (cchApiKey === '') {
|
if (cchApiKey === '') {
|
||||||
// 清除 API Key
|
// 清除 API Key
|
||||||
updateData.cchApiKey = null;
|
updateData.cchApiKey = null;
|
||||||
updateData.cchApiKeyConfigured = false;
|
updateData.cchApiKeyConfigured = false;
|
||||||
} else {
|
} else {
|
||||||
updateData.cchApiKey = cchApiKey;
|
// 加密存储 API Key
|
||||||
|
updateData.cchApiKey = encryptApiKey(cchApiKey);
|
||||||
updateData.cchApiKeyConfigured = true;
|
updateData.cchApiKeyConfigured = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,15 +172,23 @@ export async function PUT(request: Request) {
|
|||||||
updateData.saveChatHistory = saveChatHistory;
|
updateData.saveChatHistory = saveChatHistory;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否存在设置记录
|
// 3. 检查是否存在该用户的设置记录
|
||||||
const existing = await db.query.userSettings.findFirst({
|
const existing = await db.query.userSettings.findFirst({
|
||||||
where: eq(userSettings.id, 1),
|
where: eq(userSettings.userId, user.userId),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
// 创建新的设置记录
|
// 创建新的设置记录
|
||||||
await db.insert(userSettings).values({
|
await db.insert(userSettings).values({
|
||||||
id: 1,
|
userId: user.userId,
|
||||||
|
cchUrl: DEFAULT_SETTINGS.cchUrl,
|
||||||
|
defaultModel: DEFAULT_SETTINGS.defaultModel,
|
||||||
|
defaultTools: DEFAULT_SETTINGS.defaultTools,
|
||||||
|
theme: DEFAULT_SETTINGS.theme,
|
||||||
|
language: DEFAULT_SETTINGS.language,
|
||||||
|
fontSize: DEFAULT_SETTINGS.fontSize,
|
||||||
|
enableThinking: DEFAULT_SETTINGS.enableThinking,
|
||||||
|
saveChatHistory: DEFAULT_SETTINGS.saveChatHistory,
|
||||||
...updateData,
|
...updateData,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -142,27 +196,15 @@ export async function PUT(request: Request) {
|
|||||||
await db
|
await db
|
||||||
.update(userSettings)
|
.update(userSettings)
|
||||||
.set(updateData)
|
.set(updateData)
|
||||||
.where(eq(userSettings.id, 1));
|
.where(eq(userSettings.userId, user.userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回更新后的设置(不包含 API Key)
|
// 4. 返回更新后的设置(不包含 API Key)
|
||||||
const updatedSettings = await db.query.userSettings.findFirst({
|
const updatedSettings = await db.query.userSettings.findFirst({
|
||||||
where: eq(userSettings.id, 1),
|
where: eq(userSettings.userId, user.userId),
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json(formatSettingsResponse(updatedSettings ?? null));
|
||||||
cchUrl: updatedSettings?.cchUrl,
|
|
||||||
cchApiKeyConfigured: updatedSettings?.cchApiKeyConfigured,
|
|
||||||
defaultModel: updatedSettings?.defaultModel,
|
|
||||||
defaultTools: updatedSettings?.defaultTools,
|
|
||||||
systemPrompt: updatedSettings?.systemPrompt || '',
|
|
||||||
temperature: updatedSettings?.temperature || '0.7',
|
|
||||||
theme: updatedSettings?.theme,
|
|
||||||
language: updatedSettings?.language,
|
|
||||||
fontSize: updatedSettings?.fontSize || 15,
|
|
||||||
enableThinking: updatedSettings?.enableThinking,
|
|
||||||
saveChatHistory: updatedSettings?.saveChatHistory,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update settings:', error);
|
console.error('Failed to update settings:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect, use } from 'react';
|
import { useState, useRef, useEffect, use } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Share2, MoreHorizontal, Loader2, Square, Clock, ChevronDown, Pencil, Trash2, Check, X } from 'lucide-react';
|
import Link from 'next/link';
|
||||||
|
import { Share2, MoreHorizontal, Loader2, Square, Clock, ChevronDown, Pencil, Trash2, Check, X, AlertTriangle, Settings } from 'lucide-react';
|
||||||
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
||||||
import { ChatInput } from '@/components/features/ChatInput';
|
import { ChatInput } from '@/components/features/ChatInput';
|
||||||
import { MessageBubble } from '@/components/features/MessageBubble';
|
import { MessageBubble } from '@/components/features/MessageBubble';
|
||||||
@ -448,6 +449,30 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
{/* 消息列表区域 */}
|
{/* 消息列表区域 */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="max-w-[900px] mx-auto px-4 py-6">
|
<div className="max-w-[900px] mx-auto px-4 py-6">
|
||||||
|
{/* API Key 未配置提示 */}
|
||||||
|
{!settings?.cchApiKeyConfigured && (
|
||||||
|
<div className="mb-6 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-amber-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200 mb-1">
|
||||||
|
请先配置 API Key
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-amber-700 dark:text-amber-300 mb-3">
|
||||||
|
您需要配置自己的 API Key 才能使用 AI 对话功能。请前往设置页面完成配置。
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/settings"
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white text-sm font-medium rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Settings size={16} />
|
||||||
|
前往设置
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
<h2 className="text-xl font-medium text-[var(--color-text-primary)] mb-2">
|
<h2 className="text-xl font-medium text-[var(--color-text-primary)] mb-2">
|
||||||
|
|||||||
@ -50,7 +50,6 @@ export default function SettingsPage() {
|
|||||||
const { tools, loading: toolsLoading } = useTools();
|
const { tools, loading: toolsLoading } = useTools();
|
||||||
|
|
||||||
// CCH 配置状态
|
// CCH 配置状态
|
||||||
const [cchUrl, setCchUrl] = useState('');
|
|
||||||
const [cchApiKey, setCchApiKey] = useState('');
|
const [cchApiKey, setCchApiKey] = useState('');
|
||||||
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');
|
||||||
@ -69,7 +68,6 @@ export default function SettingsPage() {
|
|||||||
// 当设置加载完成后,更新本地状态
|
// 当设置加载完成后,更新本地状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
setCchUrl(settings.cchUrl || '');
|
|
||||||
setSystemPrompt(settings.systemPrompt || '');
|
setSystemPrompt(settings.systemPrompt || '');
|
||||||
setTemperature(settings.temperature || '0.7');
|
setTemperature(settings.temperature || '0.7');
|
||||||
}
|
}
|
||||||
@ -79,11 +77,9 @@ export default function SettingsPage() {
|
|||||||
const handleSaveCchConfig = async () => {
|
const handleSaveCchConfig = async () => {
|
||||||
setSaveStatus('saving');
|
setSaveStatus('saving');
|
||||||
try {
|
try {
|
||||||
const updates: Record<string, string> = { cchUrl };
|
|
||||||
if (cchApiKey) {
|
if (cchApiKey) {
|
||||||
updates.cchApiKey = cchApiKey;
|
await updateSettings({ cchApiKey });
|
||||||
}
|
}
|
||||||
await updateSettings(updates);
|
|
||||||
setSaveStatus('saved');
|
setSaveStatus('saved');
|
||||||
setCchApiKey(''); // 清除输入的 API Key
|
setCchApiKey(''); // 清除输入的 API Key
|
||||||
setTimeout(() => setSaveStatus('idle'), 2000);
|
setTimeout(() => setSaveStatus('idle'), 2000);
|
||||||
@ -317,19 +313,6 @@ export default function SettingsPage() {
|
|||||||
title="CCH 服务配置"
|
title="CCH 服务配置"
|
||||||
description="配置 Claude Code Hub 服务连接"
|
description="配置 Claude Code Hub 服务连接"
|
||||||
>
|
>
|
||||||
<SettingsItem
|
|
||||||
label="CCH 服务地址"
|
|
||||||
description="Claude Code Hub 服务的 URL"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="settings-input w-80"
|
|
||||||
value={cchUrl}
|
|
||||||
onChange={(e) => setCchUrl(e.target.value)}
|
|
||||||
placeholder="http://localhost:13500"
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
label="API Key"
|
label="API Key"
|
||||||
description={
|
description={
|
||||||
|
|||||||
88
src/lib/crypto.ts
Normal file
88
src/lib/crypto.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
// 加密密钥(32字节 = 256位,用于 AES-256)
|
||||||
|
// 生产环境应使用环境变量
|
||||||
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'lioncode-encryption-key-2024-sec';
|
||||||
|
|
||||||
|
// 确保密钥长度为32字节
|
||||||
|
const getKey = (): Buffer => {
|
||||||
|
const key = ENCRYPTION_KEY;
|
||||||
|
// 使用 SHA-256 哈希确保密钥长度为32字节
|
||||||
|
return crypto.createHash('sha256').update(key).digest();
|
||||||
|
};
|
||||||
|
|
||||||
|
// IV 长度(AES-256-GCM 推荐使用12字节)
|
||||||
|
const IV_LENGTH = 12;
|
||||||
|
|
||||||
|
// Auth Tag 长度
|
||||||
|
const AUTH_TAG_LENGTH = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密 API Key
|
||||||
|
* 使用 AES-256-GCM 加密算法
|
||||||
|
* @param plainText 明文 API Key
|
||||||
|
* @returns 加密后的字符串(格式:iv:authTag:encryptedData,Base64编码)
|
||||||
|
*/
|
||||||
|
export function encryptApiKey(plainText: string): string {
|
||||||
|
if (!plainText) return '';
|
||||||
|
|
||||||
|
const key = getKey();
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(plainText, 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
|
||||||
|
const authTag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// 组合 IV + AuthTag + 加密数据,用冒号分隔
|
||||||
|
return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密 API Key
|
||||||
|
* @param encryptedText 加密后的字符串
|
||||||
|
* @returns 解密后的明文 API Key
|
||||||
|
*/
|
||||||
|
export function decryptApiKey(encryptedText: string): string {
|
||||||
|
if (!encryptedText) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parts = encryptedText.split(':');
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
// 兼容旧的明文存储(迁移期间)
|
||||||
|
console.warn('检测到未加密的 API Key,返回原值');
|
||||||
|
return encryptedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ivBase64, authTagBase64, encrypted] = parts;
|
||||||
|
|
||||||
|
const key = getKey();
|
||||||
|
const iv = Buffer.from(ivBase64, 'base64');
|
||||||
|
const authTag = Buffer.from(authTagBase64, 'base64');
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
||||||
|
decipher.setAuthTag(authTag);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Key 解密失败:', error);
|
||||||
|
// 如果解密失败,可能是旧的明文数据,返回原值
|
||||||
|
return encryptedText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查字符串是否已加密
|
||||||
|
* @param text 待检查的字符串
|
||||||
|
* @returns 是否已加密
|
||||||
|
*/
|
||||||
|
export function isEncrypted(text: string): boolean {
|
||||||
|
if (!text) return false;
|
||||||
|
const parts = text.split(':');
|
||||||
|
return parts.length === 3;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user