refactor: repartition server-side and client-side code

This commit is contained in:
LIlGG
2025-10-11 18:26:07 +08:00
parent 7acc4949fb
commit e9b573a276
309 changed files with 631 additions and 962 deletions

427
app/.server/service/auth.ts Normal file
View File

@@ -0,0 +1,427 @@
import type { IdTokenClaims } from '@logto/node';
import { type LogtoContext, makeLogtoRemix } from '@logto/remix';
import type { LoaderFunctionArgs } from '@remix-run/node';
import { createCookieSessionStorage, redirect } from '@remix-run/node';
import type { LogtoUser } from '~/types/logto';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('auth.server');
/**
* 认证相关类型定义
*/
interface LogtoConfig {
endpoint: string;
appId: string;
appSecret: string;
baseUrl: string;
scopes?: string[];
}
interface MockUser extends Pick<LogtoContext, 'isAuthenticated' | 'userInfo' | 'claims'> {}
/**
* 虚拟用户接口,与 MockUser 类似但代表真实存在于 logto 的用户
*/
interface VirtualUser extends Pick<LogtoContext, 'isAuthenticated' | 'userInfo' | 'claims'> {
isVirtual: true;
}
// Logto路由配置类型
interface LogtoRoutes {
'sign-in': { path: string; redirectBackTo: string };
'sign-in-callback': { path: string; redirectBackTo: string };
'sign-out': { path: string; redirectBackTo: string };
'sign-up': { path: string; redirectBackTo: string };
}
/**
* 公共 Cookie 配置基础
*/
const baseCookieOptions = {
httpOnly: true,
path: '/',
sameSite: 'lax' as const,
secrets: [process.env.LOGTO_COOKIE_SECRET || 's3cr3t'],
};
/**
* 创建认证 session 存储
*/
const sessionStorage = createCookieSessionStorage({
cookie: {
...baseCookieOptions,
name: 'logto_session',
maxAge: 60 * 60 * 24 * 30, // 30 天过期
},
});
/**
* 创建虚拟用户 session 存储
*/
const virtualUserStorage = createCookieSessionStorage({
cookie: {
...baseCookieOptions,
name: 'virtual_user',
maxAge: 60 * 60 * 24 * 30,
},
});
/**
* 创建认证错误信息 session 存储
*/
const errorSessionStorage = createCookieSessionStorage({
cookie: {
...baseCookieOptions,
name: 'auth_error',
maxAge: 60, // 1分钟后过期错误信息不需要长期保存
},
});
/**
* 创建 Logto 配置
*/
const config: LogtoConfig = {
endpoint: process.env.LOGTO_ENDPOINT || '',
appId: process.env.LOGTO_APP_ID || '',
appSecret: process.env.LOGTO_APP_SECRET || '',
baseUrl: process.env.LOGTO_BASE_URL || 'http://localhost:5173',
scopes: ['email', 'profile'],
};
// 创建原始 Logto 实例(私有,不直接导出)
const originalLogto = makeLogtoRemix(config, { sessionStorage });
export function shouldEnforceAuth(): boolean {
return process.env.LOGTO_ENABLE === 'true';
}
function getMockDevUser(): MockUser {
return {
isAuthenticated: true,
userInfo: {
iss: 'https://mock.issuer.com',
sub: 'mock-user-id',
aud: 'mock-audience',
exp: Math.floor(Date.now() / 1000) + 3600,
iat: Math.floor(Date.now() / 1000),
name: 'Mock User',
username: 'user',
email: 'mock@example.com',
},
};
}
/**
* 设置认证错误信息到会话中
*/
export async function setAuthError(errorMessage: string): Promise<string> {
const session = await errorSessionStorage.getSession();
session.set('authError', errorMessage);
return errorSessionStorage.commitSession(session);
}
/**
* 获取并清除认证错误信息
*/
export async function getAuthError(
request: Request,
): Promise<{ errorMessage?: string; headers: { 'Set-Cookie': string } }> {
const session = await errorSessionStorage.getSession(request.headers.get('Cookie'));
const errorMessage = session.get('authError') as string | undefined;
// 清除错误信息
return {
errorMessage,
headers: {
'Set-Cookie': await errorSessionStorage.destroySession(session),
},
};
}
/**
* 增强版的 Logto 对象,添加错误处理和开发环境跳过
*/
export const logto = {
...originalLogto,
/**
* 增强版的 handleAuthRoutes 方法
*/
handleAuthRoutes: (routes: LogtoRoutes) => {
const originalHandler = originalLogto.handleAuthRoutes(routes);
return async (args: LoaderFunctionArgs) => {
try {
// 特殊处理退出登录路由
const { request } = args;
const url = new URL(request.url);
const path = url.pathname;
// 如果是退出登录路由,检查是否是虚拟用户
if (path === routes['sign-out'].path) {
const virtualUser = await getVirtualUser(request);
if (virtualUser?.isVirtual) {
logger.info('[Auth] 虚拟用户退出登录');
const clearCookie = await clearVirtualUser();
return redirect(routes['sign-out'].redirectBackTo, {
headers: {
'Set-Cookie': clearCookie,
},
});
}
}
return await originalHandler(args);
} catch (error) {
logger.error('[Auth] 认证服务错误:', error);
return handleAuthError(error, args);
}
};
},
getContext: originalLogto.getContext,
};
/**
* 处理认证过程中的错误
*/
async function handleAuthError(error: unknown, args: LoaderFunctionArgs) {
// 判断错误类型
let errorMessage = '认证服务暂时不可用,请稍后再试';
// 处理网络错误(认证服务器不可用)
if (
error instanceof Error &&
(error.message.includes('fetch failed') ||
error.message.includes('network') ||
error.message.includes('ECONNREFUSED') ||
error.message.includes('timeout'))
) {
errorMessage = '无法连接到认证服务器,请检查网络连接';
}
// 其他类型的错误
else if (error instanceof Error) {
errorMessage = '登录服务出现异常,请稍后再试';
}
// 确定重定向URL
const redirectUrl = determineRedirectUrl(args.request);
// 生成带有错误信息的Cookie
const cookie = await setAuthError(errorMessage);
// 重定向回原始页面同时携带错误会话Cookie
return redirect(redirectUrl, {
headers: {
'Set-Cookie': cookie,
},
});
}
/**
* 根据请求确定最合适的重定向URL
*/
function determineRedirectUrl(request: Request): string {
// 默认重定向到首页
const defaultUrl = '/';
try {
const url = new URL(request.url);
const redirectTo = url.searchParams.get('redirectTo');
if (redirectTo && redirectTo.startsWith('/') && !redirectTo.startsWith('//')) {
// 确保只重定向到应用内部URL
return redirectTo;
}
// 如果有referer头也可以考虑使用它
const referer = request.headers.get('referer');
if (referer) {
try {
const refererUrl = new URL(referer);
// 确保是同一域名下的URL
if (refererUrl.hostname === url.hostname) {
return refererUrl.pathname + refererUrl.search;
}
} catch {
// 忽略无效的referer
}
}
} catch {
// 如果URL解析失败使用默认的根路径
}
return defaultUrl;
}
/**
* 设置虚拟用户信息到 cookie
* @param userInfo 用户信息
* @returns cookie 字符串,可用于 HTTP 响应头
*/
export async function setVirtualUser(userInfo: LogtoUser): Promise<string> {
const session = await virtualUserStorage.getSession();
const virtualUserInfo = {
id: userInfo.id,
iss: process.env.LOGTO_ENDPOINT || 'https://auth.upage.io',
sub: userInfo.id,
aud: process.env.LOGTO_APP_ID || 'virtual-app',
// 30天后过期
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30,
iat: Math.floor(Date.now() / 1000),
name: userInfo.name || null,
email: userInfo.primaryEmail || null,
phone_number: userInfo.primaryPhone || null,
username: userInfo.username || null,
picture: userInfo.avatar || null,
} as IdTokenClaims;
session.set('userInfo', virtualUserInfo);
session.set('isAuthenticated', true);
return virtualUserStorage.commitSession(session);
}
/**
* 获取虚拟用户信息
* @param request 请求对象
* @returns 虚拟用户信息,如果不存在则返回 null
*/
export async function getVirtualUser(request: Request): Promise<VirtualUser | null> {
const cookieHeader = request.headers.get('Cookie');
const session = await virtualUserStorage.getSession(cookieHeader);
const isAuthenticated = session.get('isAuthenticated');
const userInfo = session.get('userInfo') as IdTokenClaims;
if (!isAuthenticated || !userInfo) {
return null;
}
// 验证用户信息是否有效
const now = Math.floor(Date.now() / 1000);
if (userInfo.exp && userInfo.exp < now) {
// 用户信息已过期,清除 session
await clearVirtualUser();
return null;
}
// 获取上次验证时间
const lastVerified = session.get('lastVerified') || 0;
const verifyInterval = 60 * 60 * 1000;
// 如果距离上次验证时间不足验证间隔,则跳过 Logto 验证
if (now * 1000 - lastVerified < verifyInterval) {
return {
isAuthenticated: true,
userInfo,
isVirtual: true,
};
}
try {
// 更新验证时间
session.set('lastVerified', Date.now());
await virtualUserStorage.commitSession(session);
return {
isAuthenticated: true,
userInfo,
isVirtual: true,
};
} catch (error) {
logger.error('[Auth] 验证虚拟用户失败:', error);
// 如果验证过程中出现错误,仍然返回用户信息
return {
isAuthenticated: true,
userInfo,
isVirtual: true,
};
}
}
/**
* 清除虚拟用户信息
* @returns cookie 字符串,用于清除虚拟用户 cookie
*/
export async function clearVirtualUser(): Promise<string> {
const session = await virtualUserStorage.getSession();
return virtualUserStorage.destroySession(session);
}
/**
* 检查用户是否已认证,如果未认证,则重定向到登录页面
*/
export async function requireUser(request: Request) {
const context = await getUser(request);
if (!context.isAuthenticated) {
return redirect('/api/auth/sign-in');
}
return context;
}
/**
* 获取当前用户信息
* 按优先级依次检查:
* 1. 开发环境模拟用户
* 2. 虚拟用户
* 3. Logto 认证用户
*/
export async function getUser(request: Request) {
// 首先检查是否为开发环境
if (!shouldEnforceAuth()) {
return getMockDevUser();
}
// 检查是否存在虚拟用户
const virtualUser = await getVirtualUser(request);
if (virtualUser) {
return virtualUser;
}
// 继续原有的 logto 认证流程
return await logto.getContext({
fetchUserInfo: true,
getAccessToken: true,
})(request);
}
/**
* 通用权限验证中间件
* 用于API和页面路由的权限验证
*
* 返回json错误或重定向到登录页面
*/
export async function requireAuth(request: Request, options: { isApi?: boolean; redirectTo?: string } = {}) {
const { isApi = false, redirectTo = '/api/auth/sign-in' } = options;
const context = await getUser(request);
if (!context.isAuthenticated) {
if (isApi) {
// API路由返回JSON错误
return new Response(
JSON.stringify({
error: 'Unauthorized',
message: '请先登录',
code: 401,
}),
{
status: 401,
headers: {
'Content-Type': 'application/json',
},
},
);
}
// 页面路由重定向到登录页面
return redirect(redirectTo);
}
return context;
}

