refactor: repartition server-side and client-side code
This commit is contained in:
427
app/.server/service/auth.ts
Normal file
427
app/.server/service/auth.ts
Normal 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;
|
||||
}
|
||||
303
app/.server/service/chat-usage.ts
Normal file
303
app/.server/service/chat-usage.ts
Normal 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
329
app/.server/service/chat.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
235
app/.server/service/connection-settings.ts
Normal file
235
app/.server/service/connection-settings.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
316
app/.server/service/deployment.ts
Normal file
316
app/.server/service/deployment.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
317
app/.server/service/message.ts
Normal file
317
app/.server/service/message.ts
Normal 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
278
app/.server/service/page.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
28
app/.server/service/prisma.ts
Normal file
28
app/.server/service/prisma.ts
Normal 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 };
|
||||
142
app/.server/service/project-service.ts
Normal file
142
app/.server/service/project-service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
234
app/.server/service/section.ts
Normal file
234
app/.server/service/section.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
181
app/.server/service/user-settings.ts
Normal file
181
app/.server/service/user-settings.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user