View File

@@ -0,0 +1,303 @@
import { prisma } from '~/.server/service/prisma';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('chatUsage.server');
/**
* 聊天使用量状态
*/
export enum ChatUsageStatus {
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
PENDING = 'PENDING',
ABORTED = 'ABORTED',
}
/**
* 聊天使用量记录参数接口
*/
export interface ChatUsageParams {
userId: string;
chatId: string;
messageId: string;
status: ChatUsageStatus;
inputTokens?: number;
outputTokens?: number;
cachedTokens?: number;
reasoningTokens?: number;
totalTokens?: number;
modelName?: string;
prompt?: string;
metadata?: Record<string, any>;
}
/**
* 日期过滤器类型
*/
interface DateFilter {
calledAt?: {
gte?: Date;
lte?: Date;
};
}
/**
* 记录聊天使用量
* @param params 使用量参数
* @returns 创建的记录
*/
export async function recordUsage(params: ChatUsageParams) {
const {
userId,
chatId,
messageId,
inputTokens = 0,
outputTokens = 0,
cachedTokens = 0,
reasoningTokens = 0,
status,
prompt,
metadata,
modelName,
} = params;
// 计算总token量
const totalTokens = inputTokens + outputTokens;
try {
// 创建记录
const record = await prisma.chatUsage.create({
data: {
userId,
messageId,
chatId,
inputTokens,
outputTokens,
cachedTokens,
reasoningTokens,
totalTokens,
status,
prompt,
metadata,
modelName,
},
});
if (status === ChatUsageStatus.PENDING) {
logger.info(`[ChatUsage] 初始化用户 ${userId}${modelName} 模型聊天使用量`);
} else {
logger.info(
`[ChatUsage] 记录了用户 ${userId}${modelName} 模型聊天使用量: ${totalTokens} tokens状态: ${status}`,
);
}
return record;
} catch (error) {
logger.error('[ChatUsage] 记录聊天使用量失败:', error);
throw error;
}
}
/**
* 获取按天统计的使用数据
* @param userId 用户ID
* @param days 天数默认为30天
* @returns 每日使用统计数据
*/
export async function getDailyUsageStats(userId: string, days = 30) {
try {
// 计算结束日期为今天(当天结束)
const endDate = new Date();
endDate.setHours(23, 59, 59, 999);
// 计算开始日期为 endDate 前推 days-1 天的开始时间
const startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - (days - 1));
startDate.setHours(0, 0, 0, 0);
const records = await prisma.chatUsage.findMany({
where: {
userId,
calledAt: {
gte: startDate,
lte: endDate,
},
},
select: {
calledAt: true,
totalTokens: true,
},
});
const dateMap: Record<string, { count: number; totalTokens: number }> = {};
// 创建从startDate到endDate的每一天映射
const currentDate = new Date(startDate);
while (currentDate <= endDate) {
const dateStr = currentDate.toISOString().split('T')[0];
dateMap[dateStr] = { count: 0, totalTokens: 0 };
// 增加一天
currentDate.setDate(currentDate.getDate() + 1);
}
// 统计数据
records.forEach((record) => {
const dateStr = record.calledAt.toISOString().split('T')[0];
if (dateMap[dateStr]) {
dateMap[dateStr].count += 1;
dateMap[dateStr].totalTokens += record.totalTokens;
}
});
return Object.entries(dateMap).map(([date, stats]) => ({
date,
count: stats.count,
totalTokens: stats.totalTokens,
}));
} catch (error) {
logger.error('[ChatUsage] 获取每日使用统计失败:', error);
throw error;
}
}
/**
* 获取用户的聊天使用统计
* @param userId 用户ID
* @param startDate 开始日期
* @param endDate 结束日期
* @returns 使用统计数据
*/
export async function getUserUsageStats(userId: string, startDate?: Date, endDate?: Date) {
const dateFilter: DateFilter = {};
if (startDate || endDate) {
dateFilter.calledAt = {};
if (startDate) {
dateFilter.calledAt.gte = startDate;
}
if (endDate) {
dateFilter.calledAt.lte = endDate;
}
}
try {
// 获取总体使用量
const stats = await prisma.chatUsage.aggregate({
where: {
userId,
...dateFilter,
},
_sum: {
inputTokens: true,
outputTokens: true,
cachedTokens: true,
reasoningTokens: true,
totalTokens: true,
},
_count: true,
});
// 按状态分组
const statusStats = await prisma.chatUsage.groupBy({
by: ['status'],
where: {
userId,
...dateFilter,
},
_count: true,
_sum: {
totalTokens: true,
},
});
// 按聊天分组
const chatStats = await prisma.chatUsage.groupBy({
by: ['chatId'],
where: {
userId,
...dateFilter,
},
_sum: {
totalTokens: true,
},
_count: true,
});
// 获取按天统计的数据
const dailyStats = await getDailyUsageStats(userId, 7);
return {
total: stats,
byStatus: statusStats,
byChat: chatStats,
byDate: dailyStats,
};
} catch (error) {
logger.error('[ChatUsage] 获取用户使用统计失败:', error);
throw error;
}
}
/**
* 更新使用记录的状态
* @param id 记录ID
* @param status 新状态
* @param additionalData 额外要更新的数据
* @returns 更新后的记录
*/
export async function updateUsageStatus(
id: string,
status: ChatUsageStatus,
additionalData?: Partial<ChatUsageParams>,
) {
try {
const updatedRecord = await prisma.chatUsage.update({
where: { id },
data: {
status,
...additionalData,
},
});
return updatedRecord;
} catch (error) {
logger.error('[ChatUsage] 更新使用记录状态失败:', error);
throw error;
}
}
export async function updateUsageError(id: string, error: string, additionalData?: Partial<ChatUsageParams>) {
return updateUsageStatus(id, ChatUsageStatus.FAILED, {
...additionalData,
metadata: {
error: error || '未知错误',
} as unknown as Record<string, any>,
});
}
/**
* 获取最近的使用记录
* @param userId 用户ID
* @param limit 限制返回记录数量
* @returns 使用记录列表
*/
export async function getRecentUsage(userId: string, limit = 10) {
try {
const records = await prisma.chatUsage.findMany({
where: {
userId,
},
orderBy: {
calledAt: 'desc',
},
take: limit,
});
return records;
} catch (error) {
logger.error('[ChatUsage] 获取最近使用记录失败:', error);
throw error;
}
}

329
app/.server/service/chat.ts Normal file
View File

@@ -0,0 +1,329 @@
import { prisma } from '~/.server/service/prisma';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('chat.server');
/**
* 聊天创建参数接口
*/
export interface ChatCreateParams {
userId: string;
id?: string;
// 聊天URL ID
urlId?: string;
// 聊天描述
description?: string;
// 包含额外信息的元数据
metadata?: Record<string, any>;
}
/**
* 聊天更新参数接口
*/
export interface ChatUpdateParams {
description?: string;
metadata?: Record<string, any>;
}
/**
* 聊天创建或更新参数接口
*/
export interface ChatUpsertParams {
id: string;
userId: string;
urlId?: string;
description?: string;
metadata?: Record<string, any>;
}
/**
* 创建新的聊天
* @param params 聊天创建参数
* @returns 创建的聊天记录
*/
export async function createChat(params: ChatCreateParams) {
const { userId, id, urlId, description, metadata } = params;
try {
const chat = await prisma.chat.create({
data: {
...(id ? { id } : {}),
userId,
urlId,
description,
metadata,
},
});
logger.info(`[Chat] 创建了用户 ${userId} 的聊天: ${chat.id}`);
return chat;
} catch (error) {
logger.error('[Chat] 创建聊天失败:', error);
throw error;
}
}
/**
* 根据ID获取聊天
* @param id 聊天ID
* @returns 聊天记录
*/
export async function getChatById(id: string) {
try {
const chat = await prisma.chat.findUnique({
where: { id },
include: {
messages: {
where: {
isDiscarded: false,
},
orderBy: {
createdAt: 'asc',
},
include: {
sections: true,
page: true,
},
},
},
});
return chat;
} catch (error) {
logger.error(`[Chat] 获取聊天 ${id} 失败:`, error);
throw error;
}
}
/**
* 根据 URL ID 获取聊天
* @param urlId 聊天的 URL ID
* @returns 聊天记录
*/
export async function getChatByUrlId(urlId: string) {
try {
const chat = await prisma.chat.findUnique({
where: { urlId },
include: {
messages: {
where: {
isDiscarded: false,
},
orderBy: {
createdAt: 'asc',
},
},
},
});
return chat;
} catch (error) {
logger.error(`[Chat] 获取聊天 URL ${urlId} 失败:`, error);
throw error;
}
}
/**
* 获取用户的所有聊天
* @param userId 用户ID
* @param limit 限制返回记录数量
* @param offset 偏移量
* @returns 聊天记录列表
*/
export async function getUserChats(userId: string, limit = 20, offset = 0) {
try {
const chats = await prisma.chat.findMany({
where: { userId },
include: {
messages: {
where: {
isDiscarded: false,
},
take: 1,
orderBy: {
createdAt: 'asc',
},
},
},
orderBy: {
updatedAt: 'desc',
},
skip: offset,
take: limit,
});
const total = await prisma.chat.count({
where: { userId },
});
return {
chats,
total,
};
} catch (error) {
logger.error(`[Chat] 获取用户 ${userId} 的聊天列表失败:`, error);
throw error;
}
}
/**
* 更新聊天信息
* @param id 聊天ID
* @param params 更新参数
* @returns 更新后的聊天记录
*/
export async function updateChat(id: string, params: ChatUpdateParams) {
try {
const updatedChat = await prisma.chat.update({
where: { id },
data: {
...params,
version: {
increment: 1,
},
},
});
logger.info(`[Chat] 更新了聊天 ${id}`);
return updatedChat;
} catch (error) {
logger.error(`[Chat] 更新聊天 ${id} 失败:`, error);
throw error;
}
}
/**
* 删除聊天
* @param id 聊天ID
* @returns 删除结果
*
* 注意:由于在 Prisma Schema 中配置了级联删除关系:
*
* 1. 删除 Chat 会自动级联删除所有关联的 Message 记录
* 2. 删除 Message 会自动级联删除关联的 Section 记录
*/
export async function deleteChat(id: string) {
try {
const chatToDelete = await prisma.chat.findUnique({
where: { id },
include: {
_count: {
select: {
messages: true,
},
},
},
});
if (!chatToDelete) {
logger.info(`[Chat] 未找到ID为 ${id} 的聊天,无法删除`);
return false;
}
await prisma.chat.delete({
where: { id },
});
logger.info(`[Chat] 删除了聊天 ${id},级联删除了 ${chatToDelete._count.messages} 条关联消息及其项目数据`);
return true;
} catch (error) {
logger.error(`[Chat] 删除聊天 ${id} 失败:`, error);
throw error;
}
}
/**
* 获取或创建指定ID的聊天
* @param chatId 指定的聊天ID
* @param params 创建聊天所需的参数
* @returns 聊天记录
*/
export async function getOrCreateChat(chatId: string, params: Omit<ChatCreateParams, 'id'>) {
try {
// 尝试查找现有聊天
const existingChat = await getChatById(chatId);
if (existingChat) {
logger.info(`[Chat] 找到现有聊天: ${chatId}`);
return existingChat;
}
// 聊天不存在创建新聊天使用指定的ID
const newChat = await createChat({
...params,
id: chatId,
});
logger.info(`[Chat] 聊天不存在,创建新聊天: ${newChat.id}`);
return newChat;
} catch (error) {
logger.error(`[Chat] 获取或创建聊天失败:`, error);
throw error;
}
}
/**
* 根据 ID 获取当前用户的聊天
* @param id 聊天ID
* @returns 聊天记录
*/
export async function getUserChatById(id: string, userId: string) {
try {
const chat = await prisma.chat.findUnique({
where: { id, userId },
include: {
messages: {
where: {
isDiscarded: false,
},
orderBy: {
createdAt: 'asc',
},
include: {
sections: true,
page: true,
},
},
},
});
return chat;
} catch (error) {
logger.error(`[Chat] 获取聊天 ${id} 失败:`, error);
throw error;
}
}
/**
* 根据ID创建或更新聊天upsert操作
* @param params 聊天创建或更新参数
* @returns 创建或更新后的聊天记录
*/
export async function upsertChat(params: ChatUpsertParams) {
const { id, userId, urlId, description, metadata } = params;
try {
const chat = await prisma.chat.upsert({
where: { id },
update: {
version: {
increment: 1,
},
description,
metadata,
},
create: {
id,
userId,
urlId,
description,
metadata,
},
});
logger.info(`[Chat] 创建或更新了聊天 ${id}`);
return chat;
} catch (error) {
logger.error(`[Chat] 创建或更新聊天 ${id} 失败:`, error);
throw error;
}
}

View File

@@ -0,0 +1,235 @@
import { deleteUserSetting, deleteUserSettings, getUserSetting, setUserSetting } from '~/.server/service/user-settings';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('connectionSettings.server');
/**
* 1Panel 连接设置
*/
export const ONEPANEL_SETTINGS = {
CATEGORY: 'connectivity',
SERVER_URL_KEY: '1panel_server_url',
API_KEY_KEY: '1panel_api_key',
};
/**
* Netlify 连接设置
*/
export const NETLIFY_SETTINGS = {
CATEGORY: 'connectivity',
TOKEN_KEY: 'netlify_token',
};
/**
* Vercel 连接设置
*/
export const VERCEL_SETTINGS = {
CATEGORY: 'connectivity',
TOKEN_KEY: 'vercel_token',
};
/**
* 获取1Panel连接设置
* @param userId 用户ID
* @returns 包含serverUrl和apiKey的对象如果未设置则返回null
*/
export async function get1PanelConnectionSettings(
userId: string,
): Promise<{ serverUrl: string; apiKey: string } | null> {
try {
const serverUrlSetting = await getUserSetting(userId, ONEPANEL_SETTINGS.CATEGORY, ONEPANEL_SETTINGS.SERVER_URL_KEY);
const apiKeySetting = await getUserSetting(userId, ONEPANEL_SETTINGS.CATEGORY, ONEPANEL_SETTINGS.API_KEY_KEY);
if (!serverUrlSetting || !apiKeySetting) {
return null;
}
return {
serverUrl: serverUrlSetting.value,
apiKey: apiKeySetting.value,
};
} catch (error) {
logger.error(`[1Panel] 获取用户 ${userId} 的连接设置失败:`, error);
return null;
}
}
/**
* 保存1Panel连接设置
* @param userId 用户ID
* @param serverUrl 服务器URL
* @param apiKey API密钥
*/
export async function save1PanelConnectionSettings(userId: string, serverUrl: string, apiKey: string): Promise<void> {
try {
await setUserSetting({
userId,
category: ONEPANEL_SETTINGS.CATEGORY,
key: ONEPANEL_SETTINGS.SERVER_URL_KEY,
value: serverUrl,
});
await setUserSetting({
userId,
category: ONEPANEL_SETTINGS.CATEGORY,
key: ONEPANEL_SETTINGS.API_KEY_KEY,
value: apiKey,
isSecret: true,
});
logger.info(`[1Panel] 保存用户 ${userId} 的连接设置成功`);
} catch (error) {
logger.error(`[1Panel] 保存用户 ${userId} 的连接设置失败:`, error);
throw error;
}
}
/**
* 删除1Panel连接设置
* @param userId 用户ID
*/
export async function delete1PanelConnectionSettings(userId: string): Promise<void> {
try {
await deleteUserSetting(userId, ONEPANEL_SETTINGS.CATEGORY, ONEPANEL_SETTINGS.SERVER_URL_KEY);
await deleteUserSetting(userId, ONEPANEL_SETTINGS.CATEGORY, ONEPANEL_SETTINGS.API_KEY_KEY);
logger.info(`[1Panel] 删除用户 ${userId} 的连接设置成功`);
} catch (error) {
logger.error(`[1Panel] 删除用户 ${userId} 的连接设置失败:`, error);
throw error;
}
}
/**
* 获取Netlify连接设置
* @param userId 用户ID
* @returns 包含token的对象如果未设置则返回null
*/
export async function getNetlifyConnectionSettings(userId: string): Promise<{ token: string } | null> {
try {
const tokenSetting = await getUserSetting(userId, NETLIFY_SETTINGS.CATEGORY, NETLIFY_SETTINGS.TOKEN_KEY);
if (!tokenSetting) {
return null;
}
return {
token: tokenSetting.value,
};
} catch (error) {
logger.error(`[Netlify] 获取用户 ${userId} 的连接设置失败:`, error);
return null;
}
}
/**
* 保存Netlify连接设置
* @param userId 用户ID
* @param token 访问令牌
*/
export async function saveNetlifyConnectionSettings(userId: string, token: string): Promise<void> {
try {
await setUserSetting({
userId,
category: NETLIFY_SETTINGS.CATEGORY,
key: NETLIFY_SETTINGS.TOKEN_KEY,
value: token,
isSecret: true,
});
logger.info(`[Netlify] 保存用户 ${userId} 的连接设置成功`);
} catch (error) {
logger.error(`[Netlify] 保存用户 ${userId} 的连接设置失败:`, error);
throw error;
}
}
/**
* 删除Netlify连接设置
* @param userId 用户ID
*/
export async function deleteNetlifyConnectionSettings(userId: string): Promise<void> {
try {
await deleteUserSetting(userId, NETLIFY_SETTINGS.CATEGORY, NETLIFY_SETTINGS.TOKEN_KEY);
logger.info(`[Netlify] 删除用户 ${userId} 的连接设置成功`);
} catch (error) {
logger.error(`[Netlify] 删除用户 ${userId} 的连接设置失败:`, error);
throw error;
}
}
/**
* 获取Vercel连接设置
* @param userId 用户ID
* @returns 包含token的对象如果未设置则返回null
*/
export async function getVercelConnectionSettings(userId: string): Promise<{ token: string } | null> {
try {
const tokenSetting = await getUserSetting(userId, VERCEL_SETTINGS.CATEGORY, VERCEL_SETTINGS.TOKEN_KEY);
if (!tokenSetting) {
return null;
}
return {
token: tokenSetting.value,
};
} catch (error) {
logger.error(`[Vercel] 获取用户 ${userId} 的连接设置失败:`, error);
return null;
}
}
/**
* 保存Vercel连接设置
* @param userId 用户ID
* @param token 访问令牌
*/
export async function saveVercelConnectionSettings(userId: string, token: string): Promise<void> {
try {
await setUserSetting({
userId,
category: VERCEL_SETTINGS.CATEGORY,
key: VERCEL_SETTINGS.TOKEN_KEY,
value: token,
isSecret: true,
});
logger.info(`[Vercel] 保存用户 ${userId} 的连接设置成功`);
} catch (error) {
logger.error(`[Vercel] 保存用户 ${userId} 的连接设置失败:`, error);
throw error;
}
}
/**
* 删除Vercel连接设置
* @param userId 用户ID
*/
export async function deleteVercelConnectionSettings(userId: string): Promise<void> {
try {
await deleteUserSetting(userId, VERCEL_SETTINGS.CATEGORY, VERCEL_SETTINGS.TOKEN_KEY);
logger.info(`[Vercel] 删除用户 ${userId} 的连接设置成功`);
} catch (error) {
logger.error(`[Vercel] 删除用户 ${userId} 的连接设置失败:`, error);
throw error;
}
}
/**
* 删除所有连接设置
* @param userId 用户ID
*/
export async function deleteAllConnectionSettings(userId: string): Promise<void> {
try {
// 使用 deleteUserSettings 删除 'connectivity' 类别下的所有设置
await deleteUserSettings(userId, 'connectivity');
logger.info(`[连接设置] 删除用户 ${userId} 的所有连接设置成功`);
} catch (error) {
logger.error(`[连接设置] 删除用户 ${userId} 的所有连接设置失败:`, error);
throw error;
}
}

View File

@@ -0,0 +1,316 @@
import { prisma } from '~/.server/service/prisma';
import type { DeploymentPlatform } from '~/types/deployment';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('deployment.server');
/**
* 部署记录创建参数接口
*/
export interface DeploymentCreateParams {
userId: string;
chatId: string;
platform: DeploymentPlatform;
deploymentId: string;
url: string;
status: string;
metadata?: Record<string, any>;
}
/**
* 创建或更新部署记录
* 当 userId、chatId 和 platform 都匹配时,更新现有记录而不是创建新记录
*
* @param params 部署记录创建参数
* @returns 创建或更新的部署记录
*/
export async function createOrUpdateDeployment(params: DeploymentCreateParams) {
const { userId, chatId, platform, deploymentId, url, status, metadata } = params;
try {
const existingDeployment = await prisma.deployment.findFirst({
where: {
userId,
chatId,
platform,
},
});
let deployment;
if (existingDeployment) {
deployment = await prisma.deployment.update({
where: { id: existingDeployment.id },
data: {
deploymentId,
url,
status,
metadata,
},
});
logger.info(`[Deployment] 更新了用户 ${userId} 的部署记录: ${deployment.id}, 平台: ${platform}`);
} else {
deployment = await prisma.deployment.create({
data: {
userId,
chatId,
platform,
deploymentId,
url,
status,
metadata,
},
});
logger.info(`[Deployment] 创建了用户 ${userId} 的部署记录: ${deployment.id}, 平台: ${platform}`);
}
return deployment;
} catch (error) {
logger.error('[Deployment] 创建或更新部署记录失败:', error);
throw error;
}
}
/**
* 根据ID获取部署记录
* @param id 部署记录ID
* @returns 部署记录
*/
export async function getDeploymentById(id: string) {
try {
const deployment = await prisma.deployment.findUnique({
where: { id },
});
return deployment;
} catch (error) {
logger.error(`[Deployment] 获取部署记录 ${id} 失败:`, error);
throw error;
}
}
/**
* 根据ID删除部署记录
* @param id 部署记录ID
* @returns 删除的部署记录
*/
export async function deleteDeploymentById(id: string) {
try {
const deployment = await prisma.deployment.delete({
where: { id },
});
return deployment;
} catch (error) {
logger.error(`[Deployment] 删除部署记录 ${id} 失败:`, error);
throw error;
}
}
/**
* 获取用户的所有部署记录
* @param userId 用户ID
* @param limit 限制返回记录数量
* @param offset 偏移量
* @returns 部署记录列表
*/
export async function getUserDeployments(userId: string, limit = 20, offset = 0) {
try {
const deployments = await prisma.deployment.findMany({
where: { userId },
orderBy: {
createdAt: 'desc',
},
skip: offset,
take: limit,
});
const total = await prisma.deployment.count({
where: { userId },
});
return {
deployments,
total,
};
} catch (error) {
logger.error(`[Deployment] 获取用户 ${userId} 的部署记录列表失败:`, error);
throw error;
}
}
/**
* 获取特定聊天的所有部署记录
* @param chatId 聊天ID
* @returns 部署记录列表
*/
export async function getChatDeployments(chatId: string) {
try {
const deployments = await prisma.deployment.findMany({
where: { chatId },
orderBy: {
createdAt: 'desc',
},
});
return deployments;
} catch (error) {
logger.error(`[Deployment] 获取聊天 ${chatId} 的部署记录列表失败:`, error);
throw error;
}
}
/**
* 获取用户在特定平台的所有部署记录
* @param userId 用户ID
* @param platform 平台名称
* @returns 部署记录列表
*/
export async function getUserPlatformDeployments(userId: string, platform: DeploymentPlatform) {
try {
const deployments = await prisma.deployment.findMany({
where: {
userId,
platform,
},
orderBy: {
createdAt: 'desc',
},
});
return deployments;
} catch (error) {
logger.error(`[Deployment] 获取用户 ${userId} 在平台 ${platform} 的部署记录列表失败:`, error);
throw error;
}
}
/**
* 获取用户在特定平台和聊天的最新部署记录
* @param userId 用户ID
* @param chatId 聊天ID
* @param platform 平台名称
* @returns 最新的部署记录,如果不存在则返回 null
*/
export async function getLatestDeployment(userId: string, chatId: string, platform: DeploymentPlatform) {
try {
const deployment = await prisma.deployment.findFirst({
where: {
userId,
chatId,
platform,
},
orderBy: {
createdAt: 'desc',
},
});
return deployment;
} catch (error) {
logger.error(`[Deployment] 获取用户 ${userId} 在平台 ${platform} 的最新部署记录失败:`, error);
return null;
}
}
/**
* 更新部署记录状态
* @param id 部署记录ID
* @param status 新状态
* @param metadata 可选的元数据更新
* @returns 更新后的部署记录
*/
export async function updateDeploymentStatus(id: string, status: string, metadata?: Record<string, any>) {
try {
const updatedDeployment = await prisma.deployment.update({
where: { id },
data: {
status,
...(metadata ? { metadata } : {}),
},
});
logger.info(`[Deployment] 更新了部署记录 ${id} 的状态为 ${status}`);
return updatedDeployment;
} catch (error) {
logger.error(`[Deployment] 更新部署记录 ${id} 状态失败:`, error);
throw error;
}
}
/**
* 根据平台和平台特定的ID删除所有相关的部署记录
*
* @param platform 平台名称
* @param platformId 平台特定的ID
* @returns 删除的记录数量
*/
export async function deleteDeploymentsByPlatformAndId(platform: DeploymentPlatform, platformId: string | number) {
try {
// 将 platformId 转换为字符串,因为在数据库中 deploymentId 是字符串类型
const deploymentId = String(platformId);
const result = await prisma.deployment.deleteMany({
where: {
platform,
deploymentId,
},
});
logger.info(`[Deployment] 删除了平台 ${platform} 上 ID 为 ${platformId}${result.count} 条部署记录`);
return result.count;
} catch (error) {
logger.error(`[Deployment] 删除平台 ${platform} 上 ID 为 ${platformId} 的部署记录失败:`, error);
throw error;
}
}
/**
* 分页获取用户在特定平台的部署记录
* @param userId 用户ID
* @param platform 平台名称(可选)
* @param limit 每页记录数
* @param offset 偏移量
* @returns 部署记录列表和总数
*/
export async function getUserPlatformDeploymentsWithPagination(
userId: string,
platform?: DeploymentPlatform,
limit = 10,
offset = 0,
) {
try {
const where = {
userId,
...(platform ? { platform } : {}),
};
const deployments = await prisma.deployment.findMany({
where,
orderBy: {
createdAt: 'desc',
},
skip: offset,
take: limit,
include: {
chat: {
select: {
id: true,
description: true,
},
},
},
});
const total = await prisma.deployment.count({ where });
return {
deployments,
total,
};
} catch (error) {
logger.error(
`[Deployment] 分页获取用户 ${userId} ${platform ? `在平台 ${platform} ` : ''}的部署记录列表失败:`,
error,
);
throw error;
}
}

View File

@@ -0,0 +1,317 @@
import type { Message } from '@prisma/client';
import type { JsonArray } from '@prisma/client/runtime/library';
import type { TextUIPart, UIMessagePart } from 'ai';
import { prisma } from '~/.server/service/prisma';
import type { SummaryAnnotation, UPageDataParts, UPageUIMessage } from '~/types/message';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('message.server');
/**
* 消息创建参数接口
*/
export interface MessageCreateParams {
chatId: string;
userId: string;
role: string;
content: string;
revisionId?: string;
annotations?: any[];
version?: number;
}
/**
* 消息更新参数接口
*/
export interface MessageUpdateParams {
content?: string;
revisionId?: string;
annotations?: any[];
version?: number;
}
/**
* 消息创建或更新参数接口
*/
export interface MessageUpsertParams {
id: string;
chatId: string;
userId: string;
role: string;
content: string;
revisionId?: string;
annotations?: any[];
version?: number;
}
/**
* 根据ID创建或更新消息upsert操作
* @param params 消息创建或更新参数
* @returns 创建或更新后的消息记录
*/
export async function upsertMessage(params: MessageUpsertParams) {
const { id, chatId, userId, role, content, revisionId, annotations } = params;
try {
const message = await prisma.message.upsert({
where: { id },
update: {
content,
revisionId,
annotations,
},
create: {
id,
chatId,
userId,
role,
content,
revisionId,
annotations,
},
});
logger.info(`[Message] 创建或更新了消息 ${id}`);
return message;
} catch (error) {
logger.error(`[Message] 创建或更新消息 ${id} 失败:`, error);
throw error;
}
}
/**
* 更新消息为遗弃消息。
*
* 此方法将会更新同一 {@param chatId} 下 startMessageId不含与 endMessageId 之间(不含)的所有消息为遗弃消息。
*
* @param chatId 聊天ID
* @param startMessageId 开始消息ID
* @param endMessageId 结束消息ID
*/
export async function updateDiscardedMessage(chatId: string, startMessageId: string) {
try {
const startMessage = await prisma.message.findUnique({
where: { id: startMessageId },
select: { createdAt: true },
});
if (!startMessage) {
logger.error(`[Message] 找不到开始消息 ${startMessageId}`);
return false;
}
// 更新 startMessageId 之后的所有消息为遗弃消息
const result = await prisma.message.updateMany({
where: {
chatId,
createdAt: {
gt: startMessage?.createdAt,
},
},
data: {
isDiscarded: true,
},
});
logger.info(`[Message] 已将聊天 ${chatId}${startMessageId} 之后的 ${result.count} 条消息标记为遗弃`);
return true;
} catch (error) {
logger.error(`[Message] 更新遗弃消息失败:`, error);
throw error;
}
}
/**
* 获取历史聊天消息接口参数
*/
export interface GetHistoryChatMessagesParams {
chatId: string;
rewindTo?: string;
}
/**
* 获取从第一条消息到指定消息之间的所有历史消息
* @param params 包含 chatId 和可选的 rewindTo 参数
* @returns 消息记录列表
*/
export async function getHistoryChatMessages(params: GetHistoryChatMessagesParams): Promise<UPageUIMessage[]> {
const { chatId, rewindTo } = params;
try {
// 如果指定了 rewindTo则获取该消息的创建时间
if (rewindTo) {
const rewindToMessage = await prisma.message.findUnique({
where: { id: rewindTo },
select: { createdAt: true },
});
if (!rewindToMessage) {
logger.warn(`[Message] 获取历史消息: 找不到指定的 rewindTo 消息 ${rewindTo}`);
// 如果找不到指定消息,则返回所有消息
return await getAllChatMessages(chatId);
}
// 获取所有在 rewindTo 消息创建时间之前(包括该消息)的消息
const messages = await prisma.message.findMany({
where: {
chatId,
isDiscarded: false,
createdAt: {
lte: rewindToMessage.createdAt,
},
},
orderBy: {
createdAt: 'asc',
},
});
logger.info(`[Message] 获取了聊天 ${chatId} 中直到消息 ${rewindTo}${messages.length} 条历史消息`);
return messages.map(convertToUIMessage);
} else {
// 如果没有指定 rewindTo则获取所有消息
return await getAllChatMessages(chatId);
}
} catch (error) {
logger.error(`[Message] 获取聊天 ${chatId} 的历史消息失败:`, error);
throw error;
}
}
function convertToUIMessage(message: Message): UPageUIMessage {
if (message.version === 2) {
return {
id: message.id,
role: message.role as 'user' | 'assistant',
parts: message.parts as any[],
metadata: message.metadata as any,
};
}
const parts: UIMessagePart<UPageDataParts, never>[] = [];
if (message.role === 'user') {
const content = JSON.parse(message.content) as TextUIPart;
parts.push({
type: 'text',
text: content.text,
});
} else {
parts.push({
type: 'text',
text: message.content,
});
}
if (message.annotations) {
const messageAnnotations = message.annotations as JsonArray;
messageAnnotations.forEach((annotation) => {
const { type } = annotation as { type: string };
if (type === 'chatSummary') {
parts.push({
type: 'data-summary',
data: annotation as unknown as SummaryAnnotation,
});
}
});
}
return {
id: message.id,
role: message.role as 'user' | 'assistant',
parts,
metadata: message.metadata as any,
};
}
/**
* 获取聊天的所有消息(内部辅助方法)
* @param chatId 聊天ID
* @returns 消息记录列表
*/
async function getAllChatMessages(chatId: string): Promise<UPageUIMessage[]> {
const messages = await prisma.message.findMany({
where: {
chatId,
isDiscarded: false,
},
orderBy: {
createdAt: 'asc',
},
});
logger.info(`[Message] 获取了聊天 ${chatId} 的所有 ${messages.length} 条历史消息`);
return messages.map(convertToUIMessage);
}
/**
* 保存聊天消息列表到数据库
* @param chatId 聊天ID
* @param messages 消息列表UPageUIMessage[]
* @returns 保存结果
*/
export async function saveChatMessages(chatId: string, messages: UPageUIMessage[]): Promise<number> {
if (!messages || messages.length === 0) {
logger.warn('[Message] 保存聊天消息: 没有提供消息数据');
return 0;
}
try {
// 获取聊天的用户ID
const chat = await prisma.chat.findUnique({
where: { id: chatId },
select: { userId: true },
});
if (!chat) {
logger.error(`[Message] 保存聊天消息: 找不到聊天 ${chatId}`);
throw new Error(`找不到聊天 ${chatId}`);
}
const userId = chat.userId;
let savedCount = 0;
// 逐条保存消息
for (const message of messages) {
// 跳过没有ID的消息
if (!message.id) {
logger.warn('[Message] 保存聊天消息: 跳过没有ID的消息');
continue;
}
// 提取消息的文本内容
const textPart = message.parts.find((part) => part.type === 'text');
const content = textPart?.text || '';
// 创建或更新消息
const updateData: any = {
content,
parts: message.parts,
metadata: message.metadata,
version: 2,
};
const createData: any = {
id: message.id,
chatId,
userId,
role: message.role,
content,
parts: message.parts,
metadata: message.metadata,
version: 2,
};
await prisma.message.upsert({
where: { id: message.id },
update: updateData,
create: createData,
});
savedCount++;
}
logger.info(`[Message] 成功保存了聊天 ${chatId}${savedCount} 条消息`);
return savedCount;
} catch (error) {
logger.error(`[Message] 保存聊天消息失败:`, error);
throw error;
}
}

278
app/.server/service/page.ts Normal file
View File

@@ -0,0 +1,278 @@
import type { JsonArray, JsonObject } from '@prisma/client/runtime/library';
import type { Page } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
import { prisma } from './prisma';
const logger = createScopedLogger('page.server');
/**
* 页面创建参数接口
*/
export interface PageCreateParams extends Page {
messageId: string;
}
/**
* 页面更新参数接口
*/
export interface PageUpdateParams {
pages?: Page[];
}
/**
* 创建新的页面
* @param params 页面创建参数
* @returns 创建的页面记录
*/
export async function createPage(params: PageCreateParams) {
const { messageId, name, title, content, actionIds } = params;
try {
const pageData = [
{
name,
title,
content,
actionIds,
},
];
const page = await prisma.page.create({
data: {
messageId,
pages: JSON.parse(JSON.stringify(pageData)),
},
});
logger.info(`[Page] 创建了消息 ${messageId} 的页面: ${page.id}`);
return page;
} catch (error) {
logger.error('[Page] 创建页面失败:', error);
throw error;
}
}
/**
* 创建或更新页面
* @param params 页面创建参数
* @returns 创建或更新的页面记录
*/
export async function createOrUpdatePage(params: PageCreateParams) {
const { messageId, name, title, content, actionIds } = params;
try {
const existingPage = await getPageByMessageId(messageId);
if (existingPage) {
const updatedPage = await updatePageByMessageId(messageId, {
pages: [
{
name,
title,
content,
actionIds,
},
],
});
return updatedPage;
}
const newPage = await createPage(params);
return newPage;
} catch (error) {
logger.error('[Page] 创建或更新页面失败:', error);
throw error;
}
}
/**
* 创建多个页面
* @param messageId 消息ID
* @param pages 页面数组
* @returns 创建的页面记录
*/
export async function createPages(messageId: string, pages: Page[]) {
try {
const page = await prisma.page.create({
data: {
messageId,
pages: JSON.parse(JSON.stringify(pages)),
},
});
logger.info(`[Page] 为消息 ${messageId} 创建了 ${pages.length} 个页面: ${page.id}`);
return page;
} catch (error) {
logger.error('[Page] 创建多个页面失败:', error);
throw error;
}
}
/**
* 创建或更新多个页面
* @param messageId 消息ID
* @param pages 页面数组
* @returns 创建或更新的页面记录
*/
export async function createOrUpdatePages(messageId: string, pages: Page[]) {
try {
const existingPage = await getPageByMessageId(messageId);
if (existingPage) {
const updatedPage = await updatePageByMessageId(messageId, { pages });
return updatedPage;
}
const newPage = await createPages(messageId, pages);
return newPage;
} catch (error) {
logger.error('[Page] 创建或更新多个页面失败:', error);
throw error;
}
}
/**
* 根据ID获取页面
* @param id 页面ID
* @returns 页面记录
*/
export async function getPageById(id: string) {
try {
const page = await prisma.page.findUnique({
where: { id },
});
return page;
} catch (error) {
logger.error(`[Page] 获取页面 ${id} 失败:`, error);
throw error;
}
}
/**
* 根据消息ID获取页面
* @param messageId 消息ID
* @returns 页面记录
*/
export async function getPageByMessageId(messageId: string) {
try {
const page = await prisma.page.findUnique({
where: { messageId },
});
return page;
} catch (error) {
logger.error(`[Page] 获取消息 ${messageId} 的页面失败:`, error);
throw error;
}
}
export async function getPageByMessageIdAndName(messageId: string, name: string): Promise<JsonObject | null> {
try {
const page = await getPageByMessageId(messageId);
if (!page) {
return null;
}
const pages = page.pages as JsonArray;
const pageData = pages.find((p) => {
const page = p as JsonObject;
return page.name === name;
});
if (!pageData) {
return null;
}
return pageData as JsonObject;
} catch (error) {
logger.error(`[Page] 获取消息 ${messageId} 的页面 ${name} 失败:`, error);
throw error;
}
}
/**
* 更新页面信息
* @param id 页面ID
* @param params 更新参数
* @returns 更新后的页面记录
*/
export async function updatePage(id: string, params: PageUpdateParams) {
try {
const updateData: any = {};
if (params.pages) {
updateData.pages = JSON.parse(JSON.stringify(params.pages));
}
const updatedPage = await prisma.page.update({
where: { id },
data: updateData,
});
logger.info(`[Page] 更新了页面 ${id}`);
return updatedPage;
} catch (error) {
logger.error(`[Page] 更新页面 ${id} 失败:`, error);
throw error;
}
}
/**
* 根据消息ID更新页面信息
* @param messageId 消息ID
* @param params 更新参数
* @returns 更新后的页面记录
*/
export async function updatePageByMessageId(messageId: string, params: PageUpdateParams) {
try {
const updateData: any = {};
if (params.pages) {
updateData.pages = JSON.parse(JSON.stringify(params.pages));
}
const updatedPage = await prisma.page.update({
where: { messageId },
data: updateData,
});
logger.info(`[Page] 更新了消息 ${messageId} 的页面`);
return updatedPage;
} catch (error) {
logger.error(`[Page] 更新消息 ${messageId} 的页面失败:`, error);
throw error;
}
}
/**
* 删除页面
* @param id 页面ID
* @returns 删除结果
*/
export async function deletePage(id: string) {
try {
await prisma.page.delete({
where: { id },
});
logger.info(`[Page] 删除了页面 ${id}`);
return true;
} catch (error) {
logger.error(`[Page] 删除页面 ${id} 失败:`, error);
throw error;
}
}
/**
* 根据消息ID删除页面
* @param messageId 消息ID
* @returns 删除结果
*/
export async function deletePageByMessageId(messageId: string) {
try {
await prisma.page.delete({
where: { messageId },
});
logger.info(`[Page] 删除了消息 ${messageId} 的页面`);
return true;
} catch (error) {
logger.error(`[Page] 删除消息 ${messageId} 的页面失败:`, error);
throw error;
}
}

View File

@@ -0,0 +1,28 @@
import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3';
import { PrismaClient } from '@prisma/client';
// 创建PrismaClient实例
let prisma: PrismaClient;
declare global {
var __db: PrismaClient | undefined;
}
const adapter = new PrismaBetterSQLite3({
url: 'file:data/upage.db',
});
// 在开发环境中使用全局变量,避免热重载时创建多个实例
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient({ adapter });
} else {
if (!global.__db) {
global.__db = new PrismaClient({
log: ['query', 'error', 'warn'],
adapter,
});
}
prisma = global.__db;
}
export { prisma };

View File

@@ -0,0 +1,142 @@
import type { Page } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
import { createOrUpdatePages, getPageByMessageId } from './page';
import { createSection, deleteMessageSections, getMessageSections, type SectionCreateParams } from './section';
const logger = createScopedLogger('projectService');
/**
* 保存项目数据接口
*/
export interface SaveProjectParams {
messageId: string;
projectData: Record<string, any>;
}
/**
* 保存项目部分接口
*/
export interface SaveSectionsParams {
messageId: string;
sections: SectionCreateParams[];
}
/**
* 保存页面数据接口
*/
export interface SavePagesParams {
messageId: string;
pages: Page[];
}
/**
* 保存项目和部分数据接口
*/
export interface SaveProjectAndSectionsParams {
messageId: string;
projectData: Record<string, any>;
sections: SectionCreateParams[];
}
/**
* 保存页面和部分数据接口
*/
export interface SavePagesAndSectionsParams {
messageId: string;
pages: Page[];
sections: SectionCreateParams[];
}
/**
* 保存页面数据
* @param params 保存页面参数
* @returns 保存结果
*/
export async function savePages(params: SavePagesParams) {
const { messageId, pages } = params;
try {
// 检查页面是否已存在
const existingPage = await getPageByMessageId(messageId);
// 创建或更新页面
const page = await createOrUpdatePages(messageId, pages);
if (existingPage) {
logger.info(`更新了消息 ${messageId} 的页面`);
return { success: true, message: '页面已更新', id: page.id };
} else {
logger.info(`创建了消息 ${messageId} 的页面: ${page.id}`);
return { success: true, message: '页面已创建', id: page.id };
}
} catch (error) {
logger.error('保存页面数据失败:', error);
throw error;
}
}
/**
* 保存项目部分数据
* @param params 保存部分参数
* @returns 保存结果
*/
export async function saveSections(params: SaveSectionsParams) {
const { messageId, sections } = params;
try {
// 获取现有部分
const existingSections = await getMessageSections(messageId);
// 如果有现有部分,则先删除
if (existingSections.length > 0) {
await deleteMessageSections(messageId);
logger.info(`删除了消息 ${messageId} 的现有部分数据`);
}
// 创建新部分
const createdSections = await Promise.all(
sections.map((section) =>
createSection({
...section,
messageId,
}),
),
);
logger.info(`为消息 ${messageId} 创建了 ${createdSections.length} 个部分`);
return {
success: true,
message: `已保存 ${createdSections.length} 个部分`,
count: createdSections.length,
};
} catch (error) {
logger.error('保存部分数据失败:', error);
throw error;
}
}
/**
* 保存页面和部分数据
* @param params 保存页面和部分参数
* @returns 保存结果
*/
export async function savePagesAndSections(params: SavePagesAndSectionsParams) {
const { messageId, pages, sections } = params;
try {
// 保存页面数据
const pagesResult = await savePages({ messageId, pages });
// 保存部分数据
const sectionsResult = await saveSections({ messageId, sections });
return {
success: true,
pages: pagesResult,
sections: sectionsResult,
};
} catch (error) {
logger.error('保存页面和部分数据失败:', error);
throw error;
}
}

View File

@@ -0,0 +1,234 @@
import { prisma } from '~/.server/service/prisma';
import type { Section } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('section.server');
/**
* section 创建参数接口
*/
export interface SectionCreateParams extends Section {
messageId: string;
actionId: string;
}
/**
* section 更新参数接口
*/
export interface SectionUpdateParams {
type?: string;
action?: string;
actionId?: string;
pageName?: string;
content?: string;
domId?: string;
rootDomId?: string;
sort?: number;
}
/**
* 创建新的 section
* @param params section 创建参数
* @returns 创建的 section 记录
*/
export async function createSection(params: SectionCreateParams) {
const { messageId, action = 'add', actionId, pageName = '', content, domId, rootDomId, sort = 0 } = params;
try {
const section = await prisma.section.create({
data: {
messageId,
action,
actionId,
pageName,
content,
domId,
rootDomId,
sort,
},
});
logger.info(`[Section] 创建了消息 ${messageId} 的 section : ${section.id}`);
return section;
} catch (error) {
logger.error('[Section] 创建 section 失败:', error);
throw error;
}
}
/**
* 批量创建多个 section
* @param params section 创建参数数组
* @returns 创建的 section 数量
*/
export async function createManySections(params: SectionCreateParams[]) {
if (!params || params.length === 0) {
logger.warn('[Section] 批量创建 section : 没有提供 section 数据');
return 0;
}
try {
const result = await prisma.section.createMany({
data: params.map(
({ messageId, action = 'add', actionId, pageName = '', content, domId, rootDomId, sort = 0 }) => ({
messageId,
action,
actionId,
pageName,
content,
domId,
rootDomId,
sort,
}),
),
});
logger.info(`[Section] 批量创建了 ${result.count} 个 section `);
return result.count;
} catch (error) {
logger.error('[Section] 批量创建 section 失败:', error);
throw error;
}
}
/**
* 根据ID获取 section
* @param id section ID
* @returns section 记录
*/
export async function getSectionById(id: string) {
try {
const section = await prisma.section.findUnique({
where: { id },
});
return section;
} catch (error) {
logger.error(`[Section] 获取 section ${id} 失败:`, error);
throw error;
}
}
/**
* 获取消息的所有 section
* @param messageId 消息ID
* @returns section 记录列表
*/
export async function getMessageSections(messageId: string) {
try {
const sections = await prisma.section.findMany({
where: { messageId },
orderBy: {
sort: 'asc',
},
});
return sections;
} catch (error) {
logger.error(`[Section] 获取消息 ${messageId} 的 section 列表失败:`, error);
throw error;
}
}
/**
* 根据DOM ID获取 section
* @param domId DOM ID
* @returns section 记录
*/
export async function getSectionByDomId(domId: string) {
try {
const sections = await prisma.section.findMany({
where: { domId },
orderBy: {
updatedAt: 'desc',
},
});
return sections;
} catch (error) {
logger.error(`[Section] 获取DOM ID ${domId} 的 section 失败:`, error);
throw error;
}
}
/**
* 获取特定页面的所有 section
* @param pageName 页面名称
* @returns section 记录列表
*/
export async function getPageSections(pageName: string) {
try {
const sections = await prisma.section.findMany({
where: { pageName },
orderBy: {
sort: 'asc',
},
});
return sections;
} catch (error) {
logger.error(`[Section] 获取页面 ${pageName} 的 section 列表失败:`, error);
throw error;
}
}
/**
* 更新 section 信息
* @param id section ID
* @param params 更新参数
* @returns 更新后的 section 记录
*/
export async function updateSection(id: string, params: SectionUpdateParams) {
try {
const updatedSection = await prisma.section.update({
where: { id },
data: {
...params,
},
});
logger.info(`[Section] 更新了 section ${id}`);
return updatedSection;
} catch (error) {
logger.error(`[Section] 更新 section ${id} 失败:`, error);
throw error;
}
}
/**
* 删除 section
* @param id section ID
* @returns 删除结果
*/
export async function deleteSection(id: string) {
try {
await prisma.section.delete({
where: { id },
});
logger.info(`[Section] 删除了 section ${id}`);
return true;
} catch (error) {
logger.error(`[Section] 删除 section ${id} 失败:`, error);
throw error;
}
}
/**
* 删除消息的所有 section
* @param messageId 消息ID
* @returns 删除结果
*/
export async function deleteMessageSections(messageId: string) {
try {
const result = await prisma.section.deleteMany({
where: { messageId },
});
logger.info(`[Section] 删除了消息 ${messageId}${result.count} 个 section `);
return result.count > 0;
} catch (error) {
logger.error(`[Section] 删除消息 ${messageId} 的 section 失败:`, error);
throw error;
}
}

View File

@@ -0,0 +1,181 @@
import { prisma } from '~/.server/service/prisma';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('userSettings.server');
/**
* 用户设置创建/更新参数接口
*/
export interface UserSettingParams {
userId: string;
category: string;
key: string;
value: string;
isSecret?: boolean;
}
/**
* 用户设置查询参数接口
*/
export interface UserSettingQueryParams {
userId: string;
category?: string;
key?: string;
includeSecrets?: boolean;
}
/**
* 创建或更新用户设置
*
* @param params 用户设置参数
* @returns 创建或更新的用户设置
*/
export async function setUserSetting(params: UserSettingParams) {
const { userId, category, key, value, isSecret = false } = params;
try {
const setting = await prisma.userSetting.upsert({
where: {
userId_category_key: {
userId,
category,
key,
},
},
update: {
value,
isSecret,
},
create: {
userId,
category,
key,
value,
isSecret,
},
});
logger.info(`[UserSetting] 设置用户 ${userId}${category}.${key} 成功`);
return setting;
} catch (error) {
logger.error(`[UserSetting] 设置用户 ${userId}${category}.${key} 失败:`, error);
throw error;
}
}
/**
* 获取用户设置
* @param params 查询参数
* @returns 用户设置列表
*/
export async function getUserSettings(params: UserSettingQueryParams) {
const { userId, category, key, includeSecrets = false } = params;
try {
const where: any = { userId };
if (category) {
where.category = category;
}
if (key) {
where.key = key;
}
// 如果不包含敏感信息,则过滤掉敏感设置
if (!includeSecrets) {
where.isSecret = false;
}
const settings = await prisma.userSetting.findMany({
where,
orderBy: [{ category: 'asc' }, { key: 'asc' }],
});
return settings;
} catch (error) {
logger.error(`[UserSetting] 获取用户 ${userId} 的设置失败:`, error);
throw error;
}
}
/**
* 获取单个用户设置
* @param userId 用户ID
* @param category 设置类别
* @param key 设置键名
* @returns 用户设置如果不存在则返回null
*/
export async function getUserSetting(userId: string, category: string, key: string) {
try {
const setting = await prisma.userSetting.findUnique({
where: {
userId_category_key: {
userId,
category,
key,
},
},
});
return setting;
} catch (error) {
logger.error(`[UserSetting] 获取用户 ${userId}${category}.${key} 失败:`, error);
throw error;
}
}
/**
* 删除用户设置
* @param userId 用户ID
* @param category 设置类别
* @param key 设置键名
* @returns 删除的用户设置
*/
export async function deleteUserSetting(userId: string, category: string, key: string) {
try {
const setting = await prisma.userSetting.delete({
where: {
userId_category_key: {
userId,
category,
key,
},
},
});
logger.info(`[UserSetting] 删除用户 ${userId}${category}.${key} 成功`);
return setting;
} catch (error) {
logger.error(`[UserSetting] 删除用户 ${userId}${category}.${key} 失败:`, error);
throw error;
}
}
/**
* 删除用户的所有设置
* @param userId 用户ID
* @param category 可选的设置类别,如果提供则只删除该类别的设置
* @returns 删除的设置数量
*/
export async function deleteUserSettings(userId: string, category?: string) {
try {
const where: any = { userId };
if (category) {
where.category = category;
}
const result = await prisma.userSetting.deleteMany({
where,
});
logger.info(
`[UserSetting] 删除用户 ${userId}${category ? ` ${category}` : '所有'}设置成功,共 ${result.count}`,
);
return result.count;
} catch (error) {
logger.error(`[UserSetting] 删除用户 ${userId}${category ? ` ${category}` : '所有'}设置失败:`, error);
throw error;
}
}