This commit is contained in:
vastxie
2025-05-31 02:28:46 +08:00
parent 0f7adc5c65
commit 86e2eecc1f
1808 changed files with 183083 additions and 86701 deletions

View File

@@ -0,0 +1,867 @@
import { handleError } from '@/common/utils';
import { correctApiBaseUrl } from '@/common/utils/correctApiBaseUrl';
import { Injectable, Logger } from '@nestjs/common';
import OpenAI from 'openai';
import { GlobalConfigService } from '../../globalConfig/globalConfig.service';
import { NetSearchService } from '../search/netSearch.service';
// 引入其他需要的模块或服务
@Injectable()
export class OpenAIChatService {
constructor(
private readonly globalConfigService: GlobalConfigService,
private readonly netSearchService: NetSearchService,
) {}
/**
* 处理深度思考逻辑
* @param messagesHistory 消息历史
* @param inputs 输入参数
* @param result 结果对象
* @returns 是否应该终止请求
*/
private async handleDeepThinking(
messagesHistory: any,
inputs: {
apiKey: any;
model: any;
proxyUrl: any;
timeout: any;
usingDeepThinking?: boolean;
deepThinkingModel?: string;
deepThinkingUrl?: string;
deepThinkingKey?: string;
searchResults?: any[];
deepThinkingType?: any;
abortController: AbortController;
onProgress?: (data: any) => void;
},
result: any,
): Promise<boolean> {
const {
apiKey,
model,
proxyUrl,
timeout,
usingDeepThinking,
searchResults,
abortController,
deepThinkingType,
onProgress,
} = inputs;
const {
openaiBaseUrl,
openaiBaseKey,
openaiBaseModel,
deepThinkingUrl,
deepThinkingKey,
deepThinkingModel,
} = await this.globalConfigService.getConfigs([
'openaiBaseUrl',
'openaiBaseKey',
'openaiBaseModel',
'deepThinkingUrl',
'deepThinkingKey',
'deepThinkingModel',
]);
// 如果不使用深度思考且不是DeepSeek模型直接返回
if (!usingDeepThinking && deepThinkingType !== 2) {
return false;
}
const deepUrl = deepThinkingType === 2 ? proxyUrl : deepThinkingUrl || openaiBaseUrl;
const deepKey = deepThinkingType === 2 ? apiKey : deepThinkingKey || openaiBaseKey;
const deepModel = deepThinkingType === 2 ? model : deepThinkingModel || openaiBaseModel;
let shouldEndThinkStream = false;
let thinkingSourceType = null; // 'reasoning_content' 或 'think_tag'
// 处理所有消息中的imageUrl类型
const processedMessages = JSON.parse(JSON.stringify(messagesHistory)).map((message: any) => {
if (message.role === 'user' && Array.isArray(message.content)) {
// 将带有image_url类型的内容转换为普通文本
message.content = message.content
.filter((item: any) => item.type !== 'image_url')
.map((item: any) => item.text || item)
.join('');
}
return message;
});
// 添加文件向量搜索、图片描述和MCP工具结果到system消息
const systemMessageIndex = processedMessages.findIndex((msg: any) => msg.role === 'system');
let additionalContent = '';
// 如果有网络搜索结果添加到system消息中
if (searchResults && searchResults.length > 0) {
// 将 searchResult 转换为 JSON 字符串
let searchPrompt = JSON.stringify(searchResults, null, 2);
additionalContent += `\n\n以下是网络搜索结果请基于这些信息回答用户问题这些信息比你的训练数据更新\n${searchPrompt}`;
}
// 将额外内容添加到system消息中
if (systemMessageIndex !== -1) {
processedMessages[systemMessageIndex].content += additionalContent;
} else if (additionalContent) {
processedMessages.unshift({
role: 'system',
content: additionalContent,
});
}
const correctedDeepUrl = await correctApiBaseUrl(deepUrl);
const thinkOpenai = new OpenAI({
apiKey: deepKey,
baseURL: correctedDeepUrl,
timeout: timeout * 5,
});
Logger.debug(
`思考流请求 - Messages: ${JSON.stringify(processedMessages)}`,
'OpenAIChatService',
);
// 构建请求配置
const requestConfig: any = {
model: deepModel,
messages: processedMessages,
stream: true,
};
// 如果是 grok-3-mini-latest 模型,添加 reasoning_effort 参数
// if (deepModel === 'grok-3-mini-latest') {
// requestConfig.reasoning_effort = 'high';
// Logger.debug('为grok-3-mini-latest模型添加reasoning_effort=high参数', 'OpenAIChatService');
// }
const stream = await thinkOpenai.chat.completions.create(requestConfig, {
signal: abortController.signal,
});
// @ts-ignore - 忽略TypeScript错误因为我们知道stream是可迭代的
for await (const chunk of stream) {
if (abortController.signal.aborted || shouldEndThinkStream) {
break;
}
const delta = chunk.choices[0]?.delta;
Logger.debug(`思考流delta: ${JSON.stringify(delta)}`, 'OpenAIChatService');
const content = delta?.content;
const reasoning_content = (delta as any)?.reasoning_content || '';
// 根据已确定的思考流来源类型处理数据
if (thinkingSourceType === 'reasoning_content') {
// 已确定使用reasoning_content字段
if (reasoning_content) {
Logger.debug(
`继续接收reasoning_content思考流: ${reasoning_content}`,
'OpenAIChatService',
);
result.reasoning_content = [
{
type: 'text',
text: reasoning_content,
},
];
result.full_reasoning_content += reasoning_content;
onProgress?.({
reasoning_content: result.reasoning_content,
});
} else if (content && !content.includes('<think>')) {
// 如果出现普通content对于非DeepSeek模型终止思考流
// 对于DeepSeek模型将内容作为正常响应处理
Logger.debug(`reasoning_content模式下收到普通content: ${content}`, 'OpenAIChatService');
if (deepThinkingType === 2) {
result.content = [
{
type: 'text',
text: content,
},
];
result.full_content += content;
onProgress?.({
content: result.content,
});
} else {
shouldEndThinkStream = true;
}
}
continue;
} else if (thinkingSourceType === 'think_tag') {
// 已确定使用think标签
if (content) {
if (content.includes('</think>')) {
// 如果包含结束标签,提取剩余思考内容
Logger.debug(`检测到</think>标签,思考流结束`, 'OpenAIChatService');
const regex = /([\s\S]*?)<\/think>([\s\S]*)/;
const matches = content.match(regex);
if (matches) {
const thinkContent = matches[1] || '';
const remainingContent = matches[2] || '';
if (thinkContent) {
result.reasoning_content = [
{
type: 'text',
text: thinkContent,
},
];
result.full_reasoning_content += thinkContent;
onProgress?.({
reasoning_content: result.reasoning_content,
});
}
// 对于DeepSeek模型如果有剩余内容作为正常响应处理
if (deepThinkingType === 2 && remainingContent) {
result.content = [
{
type: 'text',
text: remainingContent,
},
];
result.full_content += remainingContent;
onProgress?.({
content: result.content,
});
}
}
// 对于非DeepSeek模型终止思考流
// 对于DeepSeek模型只标记思考流结束但继续处理后续内容
if (deepThinkingType !== 2) {
shouldEndThinkStream = true;
} else {
thinkingSourceType = 'normal_content';
}
} else {
// 继续接收think标签内的思考内容
Logger.debug(`继续接收think标签思考流: ${content}`, 'OpenAIChatService');
result.reasoning_content = [
{
type: 'text',
text: content,
},
];
result.full_reasoning_content += content;
onProgress?.({
reasoning_content: result.reasoning_content,
});
}
}
continue;
} else if (thinkingSourceType === 'normal_content' && deepThinkingType === 2) {
// DeepSeek模型在思考流结束后的正常内容处理
if (content) {
result.content = [
{
type: 'text',
text: content,
},
];
result.full_content += content;
onProgress?.({
content: result.content,
});
}
continue;
}
// 尚未确定思考流来源类型,进行检测
if (reasoning_content) {
// 确定使用reasoning_content字段作为思考流
Logger.debug(
`首次检测到reasoning_content确定使用reasoning_content思考流方式: ${reasoning_content}`,
'OpenAIChatService',
);
thinkingSourceType = 'reasoning_content';
result.reasoning_content = [
{
type: 'text',
text: reasoning_content,
},
];
result.full_reasoning_content += reasoning_content;
onProgress?.({
reasoning_content: result.reasoning_content,
});
} else if (content) {
if (content.includes('<think>')) {
// 确定使用think标签作为思考流
Logger.debug(`首次检测到<think>标签确定使用think标签思考流方式`, 'OpenAIChatService');
thinkingSourceType = 'think_tag';
// 提取第一个块中的内容
const thinkContent = content.replace(/<think>/, '');
if (thinkContent) {
Logger.debug(`从<think>标签中提取的初始思考内容: ${thinkContent}`, 'OpenAIChatService');
result.reasoning_content = [
{
type: 'text',
text: thinkContent,
},
];
result.full_reasoning_content += thinkContent;
onProgress?.({
reasoning_content: result.reasoning_content,
});
// 如果已经包含了</think>标签,提取思考内容和剩余内容
if (content.includes('</think>')) {
Logger.debug('在首个块中检测到</think>标签', 'OpenAIChatService');
const regex = /<think>([\s\S]*?)<\/think>([\s\S]*)/;
const matches = content.match(regex);
if (matches) {
const fullThinkContent = matches[1] || '';
const remainingContent = matches[2] || '';
// 更新思考内容
result.reasoning_content = [
{
type: 'text',
text: fullThinkContent,
},
];
result.full_reasoning_content = fullThinkContent;
onProgress?.({
reasoning_content: result.reasoning_content,
});
// 对于DeepSeek模型如果有剩余内容作为正常响应处理
if (deepThinkingType === 2 && remainingContent) {
result.content = [
{
type: 'text',
text: remainingContent,
},
];
result.full_content += remainingContent;
onProgress?.({
content: result.content,
});
}
}
// 对于非DeepSeek模型终止思考流
// 对于DeepSeek模型只标记思考流结束继续处理后续内容
if (deepThinkingType !== 2) {
shouldEndThinkStream = true;
} else {
thinkingSourceType = 'normal_content';
}
}
}
} else {
// 没有任何思考流标记,不同模型有不同处理
Logger.debug(`没有检测到思考流标记,处理普通内容: ${content}`, 'OpenAIChatService');
if (deepThinkingType === 2) {
// DeepSeek模型直接处理为正常内容
thinkingSourceType = 'normal_content';
result.content = [
{
type: 'text',
text: content,
},
];
result.full_content += content;
onProgress?.({
content: result.content,
});
} else {
// 非DeepSeek模型终止思考流
shouldEndThinkStream = true;
}
}
}
}
Logger.debug('思考流处理完成', 'OpenAIChatService');
// 如果是DeepSeek模型并且有内容直接返回true表示应该终止请求
return deepThinkingType === 2 && result.full_content.length > 0;
}
/**
* 处理常规响应逻辑
* @param messagesHistory 消息历史
* @param inputs 输入参数
* @param result 结果对象
*/
private async handleRegularResponse(
messagesHistory: any,
inputs: {
apiKey: any;
model: any;
proxyUrl: any;
timeout: any;
temperature: any;
max_tokens?: any;
extraParam?: any;
searchResults?: any[];
images?: string[];
abortController: AbortController;
onProgress?: (data: any) => void;
},
result: any,
): Promise<void> {
const {
apiKey,
model,
proxyUrl,
timeout,
temperature,
max_tokens,
searchResults,
images,
abortController,
onProgress,
} = inputs;
// 步骤1: 准备和增强系统消息
const processedMessages = this.prepareSystemMessage(
messagesHistory,
{
searchResults,
images,
},
result,
);
// 步骤2: 处理OpenAI聊天API调用
await this.handleOpenAIChat(
processedMessages,
{
apiKey,
model,
proxyUrl,
timeout,
temperature,
max_tokens,
abortController,
onProgress,
},
result,
);
}
async chat(
messagesHistory: any,
inputs: {
chatId: any;
maxModelTokens?: any;
max_tokens?: any;
apiKey: any;
model: any;
modelName: any;
temperature: any;
modelType?: any;
prompt?: any;
imageUrl?: any;
isFileUpload: any;
isImageUpload?: any;
fileUrl?: any;
usingNetwork?: boolean;
timeout: any;
proxyUrl: any;
modelAvatar?: any;
usingDeepThinking?: boolean;
usingMcpTool?: boolean;
isMcpTool?: boolean;
extraParam?: any;
deepThinkingType?: any;
onProgress?: (data: {
text?: string;
content?: [];
reasoning_content?: [];
tool_calls?: string;
networkSearchResult?: string;
finishReason?: string;
// full_json?: string; // 编辑模式相关,已注释
}) => void;
onFailure?: (error: any) => void;
onDatabase?: (data: any) => void;
abortController: AbortController;
},
) {
const {
chatId,
maxModelTokens,
max_tokens,
apiKey,
model,
modelName,
temperature,
prompt,
timeout,
proxyUrl,
modelAvatar,
usingDeepThinking,
usingNetwork,
extraParam,
deepThinkingType,
onProgress,
onFailure,
onDatabase,
abortController,
} = inputs;
// 创建原始消息历史的副本
const originalMessagesHistory = JSON.parse(JSON.stringify(messagesHistory));
const result: any = {
chatId,
modelName,
modelAvatar,
model,
status: 2,
full_content: '',
full_reasoning_content: '',
networkSearchResult: '',
fileVectorResult: '',
finishReason: null,
};
try {
// 步骤1: 处理网络搜索 - 使用NetSearchService
const { searchResults, images } = await this.netSearchService.processNetSearch(
prompt || '',
{
usingNetwork,
onProgress,
onDatabase,
},
result,
);
// 步骤5: 处理深度思考
const shouldEndRequest = await this.handleDeepThinking(
messagesHistory,
{
apiKey,
model,
proxyUrl,
timeout,
usingDeepThinking,
searchResults,
abortController,
deepThinkingType,
onProgress,
},
result,
);
// 如果深度思考处理后应该终止请求,则直接返回结果
if (shouldEndRequest) {
result.content = '';
result.reasoning_content = '';
result.finishReason = 'stop';
return result;
}
// 步骤6: 处理常规响应
await this.handleRegularResponse(
originalMessagesHistory,
{
apiKey,
model,
proxyUrl,
timeout,
temperature,
max_tokens,
extraParam,
searchResults,
images,
abortController,
onProgress,
},
result,
);
result.content = [
{
type: 'text',
text: '',
},
];
result.reasoning_content = [
{
type: 'text',
text: '',
},
];
result.finishReason = 'stop';
return result;
} catch (error) {
const errorMessage = handleError(error);
Logger.error(`对话请求失败: ${errorMessage}`, 'OpenAIChatService');
result.errMsg = errorMessage;
onFailure?.(result);
return result;
}
}
async chatFree(prompt: string, systemMessage?: string, messagesHistory?: any[], imageUrl?: any) {
const {
openaiBaseUrl = '',
openaiBaseKey = '',
openaiBaseModel,
} = await this.globalConfigService.getConfigs([
'openaiBaseKey',
'openaiBaseUrl',
'openaiBaseModel',
]);
const key = openaiBaseKey;
const proxyUrl = openaiBaseUrl;
let requestData = [];
if (systemMessage) {
requestData.push({
role: 'system',
content: systemMessage,
});
}
if (messagesHistory && messagesHistory.length > 0) {
requestData = requestData.concat(messagesHistory);
} else {
if (imageUrl) {
requestData.push({
role: 'user',
content: [
{
type: 'text',
text: prompt,
},
{
type: 'image_url',
image_url: {
url: imageUrl,
},
},
],
});
} else {
requestData.push({
role: 'user',
content: prompt,
});
}
}
try {
const openai = new OpenAI({
apiKey: key,
baseURL: await correctApiBaseUrl(proxyUrl),
});
const response = await openai.chat.completions.create(
{
model: openaiBaseModel || 'gpt-4o-mini',
messages: requestData,
},
{
timeout: 30000,
},
);
return response.choices[0].message.content;
} catch (error) {
const errorMessage = handleError(error);
Logger.error(`全局模型调用失败: ${errorMessage}`, 'OpenAIChatService');
return;
}
}
/**
* 准备和增强系统消息
* @param messagesHistory 消息历史
* @param inputs 输入参数
* @param result 结果对象
* @returns 处理后的消息历史
*/
private prepareSystemMessage(
messagesHistory: any,
inputs: {
searchResults?: any[];
images?: string[];
},
result: any,
): any {
const { searchResults, images } = inputs;
// 创建消息历史的副本
const processedMessages = JSON.parse(JSON.stringify(messagesHistory));
// 查找系统消息
const systemMessage = processedMessages?.find((message: any) => message.role === 'system');
if (systemMessage) {
const imageUrlMessages =
processedMessages?.filter((message: any) => message.type === 'image_url') || [];
let updatedContent = '';
// 添加推理思考内容
if (result.full_reasoning_content) {
updatedContent = `\n\n以下是针对这个问题的思考推理思路思路不一定完全正确仅供参考\n${result.full_reasoning_content}`;
}
// 添加网络搜索结果
if (searchResults && searchResults.length > 0) {
// 将 searchResult 转换为 JSON 字符串
let searchPrompt = JSON.stringify(searchResults, null, 2); // 格式化为漂亮的 JSON 字符串
// 处理图片数据
let imagesPrompt = '';
if (images && images.length > 0) {
imagesPrompt = `\n\n以下是搜索到的相关图片链接:\n${images.join('\n')}`;
}
const now = new Date();
const options = {
timeZone: 'Asia/Shanghai', // 设置时区为 'Asia/Shanghai'(北京时间)
year: 'numeric' as const,
month: '2-digit' as const,
day: '2-digit' as const,
hour: '2-digit' as const,
minute: '2-digit' as const,
hour12: false, // 使用24小时制
};
const currentDate = new Intl.DateTimeFormat('zh-CN', options).format(now);
updatedContent += `
\n\n你的任务是根据用户的问题通过下面的搜索结果提供更精确、详细、具体的回答。
请在适当的情况下在对应部分句子末尾标注引用的链接,使用[[序号](链接地址)]格式,同时使用多个链接可连续使用比如[[2](链接地址)][[5](链接地址)],以下是搜索结果:
${searchPrompt}${imagesPrompt}
在回答时,请注意以下几点:
- 现在时间是: ${currentDate}
- 如果结果中包含图片链接可在适当位置使用MarkDown格式插入至少一张图片让回答图文并茂。
- 并非搜索结果的所有内容都与用户的问题密切相关,你需要结合问题,对搜索结果进行甄别、筛选。
- 对于列举类的问题如列举所有航班信息尽量将答案控制在10个要点以内并告诉用户可以查看搜索来源、获得完整信息。优先提供信息完整、最相关的列举项如非必要不要主动告诉用户搜索结果未提供的内容。
- 对于创作类的问题(如写论文),请务必在正文的段落中引用对应的参考编号。你需要解读并概括用户的题目要求,选择合适的格式,充分利用搜索结果并抽取重要信息,生成符合用户要求、极具思想深度、富有创造力与专业性的答案。你的创作篇幅需要尽可能延长,对于每一个要点的论述要推测用户的意图,给出尽可能多角度的回答要点,且务必信息量大、论述详尽。
- 如果回答很长请尽量结构化、分段落总结。如果需要分点作答尽量控制在5个点以内并合并相关的内容。
- 对于客观类的问答,如果问题的答案非常简短,可以适当补充一到两句相关信息,以丰富内容。
- 你需要根据用户要求和回答内容选择合适、美观的回答格式,确保可读性强。
- 你的回答应该综合多个相关网页来回答,不能只重复引用一个网页。
- 除非用户要求,否则你回答的语言需要和用户提问的语言保持一致。
`;
}
// 添加图片URL消息
if (imageUrlMessages && imageUrlMessages.length > 0) {
imageUrlMessages.forEach((imageMessage: any) => {
updatedContent = `${updatedContent}\n${JSON.stringify(imageMessage)}`;
});
}
systemMessage.content += updatedContent;
}
return processedMessages;
}
/**
* 处理OpenAI聊天API调用和流式响应
* @param messagesHistory 处理后的消息历史
* @param inputs 输入参数
* @param result 结果对象
*/
private async handleOpenAIChat(
messagesHistory: any,
inputs: {
apiKey: any;
model: any;
proxyUrl: any;
timeout: any;
temperature: any;
max_tokens?: any;
abortController: AbortController;
onProgress?: (data: any) => void;
},
result: any,
): Promise<void> {
const {
apiKey,
model,
proxyUrl,
timeout,
temperature,
max_tokens,
abortController,
onProgress,
} = inputs;
// 准备请求数据
const streamData = {
model,
messages: messagesHistory,
stream: true,
temperature,
};
// 创建OpenAI实例
const openai = new OpenAI({
apiKey: apiKey,
baseURL: await correctApiBaseUrl(proxyUrl),
timeout: timeout,
});
try {
Logger.debug(
`对话请求 - Messages: ${JSON.stringify(streamData.messages)}`,
'OpenAIChatService',
);
// 发送流式请求
const stream = await openai.chat.completions.create(
{
model: streamData.model,
messages: streamData.messages,
stream: true,
max_tokens: max_tokens,
temperature: streamData.temperature,
},
{
signal: abortController.signal,
},
);
// 处理流式响应
for await (const chunk of stream) {
if (abortController.signal.aborted) {
break;
}
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
// 处理流式内容
result.content = [
{
type: 'text',
text: content,
},
];
result.full_content += content;
onProgress?.({
content: result.content,
});
}
}
} catch (error) {
Logger.error(`OpenAI请求失败: ${handleError(error)}`, 'OpenAIChatService');
throw error;
}
}
}

View File

@@ -0,0 +1,237 @@
import { handleError } from '@/common/utils';
import { Injectable, Logger } from '@nestjs/common';
import { GlobalConfigService } from '../../globalConfig/globalConfig.service';
@Injectable()
export class NetSearchService {
constructor(private readonly globalConfigService: GlobalConfigService) {}
/**
* 处理网络搜索流程
* @param prompt 搜索关键词
* @param inputs 输入参数
* @param result 结果对象
* @returns 搜索结果对象
*/
async processNetSearch(
prompt: string,
inputs: {
usingNetwork?: boolean;
onProgress?: (data: any) => void;
onDatabase?: (data: any) => void;
},
result: any,
): Promise<{ searchResults: any[]; images: string[] }> {
const { usingNetwork, onProgress, onDatabase } = inputs;
let searchResults: any[] = [];
let images: string[] = [];
// 如果不使用网络搜索,直接返回空结果
if (!usingNetwork) {
return { searchResults, images };
}
try {
Logger.log(`[网络搜索] 开始搜索: ${prompt}`, 'NetSearchService');
// 调用网络搜索服务
const searchResponse = await this.webSearchPro(prompt);
searchResults = searchResponse.searchResults;
images = searchResponse.images;
Logger.log(
`[网络搜索] 完成,获取到 ${searchResults.length} 条结果和 ${images.length} 张图片`,
'NetSearchService',
);
// 更新结果对象
result.networkSearchResult = JSON.stringify(searchResults);
onProgress?.({
networkSearchResult: result.networkSearchResult,
});
// 存储数据到数据库
onDatabase?.({
networkSearchResult: JSON.stringify(
searchResults.map((item: { [x: string]: any; content: any }) => {
const { content, ...rest } = item; // 删除 content 部分
return rest; // 返回剩余部分
}),
null,
2,
),
});
return { searchResults, images };
} catch (error) {
Logger.error(`[网络搜索] 失败: ${handleError(error)}`, 'NetSearchService');
// 即时存储错误信息
onDatabase?.({
network_search_error: {
error: handleError(error),
query: prompt,
timestamp: new Date(),
},
});
return { searchResults: [], images: [] };
}
}
async webSearchPro(prompt: string) {
try {
const { pluginUrl, pluginKey } = await this.globalConfigService.getConfigs([
'pluginUrl',
'pluginKey',
]);
if (!pluginUrl || !pluginKey) {
Logger.warn('搜索插件配置缺失');
return { searchResults: [], images: [] };
}
// 如果有多个 key随机选择一个
const keys = pluginKey.split(',').filter(key => key.trim());
const selectedKey = keys[Math.floor(Math.random() * keys.length)];
const isBochaiApi = pluginUrl.includes('bochaai.com');
const isBigModelApi = pluginUrl.includes('bigmodel.cn');
const isTavilyApi = pluginUrl.includes('tavily.com');
Logger.log(
`[搜索] API类型: ${
isBochaiApi ? 'Bochai' : isBigModelApi ? 'BigModel' : isTavilyApi ? 'Tavily' : '未知'
}`,
);
Logger.log(`[搜索] 请求URL: ${pluginUrl}`);
Logger.log(`[搜索] 搜索关键词: ${prompt}`);
const requestBody = isBochaiApi
? {
query: prompt,
// freshness: 'oneWeek',
summary: true,
count: 20,
}
: isTavilyApi
? {
query: prompt,
search_depth: 'basic',
// search_depth: 'advanced',
include_answer: false,
// include_raw_content: true,
include_images: true,
max_results: 10,
}
: {
tool: 'web-search-pro',
stream: false,
messages: [{ role: 'user', content: prompt }],
};
Logger.log(`[搜索] 请求参数: ${JSON.stringify(requestBody, null, 2)}`);
const response = await fetch(pluginUrl, {
method: 'POST',
headers: {
Authorization: selectedKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
Logger.error(`[搜索] 接口返回错误: ${response.status}`);
return { searchResults: [], images: [] };
}
const apiResult = await response.json();
Logger.log(`[搜索] 原始返回数据: ${JSON.stringify(apiResult, null, 2)}`);
let searchResults: any[] = [];
if (isBochaiApi) {
if (apiResult?.code === 200 && apiResult?.data?.webPages?.value) {
searchResults = apiResult.data.webPages.value.map((item: any) => ({
title: item?.name || '',
link: item?.url || '',
content: item?.summary || '',
icon: item?.siteIcon || '',
media: item?.siteName || '',
}));
}
} else if (isBigModelApi) {
if (apiResult?.choices?.[0]?.message?.tool_calls?.length > 0) {
for (const toolCall of apiResult.choices[0].message.tool_calls) {
if (Array.isArray(toolCall.search_result)) {
searchResults = toolCall.search_result.map((item: any) => ({
title: item?.title || '',
link: item?.link || '',
content: item?.content || '',
icon: item?.icon || '',
media: item?.media || '',
}));
break;
}
}
}
} else if (isTavilyApi) {
if (Array.isArray(apiResult?.results)) {
searchResults = apiResult.results.map((item: any) => ({
title: item?.title || '',
link: item?.url || '',
content: item?.raw_content || item?.content || '',
icon: '',
media: '',
}));
}
}
const formattedResult = searchResults.map((item, index) => ({
resultIndex: index + 1,
...item,
}));
// 提取 Tavily API 返回的图片
let images: string[] = [];
if (isTavilyApi && Array.isArray(apiResult?.images)) {
images = apiResult.images;
}
// 处理博查API返回的图片
if (isBochaiApi) {
// 博查API的图片可能在两个不同的路径
if (apiResult?.data?.images?.value && Array.isArray(apiResult.data.images.value)) {
// 从博查API的图片结构中提取contentUrl
images = apiResult.data.images.value
.filter(img => img.contentUrl)
.map(img => img.contentUrl);
}
// else if (
// apiResult?.images?.value &&
// Array.isArray(apiResult.images.value)
// ) {
// // 备选路径
// images = apiResult.images.value
// .filter((img) => img.contentUrl)
// .map((img) => img.contentUrl);
// }
}
Logger.log(`[搜索] 格式化后的结果: ${JSON.stringify(formattedResult, null, 2)}`);
// 同时返回搜索结果和图片数组
return {
searchResults: formattedResult,
images: images,
};
} catch (fetchError) {
Logger.error('[搜索] 调用接口出错:', fetchError);
return {
searchResults: [],
images: [],
};
}
}
}

View File

@@ -0,0 +1,136 @@
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { AppService } from './app.service';
import { CollectAppDto } from './dto/collectApp.dto';
import { CreateAppDto } from './dto/createApp.dto';
import { CreateCatsDto } from './dto/createCats.dto';
import { OperateAppDto } from './dto/deleteApp.dto';
import { DeleteCatsDto } from './dto/deleteCats.dto';
import { QuerAppDto } from './dto/queryApp.dto';
import { QuerCatsDto } from './dto/queryCats.dto';
import { UpdateAppDto } from './dto/updateApp.dto';
import { UpdateCatsDto } from './dto/updateCats.dto';
@ApiTags('app')
@Controller('app')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('queryAppCats')
@ApiOperation({ summary: '获取App分类列表' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
appCatsList(@Query() query: QuerCatsDto, @Req() req: Request) {
return this.appService.appCatsList(query, req);
}
@Get('queryCats')
@ApiOperation({ summary: '用户端获取App分类列表' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
catsList(@Req() req: Request) {
const params: QuerCatsDto = { status: 1, page: 1, size: 1000, name: '' };
return this.appService.appCatsList(params, req);
}
@Get('queryOneCat')
@ApiOperation({ summary: '用户端获取App详情' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
queryOneCats(@Query() query, @Req() req: Request) {
return this.appService.queryOneCat(query, req);
}
@Post('createAppCats')
@ApiOperation({ summary: '添加App分类' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
createAppCat(@Body() body: CreateCatsDto) {
return this.appService.createAppCat(body);
}
@Post('updateAppCats')
@ApiOperation({ summary: '修改App分类' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateAppCats(@Body() body: UpdateCatsDto) {
return this.appService.updateAppCats(body);
}
@Post('delAppCats')
@ApiOperation({ summary: '删除App分类' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delAppCat(@Body() body: DeleteCatsDto) {
return this.appService.delAppCat(body);
}
@Get('queryApp')
@ApiOperation({ summary: '获取App列表' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
appList(@Req() req: Request, @Query() query: QuerAppDto) {
return this.appService.appList(req, query);
}
@Get('list')
@ApiOperation({ summary: '客户端获取App' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
list(@Req() req: Request, @Query() query: QuerAppDto) {
return this.appService.frontAppList(req, query);
}
@Post('searchList')
@ApiOperation({ summary: '客户端获取App' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async searchList(@Body() body: any, @Req() req: Request) {
body.userId = req.user.id;
return this.appService.searchAppList(body);
}
@Post('createApp')
@ApiOperation({ summary: '添加App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
createApp(@Body() body: CreateAppDto) {
return this.appService.createApp(body);
}
@Post('updateApp')
@ApiOperation({ summary: '修改App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateApp(@Body() body: UpdateAppDto) {
return this.appService.updateApp(body);
}
@Post('delApp')
@ApiOperation({ summary: '删除App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delApp(@Body() body: OperateAppDto) {
return this.appService.delApp(body);
}
@Post('collect')
@ApiOperation({ summary: '收藏/取消收藏App' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
collect(@Body() body: CollectAppDto, @Req() req: Request) {
return this.appService.collect(body, req);
}
@Get('mineApps')
@ApiOperation({ summary: '我的收藏' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
mineApps(@Req() req: Request) {
return this.appService.mineApps(req);
}
}

View File

@@ -0,0 +1,68 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'app' })
export class AppEntity extends BaseEntity {
@Column({ unique: true, comment: 'App应用名称' })
name: string;
@Column({ comment: 'App分类Id列表多个分类Id以逗号分隔', type: 'text' })
catId: string;
@Column({ comment: 'App应用描述信息' })
des: string;
@Column({ comment: 'App应用预设场景信息', type: 'text' })
preset: string;
@Column({ comment: 'App应用封面图片', nullable: true, type: 'text' })
coverImg: string;
@Column({ comment: 'App应用排序、数字越大越靠前', default: 100 })
order: number;
@Column({ comment: 'App应用是否启用中 0禁用 1启用', default: 1 })
status: number;
@Column({ comment: 'App示例数据', nullable: true, type: 'text' })
demoData: string;
@Column({ comment: 'App应用角色 system user', default: 'system' })
role: string;
@Column({ comment: 'App应用是否是GPTs', default: '0' })
isGPTs: number;
@Column({ comment: 'App应用是否是固定使用模型', default: '0' })
isFixedModel: number;
@Column({ comment: 'App应用使用的模型', type: 'text' })
appModel: string;
@Column({ comment: 'GPTs 的调用ID', default: '' })
gizmoID: string;
@Column({ comment: 'App是否共享到应用广场', default: false })
public: boolean;
@Column({ comment: '用户Id', nullable: true })
userId: number;
@Column({ comment: '是否使用flowith模型', default: 0 })
isFlowith: number;
@Column({ comment: 'flowith模型ID', nullable: true })
flowithId: string;
@Column({ comment: 'flowith模型名称', nullable: true })
flowithName: string;
@Column({ comment: 'flowith模型Key', nullable: true })
flowithKey: string;
@Column({ comment: 'App背景图', nullable: true, type: 'text' })
backgroundImg: string;
@Column({ comment: 'App提问模版', nullable: true, type: 'text' })
prompt: string;
}

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { AppController } from './app.controller';
import { AppEntity } from './app.entity';
import { AppService } from './app.service';
import { AppCatsEntity } from './appCats.entity';
import { UserAppsEntity } from './userApps.entity';
@Module({
imports: [TypeOrmModule.forFeature([AppCatsEntity, AppEntity, UserAppsEntity])],
controllers: [AppController],
providers: [AppService, UserBalanceService],
})
export class AppModule {}

View File

@@ -0,0 +1,716 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Request } from 'express';
import { In, IsNull, Like, MoreThan, Not, Repository } from 'typeorm';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { AppEntity } from './app.entity';
import { AppCatsEntity } from './appCats.entity';
import { CollectAppDto } from './dto/collectApp.dto';
import { CreateAppDto } from './dto/createApp.dto';
import { CreateCatsDto } from './dto/createCats.dto';
import { OperateAppDto } from './dto/deleteApp.dto';
import { DeleteCatsDto } from './dto/deleteCats.dto';
import { QuerAppDto } from './dto/queryApp.dto';
import { QuerCatsDto } from './dto/queryCats.dto';
import { UpdateAppDto } from './dto/updateApp.dto';
import { UpdateCatsDto } from './dto/updateCats.dto';
import { UserAppsEntity } from './userApps.entity';
@Injectable()
export class AppService {
constructor(
@InjectRepository(AppCatsEntity)
private readonly appCatsEntity: Repository<AppCatsEntity>,
@InjectRepository(AppEntity)
private readonly appEntity: Repository<AppEntity>,
@InjectRepository(UserAppsEntity)
private readonly userAppsEntity: Repository<UserAppsEntity>,
private readonly userBalanceService: UserBalanceService,
) {}
async createAppCat(body: CreateCatsDto) {
const { name } = body;
const c = await this.appCatsEntity.findOne({ where: { name } });
if (c) {
throw new HttpException('该分类名称已存在!', HttpStatus.BAD_REQUEST);
}
return await this.appCatsEntity.save(body);
}
async delAppCat(body: DeleteCatsDto) {
const { id } = body;
const c = await this.appCatsEntity.findOne({ where: { id } });
if (!c) {
throw new HttpException('该分类不存在!', HttpStatus.BAD_REQUEST);
}
// 查找所有包含该分类ID的App
const apps = await this.appEntity.find();
const appsWithThisCat = apps.filter(app => {
const catIds = app.catId.split(',');
return catIds.includes(id.toString());
});
if (appsWithThisCat.length > 0) {
throw new HttpException('该分类下存在App不可删除', HttpStatus.BAD_REQUEST);
}
const res = await this.appCatsEntity.delete(id);
if (res.affected > 0) return '删除成功';
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
}
async updateAppCats(body: UpdateCatsDto) {
const { id, name } = body;
const c = await this.appCatsEntity.findOne({
where: { name, id: Not(id) },
});
if (c) {
throw new HttpException('该分类名称已存在!', HttpStatus.BAD_REQUEST);
}
const res = await this.appCatsEntity.update({ id }, body);
if (res.affected > 0) return '修改成功';
throw new HttpException('修改失败!', HttpStatus.BAD_REQUEST);
}
async queryOneCat(params, req?: Request) {
const { id } = params;
if (!id) {
throw new HttpException('缺失必要参数!', HttpStatus.BAD_REQUEST);
}
const app = await this.appEntity.findOne({ where: { id } });
if (!app) {
throw new HttpException('应用不存在!', HttpStatus.BAD_REQUEST);
}
const appData = app as any;
return {
demoData: appData.demoData ? appData.demoData.split('\n') : [],
coverImg: appData.coverImg,
des: appData.des,
name: appData.name,
isGPTs: appData.isGPTs,
isFlowith: appData.isFlowith,
flowithId: appData.flowithId,
flowithName: appData.flowithName,
isFixedModel: appData.isFixedModel,
appModel: appData.appModel,
backgroundImg: appData.backgroundImg,
prompt: appData.prompt,
};
}
async appCatsList(query: QuerCatsDto, req?: Request) {
const { page = 1, size = 10, name, status } = query;
const where: any = {};
name && (where.name = Like(`%${name}%`));
[0, 1, '0', '1'].includes(status) && (where.status = status);
const [rows, count] = await this.appCatsEntity.findAndCount({
where,
order: { order: 'DESC' },
skip: (page - 1) * size,
take: size,
});
// 如果是超级管理员,跳过过滤逻辑
let filteredRows = [...rows];
if (req?.user?.role !== 'super') {
// 获取用户的分类ID列表
const userCatIds = await this.userBalanceService.getUserApps(Number(req.user.id));
const userCatIdsSet = new Set(userCatIds);
// 过滤分类如果分类ID在用户的分类ID列表中则保留否则检查是否需要隐藏
filteredRows = rows.filter(cat => {
// 如果分类ID在用户的分类ID列表中保留它
if (userCatIdsSet.has(cat.id.toString())) {
return true;
}
// 只过滤掉设置了hideFromNonMember的分类不考虑isMember属性
return cat.hideFromNonMember !== 1;
});
}
// 查出所有分类下对应的App数量
const catIds = filteredRows.map(item => item.id);
const apps = await this.appEntity.find();
const appCountMap = {};
// 初始化每个分类的App计数为0
catIds.forEach(id => {
appCountMap[id] = 0;
});
// 统计每个分类下的App数量
apps.forEach(item => {
const appCatIds = item.catId.split(',');
appCatIds.forEach(catId => {
const catIdNum = Number(catId);
if (catIds.includes(catIdNum)) {
appCountMap[catIdNum] = (appCountMap[catIdNum] || 0) + 1;
}
});
});
filteredRows.forEach((item: any) => (item.appCount = appCountMap[item.id] || 0));
return { rows: filteredRows, count: filteredRows.length };
}
async appList(req: Request, query: QuerAppDto, orderKey = 'id') {
const { page = 1, size = 10, name, status, catId, role } = query;
const where: any = {};
name && (where.name = Like(`%${name}%`));
// 如果指定了分类ID则查找包含该分类ID的App
let filteredByCategory = null;
if (catId) {
const apps = await this.appEntity.find();
filteredByCategory = apps
.filter(app => {
const appCatIds = app.catId.split(',');
return appCatIds.includes(catId.toString());
})
.map(app => app.id);
if (filteredByCategory.length === 0) {
return { rows: [], count: 0 };
}
where.id = In(filteredByCategory);
}
role && (where.role = role);
status && (where.status = status);
const [rows, count] = await this.appEntity.findAndCount({
where,
order: { [orderKey]: 'DESC' },
skip: (page - 1) * size,
take: size,
});
// 获取所有分类信息
const allCats = await this.appCatsEntity.find();
const catsMap = {};
allCats.forEach(cat => {
catsMap[cat.id] = cat;
});
// 为每个App添加分类名称
rows.forEach((item: any) => {
const catIds = item.catId.split(',');
const catNames = catIds
.map(id => {
const cat = catsMap[Number(id)];
return cat ? cat.name : '';
})
.filter(name => name);
item.catName = catNames.join(', ');
item.backgroundImg = item.backgroundImg;
item.prompt = item.prompt;
});
if (req?.user?.role !== 'super') {
rows.forEach((item: any) => {
delete item.preset;
});
}
return { rows, count };
}
async frontAppList(req: Request, query: QuerAppDto, orderKey = 'id') {
const { page = 1, size = 1000, catId } = query;
const where: any = [
{
status: In([1, 4]),
userId: IsNull(),
public: false,
},
{ userId: MoreThan(0), public: true },
];
const userCatIds = await this.userBalanceService.getUserApps(Number(req.user.id));
const userCatIdsSet = new Set(userCatIds);
// 如果指定了分类ID则过滤包含该分类ID的App
if (catId) {
const apps = await this.appEntity.find();
const filteredByCategory = apps
.filter(app => {
const appCatIds = app.catId.split(',');
return appCatIds.includes(catId.toString());
})
.map(app => app.id);
if (filteredByCategory.length === 0) {
return { rows: [], count: 0 };
}
// 修改查询条件只查询包含指定分类ID的App
where[0].id = In(filteredByCategory);
where[1].id = In(filteredByCategory);
}
const [rows, count] = await this.appEntity.findAndCount({
where,
order: { order: 'DESC' },
skip: (page - 1) * size,
take: size,
});
// 获取所有分类信息
const allCats = await this.appCatsEntity.find();
const catsMap = {};
allCats.forEach(cat => {
catsMap[cat.id] = cat;
});
// 如果是超级管理员,跳过过滤逻辑
let filteredRows = [...rows];
if (req?.user?.role !== 'super') {
// 过滤应用如果应用的分类ID在用户的 userCatIds 中则保留,否则检查是否需要隐藏
filteredRows = rows.filter(app => {
// 获取应用所属的所有分类
const appCatIds = app.catId.split(',').map(id => Number(id));
// 检查应用是否属于用户拥有的任何分类
for (const catId of appCatIds) {
if (userCatIdsSet.has(catId.toString())) {
return true;
}
}
// 检查应用的分类是否有会员专属且对非会员隐藏的
for (const catId of appCatIds) {
const cat = catsMap[catId];
if (cat && cat.isMember === 1 && cat.hideFromNonMember === 1) {
return false; // 过滤掉这个应用
}
}
return true; // 保留这个应用
});
}
// 为每个App添加分类名称
filteredRows.forEach((item: any) => {
const appCatIds = item.catId.split(',');
const catNames = appCatIds
.map(id => {
const cat = catsMap[Number(id)];
return cat ? cat.name : '';
})
.filter(name => name);
item.catName = catNames.join(',');
item.backgroundImg = item.backgroundImg;
});
// 只有非超级管理员需要删除 preset
if (req?.user?.role !== 'super') {
filteredRows.forEach((item: any) => {
delete item.preset;
});
}
return { rows: filteredRows, count: filteredRows.length };
}
async searchAppList(body: any) {
const { page = 1, size = 1000, keyword, catId, userId, role } = body;
// 基础查询条件
let baseWhere: any = [
{
status: In([1, 4]),
userId: IsNull(),
public: false,
},
{ userId: MoreThan(0), public: true },
];
// 如果存在关键字,修改查询条件以搜索 name
if (keyword) {
baseWhere = baseWhere.map(condition => ({
...condition,
name: Like(`%${keyword}%`),
}));
}
// 如果指定了分类ID则过滤包含该分类ID的App
if (catId && !isNaN(Number(catId))) {
const apps = await this.appEntity.find();
const filteredByCategory = apps
.filter(app => {
const appCatIds = app.catId.split(',');
return appCatIds.includes(catId.toString());
})
.map(app => app.id);
if (filteredByCategory.length === 0) {
return { rows: [], count: 0 };
}
baseWhere = baseWhere.map(condition => ({
...condition,
id: In(filteredByCategory),
}));
}
try {
// 确保 userId 是有效数字
const userIdNum = isNaN(Number(userId)) ? 0 : Number(userId);
// 获取用户的分类ID列表
const userCatIds = await this.userBalanceService.getUserApps(userIdNum);
const userCatIdsSet = new Set(userCatIds);
const [rows, count] = await this.appEntity.findAndCount({
where: baseWhere,
skip: (page - 1) * size,
take: size,
});
// 获取所有分类信息
const allCats = await this.appCatsEntity.find();
const catsMap = {};
allCats.forEach(cat => {
catsMap[cat.id] = cat;
});
// 如果是超级管理员,跳过过滤逻辑
let filteredRows = [...rows];
if (role !== 'super') {
// 过滤应用如果应用的分类在用户的分类ID列表中则保留否则检查是否需要隐藏
filteredRows = rows.filter(app => {
// 获取应用所属的所有分类
const appCatIds = app.catId.split(',').map(id => Number(id));
// 检查应用是否属于用户拥有的任何分类
for (const catId of appCatIds) {
if (userCatIdsSet.has(catId.toString())) {
return true; // 保留这个应用
}
}
// 检查应用的分类是否有会员专属且对非会员隐藏的
for (const catId of appCatIds) {
const cat = catsMap[catId];
if (cat && cat.isMember === 1 && cat.hideFromNonMember === 1) {
return false; // 过滤掉这个应用
}
}
return true; // 保留这个应用
});
}
// 为每个App添加分类名称
filteredRows.forEach((item: any) => {
const appCatIds = item.catId.split(',');
const catNames = appCatIds
.map(id => {
const cat = catsMap[Number(id)];
return cat ? cat.name : '';
})
.filter(name => name);
item.catName = catNames.join(', ');
item.backgroundImg = item.backgroundImg;
item.prompt = item.prompt;
// 只有非超级管理员需要删除 preset
if (role !== 'super') {
delete item.preset;
}
});
return { rows: filteredRows, count: filteredRows.length };
} catch (error) {
throw new HttpException('查询应用列表失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async createApp(body: CreateAppDto) {
const { name, catId } = body;
body.role = 'system';
// 检查应用名称是否已存在
const a = await this.appEntity.findOne({ where: { name } });
if (a) {
throw new HttpException('该应用名称已存在!', HttpStatus.BAD_REQUEST);
}
// 验证所有分类ID是否存在
if (!catId) {
throw new HttpException('缺少分类ID', HttpStatus.BAD_REQUEST);
}
const catIds = catId.split(',');
for (const id of catIds) {
const numId = Number(id);
if (isNaN(numId)) {
throw new HttpException(`分类ID ${id} 不是有效的数字!`, HttpStatus.BAD_REQUEST);
}
const c = await this.appCatsEntity.findOne({ where: { id: numId } });
if (!c) {
throw new HttpException(`分类ID ${id} 不存在!`, HttpStatus.BAD_REQUEST);
}
}
try {
// 添加必要的默认字段
const saveData: any = { ...body };
// 检查ID是否有效如果无效则删除
if (!saveData.id || isNaN(Number(saveData.id))) {
delete saveData.id;
}
saveData.public = false;
// 设置默认值
saveData.appModel = saveData.appModel || '';
saveData.order = isNaN(Number(saveData.order)) ? 100 : saveData.order;
saveData.status = isNaN(Number(saveData.status)) ? 1 : saveData.status;
saveData.isGPTs = isNaN(Number(saveData.isGPTs)) ? 0 : saveData.isGPTs;
saveData.isFlowith = isNaN(Number(saveData.isFlowith)) ? 0 : saveData.isFlowith;
saveData.flowithId = saveData.flowithId || '';
saveData.flowithName = saveData.flowithName || '';
saveData.flowithKey = saveData.flowithKey || '';
saveData.isFixedModel = isNaN(Number(saveData.isFixedModel)) ? 0 : saveData.isFixedModel;
saveData.backgroundImg = saveData.backgroundImg || '';
saveData.prompt = saveData.prompt || '';
// 保存应用
return await this.appEntity.save(saveData);
} catch (error) {
throw new HttpException(`保存应用失败`, HttpStatus.BAD_REQUEST);
}
}
async updateApp(body: UpdateAppDto) {
const { id, name, catId, status } = body;
// 验证ID是否有效
if (id === undefined || id === null || isNaN(Number(id))) {
throw new HttpException('无效的应用ID', HttpStatus.BAD_REQUEST);
}
const a = await this.appEntity.findOne({ where: { name, id: Not(id) } });
if (a) {
throw new HttpException('该应用名称已存在!', HttpStatus.BAD_REQUEST);
}
// 验证所有分类ID是否存在
const catIds = catId.split(',');
for (const id of catIds) {
const c = await this.appCatsEntity.findOne({ where: { id: Number(id) } });
if (!c) {
throw new HttpException(`分类ID ${id} 不存在!`, HttpStatus.BAD_REQUEST);
}
}
// 创建更新数据对象
const updateData = { ...body } as any;
const curApp = await this.appEntity.findOne({ where: { id } });
const curAppData = curApp as any;
// 设置默认值
updateData.appModel = updateData.appModel ?? (curAppData.appModel || '');
updateData.order = isNaN(Number(updateData.order)) ? 100 : updateData.order;
updateData.status = isNaN(Number(updateData.status)) ? 1 : updateData.status;
updateData.isGPTs = isNaN(Number(updateData.isGPTs)) ? 0 : updateData.isGPTs;
updateData.isFlowith = isNaN(Number(updateData.isFlowith)) ? 0 : updateData.isFlowith;
updateData.flowithId = updateData.flowithId ?? (curAppData.flowithId || '');
updateData.flowithName = updateData.flowithName ?? (curAppData.flowithName || '');
updateData.isFixedModel = isNaN(Number(updateData.isFixedModel)) ? 0 : updateData.isFixedModel;
updateData.backgroundImg = updateData.backgroundImg ?? (curAppData.backgroundImg || '');
updateData.prompt = updateData.prompt ?? (curAppData.prompt || '');
if (curAppData.status !== updateData.status) {
await this.userAppsEntity.update({ appId: id }, { status: updateData.status });
}
const res = await this.appEntity.update({ id }, updateData);
if (res.affected > 0) return '修改App信息成功';
throw new HttpException('修改App信息失败', HttpStatus.BAD_REQUEST);
}
async delApp(body: OperateAppDto) {
const { id } = body;
const a = await this.appEntity.findOne({ where: { id } });
if (!a) {
throw new HttpException('该应用不存在!', HttpStatus.BAD_REQUEST);
}
const res = await this.appEntity.delete(id);
if (res.affected > 0) return '删除App成功';
throw new HttpException('删除App失败', HttpStatus.BAD_REQUEST);
}
async collect(body: CollectAppDto, req: Request) {
const { appId } = body;
const { id: userId } = req.user;
// 验证参数
if (appId === undefined || appId === null || isNaN(Number(appId))) {
throw new HttpException('无效的应用ID', HttpStatus.BAD_REQUEST);
}
if (userId === undefined || userId === null || isNaN(Number(userId))) {
throw new HttpException('无效的用户ID', HttpStatus.BAD_REQUEST);
}
const historyApp = await this.userAppsEntity.findOne({
where: { appId, userId },
});
if (historyApp) {
const r = await this.userAppsEntity.delete({ appId, userId });
if (r.affected > 0) {
return '取消收藏成功!';
} else {
throw new HttpException('取消收藏失败!', HttpStatus.BAD_REQUEST);
}
}
const app = await this.appEntity.findOne({ where: { id: appId } });
if (!app) {
throw new HttpException('应用不存在!', HttpStatus.BAD_REQUEST);
}
const { id, role: appRole, catId } = app;
const collectInfo = {
userId,
appId: id,
catId,
appRole,
public: true,
status: 1,
};
await this.userAppsEntity.save(collectInfo);
return '已将应用加入到我的收藏!';
}
async mineApps(req: Request, query = { page: 1, size: 30 }) {
const { id } = req.user;
const { page = 1, size = 30 } = query;
let filteredRows = [];
try {
// 获取用户的分类ID列表
const userCatIds = await this.userBalanceService.getUserApps(Number(id));
const userCatIdsSet = new Set(userCatIds);
const [rows, count] = await this.userAppsEntity.findAndCount({
where: { userId: id, status: In([1, 3, 4, 5]) },
order: { id: 'DESC' },
skip: (page - 1) * size,
take: size,
});
const appIds = rows.map(item => item.appId);
const appsInfo = await this.appEntity.find({ where: { id: In(appIds) } });
// 获取所有分类信息
const allCats = await this.appCatsEntity.find();
const catsMap = {};
allCats.forEach(cat => {
catsMap[cat.id] = cat;
});
// 如果是超级管理员,跳过过滤逻辑
filteredRows = [...rows];
if (req?.user?.role !== 'super') {
filteredRows = rows.filter(item => {
const app = appsInfo.find(c => c.id === item.appId);
if (!app) return false;
// 获取应用所属的所有分类
const appCatIds = app.catId.split(',').map(id => Number(id));
// 检查应用是否属于用户拥有的任何分类
for (const catId of appCatIds) {
if (userCatIdsSet.has(catId.toString())) {
return true;
}
}
// 检查应用的分类是否有会员专属且对非会员隐藏的
for (const catId of appCatIds) {
const cat = catsMap[catId];
if (cat && cat.isMember === 1 && cat.hideFromNonMember === 1) {
return false; // 过滤掉这个应用
}
}
return true; // 保留这个应用
});
}
// 为每个应用添加详细信息
filteredRows.forEach((item: any) => {
const app = appsInfo.find(c => c.id === item.appId);
if (!app) return;
item.appName = app.name || '';
item.appRole = app.role || '';
item.appDes = app.des || '';
item.coverImg = app.coverImg || '';
item.demoData = app.demoData || '';
item.backgroundImg = app.backgroundImg || '';
// 添加分类名称
const appCatIds = app.catId.split(',');
const catNames = appCatIds
.map(id => {
const cat = catsMap[Number(id)];
return cat ? cat.name : '';
})
.filter(name => name);
item.catName = catNames.join(',');
// 处理 preset 字段
item.preset = app.userId === id ? app.preset : '******';
item.prompt = app.prompt || '';
});
} catch (error) {
throw new HttpException('获取用户应用列表失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
return { rows: filteredRows, count: filteredRows.length };
}
/**
* 检查应用是否是会员专属
* @param appId 应用ID
* @returns 返回应用是否是会员专属的布尔值
*/
async checkAppIsMemberOnly(appId: number): Promise<boolean> {
try {
// 查询应用信息
const appInfo = await this.appEntity.findOne({
where: { id: appId },
select: ['catId'],
});
if (!appInfo || !appInfo.catId) {
return false;
}
// 解析分类ID列表
const catIds = appInfo.catId
.split(',')
.map(id => Number(id.trim()))
.filter(id => id > 0);
if (catIds.length === 0) {
return false;
}
// 查询这些分类是否有会员专属的
const cats = await this.appCatsEntity.find({
where: { id: In(catIds) },
select: ['id', 'isMember'],
});
// 检查是否有任何一个分类是会员专属的
return cats.some(cat => cat.isMember === 1);
} catch (error) {
return false; // 出错时默认返回非会员专属
}
}
}

View File

@@ -0,0 +1,20 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'app_cats' })
export class AppCatsEntity extends BaseEntity {
@Column({ unique: true, comment: 'App分类名称' })
name: string;
@Column({ comment: 'App分类排序、数字越大越靠前', default: 100 })
order: number;
@Column({ comment: 'App分类是否启用中 0禁用 1启用', default: 1 })
status: number;
@Column({ comment: 'App分类是否为会员专属 0否 1是', default: 0 })
isMember: number;
@Column({ comment: '非会员是否隐藏 0否 1是', default: 0 })
hideFromNonMember: number;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class CollectAppDto {
@ApiProperty({ example: 1, description: '要收藏的appId', required: true })
@IsNumber({}, { message: 'ID必须是Number' })
appId: number;
}

View File

@@ -0,0 +1,111 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDefined, IsIn, IsNumber, IsOptional } from 'class-validator';
export class CreateAppDto {
@ApiProperty({ example: '前端助手', description: 'app名称', required: true })
@IsDefined({ message: 'app名称是必传参数' })
name: string;
@ApiProperty({
example: '1,2,3',
description: 'app分类Id列表多个分类Id以逗号分隔',
required: true,
})
@IsDefined({ message: 'app分类Id必传参数' })
catId: string;
@ApiProperty({
example: '适用于编程编码、期望成为您的编程助手',
description: 'app名称详情描述',
required: false,
})
@IsDefined({ message: 'app名称描述是必传参数' })
des: string;
@ApiProperty({
example: '你现在是一个翻译官。接下来我说的所有话帮我翻译成中文',
description: '预设的prompt',
required: false,
})
@IsOptional()
preset: string;
@ApiProperty({
example: 'GPTs 的调用ID',
description: 'GPTs 使用的 ID',
required: false,
})
@IsOptional()
gizmoID: string;
@ApiProperty({ description: '是否GPTs', required: false })
@IsOptional()
isGPTs: number;
@ApiProperty({
example: 'https://xxxx.png',
description: '套餐封面图片',
required: false,
})
@IsOptional()
coverImg: string;
@ApiProperty({
example: 100,
description: '套餐排序、数字越大越靠前',
required: false,
})
@IsOptional()
order: number;
@ApiProperty({
example: 1,
description: '套餐状态 0禁用 1启用',
required: true,
})
@IsNumber({}, { message: '套餐状态必须是Number' })
@IsIn([0, 1, 3, 4, 5], { message: '套餐状态错误' })
status: number;
@ApiProperty({
example: '这是一句示例数据',
description: 'app示例数据',
required: false,
})
demoData: string;
@ApiProperty({
example: 'system',
description: '创建的角色',
required: false,
})
role: string;
@ApiProperty({
example: 0,
description: '是否使用flowith模型',
required: false,
})
isFlowith: number;
@ApiProperty({
example: 'flowith模型ID',
description: 'flowith模型ID',
required: false,
})
flowithId: string;
@ApiProperty({
example: 'flowith模型名称',
description: 'flowith模型名称',
required: false,
})
flowithName: string;
@ApiProperty({
example: 'flowith模型Key',
description: 'flowith模型Key',
required: false,
})
flowithKey: string;
}

View File

@@ -0,0 +1,47 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDefined, IsIn, IsNumber, IsOptional } from 'class-validator';
export class CreateCatsDto {
@ApiProperty({
example: '编程助手',
description: 'app分类名称',
required: true,
})
@IsDefined({ message: 'app分类名称是必传参数' })
name: string;
@ApiProperty({
example: 100,
description: '分类排序、数字越大越靠前',
required: false,
})
@IsOptional()
order: number;
@ApiProperty({
example: 1,
description: '分类状态 0禁用 1启用',
required: true,
})
@IsNumber({}, { message: '状态必须是Number' })
@IsIn([0, 1, 3, 4, 5], { message: '套餐状态错误' })
status: number;
@ApiProperty({
example: 0,
description: '分类是否为会员专属 0否 1是',
required: true,
})
@IsNumber({}, { message: '分类是否为会员专属必须是Number' })
@IsIn([0, 1], { message: '分类是否为会员专属错误' })
isMember: number;
@ApiProperty({
example: 0,
description: '非会员是否隐藏 0否 1是',
required: true,
})
@IsNumber({}, { message: '非会员是否隐藏必须是Number' })
@IsIn([0, 1], { message: '非会员是否隐藏状态错误' })
hideFromNonMember: number;
}

View File

@@ -0,0 +1,50 @@
import { IsOptional, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CustomAppDto {
@ApiProperty({ example: '前端助手', description: 'app名称', required: true })
name: string;
@ApiProperty({ example: 1, description: 'app分类Id', required: true })
catId: number;
@ApiProperty({
example: '适用于编程编码、期望成为您的编程助手',
description: 'app名称详情描述',
required: false,
})
@IsDefined({ message: 'app名称描述是必传参数' })
des: string;
@ApiProperty({
example: '你现在是一个翻译官。接下来我说的所有话帮我翻译成中文',
description: '预设的prompt',
required: true,
})
preset: string;
@ApiProperty({
example: 'https://xxxx.png',
description: '套餐封面图片',
required: false,
})
coverImg: string;
@ApiProperty({
example: '这是一句示例数据',
description: 'app示例数据',
required: false,
})
demoData: string;
@ApiProperty({
example: false,
description: '是否共享到所有人',
required: false,
})
public: boolean;
@ApiProperty({ example: 1, description: '应用ID', required: false })
@IsOptional()
appId: number;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class OperateAppDto {
@ApiProperty({ example: 1, description: '要删除的appId', required: true })
@IsNumber({}, { message: 'ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class DeleteCatsDto {
@ApiProperty({ example: 1, description: '要删除app分类Id', required: true })
@IsNumber({}, { message: 'ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,40 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
export class QuerAppDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'name', description: 'app名称', required: false })
@IsOptional()
name: string;
@ApiProperty({
example: 1,
description: 'app状态 0禁用 1启用 3:审核加入广场中 4已拒绝加入广场',
required: false,
})
@IsOptional()
status: number;
@ApiProperty({ example: 2, description: 'app分类Id', required: false })
@IsOptional()
catId: number;
@ApiProperty({ example: 'role', description: 'app角色', required: false })
@IsOptional()
role: string;
@ApiProperty({
example: '关键词',
description: '搜索关键词',
required: false,
})
@IsOptional()
keyword: string;
}

View File

@@ -0,0 +1,24 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QuerCatsDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'name', description: '分类名称', required: false })
@IsOptional()
name: string;
@ApiProperty({
example: 1,
description: '分类状态 0禁用 1启用',
required: false,
})
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,9 @@
import { IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { CreateAppDto } from './createApp.dto';
export class UpdateAppDto extends CreateAppDto {
@ApiProperty({ example: 1, description: '要修改的分类Id', required: true })
@IsNumber({}, { message: '分类ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,9 @@
import { IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { CreateCatsDto } from './createCats.dto';
export class UpdateCatsDto extends CreateCatsDto {
@ApiProperty({ example: 1, description: '要修改的分类Id', required: true })
@IsNumber({}, { message: '分类ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,23 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'user_apps' })
export class UserAppsEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '应用ID' })
appId: number;
@Column({ comment: 'app类型 system/user', default: 'user' })
appType: string;
@Column({ comment: '是否公开到公告菜单', default: false })
public: boolean;
@Column({ comment: 'app状态 1正常 2审核 3违规', default: 1 })
status: number;
@Column({ comment: 'App应用排序、数字越大越靠前', default: 100 })
order: number;
}

View File

@@ -0,0 +1,80 @@
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Body, Controller, Get, Logger, Post, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { UserLoginDto } from './dto/authLogin.dto';
import { UpdatePassByOtherDto } from './dto/updatePassByOther.dto';
import { UpdatePasswordDto } from './dto/updatePassword.dto';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('login')
@ApiOperation({ summary: '用户登录' })
async login(@Body() body: UserLoginDto, @Req() req: Request) {
return this.authService.login(body, req);
}
@Post('updatePassword')
@ApiOperation({ summary: '用户更改密码' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async updatePassword(@Req() req: Request, @Body() body: UpdatePasswordDto) {
return this.authService.updatePassword(req, body);
}
@Post('updatePassByOther')
@ApiOperation({ summary: '管理员更改用户密码' })
@UseGuards(JwtAuthGuard)
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async updatePassByOther(@Req() req: Request, @Body() body: UpdatePassByOtherDto) {
return this.authService.updatePassByOther(req, body);
}
@Get('getInfo')
@ApiOperation({ summary: '获取用户个人信息' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async getInfo(@Req() req: Request) {
const { id, role } = req.user || {};
const fingerprint = req.headers.fingerprint;
Logger.debug(
`用户信息请求 | ID: ${id} | 角色: ${role} | 指纹: ${fingerprint}`,
'AuthController',
);
return this.authService.getInfo(req);
}
@Post('sendCode')
@ApiOperation({ summary: '发送验证码' })
async sendCode(@Body() parmas: any) {
return this.authService.sendCode(parmas);
}
@Post('sendPhoneCode')
@ApiOperation({ summary: '发送手机验证码' })
async sendPhoneCode(@Body() parmas: any) {
return this.authService.sendPhoneCode(parmas);
}
@Post('verifyIdentity')
@ApiOperation({ summary: '验证身份' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async verifyIdentity(@Req() req: Request, @Body() body: any) {
return this.authService.verifyIdentity(req, body);
}
@Post('verifyPhoneIdentity')
@ApiOperation({ summary: '验证手机号' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async verifyPhoneIdentity(@Req() req: Request, @Body() body: any) {
return this.authService.verifyPhoneIdentity(req, body);
}
}

View File

@@ -0,0 +1,64 @@
import { JwtStrategy } from '@/common/auth/jwt.strategy';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { Global, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { MailerService } from '../mailer/mailer.service';
import { RedisCacheModule } from '../redisCache/redisCache.module';
import { RedisCacheService } from '../redisCache/redisCache.service';
import { UserEntity } from '../user/user.entity';
import { UserModule } from '../user/user.module';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { BalanceEntity } from '../userBalance/balance.entity';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { VerificationEntity } from './../verification/verification.entity';
import { VerificationService } from './../verification/verification.service';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
@Global()
@Module({
imports: [
UserModule,
RedisCacheModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
inject: [RedisCacheService],
useFactory: async (redisService: RedisCacheService) => ({
secret: await redisService.getJwtSecret(),
signOptions: { expiresIn: '7d' },
}),
}),
TypeOrmModule.forFeature([
VerificationEntity,
BalanceEntity,
AccountLogEntity,
ConfigEntity,
CramiPackageEntity,
UserBalanceEntity,
UserEntity,
FingerprintLogEntity,
ChatLogEntity,
ChatGroupEntity,
]),
],
controllers: [AuthController],
providers: [
AuthService,
JwtStrategy,
JwtAuthGuard,
MailerService,
VerificationService,
UserBalanceService,
RedisCacheService,
],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,491 @@
import { UserStatusEnum, UserStatusErrMsg } from '@/common/constants/user.constant';
import { createRandomCode, createRandomUid, getClientIp } from '@/common/utils';
import { GlobalConfigService } from '@/modules/globalConfig/globalConfig.service';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcryptjs';
import { Request } from 'express';
import * as os from 'os';
import { Repository } from 'typeorm';
import { ConfigEntity } from '../globalConfig/config.entity';
import { MailerService } from '../mailer/mailer.service';
import { RedisCacheService } from '../redisCache/redisCache.service';
import { UserService } from '../user/user.service';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { UserEntity } from './../user/user.entity';
import { VerificationService } from './../verification/verification.service';
import { UserLoginDto } from './dto/authLogin.dto';
import { UpdatePassByOtherDto } from './dto/updatePassByOther.dto';
import { UpdatePasswordDto } from './dto/updatePassword.dto';
@Injectable()
export class AuthService {
private ipAddress: string;
constructor(
@InjectRepository(ConfigEntity)
private readonly configEntity: Repository<ConfigEntity>,
private userService: UserService,
private jwtService: JwtService,
private mailerService: MailerService,
private readonly verificationService: VerificationService,
private readonly userBalanceService: UserBalanceService,
private readonly redisCacheService: RedisCacheService,
private readonly globalConfigService: GlobalConfigService, // private readonly userEntity: Repository<UserEntity>
) {}
async onModuleInit() {
this.getIp();
}
async login(user: UserLoginDto, req: Request): Promise<string> {
Logger.debug(`用户登录尝试,用户名: ${user.username}`, 'authService');
// 检查是否是验证码登录
if (user.captchaId) {
Logger.debug(`检测到验证码登录,联系方式: ${user.username}`, 'authService');
return await this.loginWithCaptcha({ contact: user.username, code: user.captchaId }, req);
}
// 密码登录流程
const u: UserEntity = await this.userService.verifyUserCredentials(user);
if (!u) {
Logger.error(`登录失败: 用户凭证无效 - 用户名: ${user.username}`, 'authService');
throw new HttpException('登录失败,用户凭证无效。', HttpStatus.UNAUTHORIZED);
}
const { username, id, email, role, openId, client, phone } = u;
Logger.debug(`用户${username}(ID: ${id})登录成功`, 'authService');
// 保存登录IP
const ip = getClientIp(req);
await this.userService.savaLoginIp(id, ip);
// 生成JWT令牌
const token = await this.jwtService.sign({
username,
id,
email,
role,
openId,
client,
phone,
});
// 保存令牌到Redis
await this.redisCacheService.saveToken(id, token);
Logger.debug(`用户${username}(ID: ${id})登录完成IP: ${ip}`, 'authService');
return token;
}
async loginWithCaptcha(body: any, req: Request): Promise<string> {
const { contact, code } = body;
let email = '',
phone = '';
// 判断 contact 是邮箱还是手机号
const isEmail = /\S+@\S+\.\S+/.test(contact);
const isPhone = /^\d{10,}$/.test(contact); // 根据实际需求调整正则表达式
Logger.debug(`验证码登录 | 联系方式: ${contact}`, 'authService');
if (isEmail) {
email = contact;
} else if (isPhone) {
phone = contact;
} else {
throw new HttpException('请提供有效的邮箱地址或手机号码。', HttpStatus.BAD_REQUEST);
}
// 验证短信或邮箱验证码
const nameSpace = await this.globalConfigService.getNamespace();
const codeKey = `${nameSpace}:CODE:${contact}`;
// 获取验证码
const savedCode = await this.redisCacheService.get({ key: codeKey });
if (savedCode) {
// 验证码存在,检查是否匹配
if (savedCode !== code) {
Logger.log(`验证码错误 | 联系方式: ${contact}`, 'authService');
throw new HttpException('验证码错误', HttpStatus.BAD_REQUEST);
}
Logger.debug(`验证码验证成功`);
// 验证码验证成功后,立即删除缓存中的验证码,避免重复使用
await this.redisCacheService.del({ key: codeKey });
// 处理用户登录
return await this.processUserLogin(email, phone, contact, req);
} else {
Logger.log(`验证码不存在或已过期 | 联系方式: ${contact}`, 'authService');
throw new HttpException('验证码不存在或已过期,请重新获取', HttpStatus.BAD_REQUEST);
}
}
// 抽取用户登录处理逻辑为独立方法
private async processUserLogin(
email: string,
phone: string,
contact: string,
req: Request,
): Promise<string> {
// 检查用户是否存在
let u = await this.userService.getUserByContact({ email, phone });
// 如果用户不存在,创建新用户
if (!u) {
Logger.log(`创建新用户 | 联系方式: ${contact}`, 'authService');
// 创建随机用户名
let username = createRandomUid();
while (true) {
const usernameTaken = await this.userService.verifyUserRegister({
username,
});
if (usernameTaken) {
break;
}
username = createRandomUid();
}
// 创建新用户对象
let newUser: any = {
username,
status: UserStatusEnum.ACTIVE,
};
// 根据联系方式类型添加相应字段
const isEmail = /\S+@\S+\.\S+/.test(contact);
if (isEmail) {
newUser.email = contact;
} else {
// 为手机用户创建一个随机邮箱
newUser.email = `${createRandomUid()}@aiweb.com`;
newUser.phone = contact;
}
// 创建随机密码并加密
const randomPassword = createRandomUid().substring(0, 8);
const hashedPassword = bcrypt.hashSync(randomPassword, 10);
newUser.password = hashedPassword;
// 保存新用户到数据库
u = await this.userService.createUser(newUser);
Logger.log(`用户创建成功 | 用户ID: ${u.id}`, 'authService');
// 为新用户添加初始余额
await this.userBalanceService.addBalanceToNewUser(u.id);
}
if (!u) {
throw new HttpException('登录失败,用户创建失败。', HttpStatus.INTERNAL_SERVER_ERROR);
}
const { username, id, role, openId, client } = u;
// 保存登录IP
const ip = getClientIp(req);
await this.userService.savaLoginIp(id, ip);
// 生成JWT令牌
const token = await this.jwtService.sign({
username,
id,
email,
role,
openId,
client,
phone,
});
// 保存令牌到Redis
await this.redisCacheService.saveToken(id, token);
Logger.log(`用户登录成功 | 用户ID: ${id} | 联系方式: ${contact}`, 'authService');
return token;
}
async loginByOpenId(user: UserEntity, req: Request): Promise<string> {
const { status } = user;
if (status !== UserStatusEnum.ACTIVE) {
throw new HttpException(UserStatusErrMsg[status], HttpStatus.BAD_REQUEST);
}
const { username, id, email, role, openId, client } = user;
const ip = getClientIp(req);
await this.userService.savaLoginIp(id, ip);
const token = await this.jwtService.sign({
username,
id,
email,
role,
openId,
client,
});
await this.redisCacheService.saveToken(id, token);
return token;
}
async getInfo(req: Request) {
const { id, role } = req.user;
Logger.debug(`获取用户信息 | 用户ID: ${id} | 角色: ${role}`, 'AuthService-getInfo');
// 记录请求头中的指纹
if (req.headers.fingerprint) {
Logger.debug(`请求包含指纹头: ${req.headers.fingerprint}`, 'AuthService-getInfo');
}
try {
const result = await this.userService.getUserInfo(id);
Logger.debug(`成功获取用户信息 | 用户ID: ${id}`, 'AuthService-getInfo');
return result;
} catch (error) {
Logger.error(`获取用户信息失败: ${error.message}`, 'AuthService-getInfo');
throw error;
}
}
async updatePassword(req: Request, body: UpdatePasswordDto) {
const { id, client, role } = req.user;
if (client && Number(client) > 0) {
throw new HttpException('无权此操作、请联系管理员!', HttpStatus.BAD_REQUEST);
}
if (role === 'admin') {
throw new HttpException('非法操作、请联系管理员!', HttpStatus.BAD_REQUEST);
}
// const bool = await this.userService.verifyUserPassword(id, body.oldPassword);
// if (!bool) {
// throw new HttpException('旧密码错误、请检查提交', HttpStatus.BAD_REQUEST);
// }
this.userService.updateUserPassword(id, body.password);
return '密码修改成功';
}
async updatePassByOther(req: Request, body: UpdatePassByOtherDto) {
const { id, client } = req.user;
if (!client) {
throw new HttpException('无权此操作!', HttpStatus.BAD_REQUEST);
}
this.userService.updateUserPassword(id, body.password);
return '密码修改成功';
}
getIp() {
let ipAddress: string;
const interfaces = os.networkInterfaces();
Object.keys(interfaces).forEach(interfaceName => {
const interfaceInfo = interfaces[interfaceName];
for (let i = 0; i < interfaceInfo.length; i++) {
const alias = interfaceInfo[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
ipAddress = alias.address;
break;
}
}
});
this.ipAddress = ipAddress;
}
/* 发送验证证码 */
async sendCode(body: any) {
const { contact, isLogin } = body;
let email = '',
phone = '';
const code = createRandomCode();
// 判断 contact 是邮箱还是手机号
const isEmail = /\S+@\S+\.\S+/.test(contact);
const isPhone = /^\d{10,}$/.test(contact); // 根据实际需求调整正则表达式
Logger.debug(`发送验证码 | 联系方式: ${contact}`);
if (!isEmail && !isPhone) {
throw new HttpException('请提供有效的邮箱地址或手机号码。', HttpStatus.BAD_REQUEST);
}
// 注册时才检查用户是否已存在
if (!isLogin) {
if (isEmail) {
email = contact;
} else if (isPhone) {
phone = contact;
}
}
const nameSpace = await this.globalConfigService.getNamespace();
const key = `${nameSpace}:CODE:${contact}`;
// 检查Redis中是否已经有验证码且未过期
const ttl = await this.redisCacheService.ttl(key);
if (ttl && ttl > 0 && isPhone) {
throw new HttpException(`${ttl}秒内不得重复发送验证码!`, HttpStatus.BAD_REQUEST);
}
if (isEmail) {
// 检查Redis中是否已经有验证码
const existingCode = await this.redisCacheService.get({ key });
if (existingCode) {
// 如果存在有效的验证码,则直接使用这个验证码,而不生成新的
await this.mailerService.sendMail({
to: email,
context: {
// 这里传入模板中使用的变量和数据
code: existingCode,
},
});
Logger.log(`重发验证码 | 邮箱: ${email}`, 'authService');
return `验证码发送成功、请填写验证码完成${isLogin ? '登录' : '注册'}`;
} else {
// 如果没有现有验证码或验证码已过期,则生成新的验证码
try {
await this.mailerService.sendMail({
to: email,
context: {
// 这里传入模板中使用的变量和数据
code: code,
},
});
Logger.log(`发送新验证码 | 邮箱: ${email}`, 'authService');
} catch (error) {
Logger.error(`邮件发送失败: ${error.message}`, 'authService');
throw new HttpException('验证码发送失败,请稍后重试', HttpStatus.INTERNAL_SERVER_ERROR);
}
await this.redisCacheService.set({ key, val: code }, 10 * 60); // 设置验证码600秒过期
return `验证码发送成功、请填写验证码完成${isLogin ? '登录' : '注册'}`;
}
} else if (isPhone) {
const messageInfo = { phone, code };
await this.verificationService.sendPhoneCode(messageInfo);
await this.redisCacheService.set({ key, val: code }, 10 * 60);
Logger.log(`发送验证码 | 手机号: ${phone}`, 'authService');
return `验证码发送成功、请填写验证码完成${isLogin ? '登录' : '注册'}`;
}
}
/* 发送验证证码 */
async sendPhoneCode(body: any) {
const { phone, isLogin } = body;
// const { id } = req.user;
const code = createRandomCode();
// 判断 contact 是邮箱还是手机号
const isPhone = /^\d{10,}$/.test(phone); // 根据实际需求调整正则表达式
Logger.debug(`发送手机验证码 | 手机号: ${phone}`);
if (!isPhone) {
throw new HttpException('请提供有效的手机号码。', HttpStatus.BAD_REQUEST);
}
// 仅在注册流程且指定登录标记时校验已存在用户
if (isLogin === false) {
const isAvailable = await this.userService.verifyUserRegister({
phone,
});
if (!isAvailable) {
throw new HttpException('当前手机号已注册,请勿重复注册!', HttpStatus.BAD_REQUEST);
}
}
const nameSpace = await this.globalConfigService.getNamespace();
const key = `${nameSpace}:CODE:${phone}`;
// 检查Redis中是否已经有验证码且未过期
const ttl = await this.redisCacheService.ttl(key);
if (ttl && ttl > 0 && isPhone) {
throw new HttpException(`${ttl}秒内不得重复发送验证码!`, HttpStatus.BAD_REQUEST);
}
const messageInfo = { phone, code };
await this.redisCacheService.set({ key, val: code }, 10 * 60);
await this.verificationService.sendPhoneCode(messageInfo);
Logger.log(`发送验证码 | 手机号: ${phone}`, 'authService');
return `验证码发送成功、请填写验证码完成${isLogin === false ? '注册' : '验证/登录'}`;
}
/* create token */
createTokenFromFingerprint(fingerprint) {
const token = this.jwtService.sign({
username: `游客${fingerprint}`,
id: fingerprint,
email: `${fingerprint}@visitor.com`,
role: 'visitor',
openId: null,
client: null,
});
return token;
}
async verifyIdentity(req: Request, body) {
Logger.debug('开始实名认证流程');
const { name, idCard } = body;
const { id } = req.user;
try {
// 调用验证服务进行身份验证
const result = await this.verificationService.verifyIdentity(body);
// 输出验证结果到日志
Logger.debug(`实名认证结果: ${result}`);
// 检查验证结果
if (!result) {
throw new HttpException('身份验证错误,请检查实名信息', HttpStatus.BAD_REQUEST);
}
// 保存用户的实名信息
await this.userService.saveRealNameInfo(id, name, idCard);
return '认证成功';
} catch (error) {
// 处理可能的错误并记录错误信息
Logger.error('验证过程出现错误', error);
throw new HttpException('认证失败,请检查相关信息', HttpStatus.BAD_REQUEST);
}
}
async verifyPhoneIdentity(req: Request, body) {
Logger.debug('开始手机号认证流程');
const { phone, username, password, code } = body;
const { id } = req.user;
// 校验验证码是否过期或错误
const nameSpace = this.globalConfigService.getNamespace();
const key = `${nameSpace}:CODE:${phone}`;
const redisCode = await this.redisCacheService.get({ key });
Logger.debug(`Retrieved redisCode for ${phone}: ${redisCode}`);
if (code === '') {
throw new HttpException('请输入验证码', HttpStatus.BAD_REQUEST);
}
if (!redisCode) {
Logger.log(`验证码过期: ${phone}`, 'authService');
throw new HttpException('验证码已过期,请重新发送!', HttpStatus.BAD_REQUEST);
}
if (code !== redisCode) {
Logger.log(
`验证码错误: ${phone} 输入的验证码: ${code}, 期望的验证码: ${redisCode}`,
'authService',
);
throw new HttpException('验证码填写错误,请重新输入!', HttpStatus.BAD_REQUEST);
}
// 验证用户名是否已存在
if (username) {
const usernameTaken = await this.userService.isUsernameTaken(body.username, id);
if (usernameTaken) {
throw new HttpException('用户名已存在!', HttpStatus.BAD_REQUEST);
}
}
try {
// 保存用户的实名信息
await this.userService.updateUserPhone(id, phone, username, password);
return '认证成功';
} catch (error) {
// 处理可能的错误并记录错误信息
Logger.error('验证过程出现错误', error);
throw new HttpException('身份验证错误,请检查相关信息', HttpStatus.BAD_REQUEST);
}
}
}

View File

@@ -0,0 +1,17 @@
import { IsNotEmpty, MinLength, MaxLength, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AdminLoginDto {
@ApiProperty({ example: 'super', description: '邮箱' })
@IsNotEmpty({ message: '用户名不能为空!' })
@MinLength(2, { message: '用户名最短是两位数!' })
@MaxLength(30, { message: '用户名最长不得超过30位' })
@IsOptional()
username?: string;
@ApiProperty({ example: '999999', description: '密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, MaxLength, MinLength } from 'class-validator';
export class UserLoginDto {
@ApiProperty({ example: 'super', description: '邮箱' })
@IsNotEmpty({ message: '用户名不能为空!' })
@MinLength(2, { message: '用户名最短是两位数!' })
@MaxLength(30, { message: '用户名最长不得超过30位' })
@IsOptional()
username?: string;
@ApiProperty({ example: 1, description: '用户ID' })
@IsOptional()
uid?: number;
@ApiProperty({ example: '999999', description: '密码' })
@IsOptional()
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password?: string;
@ApiProperty({ example: 'abc123', description: '图形验证码ID' })
@IsOptional()
captchaId?: string;
}

View File

@@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, MaxLength, MinLength } from 'class-validator';
export class UserRegisterDto {
@ApiProperty({ example: 'cooper', description: '用户名称' })
// @IsNotEmpty({ message: '用户名不能为空!' })
// @MinLength(2, { message: '用户名最低需要大于2位数' })
// @MaxLength(12, { message: '用户名不得超过12位' })
username?: string;
@ApiProperty({ example: '123456', description: '用户密码' })
@IsNotEmpty({ message: '用户密码不能为空' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
@ApiProperty({ example: 'ai@aiweb.com', description: '用户邮箱' })
// @IsEmail({}, { message: '请填写正确格式的邮箱!' })
// @IsNotEmpty({ message: '邮箱不能为空!' })
email: string;
@ApiProperty({
example: '',
description: '用户头像',
required: false,
})
@IsOptional()
avatar: string;
@ApiProperty({
example: 'default',
description: '用户注册来源',
required: false,
})
@IsOptional()
client: string;
}

View File

@@ -0,0 +1,15 @@
import { IsNotEmpty, MinLength, MaxLength, IsPhoneNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LoginByPhoneDto {
@ApiProperty({ example: '19999999', description: '手机号' })
@IsNotEmpty({ message: '手机号不能为空!' })
@IsPhoneNumber('CN', { message: '手机号格式不正确!' })
phone?: string;
@ApiProperty({ example: '999999', description: '密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,18 @@
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SendPhoneCodeDto {
@ApiProperty({ example: '199999999', description: '手机号' })
@IsNotEmpty({ message: '手机号不能为空' })
@MinLength(11, { message: '手机号长度为11位' })
@MaxLength(11, { message: '手机号长度为11位' })
phone?: string;
@ApiProperty({ example: '2b4i1b4', description: '图形验证码KEY' })
@IsNotEmpty({ message: '验证码KEY不能为空' })
captchaId?: string;
@ApiProperty({ example: '1g4d', description: '图形验证码' })
@IsNotEmpty({ message: '验证码不能为空' })
captchaCode?: string;
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdatePassByOtherDto {
@ApiProperty({ example: '666666', description: '三方用户更新新密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, MaxLength, MinLength } from 'class-validator';
export class UpdatePasswordDto {
@ApiProperty({ example: '666666', description: '用户更新新密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsPhoneNumber, MaxLength, MinLength } from 'class-validator';
export class UserRegisterByPhoneDto {
@ApiProperty({ example: 'cooper', description: '用户名称' })
@IsNotEmpty({ message: '用户名不能为空!' })
@MinLength(2, { message: '用户名最低需要大于2位数' })
@MaxLength(12, { message: '用户名不得超过12位' })
username?: string;
@ApiProperty({ example: '123456', description: '用户密码' })
@IsNotEmpty({ message: '用户密码不能为空' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
@ApiProperty({ example: '19999999999', description: '用户手机号码' })
@IsPhoneNumber('CN', { message: '手机号码格式不正确!' })
@IsNotEmpty({ message: '手机号码不能为空!' })
phone: string;
@ApiProperty({ example: '152546', description: '手机验证码' })
@IsNotEmpty({ message: '手机验证码不能为空!' })
phoneCode: string;
}

View File

@@ -0,0 +1,47 @@
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AutoReplyService } from './autoReply.service';
import { AddAutoReplyDto } from './dto/addAutoReply.dto';
import { DelAutoReplyDto } from './dto/delBadWords.dto';
import { QueryAutoReplyDto } from './dto/queryAutoReply.dto';
import { UpdateAutoReplyDto } from './dto/updateAutoReply.dto';
@ApiTags('autoReply')
@Controller('autoReply')
export class AutoReplyController {
constructor(private readonly autoReplyService: AutoReplyService) {}
@Get('query')
@ApiOperation({ summary: '查询自动回复' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
queryAutoReply(@Query() query: QueryAutoReplyDto) {
return this.autoReplyService.queryAutoReply(query);
}
@Post('add')
@ApiOperation({ summary: '添加自动回复' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
addAutoReply(@Body() body: AddAutoReplyDto) {
return this.autoReplyService.addAutoReply(body);
}
@Post('update')
@ApiOperation({ summary: '修改自动回复' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateAutoReply(@Body() body: UpdateAutoReplyDto) {
return this.autoReplyService.updateAutoReply(body);
}
@Post('del')
@ApiOperation({ summary: '删除自动回复' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delAutoReply(@Body() body: DelAutoReplyDto) {
return this.autoReplyService.delAutoReply(body);
}
}

View File

@@ -0,0 +1,17 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'auto_reply' })
export class AutoReplyEntity extends BaseEntity {
@Column({ comment: '提问的问题', type: 'text' })
prompt: string;
@Column({ comment: '定义的答案', type: 'text' })
answer: string;
@Column({ default: 1, comment: '是否开启AI回复0关闭 1启用' })
isAIReplyEnabled: number;
@Column({ default: 1, comment: '启用当前自动回复状态, 0关闭 1启用' })
status: number;
}

View File

@@ -0,0 +1,14 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AutoReplyController } from './autoReply.controller';
import { AutoReplyEntity } from './autoReply.entity';
import { AutoReplyService } from './autoReply.service';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([AutoReplyEntity])],
controllers: [AutoReplyController],
providers: [AutoReplyService],
exports: [AutoReplyService],
})
export class AutoReplyModule {}

View File

@@ -0,0 +1,128 @@
import { HttpException, HttpStatus, Injectable, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Like, Repository } from 'typeorm';
import { AutoReplyEntity } from './autoReply.entity';
import { AddAutoReplyDto } from './dto/addAutoReply.dto';
import { DelAutoReplyDto } from './dto/delBadWords.dto';
import { QueryAutoReplyDto } from './dto/queryAutoReply.dto';
import { UpdateAutoReplyDto } from './dto/updateAutoReply.dto';
@Injectable()
export class AutoReplyService implements OnModuleInit {
private autoReplyKes: { prompt: string; keywords: string[] }[] = [];
private autoReplyMap = {};
private autoReplyFuzzyMatch = true;
constructor(
@InjectRepository(AutoReplyEntity)
private readonly autoReplyEntity: Repository<AutoReplyEntity>,
) {}
async onModuleInit() {
await this.loadAutoReplyList();
}
async loadAutoReplyList() {
const res = await this.autoReplyEntity.find({
where: { status: 1 },
select: ['prompt', 'answer', 'isAIReplyEnabled'],
});
this.autoReplyMap = {};
this.autoReplyKes = [];
res.forEach(t => {
this.autoReplyMap[t.prompt] = {
answer: t.answer,
isAIReplyEnabled: t.isAIReplyEnabled,
};
const keywords = t.prompt.split(' ').map(k => k.trim()); // 关键词以空格分词
this.autoReplyKes.push({ prompt: t.prompt, keywords });
});
}
async checkAutoReply(prompt: string) {
const answers = [];
let isAIReplyEnabled = 0;
const seenGroups = new Set<string>();
// Logger.debug('checkAutoReply', prompt);
// Logger.debug('checkAutoReply', this.autoReplyKes);
// Logger.debug('autoReplyMap', this.autoReplyMap);
if (this.autoReplyFuzzyMatch) {
for (const item of this.autoReplyKes) {
if (item.keywords.some(keyword => prompt.includes(keyword))) {
if (!seenGroups.has(item.prompt)) {
answers.push(this.autoReplyMap[item.prompt].answer);
seenGroups.add(item.prompt);
if (this.autoReplyMap[item.prompt].isAIReplyEnabled === 1) {
isAIReplyEnabled = 1;
}
}
}
}
} else {
const matches = this.autoReplyKes.filter(item => item.prompt === prompt);
for (const match of matches) {
if (!seenGroups.has(match.prompt)) {
answers.push(this.autoReplyMap[match.prompt].answer);
seenGroups.add(match.prompt);
if (this.autoReplyMap[match.prompt].isAIReplyEnabled === 1) {
isAIReplyEnabled = 1;
}
}
}
}
return {
answer: answers.join('\n'), // 拼接所有匹配到的答案
isAIReplyEnabled,
};
}
async queryAutoReply(query: QueryAutoReplyDto) {
const { page = 1, size = 10, prompt, status } = query;
const where: any = {};
[0, 1, '0', '1'].includes(status) && (where.status = status);
prompt && (where.prompt = Like(`%${prompt}%`));
const [rows, count] = await this.autoReplyEntity.findAndCount({
where,
skip: (page - 1) * size,
take: size,
order: { id: 'DESC' },
});
return { rows, count };
}
async addAutoReply(body: AddAutoReplyDto) {
// 直接保存新的自动回复
await this.autoReplyEntity.save(body);
// 重新加载自动回复列表
await this.loadAutoReplyList();
return '添加问题成功!';
}
async updateAutoReply(body: UpdateAutoReplyDto) {
const { id } = body;
const res = await this.autoReplyEntity.update({ id }, body);
if (res.affected > 0) {
await this.loadAutoReplyList();
return '更新问题成功';
}
throw new HttpException('更新失败', HttpStatus.BAD_REQUEST);
}
async delAutoReply(body: DelAutoReplyDto) {
const { id } = body;
const z = await this.autoReplyEntity.findOne({ where: { id } });
if (!z) {
throw new HttpException('该问题不存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
}
const res = await this.autoReplyEntity.delete({ id });
if (res.affected > 0) {
await this.loadAutoReplyList();
return '删除问题成功';
}
throw new HttpException('删除失败', HttpStatus.BAD_REQUEST);
}
}

View File

@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
export class AddAutoReplyDto {
@ApiProperty({ example: '你是谁', description: '提问的问题', required: true })
prompt: string;
@ApiProperty({
example: '我是AIWeb提供的Ai服务机器人',
description: '回答的答案',
required: true,
})
answer: string;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class DelAutoReplyDto {
@ApiProperty({ example: 1, description: '自动回复id', required: true })
id: number;
}

View File

@@ -0,0 +1,20 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QueryAutoReplyDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: '你是谁', description: '提问问题', required: false })
@IsOptional()
prompt: string;
@ApiProperty({ example: 1, description: '问题状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
export class UpdateAutoReplyDto {
@ApiProperty({ example: 1, description: '自动回复id', required: true })
@IsOptional()
id: number;
@ApiProperty({ example: '你可以干嘛', description: '问题', required: false })
@IsOptional()
prompt: string;
@ApiProperty({
example: '我可以干很多事情.......',
description: '答案',
required: false,
})
@IsOptional()
answer: string;
@ApiProperty({ example: 0, description: '状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,55 @@
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { BadWordsService } from './badWords.service';
import { AddBadWordDto } from './dto/addBadWords.dto';
import { DelBadWordsDto } from './dto/delBadWords.dto';
import { QueryBadWordsDto } from './dto/queryBadWords.dto';
import { QueryViolationDto } from './dto/queryViolation.dto';
import { UpdateBadWordsDto } from './dto/updateBadWords.dto';
@ApiTags('badWords')
@Controller('badWords')
export class BadWordsController {
constructor(private readonly badWordsService: BadWordsService) {}
@Get('query')
@ApiOperation({ summary: '查询所有敏感词' })
queryBadWords(@Query() query: QueryBadWordsDto) {
return this.badWordsService.queryBadWords(query);
}
@Post('del')
@ApiOperation({ summary: '删除敏感词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delBadWords(@Body() body: DelBadWordsDto) {
return this.badWordsService.delBadWords(body);
}
@Post('update')
@ApiOperation({ summary: '更新敏感词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateBadWords(@Body() body: UpdateBadWordsDto) {
return this.badWordsService.updateBadWords(body);
}
@Post('add')
@ApiOperation({ summary: '新增敏感词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
addBadWord(@Body() body: AddBadWordDto) {
return this.badWordsService.addBadWord(body);
}
@Get('violation')
@ApiOperation({ summary: '查询违规记录' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
violation(@Req() req: Request, @Query() query: QueryViolationDto) {
return this.badWordsService.violation(req, query);
}
}

View File

@@ -0,0 +1,14 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'bad_words' })
export class BadWordsEntity extends BaseEntity {
@Column({ length: 20, comment: '敏感词' })
word: string;
@Column({ default: 1, comment: '敏感词开启状态' })
status: number;
@Column({ default: 0, comment: '敏感词触发次数' })
count: number;
}

View File

@@ -0,0 +1,16 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from '../user/user.entity';
import { BadWordsController } from './badWords.controller';
import { BadWordsEntity } from './badWords.entity';
import { BadWordsService } from './badWords.service';
import { ViolationLogEntity } from './violationLog.entity';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([BadWordsEntity, ViolationLogEntity, UserEntity])],
providers: [BadWordsService],
controllers: [BadWordsController],
exports: [BadWordsService],
})
export class BadWordsModule {}

View File

@@ -0,0 +1,244 @@
import { hideString } from '@/common/utils';
import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import axios from 'axios';
import { In, Like, Repository } from 'typeorm';
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
import { UserEntity } from '../user/user.entity';
import { BadWordsEntity } from './badWords.entity';
import { AddBadWordDto } from './dto/addBadWords.dto';
import { DelBadWordsDto } from './dto/delBadWords.dto';
import { QueryBadWordsDto } from './dto/queryBadWords.dto';
import { UpdateBadWordsDto } from './dto/updateBadWords.dto';
import { ViolationLogEntity } from './violationLog.entity';
@Injectable()
export class BadWordsService implements OnModuleInit {
private badWords: string[];
constructor(
@InjectRepository(BadWordsEntity)
private readonly badWordsEntity: Repository<BadWordsEntity>,
@InjectRepository(ViolationLogEntity)
private readonly violationLogEntity: Repository<ViolationLogEntity>,
@InjectRepository(UserEntity)
private readonly userEntity: Repository<UserEntity>,
private readonly globalConfigService: GlobalConfigService,
) {
this.badWords = [];
}
async onModuleInit() {
this.loadBadWords();
}
/* 敏感词匹配 */
async customSensitiveWords(content, userId) {
const triggeredWords = [];
// 遍历敏感词列表,查找内容中是否包含敏感词
for (let i = 0; i < this.badWords.length; i++) {
const word = this.badWords[i];
if (content.includes(word)) {
triggeredWords.push(word); // 如果包含敏感词,将其加入到数组中
}
}
if (triggeredWords.length) {
// 如果找到敏感词,记录用户提交的违规内容
await this.recordUserBadWords(userId, content, triggeredWords, ['自定义'], '自定义检测');
}
// 返回检测到的敏感词列表(如果没有敏感词,返回空数组)
return triggeredWords;
}
/* 敏感词检测 先检测百度敏感词 后检测自定义的 */
async checkBadWords(content: string, userId: number) {
const config = await this.globalConfigService.getSensitiveConfig();
/* 如果有则启动配置检测 没有则跳过 */
if (config) {
await this.checkBadWordsByConfig(content, config, userId);
}
/* 自定义敏感词检测 */
return await this.customSensitiveWords(content, userId);
}
/* 通过配置信息去检测敏感词 */
async checkBadWordsByConfig(content: string, config: any, userId) {
const { useType } = config;
useType === 'baidu' &&
(await this.baiduCheckBadWords(content, config.baiduTextAccessToken, userId));
}
/* 提取百度云敏感词违规类型 */
extractContent(str) {
const pattern = /存在(.*?)不合规/;
const match = str.match(pattern);
return match ? match[1] : '';
}
/* 通过百度云敏感词检测 */
async baiduCheckBadWords(content: string, accessToken: string, userId: number) {
if (!accessToken) return;
if (!content || content.trim() === '') {
Logger.debug('提交的内容为空,跳过百度敏感词检测', 'BadWordsService');
return;
}
const url = `https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token=${accessToken}}`;
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
};
try {
const response = await axios.post(url, { text: content }, { headers });
const { conclusion, error_code, error_msg, conclusionType, data } = response.data;
// 如果API返回错误记录错误并直接返回
if (error_code) {
Logger.warn(`百度文本检测出现错误、请查看配置信息: ${error_msg}`, 'BadWordsService');
return;
}
// conclusion 审核结果,可取值:合规、不合规、疑似、审核失败
// conclusionType 1.合规2.不合规3.疑似4.审核失败
if (conclusionType !== 1 && data && Array.isArray(data)) {
const types = [...new Set(data.map(item => this.extractContent(item.msg)))];
await this.recordUserBadWords(userId, content, ['***'], types, '百度云检测');
const tips = `您提交的信息中包含${types.join(
',',
)}的内容、我们已对您的账户进行标记、请合规使用!`;
throw new HttpException(tips, HttpStatus.BAD_REQUEST);
}
} catch (error) {
Logger.error(`百度敏感词检测服务异常: ${error.message}`, error.stack, 'BadWordsService');
// 出错时不阻止后续流程,继续使用自定义词库检测
}
}
/* formarTips */
formarTips(wordList) {
const categorys = wordList.map(t => t.category);
const unSet = [...new Set(categorys)];
return `您提交的内容中包含${unSet.join(',')}的信息、我们已对您账号进行标记、请合规使用!`;
}
/* 加载自定义的敏感词 */
async loadBadWords() {
const data = await this.badWordsEntity.find({
where: { status: 1 },
select: ['word'],
});
this.badWords = data.map(t => t.word);
}
/* 查询自定义的敏感词 */
async queryBadWords(query: QueryBadWordsDto) {
const { page = 1, size = 500, word, status } = query;
const where: any = {};
[0, 1, '0', '1'].includes(status) && (where.status = status);
word && (where.word = Like(`%${word}%`));
const [rows, count] = await this.badWordsEntity.findAndCount({
where,
skip: (page - 1) * size,
take: size,
order: { id: 'ASC' },
});
return { rows, count };
}
/* 删除自定义敏感词 */
async delBadWords(body: DelBadWordsDto) {
const b = await this.badWordsEntity.findOne({ where: { id: body.id } });
if (!b) {
throw new HttpException('敏感词不存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
}
const res = await this.badWordsEntity.delete({ id: body.id });
if (res.affected > 0) {
await this.loadBadWords();
return '删除敏感词成功';
} else {
throw new HttpException('删除敏感词失败', HttpStatus.BAD_REQUEST);
}
}
/* 修改自定义敏感词 */
async updateBadWords(body: UpdateBadWordsDto) {
const { id, word, status } = body;
const b = await this.badWordsEntity.findOne({ where: { word } });
if (b) {
throw new HttpException('敏感词已经存在了、请勿重复添加', HttpStatus.BAD_REQUEST);
}
const res = await this.badWordsEntity.update({ id }, { word, status });
if (res.affected > 0) {
await this.loadBadWords();
return '更新敏感词成功';
} else {
throw new HttpException('更新敏感词失败', HttpStatus.BAD_REQUEST);
}
}
async addBadWord(body: AddBadWordDto) {
const { word } = body;
const b = await this.badWordsEntity.findOne({ where: { word } });
if (b) {
throw new HttpException('敏感词已存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
}
await this.badWordsEntity.save({ word });
await this.loadBadWords();
return '添加敏感词成功';
}
/* 记录用户违规次数内容 */
async recordUserBadWords(userId, content, words, typeCn, typeOriginCn) {
const data = {
userId,
content,
words: JSON.stringify(words),
typeCn: JSON.stringify(typeCn),
typeOriginCn,
};
try {
await this.userEntity
.createQueryBuilder()
.update(UserEntity)
.set({ violationCount: () => 'violationCount + 1' })
.where('id = :userId', { userId })
.execute();
await this.violationLogEntity.save(data);
} catch (error) {
console.log('error: ', error);
}
}
/* 违规记录 */
async violation(req, query) {
const { role } = req.user;
const { page = 1, size = 10, userId, typeOriginCn } = query;
const where = {};
userId && (where['userId'] = userId);
typeOriginCn && (where['typeOriginCn'] = typeOriginCn);
const [rows, count] = await this.violationLogEntity.findAndCount({
where,
skip: (page - 1) * size,
take: size,
order: { id: 'DESC' },
});
const userIds = [...new Set(rows.map(t => t.userId))];
const usersInfo = await this.userEntity.find({
where: { id: In(userIds) },
select: ['id', 'avatar', 'username', 'email', 'violationCount', 'status'],
});
rows.forEach((t: any) => {
// 查找用户信息,如果没有找到,返回空对象
const user: any = usersInfo.find(u => u.id === t.userId) || {};
// const user: any = usersInfo.find((u) => u.id === t.userId);
role !== 'super' && (user.email = hideString(user.email));
t.userInfo = user;
});
return { rows, count };
}
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class AddBadWordDto {
@ApiProperty({ example: 'test', description: '敏感词', required: true })
word: string;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class DelBadWordsDto {
@ApiProperty({ example: 1, description: '敏感词id', required: true })
id: number;
}

View File

@@ -0,0 +1,20 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QueryBadWordsDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'test', description: '敏感词内容', required: false })
@IsOptional()
word: string;
@ApiProperty({ example: 1, description: '关键词状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,24 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QueryViolationDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 1, description: '用户ID', required: false })
@IsOptional()
userId: number;
@ApiProperty({
example: '百度云检测',
description: '检测平台来源',
required: false,
})
@IsOptional()
typeOriginCn: string;
}

View File

@@ -0,0 +1,16 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdateBadWordsDto {
@ApiProperty({ example: 1, description: '敏感词id', required: true })
@IsOptional()
id: number;
@ApiProperty({ example: 'test', description: '敏感词内容', required: false })
@IsOptional()
word: string;
@ApiProperty({ example: 1, description: '关键词状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,20 @@
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'violation_log' })
export class ViolationLogEntity extends BaseEntity {
@Column({ comment: '用户id' })
userId: number;
@Column({ comment: '违规内容', type: 'text' })
content: string;
@Column({ comment: '敏感词', type: 'text' })
words: string;
@Column({ comment: '违规类型' })
typeCn: string;
@Column({ comment: '违规检测失败于哪个平台' })
typeOriginCn: string;
}

View File

@@ -0,0 +1,29 @@
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../common/auth/jwtAuth.guard';
import { ChatService } from './chat.service';
import { Body, Controller, Post, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { ChatProcessDto } from './dto/chatProcess.dto';
@ApiTags('chatgpt')
@Controller('chatgpt')
export class ChatController {
constructor(private readonly chatService: ChatService) {}
@Post('chat-process')
@ApiOperation({ summary: 'gpt聊天对话' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
chatProcess(@Body() body: ChatProcessDto, @Req() req: Request, @Res() res: Response) {
return this.chatService.chatProcess(body, req, res);
}
@Post('tts-process')
@ApiOperation({ summary: 'tts语音播报' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
ttsProcess(@Body() body: any, @Req() req: Request, @Res() res: Response) {
return this.chatService.ttsProcess(body, req, res);
}
}

View File

@@ -0,0 +1,85 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OpenAIChatService } from '../aiTool/chat/chat.service';
import { NetSearchService } from '../aiTool/search/netSearch.service';
import { AppEntity } from '../app/app.entity';
import { AppService } from '../app/app.service';
import { AppCatsEntity } from '../app/appCats.entity';
import { UserAppsEntity } from '../app/userApps.entity';
import { AutoReplyEntity } from '../autoReply/autoReply.entity';
import { AutoReplyService } from '../autoReply/autoReply.service';
import { BadWordsEntity } from '../badWords/badWords.entity';
import { BadWordsService } from '../badWords/badWords.service';
import { ViolationLogEntity } from '../badWords/violationLog.entity';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { ChatGroupService } from '../chatGroup/chatGroup.service';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { ChatLogService } from '../chatLog/chatLog.service';
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
import { MailerService } from '../mailer/mailer.service';
import { ModelsEntity } from '../models/models.entity';
import { ModelsService } from '../models/models.service';
import { PluginEntity } from '../plugin/plugin.entity';
import { RedisCacheService } from '../redisCache/redisCache.service';
import { UploadService } from '../upload/upload.service';
import { UserEntity } from '../user/user.entity';
import { UserService } from '../user/user.service';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { BalanceEntity } from '../userBalance/balance.entity';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { VerificationEntity } from '../verification/verification.entity';
import { VerificationService } from '../verification/verification.service';
import { ChatController } from './chat.controller';
import { ChatService } from './chat.service';
@Global()
@Module({
imports: [
TypeOrmModule.forFeature([
BalanceEntity,
UserEntity,
PluginEntity,
VerificationEntity,
ChatLogEntity,
AccountLogEntity,
ConfigEntity,
UserEntity,
CramiPackageEntity,
ChatGroupEntity,
AppEntity,
UserBalanceEntity,
FingerprintLogEntity,
AppCatsEntity,
UserAppsEntity,
AutoReplyEntity,
BadWordsEntity,
ViolationLogEntity,
ModelsEntity,
]),
],
controllers: [ChatController],
providers: [
ChatService,
UserBalanceService,
UserService,
VerificationService,
ChatLogService,
RedisCacheService,
MailerService,
GlobalConfigService,
UploadService,
AutoReplyService,
BadWordsService,
ChatGroupService,
ModelsService,
OpenAIChatService,
NetSearchService,
AppService,
],
exports: [ChatService],
})
export class ChatModule {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ChatDrawDto {
@ApiProperty({
example: 'Draw a cute little dog',
description: '绘画描述信息',
})
prompt: string;
@ApiProperty({ example: 1, description: '绘画张数', required: true })
n: number;
@ApiProperty({
example: '1024x1024',
description: '图片尺寸',
required: true,
})
size: string;
@ApiProperty({ example: 'standard', description: '图片质量', required: true })
quality: string;
@ApiProperty({
example:
'close-up polaroid photo, of a little joyful cute panda, in the forest, sun rays coming, photographic, sharp focus, depth of field, soft lighting, heigh quality, 24mm, Nikon Z FX',
description: '绘画提示词!',
required: true,
})
@ApiProperty({
example: '--ar 16:9 --c 0',
description: '除了prompt的额外参数',
})
@IsOptional()
extraParam?: string;
@ApiProperty({
example: 'https://xsdasdasd.com',
description: '垫图图片地址',
})
@IsOptional()
imgUrl?: string;
@ApiProperty({
example: 'IMAGINE',
description:
'任务类型,可用值:IMAGINE,UPSCALE,VARIATION,ZOOM,PAN,DESCRIBE,BLEND,SHORTEN,SWAP_FACE',
})
@IsOptional()
action: string;
@ApiProperty({ example: 1, description: '变体或者放大的序号' })
@IsOptional()
orderId: number;
@ApiProperty({ example: 1, description: '绘画的DBID' })
@IsOptional()
drawId: number;
@ApiProperty({ example: 1, description: 'customId' })
@IsOptional()
customId: string;
@ApiProperty({ example: 1, description: 'base64' })
@IsOptional()
base64: string;
@ApiProperty({ example: 1, description: '任务ID' })
@IsOptional()
taskId: number;
}

View File

@@ -0,0 +1,53 @@
import { IsNotEmpty, IsString, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class Options {
@IsString()
parentMessageId: string;
model?: string;
temperature?: number;
top_p?: number;
groupId?: number;
}
export class ChatProcessDto {
@ApiProperty({ example: 'hello, Who are you', description: '对话信息' })
@IsNotEmpty({ message: '提问信息不能为空!' })
prompt: string;
@ApiProperty({
example: 'https://aiweb.com',
description: '对话附带的链接',
required: false,
})
url: string;
@ApiProperty({
example: '{ parentMessageId: 0 }',
description: '上次对话信息',
required: false,
})
@Type(() => Options)
options: Options;
@ApiProperty({
example:
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.",
description: '系统预设信息',
})
@IsOptional()
systemMessage?: string;
@ApiProperty({ example: 1, description: '应用id', required: false })
@IsOptional()
appId: number;
@ApiProperty({
example: 'gpt-3.5-turbo',
description: '使用模型',
required: false,
})
@IsOptional()
model: string;
}

View File

@@ -0,0 +1,69 @@
/**
* @desc 处理不同模型返回的最后一次汇总内容 输出为相同格式 方便后面使用
* @param keyType 模型key类型
* @param response 模型返回的整体内容
*/
export function unifiedFormattingResponse(keyType, response, others) {
let formatRes = {
keyType, // 模型类型
parentMessageId: '', // 父级对话id
text: '', //本次回复内容
usage: {
prompt_tokens: 0, //提问token
completion_tokens: 0, // 回答token
total_tokens: 0, // 总消耗token
},
};
/* openai */
// if([1].includes(Number(keyType))){
const { parentMessageId } = response?.detail;
let { usage } = response?.detail;
if (!usage) {
usage = {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
};
}
const { prompt_tokens, completion_tokens, total_tokens } = usage;
formatRes = {
keyType,
parentMessageId,
text: response.text,
usage: {
prompt_tokens,
completion_tokens,
total_tokens,
},
};
// }
/* 百度 */
// if([2, 3].includes(Number(keyType))) {
// const { usage, text } = response
// const { prompt_tokens, completion_tokens, total_tokens } = usage
// const { model, parentMessageId } = others
// formatRes = {
// keyType,
// model,
// parentMessageId,
// text,
// usage: {
// prompt_tokens,
// completion_tokens,
// total_tokens
// }
// }
// }
return formatRes;
}
/*百度的模型不允许传入偶数的message数组 让round为奇数的时候 加一 */
export function addOneIfOdd(num) {
if (num % 2 !== 0) {
return num + 1;
} else {
return num;
}
}

View File

@@ -0,0 +1,54 @@
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ChatGroupService } from './chatGroup.service';
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { CreateGroupDto } from './dto/createGroup.dto';
import { Request } from 'express';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { DelGroupDto } from './dto/delGroup.dto';
import { UpdateGroupDto } from './dto/updateGroup.dto';
@ApiTags('group')
@Controller('group')
export class ChatGroupController {
constructor(private readonly chatGroupService: ChatGroupService) {}
@Post('create')
@ApiOperation({ summary: '创建对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
create(@Body() body: CreateGroupDto, @Req() req: Request) {
return this.chatGroupService.create(body, req);
}
@Get('query')
@ApiOperation({ summary: '查询对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
query(@Req() req: Request) {
return this.chatGroupService.query(req);
}
@Post('update')
@ApiOperation({ summary: '更新对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
update(@Body() body: UpdateGroupDto, @Req() req: Request) {
return this.chatGroupService.update(body, req);
}
@Post('del')
@ApiOperation({ summary: '删除对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
del(@Body() body: DelGroupDto, @Req() req: Request) {
return this.chatGroupService.del(body, req);
}
@Post('delAll')
@ApiOperation({ summary: '删除对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
delAll(@Req() req: Request) {
return this.chatGroupService.delAll(req);
}
}

View File

@@ -0,0 +1,50 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'chat_group' })
export class ChatGroupEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '是否置顶聊天', type: 'boolean', default: false })
isSticky: boolean;
@Column({ comment: '分组名称', nullable: true })
title: string;
@Column({ comment: '应用ID', nullable: true })
appId: number;
// @Column({ comment: '模型', nullable: true })
// model: string;
// @Column({ comment: '模型名称', nullable: true })
// modelName: string;
// @Column({ comment: '扣费类型', nullable: true })
// mdeductType: string;
// @Column({ comment: '是否支持文件上传', nullable: true })
// isFileUpload: boolean;
// @Column({ comment: '是否固定模型', default: 0 })
// isFixedModel: boolean;
// @Column({ comment: '模型类型', nullable: true })
// keyType: number;
@Column({ comment: '是否删除了', default: false })
isDelete: boolean;
@Column({ comment: '配置', nullable: true, default: null, type: 'text' })
config: string;
@Column({ comment: '附加参数', nullable: true, type: 'text' })
params: string;
@Column({ comment: '文件链接', nullable: true, default: null, type: 'text' })
fileUrl: string;
@Column({ comment: 'PDF中的文字内容', nullable: true, type: 'mediumtext' })
pdfTextContent: string;
}

View File

@@ -0,0 +1,15 @@
import { Global, Module } from '@nestjs/common';
import { ChatGroupController } from './chatGroup.controller';
import { ChatGroupService } from './chatGroup.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatGroupEntity } from './chatGroup.entity';
import { AppEntity } from '../app/app.entity';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([ChatGroupEntity, AppEntity])],
controllers: [ChatGroupController],
providers: [ChatGroupService],
exports: [ChatGroupService],
})
export class ChatGroupModule {}

View File

@@ -0,0 +1,252 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import axios from 'axios';
import { Request } from 'express';
import * as pdf from 'pdf-parse';
import { In, Repository } from 'typeorm';
import { AppEntity } from '../app/app.entity';
import { ModelsService } from '../models/models.service';
import { ChatGroupEntity } from './chatGroup.entity';
import { CreateGroupDto } from './dto/createGroup.dto';
import { DelGroupDto } from './dto/delGroup.dto';
@Injectable()
export class ChatGroupService {
constructor(
@InjectRepository(ChatGroupEntity)
private readonly chatGroupEntity: Repository<ChatGroupEntity>,
@InjectRepository(AppEntity)
private readonly appEntity: Repository<AppEntity>,
private readonly modelsService: ModelsService,
) {}
async create(body: CreateGroupDto, req: Request) {
const { id } = req.user; // 从请求中获取用户ID
const { appId, modelConfig: bodyModelConfig, params } = body; // 从请求体中提取appId和modelConfig
// 尝试使用从请求体中提供的 modelConfig否则获取默认配置
let modelConfig = bodyModelConfig || (await this.modelsService.getBaseConfig());
const modelDetail = await this.modelsService.getModelDetailByName(modelConfig.modelInfo.model);
if (modelDetail) {
modelConfig.modelInfo.modelName = modelDetail.modelName;
modelConfig.modelInfo.deductType = modelDetail.deductType;
modelConfig.modelInfo.deduct = modelDetail.deduct;
modelConfig.modelInfo.isFileUpload = modelDetail.isFileUpload;
modelConfig.modelInfo.isImageUpload = modelDetail.isImageUpload;
modelConfig.modelInfo.isNetworkSearch = modelDetail.isNetworkSearch;
modelConfig.modelInfo.deepThinkingType = modelDetail.deepThinkingType;
modelConfig.modelInfo.isMcpTool = modelDetail.isMcpTool;
}
if (!modelConfig) {
throw new HttpException(
'管理员未配置任何AI模型、请先联系管理员开通聊天模型配置',
HttpStatus.BAD_REQUEST,
);
}
// 使用 JSON.parse(JSON.stringify(object)) 进行深拷贝以避免直接修改原对象
modelConfig = JSON.parse(JSON.stringify(modelConfig));
// 初始化创建对话组的参数
const groupParams = { title: '新对话', userId: id, appId, params };
// const params = { title: 'New chat', userId: id };
// 如果指定了appId查找并验证应用信息
if (appId) {
const appInfo = await this.appEntity.findOne({ where: { id: appId } });
if (!appInfo) {
throw new HttpException('非法操作、您在使用一个不存在的应用!', HttpStatus.BAD_REQUEST);
}
// 应用存在,提取并验证应用信息
const { status, name, isFixedModel, isGPTs, coverImg, appModel, isFlowith } = appInfo;
if (isFixedModel && appModel) {
const modelDetail = await this.modelsService.getModelDetailByName(appModel);
Logger.debug(`modelDetail: ${modelDetail}`);
if (modelDetail) {
modelConfig.modelInfo.modelName = modelDetail.modelName;
modelConfig.modelInfo.deductType = modelDetail.deductType;
modelConfig.modelInfo.deduct = modelDetail.deduct;
modelConfig.modelInfo.isFileUpload = modelDetail.isFileUpload;
modelConfig.modelInfo.isImageUpload = modelDetail.isImageUpload;
modelConfig.modelInfo.isNetworkSearch = modelDetail.isNetworkSearch;
modelConfig.modelInfo.deepThinkingType = modelDetail.deepThinkingType;
modelConfig.modelInfo.isMcpTool = modelDetail.isMcpTool;
}
}
// 更新 modelConfig 以反映应用的特定配置
Object.assign(modelConfig.modelInfo, {
isGPTs,
isFixedModel,
isFlowith,
modelAvatar: coverImg,
modelName: name,
});
// 如果是固定模型或GPTs模型获取并设置额外的模型信息
if (isGPTs === 1 || isFixedModel === 1 || isFlowith === 1) {
const appModelKey = await this.modelsService.getCurrentModelKeyInfo(
isFixedModel === 1 ? appModel : isFlowith === 1 ? 'flowith' : isGPTs === 1 ? 'gpts' : '',
);
Object.assign(modelConfig.modelInfo, {
deductType: appModelKey.deductType,
deduct: appModelKey.deduct,
model: appModel,
isFileUpload: appModelKey.isFileUpload,
isImageUpload: appModelKey.isImageUpload,
});
}
// 检查应用状态是否允许创建对话组
if (![1, 3, 4, 5].includes(status)) {
throw new HttpException('非法操作、您在使用一个未启用的应用!', HttpStatus.BAD_REQUEST);
}
// 如果应用有名称,则使用它作为对话组标题
if (name) {
groupParams.title = name;
}
}
// 创建新的聊天组并保存
const newGroup = await this.chatGroupEntity.save({
...groupParams,
config: JSON.stringify(modelConfig), // 将 modelConfig 对象转换为 JSON 字符串进行保存
});
return newGroup; // 返回新创建的聊天组
}
async query(req: Request) {
try {
const { id } = req.user;
const params = { userId: id, isDelete: false };
const res = await this.chatGroupEntity.find({
where: params,
order: { isSticky: 'DESC', updatedAt: 'DESC' },
});
return res;
// const res = await this.chatGroupEntity.find({ where: params, order: { isSticky: 'DESC', id: 'DESC' } });
const appIds = res.filter(t => t.appId).map(t => t.appId);
const appInfos = await this.appEntity.find({ where: { id: In(appIds) } });
return res.map((item: any) => {
item.appLogo = appInfos.find(t => t.id === item.appId)?.coverImg;
return item;
});
} catch (error) {
console.log('error: ', error);
}
}
async update(body: any, req: Request) {
// Logger.debug(`body: ${JSON.stringify(body)}`);
const { title, isSticky, groupId, config, fileUrl } = body;
const { id } = req.user;
const g = await this.chatGroupEntity.findOne({
where: { id: groupId, userId: id },
});
if (!g) {
throw new HttpException('请先选择一个对话或者新加一个对话再操作!', HttpStatus.BAD_REQUEST);
}
const { appId } = g;
if (appId && !title) {
try {
const parseData = JSON.parse(config);
if (Number(parseData.keyType) !== 1) {
throw new HttpException('应用对话名称不能修改哟!', HttpStatus.BAD_REQUEST);
}
} catch (error) {
// ignore
}
}
const data = {};
title && (data['title'] = title);
typeof isSticky !== 'undefined' && (data['isSticky'] = isSticky);
config && (data['config'] = config);
typeof fileUrl !== 'undefined' && (data['fileUrl'] = fileUrl);
const u = await this.chatGroupEntity.update({ id: groupId }, data);
if (u.affected) {
// // 如果 fileUrl 不为空,异步处理 PDF 内容读取
// if (fileUrl) {
// this.handlePdfExtraction(fileUrl, groupId);
// }
return true;
} else {
throw new HttpException('更新对话失败!', HttpStatus.BAD_REQUEST);
}
}
// 从 PDF 文件 URL 中提取文本内容
private async extractPdfText(fileUrl: string): Promise<string> {
try {
const response = await axios.get(fileUrl, {
responseType: 'arraybuffer',
});
const dataBuffer = Buffer.from(response.data);
const pdfData = await pdf(dataBuffer);
return pdfData.text;
} catch (error) {
console.error('PDF 解析失败:', error);
throw new Error('PDF 解析失败');
}
}
async updateTime(groupId: number) {
await this.chatGroupEntity.update(groupId, {
updatedAt: new Date(),
});
}
async del(body: DelGroupDto, req: Request) {
const { groupId } = body;
const { id } = req.user;
const g = await this.chatGroupEntity.findOne({
where: { id: groupId, userId: id },
});
if (!g) {
throw new HttpException('非法操作、您在删除一个非法资源!', HttpStatus.BAD_REQUEST);
}
const r = await this.chatGroupEntity.update({ id: groupId }, { isDelete: true });
if (r.affected) {
return '删除成功';
} else {
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
}
}
/* 删除非置顶开启的所有对话记录 */
async delAll(req: Request) {
const { id } = req.user;
const r = await this.chatGroupEntity.update(
{ userId: id, isSticky: false, isDelete: false },
{ isDelete: true },
);
if (r.affected) {
return '删除成功';
} else {
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
}
}
/* 通过groupId查询当前对话组的详细信息 */
async getGroupInfoFromId(id) {
if (!id) return;
const groupInfo = await this.chatGroupEntity.findOne({ where: { id } });
if (groupInfo) {
const { pdfTextContent, ...rest } = groupInfo;
return rest;
}
}
async getGroupPdfText(groupId: number) {
const groupInfo = await this.chatGroupEntity.findOne({
where: { id: groupId },
});
if (groupInfo) {
return groupInfo.pdfTextContent;
}
}
}

View File

@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
export class CreateGroupDto {
@ApiProperty({ example: 10, description: '应用ID', required: false })
@IsOptional()
appId: number;
@ApiProperty({
example: '',
description: '对话模型配置项序列化的字符串',
required: false,
})
modelConfig?: any;
@ApiProperty({
example: '',
description: '对话组参数序列化的字符串',
required: false,
})
params?: string;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class DelGroupDto {
@ApiProperty({ example: 1, description: '对话分组ID', required: true })
groupId: number;
}

View File

@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
export class UpdateGroupDto {
@ApiProperty({ example: 1, description: '修改的对话ID', required: false })
@IsOptional()
groupId: number;
@ApiProperty({ example: 10, description: '对话组title', required: false })
@IsOptional()
title: string;
@ApiProperty({ example: 10, description: '对话组是否置顶', required: false })
@IsOptional()
isSticky: boolean;
@ApiProperty({
example: '',
description: '对话模型配置项序列化的字符串',
required: false,
})
config: string;
}

View File

@@ -0,0 +1,101 @@
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Body, Controller, Get, Post, Query, Req, Res, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { ChatLogService } from './chatLog.service';
import { ChatListDto } from './dto/chatList.dto';
import { DelDto } from './dto/del.dto';
import { DelByGroupDto } from './dto/delByGroup.dto';
import { ExportExcelChatlogDto } from './dto/exportExcelChatlog.dto';
import { QuerAllChatLogDto } from './dto/queryAllChatLog.dto';
import { QueryByAppIdDto } from './dto/queryByAppId.dto';
import { QuerMyChatLogDto } from './dto/queryMyChatLog.dto';
import { QuerySingleChatDto } from './dto/querySingleChat.dto';
import { recDrawImgDto } from './dto/recDrawImg.dto';
@Controller('chatLog')
@ApiTags('chatLog')
export class ChatLogController {
constructor(private readonly chatLogService: ChatLogService) {}
@Get('draw')
@ApiOperation({ summary: '查询我的绘制记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
querDrawLog(@Query() query: QuerMyChatLogDto, @Req() req: Request) {
return this.chatLogService.querDrawLog(req, query);
}
@Post('recDrawImg')
@ApiOperation({ summary: '推荐此图片对外展示' })
@ApiBearerAuth()
@UseGuards(SuperAuthGuard)
recDrawImg(@Body() body: recDrawImgDto) {
return this.chatLogService.recDrawImg(body);
}
@Get('chatAll')
@ApiOperation({ summary: '查询所有的问答记录' })
@ApiBearerAuth()
@UseGuards(AdminAuthGuard)
queryAllChatLog(@Query() params: QuerAllChatLogDto, @Req() req: Request) {
return this.chatLogService.querAllChatLog(params, req);
}
@Post('exportExcel')
@ApiOperation({ summary: '导出问答记录' })
@ApiBearerAuth()
exportExcel(@Body() body: ExportExcelChatlogDto, @Res() res: Response) {
return this.chatLogService.exportExcel(body, res);
}
@Get('chatList')
@ApiOperation({ summary: '查询我的问答记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
chatList(@Req() req: Request, @Query() params: ChatListDto) {
return this.chatLogService.chatList(req, params);
}
@Post('del')
@ApiOperation({ summary: '删除我的问答记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
del(@Req() req: Request, @Body() body: DelDto) {
return this.chatLogService.deleteChatLog(req, body);
}
@Post('delByGroupId')
@ApiOperation({ summary: '清空一组对话' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
delByGroupId(@Req() req: Request, @Body() body: DelByGroupDto) {
return this.chatLogService.delByGroupId(req, body);
}
@Post('deleteChatsAfterId')
@ApiOperation({ summary: '删除对话组中某条对话及其后的所有对话' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
deleteChatsAfterId(@Req() req: Request, @Body() body: any) {
return this.chatLogService.deleteChatsAfterId(req, body);
}
@Get('byAppId')
@ApiOperation({ summary: '查询某个应用的问答记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
byAppId(@Req() req: Request, @Query() params: QueryByAppIdDto) {
return this.chatLogService.byAppId(req, params);
}
@Get('querySingleChat')
@ApiOperation({ summary: '查询单条消息的状态和内容' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
querySingleChat(@Req() req: Request, @Query() params: QuerySingleChatDto) {
return this.chatLogService.querySingleChat(req, params);
}
}

View File

@@ -0,0 +1,122 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'chatlog' })
export class ChatLogEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '使用的模型', nullable: true })
model: string;
@Column({ comment: 'role system user assistant', nullable: true })
role: string;
@Column({ comment: '模型内容', nullable: true, type: 'mediumtext' })
content: string;
@Column({ comment: '模型推理内容', nullable: true, type: 'text' })
reasoning_content: string;
@Column({ comment: '模型工具调用', nullable: true, type: 'text' })
tool_calls: string;
@Column({ comment: '图片Url', nullable: true, type: 'text' })
imageUrl: string;
@Column({ comment: '视频Url', nullable: true, type: 'text' })
videoUrl: string;
@Column({ comment: '音频Url', nullable: true, type: 'text' })
audioUrl: string;
@Column({ comment: '文件Url', nullable: true, type: 'text' })
fileUrl: string;
@Column({
comment: '使用类型1: 普通对话 2: 绘图 3: 拓展性对话',
nullable: true,
default: 1,
})
type: number;
@Column({ comment: '自定义的模型名称', nullable: true, default: 'AI' })
modelName: string;
@Column({ comment: '自定义的模型头像', nullable: false, default: '' })
modelAvatar: string;
@Column({ comment: 'Ip地址', nullable: true })
curIp: string;
//废弃字段
@Column({ comment: '询问的问题', type: 'text', nullable: true })
prompt: string;
@Column({ comment: '附加参数', nullable: true })
extraParam: string;
@Column({ comment: '插件参数', nullable: true })
pluginParam: string;
@Column({ comment: '回答的答案', type: 'text', nullable: true })
answer: string;
@Column({ comment: '提问的token', nullable: true })
promptTokens: number;
@Column({ comment: '回答的token', nullable: true })
completionTokens: number;
@Column({ comment: '总花费的token', nullable: true })
totalTokens: number;
@Column({ comment: '任务进度', nullable: true })
progress: string;
@Column({ comment: '任务状态', nullable: true, default: 3 })
status: number;
@Column({ comment: '任务类型', nullable: true })
action: string;
@Column({ comment: '对图片操作的按钮ID', type: 'text', nullable: true })
customId: string;
@Column({ comment: '绘画的ID每条不一样', nullable: true })
drawId: string;
@Column({ comment: '对话转语音的链接', nullable: true, type: 'text' })
ttsUrl: string;
@Column({ comment: '是否推荐0: 默认 1: 推荐', nullable: true, default: 0 })
rec: number;
@Column({ comment: '分组ID', nullable: true })
groupId: number;
@Column({ comment: '使用的应用id', nullable: true })
appId: number;
@Column({ comment: '是否删除', default: false })
isDelete: boolean;
@Column({ comment: '任务ID', nullable: true })
taskId: string;
@Column({ comment: '任务数据', nullable: true, type: 'text' })
taskData: string;
@Column({ comment: '文件信息', nullable: true, type: 'text' })
fileInfo: string;
@Column({ comment: '提问参考', nullable: true })
promptReference: string;
@Column({ comment: '联网搜索结果', nullable: true, type: 'text' })
networkSearchResult: string;
@Column({ comment: '文件向量搜索结果', nullable: true, type: 'mediumtext' })
fileVectorResult: string;
}

View File

@@ -0,0 +1,16 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { UserEntity } from '../user/user.entity';
import { ChatLogController } from './chatLog.controller';
import { ChatLogEntity } from './chatLog.entity';
import { ChatLogService } from './chatLog.service';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([ChatLogEntity, UserEntity, ChatGroupEntity])],
controllers: [ChatLogController],
providers: [ChatLogService],
exports: [ChatLogService],
})
export class ChatLogModule {}

View File

@@ -0,0 +1,527 @@
import { ChatType } from '@/common/constants/balance.constant';
import { formatDate, maskEmail } from '@/common/utils';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import excel from 'exceljs';
import { Request, Response } from 'express';
import { In, Like, MoreThan, MoreThanOrEqual, Repository } from 'typeorm';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { UserEntity } from '../user/user.entity';
import { ChatLogEntity } from './chatLog.entity';
import { ChatListDto } from './dto/chatList.dto';
import { DelDto } from './dto/del.dto';
import { DelByGroupDto } from './dto/delByGroup.dto';
import { ExportExcelChatlogDto } from './dto/exportExcelChatlog.dto';
import { QuerAllChatLogDto } from './dto/queryAllChatLog.dto';
import { QueryByAppIdDto } from './dto/queryByAppId.dto';
import { QuerMyChatLogDto } from './dto/queryMyChatLog.dto';
import { recDrawImgDto } from './dto/recDrawImg.dto';
// import { ModelsTypeEntity } from '../models/modelType.entity';
import { JwtPayload } from 'src/types/express';
import { ModelsService } from '../models/models.service';
import { QuerySingleChatDto } from './dto/querySingleChat.dto';
@Injectable()
export class ChatLogService {
constructor(
@InjectRepository(ChatLogEntity)
private readonly chatLogEntity: Repository<ChatLogEntity>,
@InjectRepository(UserEntity)
private readonly userEntity: Repository<UserEntity>,
@InjectRepository(ChatGroupEntity)
private readonly chatGroupEntity: Repository<ChatGroupEntity>,
private readonly modelsService: ModelsService,
) {}
/* 记录问答日志 */
async saveChatLog(logInfo): Promise<any> {
const savedLog = await this.chatLogEntity.save(logInfo);
return savedLog; // 这里返回保存后的实体,包括其 ID
}
/* 更新问答日志 */
async updateChatLog(id, logInfo) {
return await this.chatLogEntity.update({ id }, logInfo);
}
async findOneChatLog(id) {
return await this.chatLogEntity.findOne({ where: { id } });
}
/* 查询我的绘制记录 */
async querDrawLog(req: Request, query: QuerMyChatLogDto) {
const { id } = req.user;
const { model } = query;
const where: any = { userId: id, type: ChatType.PAINT };
if (model) {
where.model = model;
if (model === 'DALL-E2') {
where.model = In(['DALL-E2', 'dall-e-3']);
}
}
const data = await this.chatLogEntity.find({
where,
order: { id: 'DESC' },
select: ['id', 'answer', 'prompt', 'model', 'type', 'fileInfo'],
});
data.forEach((r: any) => {
if (r.type === 'paintCount') {
const w = r.model === 'mj' ? 310 : 160;
const imgType = r.answer.includes('cos') ? 'tencent' : 'ali';
const compress =
imgType === 'tencent'
? `?imageView2/1/w/${w}/q/55`
: `?x-oss-process=image/resize,w_${w}`;
r.thumbImg = r.answer + compress;
try {
r.fileInfo = r.fileInfo ? JSON.parse(r.fileInfo) : null;
} catch (error) {
r.fileInfo = {};
}
}
});
return data;
}
/* 推荐图片到对外展示 */
async recDrawImg(body: recDrawImgDto) {
const { id } = body;
const l = await this.chatLogEntity.findOne({
where: { id, type: ChatType.PAINT },
});
if (!l) {
throw new HttpException('你推荐的图片不存在、请检查!', HttpStatus.BAD_REQUEST);
}
const rec = l.rec === 1 ? 0 : 1;
const res = await this.chatLogEntity.update({ id }, { rec });
if (res.affected > 0) {
return `${rec ? '推荐' : '取消推荐'}图片成功!`;
}
throw new HttpException('你操作的图片不存在、请检查!', HttpStatus.BAD_REQUEST);
}
/* 导出为excel对话记录 */
async exportExcel(body: ExportExcelChatlogDto, res: Response) {
const where = { type: ChatType.NORMAL_CHAT };
const { page = 1, size = 30, prompt, email } = body;
prompt && Object.assign(where, { prompt: Like(`%${prompt}%`) });
if (email) {
const user = await this.userEntity.findOne({ where: { email } });
user?.id && Object.assign(where, { userId: user.id });
}
const [rows, count] = await this.chatLogEntity.findAndCount({
order: { id: 'DESC' },
skip: (page - 1) * size,
take: size,
where,
});
const userIds = rows.map(r => r.userId);
const userInfos = await this.userEntity.find({
where: { id: In(userIds) },
});
const data = rows.map(r => {
const userInfo = userInfos.find(u => u.id === r.userId);
return {
username: userInfo ? userInfo.username : '',
email: userInfo ? userInfo.email : '',
prompt: r.prompt,
answer: r.answer,
createdAt: formatDate(r.createdAt),
};
});
const workbook = new excel.Workbook();
const worksheet = workbook.addWorksheet('chatlog');
worksheet.columns = [
{ header: '用户名', key: 'username', width: 20 },
{ header: '用户邮箱', key: 'email', width: 20 },
{ header: '提问时间', key: 'createdAt', width: 20 },
{ header: '提问问题', key: 'prompt', width: 80 },
{ header: '回答答案', key: 'answer', width: 150 },
];
data.forEach(row => worksheet.addRow(row));
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
res.setHeader('Content-Disposition', 'attachment; filename=' + 'chat.xlsx');
await workbook.xlsx.write(res);
res.end();
}
/* 查询所有对话记录 */
async querAllChatLog(params: QuerAllChatLogDto, req: Request) {
const { page = 1, size = 20, userId, prompt, type, model } = params;
// const where = { type: ChatType.NORMAL_CHAT, content: Not('') };
const where: any = {};
userId && Object.assign(where, { userId });
prompt && Object.assign(where, { prompt: Like(`%${prompt}%`) });
type && Object.assign(where, { type });
model && Object.assign(where, { model });
const [rows, count] = await this.chatLogEntity.findAndCount({
order: { id: 'DESC' },
skip: (page - 1) * size,
take: size,
where,
});
const userIds = rows.map(item => item.userId);
const userInfo = await this.userEntity.find({
where: { id: In(userIds) },
select: ['id', 'username', 'email', 'nickname'],
});
rows.forEach((item: any) => {
const { username, email, nickname } = userInfo.find(u => u.id === item.userId) || {};
item.username = username;
item.email = email;
item.nickname = nickname;
});
req.user.role !== 'super' && rows.forEach((t: any) => (t.email = maskEmail(t.email)));
rows.forEach((item: any) => {
!item.email && (item.email = `${item?.userId}@aiweb.com`);
!item.username && (item.username = `游客${item?.userId}`);
});
return { rows, count };
}
/* 查询当前对话的列表 */
async chatList(req: Request, params: ChatListDto) {
const { id } = req.user;
const { groupId } = params;
const where = { userId: id, isDelete: false };
groupId && Object.assign(where, { groupId });
if (groupId) {
const count = await this.chatGroupEntity.count({
where: { isDelete: false },
});
if (count === 0) return [];
}
const list = await this.chatLogEntity.find({ where });
return list.map(item => {
const {
prompt,
role,
answer,
createdAt,
model,
modelName,
type,
status,
action,
drawId,
id,
imageUrl,
fileInfo,
fileUrl,
ttsUrl,
videoUrl,
audioUrl,
customId,
pluginParam,
progress,
modelAvatar,
taskData,
promptReference,
networkSearchResult,
fileVectorResult,
taskId,
reasoning_content,
tool_calls,
content,
} = item;
return {
chatId: id,
dateTime: formatDate(createdAt),
content: content || (role === 'assistant' ? answer : prompt),
reasoningText: reasoning_content,
tool_calls: tool_calls,
modelType: type,
status: status,
action: action,
drawId: drawId,
customId: customId,
role: role,
error: false,
imageUrl: imageUrl || fileInfo || '',
fileUrl: fileUrl,
ttsUrl: ttsUrl,
videoUrl: videoUrl,
audioUrl: audioUrl,
progress,
model: model,
modelName: modelName,
pluginParam: pluginParam,
modelAvatar: modelAvatar,
taskData: taskData,
promptReference: promptReference,
networkSearchResult: networkSearchResult,
fileVectorResult: fileVectorResult,
taskId: taskId,
};
});
}
/* 查询历史对话的列表 */
async chatHistory(groupId: number, rounds: number) {
// Logger.debug(`查询历史对话的列表, groupId: ${groupId}, rounds: ${rounds}`);
if (rounds === 0) {
// Logger.debug('轮次为0返回空数组');
return [];
}
const where = { isDelete: false, groupId: groupId };
// Logger.debug('查询条件:', JSON.stringify(where, null, 2));
const list = await this.chatLogEntity.find({
where,
order: {
createdAt: 'DESC',
},
take: rounds * 2, // 只取最新的rounds条记录
});
// Logger.debug('查询结果:', JSON.stringify(list, null, 2));
const result = list
.map(item => {
const {
role,
content,
answer,
prompt,
imageUrl,
fileInfo,
fileUrl,
ttsUrl,
videoUrl,
audioUrl,
reasoning_content,
tool_calls,
progress,
} = item;
const record = {
role: role,
content: content || (role === 'assistant' ? answer : prompt),
imageUrl: imageUrl || fileInfo || '',
fileUrl: fileUrl,
ttsUrl: ttsUrl,
videoUrl: videoUrl,
audioUrl: audioUrl,
reasoningText: reasoning_content,
tool_calls: tool_calls,
progress,
};
// Logger.debug('处理记录:', JSON.stringify(record, null, 2));
return record;
})
.reverse(); // 添加.reverse()来反转数组,使结果按时间从旧到新排列
// Logger.debug('处理后的结果:', JSON.stringify(result, null, 2));
return result;
}
/* 删除单条对话记录 */
async deleteChatLog(req: Request, body: DelDto) {
const { id: userId } = req.user;
const { id } = body;
const c = await this.chatLogEntity.findOne({ where: { id, userId } });
if (!c) {
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
}
const r = await this.chatLogEntity.update({ id }, { isDelete: true });
if (r.affected > 0) {
return '删除对话记录成功!';
} else {
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
}
}
/* 清空一组对话记录 */
async delByGroupId(req: Request, body: DelByGroupDto) {
const { groupId } = body;
const { id } = req.user;
const g = await this.chatGroupEntity.findOne({
where: { id: groupId, userId: id },
});
if (!g) {
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
}
const r = await this.chatLogEntity.update({ groupId }, { isDelete: true });
if (r.affected > 0) {
return '删除对话记录成功!';
}
if (r.affected === 0) {
throw new HttpException('当前页面已经没有东西可以删除了!', HttpStatus.BAD_REQUEST);
}
}
/* 删除对话组中某条对话及其后的所有对话 */
async deleteChatsAfterId(req: Request, body: any) {
const { id } = body; // 从请求体中获取对话记录 id
const { id: userId } = req.user; // 从请求中获取用户ID
// 查找该对话记录,确保其存在且属于当前用户
const chatLog = await this.chatLogEntity.findOne({ where: { id, userId } });
if (!chatLog) {
// 如果对话记录不存在,抛出异常
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
}
const { groupId } = chatLog; // 获取该对话记录所在的对话组ID
// 删除该对话组中所有 ID 大于等于当前 id 的对话记录
const result = await this.chatLogEntity.update(
{ groupId, id: MoreThanOrEqual(id) },
{ isDelete: true },
);
if (result.affected > 0) {
// 如果更新成功,返回成功消息
return '删除对话记录成功!';
} else {
// 如果没有任何记录被更新,抛出异常
throw new HttpException('当前页面已经没有东西可以删除了!', HttpStatus.BAD_REQUEST);
}
}
/* 查询单个应用的使用记录 */
async byAppId(req: Request, body: QueryByAppIdDto) {
const { id } = req.user;
const { appId, page = 1, size = 10 } = body;
const [rows, count] = await this.chatLogEntity.findAndCount({
where: { userId: id, appId, role: 'assistant' },
order: { id: 'DESC' },
take: size,
skip: (page - 1) * size,
});
return { rows, count };
}
async checkModelLimits(userId: JwtPayload, model: string) {
const ONE_HOUR_IN_MS = 3600 * 1000;
const oneHourAgo = new Date(Date.now() - ONE_HOUR_IN_MS);
try {
// 计算一小时内模型的使用次数
const usageCount = await this.chatLogEntity.count({
where: {
userId: userId.id,
model,
createdAt: MoreThan(oneHourAgo),
},
});
const adjustedUsageCount = Math.ceil(usageCount / 2);
Logger.log(
`用户ID: ${userId.id} 一小时内调用 ${model} 模型 ${adjustedUsageCount + 1}`,
'ChatLogService',
);
// 获取模型的使用限制
let modelInfo;
if (model.startsWith('gpt-4-gizmo')) {
modelInfo = await this.modelsService.getCurrentModelKeyInfo('gpts');
} else {
modelInfo = await this.modelsService.getCurrentModelKeyInfo(model);
}
const modelLimits = Number(modelInfo.modelLimits);
Logger.log(`模型 ${model} 的使用次数限制为 ${modelLimits}`, 'ChatLogService');
// 检查是否超过使用限制
if (adjustedUsageCount > modelLimits) {
return true;
}
return false;
} catch (error) {
Logger.error(
`查询数据库出错 - 用户ID: ${userId.id}, 模型: ${model}, 错误信息: ${error.message}`,
error.stack,
'ChatLogService',
);
}
}
/**
* 查询单条聊天记录
* @param req 请求对象
* @param params 查询参数,包含 chatId
* @returns 返回格式化后的查询结果
*/
async querySingleChat(req: Request, params: QuerySingleChatDto) {
try {
const { chatId } = params;
// 参数验证
if (!chatId) {
return '请输入正确的聊天ID';
}
// 查询单条消息将chatId转换为数字类型
const chatLog = await this.chatLogEntity.findOne({
where: { id: Number(chatId) },
});
// 消息不存在处理
if (!chatLog) {
Logger.warn(`未找到ID为 ${chatId} 的消息记录`, 'ChatLogService');
return '未找到该消息';
}
// 格式化查询结果
const formattedResult = {
id: chatLog.id,
action: chatLog.action || '',
taskData: chatLog.taskData || '',
chatId: chatLog.id, // 保持兼容性
content:
chatLog.content || (chatLog.role === 'assistant' ? chatLog.answer : chatLog.prompt) || '',
reasoningText: chatLog.reasoning_content || '',
tool_calls: chatLog.tool_calls || '',
role: chatLog.role || 'assistant',
status: chatLog.status || 0,
model: chatLog.model || '',
modelName: chatLog.modelName || '',
modelType: chatLog.type || 1,
imageUrl: chatLog.imageUrl || chatLog.fileInfo || '',
fileUrl: chatLog.fileUrl || '',
drawId: chatLog.drawId || '',
customId: chatLog.customId || '',
inversion: chatLog.role === 'user',
createdAt: chatLog.createdAt,
progress: chatLog.progress || 0,
updatedAt: chatLog.updatedAt,
ttsUrl: chatLog.ttsUrl || '',
videoUrl: chatLog.videoUrl || '',
audioUrl: chatLog.audioUrl || '',
taskId: chatLog.taskId || '',
promptReference: chatLog.promptReference || '',
networkSearchResult: chatLog.networkSearchResult || '',
fileVectorResult: chatLog.fileVectorResult || '',
pluginParam: chatLog.pluginParam || '',
modelAvatar: chatLog.modelAvatar || '',
};
// 返回成功结果
return formattedResult;
} catch (error) {
// 详细记录错误信息
Logger.error(`查询单条消息失败: ${error.message}`, error.stack, 'ChatLogService');
// 返回错误响应
return error.message;
}
}
}

View File

@@ -0,0 +1,8 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ChatListDto {
@ApiProperty({ example: 1, description: '对话分组ID', required: false })
@IsOptional()
groupId: number;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class DelDto {
@ApiProperty({ example: 1, description: '对话Id', required: true })
id: number;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class DelByGroupDto {
@ApiProperty({ example: 1, description: '对话组Id', required: true })
groupId: number;
}

View File

@@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
export class ExportExcelChatlogDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({
example: '您好',
description: '用户询问的问题',
required: false,
})
@IsOptional()
prompt: string;
@ApiProperty({
example: 'aiweb@aiweb.com',
description: '用户邮箱',
required: false,
})
@IsOptional()
email: string;
}

View File

@@ -0,0 +1,35 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';
export class QuerAllChatLogDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 99, description: '对话的用户id', required: false })
@IsOptional()
userId: number;
@ApiProperty({
example: '您好',
description: '用户询问的问题',
required: false,
})
@IsOptional()
prompt: string;
@ApiProperty({ example: 'user', description: '身份', required: false })
role: string;
@ApiProperty({ example: '1', description: '类型', required: false })
@IsOptional()
type: string;
@ApiProperty({ example: 'gpt-4o-mini', description: '模型', required: false })
@IsOptional()
model: string;
}

View File

@@ -0,0 +1,36 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QuerAllDrawLogDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({
example: 1,
description: '是否推荐0: 默认 1: 推荐',
required: false,
})
@IsOptional()
rec: number;
@ApiProperty({
example: 99,
description: '生成图片的用户id',
required: false,
})
@IsOptional()
userId: number;
@ApiProperty({
example: 'DALL-E2',
description: '生成图片使用的模型',
required: false,
})
@IsOptional()
model: string;
}

View File

@@ -0,0 +1,16 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QueryByAppIdDto {
@ApiProperty({ example: 1, description: '应用Id', required: true })
@IsOptional()
appId: number;
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
}

View File

@@ -0,0 +1,8 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QuerMyChatLogDto {
@ApiProperty({ example: 'mj', description: '使用的模型', required: false })
@IsOptional()
model: string;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class QuerySingleChatDto {
@ApiProperty({ example: '123', description: '聊天记录ID' })
@IsNotEmpty({ message: '聊天记录ID不能为空' })
chatId: number;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class recDrawImgDto {
@ApiProperty({ example: 1, description: '推荐图片的id' })
id: number;
}

View File

@@ -0,0 +1,97 @@
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { CramiService } from './crami.service';
import { BatchDelCramiDto } from './dto/batchDelCrami.dto';
import { CreatCramiDto } from './dto/createCrami.dto';
import { CreatePackageDto } from './dto/createPackage.dto';
import { DeletePackageDto } from './dto/deletePackage.dto';
import { QuerAllCramiDto } from './dto/queryAllCrami.dto';
import { QuerAllPackageDto } from './dto/queryAllPackage.dto';
import { UpdatePackageDto } from './dto/updatePackage.dto';
import { UseCramiDto } from './dto/useCrami.dto';
@ApiTags('crami')
@Controller('crami')
export class CramiController {
constructor(private readonly cramiService: CramiService) {}
@Get('queryOnePackage')
@ApiOperation({ summary: '查询单个套餐' })
async queryOnePackage(@Query('id') id: number) {
return this.cramiService.queryOnePackage(id);
}
@Get('queryAllPackage')
@ApiOperation({ summary: '查询所有套餐' })
async queryAllPackage(@Query() query: QuerAllPackageDto) {
return this.cramiService.queryAllPackage(query);
}
@Post('createPackage')
@ApiOperation({ summary: '创建套餐' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async createPackage(@Body() body: CreatePackageDto) {
return this.cramiService.createPackage(body);
}
@Post('updatePackage')
@ApiOperation({ summary: '更新套餐' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async updatePackage(@Body() body: UpdatePackageDto) {
return this.cramiService.updatePackage(body);
}
@Post('delPackage')
@ApiOperation({ summary: '删除套餐' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delPackage(@Body() body: DeletePackageDto) {
return this.cramiService.delPackage(body);
}
@Post('createCrami')
@ApiOperation({ summary: '生成卡密' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async createCrami(@Body() body: CreatCramiDto) {
return this.cramiService.createCrami(body);
}
@Post('useCrami')
@ApiOperation({ summary: '使用卡密' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async useCrami(@Req() req: Request, @Body() body: UseCramiDto) {
return this.cramiService.useCrami(req, body);
}
@Get('queryAllCrami')
@ApiOperation({ summary: '查询所有卡密' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
async queryAllCrami(@Query() params: QuerAllCramiDto, @Req() req: Request) {
return this.cramiService.queryAllCrami(params, req);
}
@Post('delCrami')
@ApiOperation({ summary: '删除卡密' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delCrami(@Body('id') id: number) {
return this.cramiService.delCrami(id);
}
@Post('batchDelCrami')
@ApiOperation({ summary: '批量删除卡密' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async batchDelCrami(@Body() body: BatchDelCramiDto) {
return this.cramiService.batchDelCrami(body);
}
}

View File

@@ -0,0 +1,44 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'crami' })
export class CramiEntity extends BaseEntity {
@Column({ unique: true, comment: '存储卡密CDK编码', length: 50 })
code: string;
@Column({
comment: '卡密CDK类型1 普通 | 2 单人可使用一次 ',
default: 1,
})
cramiType: number;
@Column({
comment: '卡密CDK类型 默认套餐类型 | 不填就是自定义类型',
nullable: true,
})
packageId: number;
@Column({ comment: '卡密CDK状态如已使用、未使用等', default: 0 })
status: number;
@Column({ comment: '卡密使用账户用户ID信息', nullable: true })
useId: number;
@Column({
comment: '卡密有效期天数、从生成创建的时候开始计算设为0则不限时间',
default: 0,
})
days: number;
@Column({ comment: '卡密模型3额度', nullable: true })
model3Count: number;
@Column({ comment: '卡密模型4额度', nullable: true })
model4Count: number;
@Column({ comment: '卡密MJ绘画额度', nullable: true })
drawMjCount: number;
@Column({ comment: '卡密应用分类列表', nullable: true, default: '' })
appCats: string;
}

View File

@@ -0,0 +1,37 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { UserEntity } from '../user/user.entity';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { BalanceEntity } from '../userBalance/balance.entity';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { CramiController } from './crami.controller';
import { CramiEntity } from './crami.entity';
import { CramiService } from './crami.service';
import { CramiPackageEntity } from './cramiPackage.entity';
@Global()
@Module({
imports: [
TypeOrmModule.forFeature([
CramiEntity,
CramiPackageEntity,
UserEntity,
BalanceEntity,
AccountLogEntity,
ConfigEntity,
UserBalanceEntity,
FingerprintLogEntity,
ChatLogEntity,
ChatGroupEntity,
]),
],
providers: [CramiService, UserBalanceService],
controllers: [CramiController],
exports: [CramiService],
})
export class CramiModule {}

View File

@@ -0,0 +1,264 @@
import { RechargeType } from '@/common/constants/balance.constant';
import { generateCramiCode, maskCrami, maskEmail } from '@/common/utils';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Request } from 'express';
import { In, LessThanOrEqual, Like, MoreThan, Not, Repository } from 'typeorm';
import { UserEntity } from '../user/user.entity';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { CramiEntity } from './crami.entity';
import { CramiPackageEntity } from './cramiPackage.entity';
import { BatchDelCramiDto } from './dto/batchDelCrami.dto';
import { CreatCramiDto } from './dto/createCrami.dto';
import { CreatePackageDto } from './dto/createPackage.dto';
import { DeletePackageDto } from './dto/deletePackage.dto';
import { QuerAllCramiDto } from './dto/queryAllCrami.dto';
import { QuerAllPackageDto } from './dto/queryAllPackage.dto';
import { UseCramiDto } from './dto/useCrami.dto';
@Injectable()
export class CramiService {
constructor(
@InjectRepository(CramiEntity)
private readonly cramiEntity: Repository<CramiEntity>,
@InjectRepository(CramiPackageEntity)
private readonly cramiPackageEntity: Repository<CramiPackageEntity>,
@InjectRepository(UserEntity)
private readonly userEntity: Repository<UserEntity>,
private readonly userBalanceService: UserBalanceService,
) {}
/* 查询单个套餐 */
async queryOnePackage(id) {
return await this.cramiPackageEntity.findOne({ where: { id } });
}
/* 查询所有套餐 */
async queryAllPackage(query: QuerAllPackageDto) {
try {
const { page = 1, size = 10, name, status, type } = query;
const where = {};
name && Object.assign(where, { name: Like(`%${name}%`) });
status && Object.assign(where, { status });
if (type) {
if (type > 0) {
Object.assign(where, { days: MoreThan(0) });
} else {
Object.assign(where, { days: LessThanOrEqual(0) });
}
}
const [rows, count] = await this.cramiPackageEntity.findAndCount({
skip: (page - 1) * size,
take: size,
where,
order: { order: 'DESC' },
});
return { rows, count };
} catch (error) {
console.log('error: ', error);
}
}
/* 创建套餐 */
async createPackage(body: CreatePackageDto) {
const { name, weight } = body;
const p = await this.cramiPackageEntity.findOne({
where: [{ name }, { weight }],
});
if (p) {
throw new HttpException('套餐名称或套餐等级重复、请检查!', HttpStatus.BAD_REQUEST);
}
try {
return await this.cramiPackageEntity.save(body);
} catch (error) {
console.log('error: ', error);
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
}
/* 更新套餐 E */
async updatePackage(body) {
const { id, name, weight } = body;
const op = await this.cramiPackageEntity.findOne({ where: { id } });
if (!op) {
throw new HttpException('当前套餐不存在、请检查你的输入参数!', HttpStatus.BAD_REQUEST);
}
const count = await this.cramiPackageEntity.count({
where: [
{ name, id: Not(id) },
{ weight, id: Not(id) },
],
});
if (count) {
throw new HttpException('套餐名称或套餐等级重复、请检查!', HttpStatus.BAD_REQUEST);
}
const res = await this.cramiPackageEntity.update({ id }, body);
if (res.affected > 0) {
return '更新套餐成功!';
} else {
throw new HttpException('更新套餐失败、请重试!', HttpStatus.BAD_REQUEST);
}
}
/* 删除套餐 */
async delPackage(body: DeletePackageDto) {
const { id } = body;
const count = await this.cramiEntity.count({ where: { packageId: id } });
if (count) {
throw new HttpException(
'当前套餐下存在卡密、请先删除卡密后才可删除套餐!',
HttpStatus.BAD_REQUEST,
);
}
return await this.cramiPackageEntity.delete({ id });
}
/* 生成卡密 */
async createCrami(body: CreatCramiDto) {
const { packageId, count = 1 } = body;
/* 创建有套餐的卡密 */
if (packageId) {
const pkg = await this.cramiPackageEntity.findOne({
where: { id: packageId },
});
if (!pkg) {
throw new HttpException(
'当前套餐不存在、请确认您选择的套餐是否存在!',
HttpStatus.BAD_REQUEST,
);
}
const { days = -1, model3Count = 0, model4Count = 0, drawMjCount = 0, appCats = '' } = pkg;
const baseCrami = {
packageId,
days,
model3Count,
model4Count,
drawMjCount,
appCats,
};
return await this.generateCrami(baseCrami, count);
}
/* 创建自定义的卡密 */
if (!packageId) {
const { model3Count = 0, model4Count = 0, drawMjCount = 0 } = body;
if ([model3Count, model4Count, drawMjCount].every(v => !v)) {
throw new HttpException('自定义卡密必须至少一项余额不为0', HttpStatus.BAD_REQUEST);
}
const baseCrami = { days: -1, model3Count, model4Count, drawMjCount };
return await this.generateCrami(baseCrami, count);
}
}
/* 创建卡密 */
async generateCrami(cramiInfo, count: number) {
const cramiList = [];
for (let i = 0; i < count; i++) {
const code = generateCramiCode();
const crami = this.cramiEntity.create({ ...cramiInfo, code });
cramiList.push(crami);
}
return await this.cramiEntity.save(cramiList);
}
/* 使用卡密 */
async useCrami(req: Request, body: UseCramiDto) {
const { id } = req.user;
const crami = await this.cramiEntity.findOne({
where: { code: body.code },
});
if (!crami) {
throw new HttpException(
'当前卡密不存在、请确认您输入的卡密是否正确!',
HttpStatus.BAD_REQUEST,
);
}
const {
status,
days = -1,
model3Count = 0,
model4Count = 0,
drawMjCount = 0,
packageId,
appCats,
} = crami;
if (status === 1) {
throw new HttpException(
'当前卡密已被使用、请确认您输入的卡密是否正确!',
HttpStatus.BAD_REQUEST,
);
}
const balanceInfo = {
model3Count,
model4Count,
drawMjCount,
packageId,
appCats,
};
await this.userBalanceService.addBalanceToUser(id, { ...balanceInfo }, days);
await this.userBalanceService.saveRecordRechargeLog({
userId: id,
rechargeType: RechargeType.PACKAGE_GIFT,
model3Count,
model4Count,
drawMjCount,
days,
});
await this.cramiEntity.update({ code: body.code }, { useId: id, status: 1 });
return '使用卡密成功';
}
/* 查询所有卡密 */
async queryAllCrami(params: QuerAllCramiDto, req: Request) {
const { page = 1, size = 10, status, useId } = params;
const where = {};
status && Object.assign(where, { status });
useId && Object.assign(where, { useId });
const [rows, count] = await this.cramiEntity.findAndCount({
skip: (page - 1) * size,
take: size,
order: { createdAt: 'DESC' },
where,
});
const userIds = rows.map(t => t.useId);
const packageIds = rows.map(t => t.packageId);
const userInfos = await this.userEntity.find({
where: { id: In(userIds) },
});
const packageInfos = await this.cramiPackageEntity.find({
where: { id: In(packageIds) },
});
rows.forEach((t: any) => {
t.username = userInfos.find(u => u.id === t.useId)?.username;
t.email = userInfos.find(u => u.id === t.useId)?.email;
t.packageName = packageInfos.find(p => p.id === t.packageId)?.name;
});
req.user.role !== 'super' && rows.forEach((t: any) => (t.email = maskEmail(t.email)));
req.user.role !== 'super' && rows.forEach((t: any) => (t.code = maskCrami(t.code)));
return { rows, count };
}
/* 删除卡密 */
async delCrami(id) {
const c = await this.cramiEntity.findOne({ where: { id } });
if (!c) {
throw new HttpException(
'当前卡密不存在、请确认您要删除的卡密是否存在!',
HttpStatus.BAD_REQUEST,
);
}
if (c.status === 1) {
throw new HttpException('当前卡密已被使用、已使用的卡密禁止删除!', HttpStatus.BAD_REQUEST);
}
return await this.cramiEntity.delete({ id });
}
async batchDelCrami(body: BatchDelCramiDto) {
const { ids } = body;
const res = await this.cramiEntity.delete(ids);
if (res.affected > 0) {
return '删除卡密成功!';
} else {
throw new HttpException('删除卡密失败、请重试!', HttpStatus.BAD_REQUEST);
}
}
}

View File

@@ -0,0 +1,44 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'crami_package' })
export class CramiPackageEntity extends BaseEntity {
@Column({ unique: true, comment: '套餐名称' })
name: string;
@Column({ comment: '套餐介绍详细信息' })
des: string;
@Column({ comment: '套餐封面图片', nullable: true })
coverImg: string;
@Column({ comment: '套餐价格¥', type: 'decimal', scale: 2, precision: 10 })
price: number;
@Column({ comment: '套餐排序、数字越大越靠前', default: 100 })
order: number;
@Column({ comment: '套餐是否启用中 0禁用 1启用', default: 1 })
status: number;
@Column({ comment: '套餐权重、数字越大表示套餐等级越高越贵', unique: true })
weight: number;
@Column({
comment: '卡密有效期天数、从使用的时候开始计算,设为-1则不限时间',
default: 0,
})
days: number;
@Column({ comment: '套餐包含的模型3数量', default: 0, nullable: true })
model3Count: number;
@Column({ comment: '套餐包含的模型4数量', default: 0, nullable: true })
model4Count: number;
@Column({ comment: '套餐包含的MJ绘画数量', default: 0, nullable: true })
drawMjCount: number;
@Column({ comment: '套餐包含的应用分类列表', default: '', nullable: true })
appCats: string;
}

View File

@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsArray } from 'class-validator';
export class BatchDelCramiDto {
@ApiProperty({ example: 1, description: '要删除的套餐Ids', required: true })
@IsArray({ message: '参数类型为数组' })
@ArrayMinSize(1, { message: '最短长度为1' })
ids: number[];
}

View File

@@ -0,0 +1,31 @@
import { IsOptional, IsNumber, Max, Min } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreatCramiDto {
@ApiProperty({ example: 1, description: '套餐类型', required: true })
@IsNumber({}, { message: '套餐类型必须是number' })
@IsOptional()
packageId: number;
@ApiProperty({ example: 1, description: '单次生成卡密数量' })
@IsNumber({}, { message: '创建卡密的张数数量' })
@Max(50, { message: '单次创建卡密的张数数量不能超过50张' })
@Min(1, { message: '单次创建卡密的张数数量不能少于1张' })
@IsOptional()
count: number;
@ApiProperty({ example: 0, description: '卡密携带模型3额度' })
@IsNumber({}, { message: '卡密携带的余额必须是number' })
@IsOptional()
model3Count: number;
@ApiProperty({ example: 100, description: '卡密携带模型4额度' })
@IsNumber({}, { message: '卡密携带额度类型必须是number' })
@IsOptional()
model4Count: number;
@ApiProperty({ example: 3, description: '卡密携带MJ绘画额度' })
@IsNumber({}, { message: '卡密携带额度类型必须是number' })
@IsOptional()
drawMjCount: number;
}

View File

@@ -0,0 +1,77 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsDefined, IsIn, IsNumber, IsOptional } from 'class-validator';
export class CreatePackageDto {
@ApiProperty({
example: '基础套餐100次卡',
description: '套餐名称',
required: true,
})
@IsDefined({ message: '套餐名称是必传参数' })
name: string;
@ApiProperty({
example:
'这是一个100次对话余额的套餐我们将为您额外赠送3次绘画余额活动期间我们将在套餐基础上额外赠送您十次对话余额和1次绘画余额',
description: '套餐详情描述',
required: true,
})
@IsDefined({ message: '套餐描述是必传参数' })
des: string;
@ApiProperty({ example: 7, default: 0, description: '套餐等级设置' })
@IsNumber({}, { message: '套餐等级权重必须为数字' })
weight: number;
@ApiProperty({
example: 1,
description: '套餐扣费类型 1按次数 2按Token',
required: true,
})
deductionType: number;
@ApiProperty({ example: 'https://xxxx.png', description: '套餐封面图片' })
@IsOptional()
coverImg: string;
@Transform(({ value }) => parseFloat(value))
price: number;
@ApiProperty({ example: 100, description: '套餐排序、数字越大越靠前' })
@IsOptional()
order?: number;
@ApiProperty({
example: 1,
description: '套餐状态 0禁用 1启用',
required: true,
})
@IsNumber({}, { message: '套餐状态必须是Number' })
@IsIn([0, 1], { message: '套餐状态错误' })
status: number;
@ApiProperty({
example: 7,
default: 0,
description: '套餐有效期 -1为永久不过期',
})
@IsNumber({}, { message: '套餐有效期天数类型必须是number' })
days: number;
@ApiProperty({ example: 1000, default: 0, description: '模型3对话次数' })
@IsNumber({}, { message: '模型3对话次数必须是number类型' })
model3Count: number;
@ApiProperty({ example: 10, default: 0, description: '模型4对话次数' })
@IsNumber({}, { message: '模型4对话次数必须是number类型' })
model4Count: number;
@ApiProperty({ example: 10, default: 0, description: 'MJ绘画次数' })
@IsNumber({}, { message: 'MJ绘画次数必须是number类型' })
drawMjCount: number;
@ApiProperty({ example: '1,2,3', description: '套餐包含的应用分类' })
@IsOptional()
appCats?: string;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class DeletePackageDto {
@ApiProperty({ example: 1, description: '要修改的套餐Id', required: true })
@IsNumber({}, { message: '套餐ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,24 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QuerAllCramiDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 1, description: '使用人Id', required: false })
@IsOptional()
useId: number;
@ApiProperty({
example: 1,
description: '卡密状态 0未使用 1已消费',
required: false,
})
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,32 @@
import { IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QuerAllPackageDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'name', description: '套餐名称', required: false })
@IsOptional()
name: string;
@ApiProperty({
example: 1,
description: '套餐状态 0禁用 1启用',
required: false,
})
@IsOptional()
status: number;
@ApiProperty({
example: 1,
description: '套餐类型 -1永久套餐 1限时套餐',
required: false,
})
@IsOptional()
type: number;
}

View File

@@ -0,0 +1,9 @@
import { IsNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { CreatePackageDto } from './createPackage.dto';
export class UpdatePackageDto extends CreatePackageDto {
@ApiProperty({ example: 1, description: '要修改的套餐Id', required: true })
@IsNumber({}, { message: '套餐ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,12 @@
import { IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UseCramiDto {
@ApiProperty({
example: 'ffar684rv254fs4f',
description: '卡密信息',
required: true,
})
@IsDefined({ message: '套餐名称是必传参数' })
code: string;
}

View File

@@ -0,0 +1,84 @@
import { Logger, Module, OnModuleInit } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSource, DataSourceOptions } from 'typeorm';
import { DatabaseService } from './database.service';
// Import all entities explicitly
import { AppEntity } from '../app/app.entity';
import { AppCatsEntity } from '../app/appCats.entity';
import { UserAppsEntity } from '../app/userApps.entity';
import { AutoReplyEntity } from '../autoReply/autoReply.entity';
import { BadWordsEntity } from '../badWords/badWords.entity';
import { ViolationLogEntity } from '../badWords/violationLog.entity';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { CramiEntity } from '../crami/crami.entity';
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { ModelsEntity } from '../models/models.entity';
import { OrderEntity } from '../order/order.entity';
import { PluginEntity } from '../plugin/plugin.entity';
import { Share } from '../share/share.entity';
import { SigninEntity } from '../signin/signIn.entity';
import { UserEntity } from '../user/user.entity';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { BalanceEntity } from '../userBalance/balance.entity';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { VerificationEntity } from '../verification/verification.entity';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: () =>
({
type: 'mysql',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10),
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_DATABASE,
// entities: [__dirname + '/../**/*.entity{.ts,.js}'], // <-- Remove glob pattern
entities: [
// <-- Use explicit array of imported classes
Share,
AutoReplyEntity,
CramiEntity,
CramiPackageEntity,
BadWordsEntity,
ChatGroupEntity,
VerificationEntity,
SigninEntity,
ViolationLogEntity,
ModelsEntity,
UserEntity,
AccountLogEntity,
FingerprintLogEntity,
BalanceEntity,
UserBalanceEntity,
PluginEntity,
ConfigEntity,
ChatLogEntity,
UserAppsEntity,
AppCatsEntity,
AppEntity,
OrderEntity,
],
synchronize: false,
logging: false,
charset: 'utf8mb4',
timezone: '+08:00',
} as DataSourceOptions),
}),
],
providers: [DatabaseService],
})
export class DatabaseModule implements OnModuleInit {
constructor(private readonly connection: DataSource) {}
private readonly logger = new Logger(DatabaseModule.name);
async onModuleInit(): Promise<void> {
const { database } = this.connection.options;
this.logger.log(`Your MySQL database named ${database} has been connected`);
}
}

View File

@@ -0,0 +1,209 @@
import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { Connection } from 'typeorm';
interface UserInfo {
username: string;
password: string;
status: number;
email: string;
sex: number;
role: string;
}
@Injectable()
export class DatabaseService implements OnModuleInit {
constructor(private connection: Connection) {}
async onModuleInit() {
await this.checkSuperAdmin();
await this.checkSiteBaseConfig();
}
/* 默认创建一个超级管理员账户 */
async checkSuperAdmin() {
const user = await this.connection.query(`SELECT * FROM users WHERE role = 'super'`);
if (!user || user.length === 0) {
const superPassword = bcrypt.hashSync('123456', 10);
// const adminPassword = bcrypt.hashSync('123456', 10);
const superEmail = 'super';
// const adminEmail = 'admin';
const superUserinfo = {
username: 'super',
password: superPassword,
status: 1,
email: superEmail,
sex: 1,
role: 'super',
};
// const adminUserinfo = {
// username: 'admin',
// password: adminPassword,
// status: 0,
// email: adminEmail,
// sex: 1,
// role: 'admin',
// };
await this.createDefaultUser(superUserinfo);
// await this.createDefaultUser(adminUserinfo);
}
}
/* 初始化创建 超级管理员和管理员 */
async createDefaultUser(userInfo: UserInfo) {
try {
const { username, password, status, email, role } = userInfo;
const user = await this.connection.query(
`INSERT INTO users (username, password, status, email, role) VALUES ('${username}', '${password}', '${status}', '${email}', '${role}')`,
);
const userId = user.insertId;
await this.connection.query(
`INSERT INTO balance (userId, balance, usesLeft, paintCount) VALUES ('${userId}', 0, 1000, 100)`,
);
Logger.log(
`初始化创建${role}用户成功、用户名为[${username}]、初始密码为[${
username === 'super' ? 'super' : '123456'
}] ==============> 请注意查阅`,
'DatabaseService',
);
} catch (error) {
console.log('error: ', error);
throw new HttpException('创建默认超级管理员失败!', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/* 检测有没有网站基础配置 */
async checkSiteBaseConfig() {
const keys = ['siteName', 'robotAvatar'];
const result = await this.connection.query(`
SELECT COUNT(*) AS count FROM config WHERE \`configKey\` IN (${keys.map(k => `'${k}'`).join(',')})
`);
const count = parseInt(result[0].count);
if (count === 0) {
await this.createBaseSiteConfig();
}
}
/* 创建基础的网站数据 */
async createBaseSiteConfig() {
try {
const code = ``;
const noticeInfo = `
#### AIWeb 欢迎您
- 欢迎使用 AIWeb
- 初始管理员账号密码 super 123456 【前台后台登录都可以修改】
- 初始预览账号密码 admin 123456 【为后台查看账号 仅可查看部分非敏感数据】
`;
const defaultConfig = [
{ configKey: 'siteName', configVal: '', public: 1, encrypt: 0 },
{ configKey: 'robotAvatar', configVal: '', public: 1, encrypt: 0 },
{
configKey: 'userDefaultAvatar',
configVal: '',
public: 0,
encrypt: 0,
},
{ configKey: 'baiduCode', configVal: code, public: 1, encrypt: 0 },
{ configKey: 'baiduSiteId', configVal: '', public: 0, encrypt: 0 },
{
configKey: 'baiduToken',
configVal: '',
public: 0,
encrypt: 0,
},
{
configKey: 'openaiBaseUrl',
configVal: 'https://api.lightai.io',
public: 0,
encrypt: 0,
},
{ configKey: 'openaiBaseKey', configVal: 'sk-', public: 0, encrypt: 0 },
{ configKey: 'noticeInfo', configVal: noticeInfo, public: 1, encrypt: 0 },
{
configKey: 'registerSendStatus',
configVal: '1',
public: 1,
encrypt: 0,
},
{
configKey: 'registerSendModel3Count',
configVal: '30',
public: 1,
encrypt: 0,
},
{
configKey: 'registerSendModel4Count',
configVal: '3',
public: 1,
encrypt: 0,
},
{
configKey: 'registerSendDrawMjCount',
configVal: '3',
public: 1,
encrypt: 0,
},
{
configKey: 'firstRegisterSendStatus',
configVal: '1',
public: 1,
encrypt: 0,
},
{
configKey: 'firstRegisterSendRank',
configVal: '500',
public: 1,
encrypt: 0,
},
{
configKey: 'firstRegisterSendModel3Count',
configVal: '10',
public: 1,
encrypt: 0,
},
{
configKey: 'firstRegisterSendModel4Count',
configVal: '10',
public: 1,
encrypt: 0,
},
{
configKey: 'firstRegisterSendDrawMjCount',
configVal: '10',
public: 1,
encrypt: 0,
},
{ configKey: 'isVerifyEmail', configVal: '1', public: 1, encrypt: 0 },
{ configKey: 'model3Name', configVal: '普通积分', public: 1, encrypt: 0 },
{ configKey: 'model4Name', configVal: '高级积分', public: 1, encrypt: 0 },
{ configKey: 'drawMjName', configVal: '绘画积分', public: 1, encrypt: 0 },
{
configKey: 'drawingStyles',
configVal:
'油画风格,像素风格,赛博朋克,动漫,日系,超现实主义,油画,卡通,插画,海报,写实,扁平,中国风,水墨画,唯美二次元,印象派,炫彩插画,像素艺术,艺术创想,色彩主义,数字艺术',
public: 1,
encrypt: 0,
},
];
const res = await this.connection.query(
`INSERT INTO config (configKey, configVal, public, encrypt) VALUES ${defaultConfig
.map(
d =>
`('${d.configKey}', '${d.configVal.replace(/'/g, "\\'")}', '${d.public}', '${
d.encrypt
}')`,
)
.join(', ')}`,
);
Logger.log(
`初始化网站配置信息成功、如您需要修改网站配置信息,请前往管理系统系统配置设置 ==============> 请注意查阅`,
'DatabaseService',
);
} catch (error) {
console.log('error: ', error);
throw new HttpException('创建默认网站配置失败!', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -0,0 +1,280 @@
import { Logger } from '@nestjs/common';
import { config as loadEnv } from 'dotenv';
import * as mysql from 'mysql2/promise';
import { DataSource, DataSourceOptions } from 'typeorm';
import { AppEntity } from '../app/app.entity';
import { AppCatsEntity } from '../app/appCats.entity';
import { UserAppsEntity } from '../app/userApps.entity';
import { AutoReplyEntity } from '../autoReply/autoReply.entity';
import { BadWordsEntity } from '../badWords/badWords.entity';
import { ViolationLogEntity } from '../badWords/violationLog.entity';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { CramiEntity } from '../crami/crami.entity';
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { ModelsEntity } from '../models/models.entity';
import { OrderEntity } from '../order/order.entity';
import { PluginEntity } from '../plugin/plugin.entity';
import { Share } from '../share/share.entity';
import { SigninEntity } from '../signin/signIn.entity';
import { UserEntity } from '../user/user.entity';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { BalanceEntity } from '../userBalance/balance.entity';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { VerificationEntity } from '../verification/verification.entity';
loadEnv();
const dataSourceOptions: DataSourceOptions = {
type: 'mysql',
port: parseInt(process.env.DB_PORT, 10),
host: process.env.DB_HOST,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_DATABASE,
entities: [
Share,
AutoReplyEntity,
CramiEntity,
CramiPackageEntity,
BadWordsEntity,
ChatGroupEntity,
VerificationEntity,
SigninEntity,
ViolationLogEntity,
ModelsEntity,
UserEntity,
AccountLogEntity,
FingerprintLogEntity,
BalanceEntity,
UserBalanceEntity,
PluginEntity,
ConfigEntity,
ChatLogEntity,
UserAppsEntity,
AppCatsEntity,
AppEntity,
OrderEntity,
],
synchronize: false, // 禁用自动同步,改为根据情况动态开启
charset: 'utf8mb4',
timezone: '+08:00',
};
/**
* 通用的列类型迁移函数
* @param tableName 表名
* @param columnName 列名
* @param targetType 目标数据类型
* @param conn 数据库连接
* @returns 是否进行了迁移操作
*/
async function migrateColumnType(
tableName: string,
columnName: string,
targetType: string,
conn: mysql.Connection,
): Promise<boolean> {
try {
// 1. 检查表是否存在
const [tables] = (await conn.execute(
`SHOW TABLES LIKE '${tableName}'`,
)) as mysql.RowDataPacket[][];
if (tables.length === 0) {
Logger.log(`${tableName} 不存在,跳过 ${columnName} 列的迁移`, 'Database');
return false;
}
// 2. 检查列是否存在和当前数据类型
const [columns] = (await conn.execute(
`SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
[process.env.DB_DATABASE, tableName, columnName],
)) as mysql.RowDataPacket[][];
if (columns.length === 0) {
Logger.log(`${tableName} 中不存在 ${columnName} 列,跳过迁移`, 'Database');
return false;
}
const currentType = columns[0].DATA_TYPE.toLowerCase();
if (currentType === targetType.toLowerCase()) {
Logger.log(`${tableName} 中的 ${columnName} 列已经是 ${targetType} 类型`, 'Database');
return false;
}
// 3. 处理外键约束和索引(如果有)
const [constraints] = (await conn.execute(
`SELECT k.CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
WHERE k.TABLE_SCHEMA = ? AND k.TABLE_NAME = ? AND k.COLUMN_NAME = ?`,
[process.env.DB_DATABASE, tableName, columnName],
)) as mysql.RowDataPacket[][];
// 处理外键约束
for (const constraint of constraints) {
if (constraint.CONSTRAINT_NAME) {
Logger.log(
`发现 ${columnName} 列有约束: ${constraint.CONSTRAINT_NAME},尝试删除`,
'Database',
);
try {
await conn.execute(
`ALTER TABLE ${tableName} DROP FOREIGN KEY ${constraint.CONSTRAINT_NAME}`,
);
Logger.log(`成功删除 ${columnName} 外键约束`, 'Database');
} catch (constraintError) {
Logger.error(`删除外键约束失败: ${constraintError.message}`, 'Database');
}
}
}
// 检查并处理索引
const [indexes] = (await conn.execute(
`SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
[process.env.DB_DATABASE, tableName, columnName],
)) as mysql.RowDataPacket[][];
for (const idx of indexes) {
if (idx.INDEX_NAME !== 'PRIMARY') {
// 不处理主键
Logger.log(
`发现 ${columnName} 是索引 ${idx.INDEX_NAME} 的一部分,尝试删除索引`,
'Database',
);
try {
await conn.execute(`ALTER TABLE ${tableName} DROP INDEX ${idx.INDEX_NAME}`);
Logger.log(`成功删除索引 ${idx.INDEX_NAME}`, 'Database');
} catch (indexError) {
Logger.error(`删除索引失败: ${indexError.message}`, 'Database');
}
}
}
// 4. 执行列类型迁移
Logger.log(
`开始将 ${tableName} 表中的 ${columnName} 列类型从 ${currentType} 升级为 ${targetType}`,
'Database',
);
await conn.execute(`ALTER TABLE ${tableName} MODIFY COLUMN \`${columnName}\` ${targetType}`);
Logger.log(`${tableName} 表中的 ${columnName} 列类型已成功升级为 ${targetType}`, 'Database');
return true;
} catch (error) {
Logger.error(`迁移 ${tableName}.${columnName} 列类型时出错:`, error, 'Database');
return false;
}
}
/**
* 执行所有数据库迁移
*/
async function runAllMigrations() {
const conn = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
port: parseInt(process.env.DB_PORT, 10),
database: process.env.DB_DATABASE,
});
try {
// 1. TEXT类型迁移
try {
await migrateColumnType('config', 'configVal', 'TEXT', conn);
} catch (error) {
Logger.log(`迁移config表configVal列时跳过: ${error.message}`, 'Database');
}
try {
await migrateColumnType('app', 'coverImg', 'TEXT', conn);
} catch (error) {
Logger.log(`迁移app表coverImg列时跳过: ${error.message}`, 'Database');
}
// 2. catId迁移
try {
const appCatIdMigrated = await migrateColumnType('app', 'catId', 'TEXT', conn);
if (appCatIdMigrated) {
// 如果app表中的catId被迁移了考虑更新所有现有数据
const [apps] = (await conn.execute(`SELECT id, catId FROM app`)) as mysql.RowDataPacket[][];
for (const app of apps) {
if (app.catId !== null && app.catId !== undefined) {
await conn.execute(`UPDATE app SET catId = ? WHERE id = ?`, [
app.catId.toString(),
app.id,
]);
}
}
}
} catch (error) {
Logger.log(`迁移app表catId列时跳过: ${error.message}`, 'Database');
}
// 3. MEDIUMTEXT类型迁移
const chatlogColumns = ['content', 'fileVectorResult'];
for (const column of chatlogColumns) {
try {
await migrateColumnType('chatlog', column, 'MEDIUMTEXT', conn);
} catch (error) {
Logger.log(`迁移chatlog表${column}列时跳过: ${error.message}`, 'Database');
}
}
} finally {
await conn.end();
}
}
export async function initDatabase() {
try {
Logger.log('开始数据库初始化流程', 'Database');
// 执行所有迁移操作
Logger.log('执行数据库迁移操作', 'Database');
await runAllMigrations();
Logger.log('数据迁移操作完成,现在执行同步确保所有新表和字段存在', 'Database');
// 先使用禁用同步的连接,确保不会重置迁移后的数据
const dataSource = new DataSource(dataSourceOptions);
await dataSource.initialize();
Logger.log('已连接到数据库,准备同步结构', 'Database');
// 关闭初始连接
if (dataSource.isInitialized) {
await dataSource.destroy();
}
// 创建启用同步的连接,确保所有新表和字段被创建
const syncOptions: DataSourceOptions = {
...dataSourceOptions,
synchronize: true,
};
const syncDataSource = new DataSource(syncOptions);
await syncDataSource.initialize();
Logger.log('数据库结构同步完成', 'Database');
// 关闭同步连接
if (syncDataSource.isInitialized) {
await syncDataSource.destroy();
}
Logger.log('数据库初始化成功完成', 'Database');
} catch (error) {
Logger.error(`数据库初始化错误: ${error.message}`, 'Database');
if (error instanceof SyntaxError) {
Logger.error(
`语法错误详情: ${JSON.stringify({
name: error.name,
message: error.message,
stack: error.stack,
})}`,
'Database',
);
}
}
}

View File

@@ -0,0 +1,26 @@
import { BaseEntity } from 'src/common/entity/baseEntity';
import { Column, Entity } from 'typeorm';
@Entity({ name: 'config' })
export class ConfigEntity extends BaseEntity {
@Column({ length: 255, comment: '配置名称', nullable: true })
configKey: string;
@Column({ type: 'text', comment: '配置内容', nullable: true })
configVal: string;
@Column({
default: 0,
comment: '配置是否公开,公开内容对前端项目展示 0不公开 1公开',
})
public: number;
@Column({
default: 0,
comment: '配置是否加密加密内容仅仅super权限可看 0不加 1加',
})
encrypt: number;
@Column({ default: 1, comment: '配置状态 0:关闭 1启用' })
status: number;
}

View File

@@ -0,0 +1,14 @@
import { IsArray, ArrayNotEmpty } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
export class QueryConfigDto {
@ApiProperty({
example: ['siteName', 'qqNumber'],
description: '想要查询的配置key',
})
@IsArray()
@ArrayNotEmpty()
@Type(() => String)
keys: string[];
}

View File

@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, ValidateNested } from 'class-validator';
interface KeyValue {
configKey: string;
configVal: any;
}
export class SetConfigDto {
@ApiProperty({
example: [{ configKey: 'siteName', configVal: 'AIWeb' }],
description: '设置配置信息',
})
@IsArray()
@ArrayNotEmpty()
@ValidateNested({ each: true })
@Type(() => Object)
settings: KeyValue[];
}

View File

@@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';
interface KeyValue {
configKey: string;
configVal: any;
infoKey: string;
}
export class SetConfigCustomDto {
@ApiProperty({
example: { configKey: 'siteName', configVal: 'AIWeb', infoKey: 'AIWeb' },
description: '设置更新配置信息',
})
@ValidateNested({ each: true })
@Type(() => Object)
settings: KeyValue;
}

View File

@@ -0,0 +1,50 @@
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { QueryConfigDto } from './dto/queryConfig.dto';
import { SetConfigDto } from './dto/setConfig.dto';
import { GlobalConfigService } from './globalConfig.service';
@ApiTags('config')
@Controller('config')
export class GlobalConfigController {
constructor(private readonly globalConfigService: GlobalConfigService) {}
@ApiOperation({ summary: '查询所有配置' })
@Get('queryAll')
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
queryAllConfig(@Req() req: Request) {
return this.globalConfigService.queryAllConfig(req);
}
@ApiOperation({ summary: '查询前端网站的所有配置' })
@Get('queryFront')
queryFrontConfig(@Query() query: any, @Req() req: Request) {
return this.globalConfigService.queryFrontConfig(query, req);
}
@ApiOperation({ summary: '查询所有配置' })
@Post('query')
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
queryConfig(@Body() body: QueryConfigDto, @Req() req: Request) {
return this.globalConfigService.queryConfig(body, req);
}
@ApiOperation({ summary: '设置配置信息' })
@Post('set')
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
setConfig(@Body() body: SetConfigDto) {
return this.globalConfigService.setConfig(body);
}
@ApiOperation({ summary: '用户端查询公告信息' })
@Get('notice')
queryNotice() {
return this.globalConfigService.queryNotice();
}
}

View File

@@ -0,0 +1,15 @@
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { ConfigEntity } from './config.entity';
import { GlobalConfigController } from './globalConfig.controller';
import { GlobalConfigService } from './globalConfig.service';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([ConfigEntity, ChatLogEntity])],
providers: [GlobalConfigService],
controllers: [GlobalConfigController],
exports: [GlobalConfigService],
})
export class GlobalConfigModule {}

View File

@@ -0,0 +1,655 @@
import { formatUrl, hideString } from '@/common/utils';
import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import axios from 'axios';
import { Request } from 'express';
import * as fs from 'fs';
import { In, Repository } from 'typeorm';
import { ChatLogEntity } from './../chatLog/chatLog.entity';
import { ModelsService } from './../models/models.service';
import { ConfigEntity } from './config.entity';
import { QueryConfigDto } from './dto/queryConfig.dto';
import { SetConfigDto } from './dto/setConfig.dto';
const packageJsonContent = fs.readFileSync('package.json', 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
const version = packageJson.version;
console.log(' current use version in ------>: ', version);
@Injectable()
export class GlobalConfigService implements OnModuleInit {
constructor(
@InjectRepository(ConfigEntity)
private readonly configEntity: Repository<ConfigEntity>,
@InjectRepository(ChatLogEntity)
private readonly chatLogEntity: Repository<ChatLogEntity>,
private readonly modelsService: ModelsService,
) {}
private globalConfigs: any = {};
private wechatAccessToken: string;
private wechatJsapiTicket: string;
private oldWechatAccessToken: string;
private oldWechatJsapiTicket: string;
async onModuleInit() {
await this.initGetAllConfig();
}
/* 对外提供给其他service */
async getConfigs(configKey: string[]) {
if (configKey.length === 0) return;
/* 微信token特殊处理 */
if (configKey.includes('wechatAccessToken') && configKey.length === 1) {
return this.wechatAccessToken;
}
if (configKey.includes('wechatJsapiTicket') && configKey.length === 1) {
return this.wechatJsapiTicket;
}
/* 旧微信token特殊处理 */
if (configKey.includes('oldWechatAccessToken') && configKey.length === 1) {
return this.oldWechatAccessToken;
}
if (configKey.includes('oldWechatJsapiTicket') && configKey.length === 1) {
return this.oldWechatJsapiTicket;
}
if (configKey.length === 1) {
return this.globalConfigs[configKey[0]];
} else {
const result = {};
configKey.forEach(key => (result[key] = this.globalConfigs[key]));
return result;
}
}
/* 初始化查询所有config 不对外调用 */
async initGetAllConfig() {
const data = await this.configEntity.find();
this.globalConfigs = data.reduce((prev, cur) => {
prev[cur.configKey] = cur.configVal;
return prev;
}, {});
this.initBaiduSensitive();
}
/* 初始化百度敏感词 拿到百度的access_token isInit: 初始化报错不检测 管理端手动修改则提示 */
async initBaiduSensitive(isInit = true) {
const { baiduTextApiKey, baiduTextSecretKey } = await this.getConfigs([
'baiduTextApiKey',
'baiduTextSecretKey',
]);
if (!baiduTextApiKey || !baiduTextSecretKey) {
// Logger.error('百度敏感词初始化失败,如果需要敏感检测、请前往后台系统配置!', 'GlobalConfigService');
return;
}
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
};
const url = `https://aip.baidubce.com/oauth/2.0/token?client_id=${baiduTextApiKey}&client_secret=${baiduTextSecretKey}&grant_type=client_credentials`;
try {
const response = await axios.post(url, { headers });
this.globalConfigs.baiduTextAccessToken = response.data.access_token;
} catch (error) {
if (isInit) {
// Logger.error('百度敏感词配置检测失败,您的参数可能配置的不正确!', 'GlobalConfigService');
} else {
throw new HttpException(error.response.data.error_description, HttpStatus.BAD_REQUEST);
}
}
}
/* 定时刷新 access_token */
async getWechatAccessToken(isInit = false) {
const { wechatOfficialAppId: appId, wechatOfficialAppSecret: secret } = await this.getConfigs([
'wechatOfficialAppId',
'wechatOfficialAppSecret',
]);
if (!appId || !secret) {
return Logger.error(
'还未配置微信的appId和secret、配置后才可进行微信扫码登录',
'OfficialService',
);
}
this.wechatAccessToken = await this.fetchBaseAccessToken(appId, secret, isInit);
// this.wechatJsapiTicket = await this.fetchJsapiTicket(this.wechatAccessToken);
Logger.log(`wechat refresh access_token ==> ${this.wechatAccessToken}`, 'OfficialService');
}
/* 定时刷新旧账号的 access_token */
async getOldWechatAccessToken(isInit = false) {
try {
const { oldWechatOfficialAppId: appId, oldWechatOfficialAppSecret: secret } =
await this.getConfigs(['oldWechatOfficialAppId', 'oldWechatOfficialAppSecret']);
if (!appId || !secret) {
return null;
}
if (process.env.ISDEV === 'true') {
Logger.log('开发环境下返回空token', 'GlobalConfigService');
return null;
}
// 获取旧公众号access_token
this.oldWechatAccessToken = await this.fetchBaseAccessToken(appId, secret, isInit);
// this.oldWechatJsapiTicket = await this.fetchJsapiTicket(this.oldWechatAccessToken);
} catch (error) {
// Logger.error(`获取旧公众号access_token异常: ${error.message}`, 'GlobalConfigService');
return null;
}
}
/* 获取微信access_token */
async fetchBaseAccessToken(appId: string, secret: string, isInit = false) {
Logger.log(
`开始获取access_token, appId: ${appId.substring(0, 5)}..., isInit: ${isInit}`,
'GlobalConfigService',
);
// 检查开发模式标志开发模式下也应该需要获取token
if (process.env.ISDEV === 'true') {
return '';
}
try {
const Url = formatUrl(
process.env.weChatApiUrlToken || 'https://api.weixin.qq.com/cgi-bin/token',
);
const requestUrl = `${Url}?grant_type=client_credential&appid=${appId}&secret=${secret}`;
Logger.log(`请求access_token URL: ${requestUrl}`, 'GlobalConfigService');
const response = await axios.get(requestUrl);
Logger.log(`获取access_token响应: ${JSON.stringify(response.data)}`, 'GlobalConfigService');
const { errmsg, access_token, errcode } = response.data;
if (errmsg || errcode) {
Logger.error(
`获取access_token失败 - 错误码: ${errcode}, 错误信息: ${errmsg}`,
'GlobalConfigService',
);
return '';
}
if (!access_token) {
Logger.error('未获取到access_token', 'GlobalConfigService');
return '';
}
Logger.log(
`成功获取access_token: ${access_token.substring(0, 10)}...`,
'GlobalConfigService',
);
return access_token;
} catch (error) {
Logger.error(`获取access_token异常: ${error.message}`, 'GlobalConfigService');
return '';
}
}
/* 获取微信jsapi_ticket */
async fetchJsapiTicket(accessToken: string) {
if (process.env.ISDEV === 'true') {
this.wechatJsapiTicket = '';
return;
}
const Url = formatUrl(process.env.weChatApiUrl || 'https://api.weixin.qq.com');
const res = await axios.get(
`${Url}/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`,
);
return res?.data?.ticket;
}
/* 查询所有配置信息 */
async queryAllConfig(req: Request) {
const { role } = req.user;
return this.globalConfigs;
}
/* 前端网站的所有查阅权限的配置信息 */
async queryFrontConfig(query, req) {
/* 指定前端可以访问范围 */
const allowKeys = [
'registerSendStatus',
'registerSendModel3Count',
'registerSendModel4Count',
'registerSendDrawMjCount',
'firstRegisterSendStatus',
'firstRegisterSendRank',
'firstRegisterSendModel3Count',
'firstRegisterSendModel4Count',
'firstRegisterSendDrawMjCount',
'clientHomePath',
'clientLogoPath',
'clientFaviconPath',
'drawingStyles',
'isUseWxLogin',
'siteName',
'siteUrl',
'robotAvatar',
'siteRobotName',
'buyCramiAddress',
'mindDefaultData',
'baiduCode',
'payEpayChannel',
'payDuluPayChannel',
'payMpayChannel',
'payEpayApiPayUrl',
'payEpayStatus',
'payDuluPayStatus',
'payHupiStatus',
'payWechatStatus',
'payDuluPayApiPayUrl',
'payDuluPayRedirect',
'payMpayStatus',
'payLtzfStatus',
'isAutoOpenNotice',
'isShowAppCatIcon',
'salesBaseRatio',
'salesSeniorRatio',
'salesAllowDrawMoney',
'companyName',
'filingNumber',
'emailLoginStatus',
'phoneLoginStatus',
'openIdentity',
'openPhoneValidation',
'wechatRegisterStatus',
'wechatSilentLoginStatus',
'oldWechatMigrationStatus',
'officialOldAccountSuccessText',
'officialOldAccountFailText',
'officialOldAccountNotFoundText',
'signInStatus',
'signInModel3Count',
'signInModel4Count',
'signInMjDrawToken',
'appMenuHeaderTips',
'pluginFirst',
'mjUseBaiduFy',
'mjHideNotBlock',
'mjHideWorkIn',
'isVerifyEmail',
'showWatermark',
'showCrami',
'isHideTts',
'isHideDefaultPreset',
'isHideModel3Point',
'isHideModel4Point',
'isHideDrawMjPoint',
'isHidePlugin',
'model3Name',
'model4Name',
'drawMjName',
'isModelInherited',
'noVerifyRegister',
'noticeInfo',
'homeHtml',
'isAutoOpenAgreement',
'agreementInfo',
'agreementTitle',
'isEnableExternalLinks',
'externalLinks',
'clearCacheEnabled',
'noticeTitle',
'streamCacheEnabled',
'homeWelcomeContent',
'enableHtmlRender',
'sideDrawingEditModel',
];
const data = await this.configEntity.find({
where: { configKey: In(allowKeys) },
});
const { domain } = query;
const domainDb = this.globalConfigs['domain'];
if (domainDb !== domain) {
this.createOrUpdate({
configKey: `domain`,
configVal: domain,
status: 1,
});
await this.initGetAllConfig();
}
const publicConfig = data.reduce((prev, cur) => {
prev[cur.configKey] = cur.configVal;
return prev;
}, {});
/* 追加一些自定义的配置 */
const { wechatOfficialAppId, wechatOfficialAppSecret } = await this.getConfigs([
'wechatOfficialAppId',
'wechatOfficialAppSecret',
]);
const isUseWxLogin = !!(wechatOfficialAppId && wechatOfficialAppSecret);
/* 查看是否有本机未同步数据 */
return { ...publicConfig, isUseWxLogin };
}
/* 查询配置 */
async queryConfig(body: QueryConfigDto, req: Request) {
const { role } = req.user;
const { keys } = body;
const data = await this.configEntity.find({
where: { configKey: In(keys) },
});
/* 对演示账户的一些敏感配置修改处理 */
if (role !== 'super') {
// data = data.filter((t) => !t.configKey.includes('Key'));
data.forEach(item => {
if (
item.configKey.includes('mj') ||
item.configKey.includes('Key') ||
item.configKey.includes('gpt') ||
item.configKey.includes('cos') ||
item.configKey.includes('baidu') ||
item.configKey.includes('ali') ||
item.configKey.includes('tencent') ||
item.configKey.includes('pay') ||
item.configKey.includes('wechat') ||
item.configKey.includes('mjProxyImgUrl') ||
item.configKey === 'openaiBaseUrl'
) {
/* 比较长的隐藏内容自定义 */
const longKeys = ['payWeChatPublicKey', 'payWeChatPrivateKey'];
if (longKeys.includes(item.configKey)) {
return (item.configVal = hideString(item.configVal, '隐私内容、非超级管理员无权查看'));
}
const whiteListKey = ['payEpayStatus', 'payHupiStatus', 'mjProxy', 'payLtzfStatus'];
if (!whiteListKey.includes(item.configKey) && !item.configKey.includes('Status')) {
item.configVal = hideString(item.configVal);
}
}
});
}
return data.reduce((prev, cur) => {
prev[cur.configKey] = cur.configVal;
return prev;
}, {});
}
/* 设置配置信息 */
async setConfig(body: SetConfigDto) {
try {
const { settings } = body;
for (const item of settings) {
await this.createOrUpdate(item);
}
await this.initGetAllConfig();
const keys = settings.map(t => t.configKey);
/* 如果修改的包含了百度云文本检测选择、则需要触发更新重新获取token */
if (keys.includes('baiduTextApiKey') || keys.includes('baiduTextSecretKey')) {
await this.initBaiduSensitive(false);
}
/* 如果变更微信配置 则需要手动刷新微信 access_token */
if (keys.includes('wechatOfficialAppId') || keys.includes('wechatOfficialAppSecret')) {
await this.getWechatAccessToken();
}
return '设置完成!';
} catch (error) {
console.log('error: ', error);
}
}
/* 创建或更新配置信息 */
async createOrUpdate(setting) {
/* 后期追加配置非自动化的需要手动追加为public让前端查找 */
try {
const { configKey, configVal, status = 1 } = setting;
const c = await this.configEntity.findOne({ where: { configKey } });
if (c) {
const res = await this.configEntity.update({ configKey }, { configVal, status });
} else {
const save = await this.configEntity.save({
configKey,
configVal,
status,
});
}
} catch (error) {
console.log('error: ', error);
throw new HttpException('设置配置信息错误!', HttpStatus.BAD_REQUEST);
}
}
/* 查询公告信息 */
async queryNotice() {
return await this.getConfigs(['noticeInfo', 'noticeTitle']);
}
/* 开启多个支付规则的时候 按顺序只使用一个 */
async queryPayType() {
const {
payHupiStatus = 0,
payEpayStatus = 0,
payWechatStatus = 0,
payMpayStatus = 0,
payLtzfStatus = 0,
payDuluPayStatus = 0,
} = await this.getConfigs([
'payHupiStatus',
'payEpayStatus',
'payMpayStatus',
'payWechatStatus',
'payLtzfStatus',
'payDuluPayStatus',
]);
if (
[
payHupiStatus,
payEpayStatus,
payWechatStatus,
payMpayStatus,
payLtzfStatus,
payDuluPayStatus,
].every(status => status === 0)
) {
throw new HttpException('支付功能暂未开放!', HttpStatus.BAD_REQUEST);
}
if (Number(payWechatStatus) === 1) {
return 'wechat';
}
if (Number(payDuluPayStatus) === 1) {
return 'dulu';
}
if (Number(payEpayStatus) === 1) {
return 'epay';
}
if (Number(payMpayStatus) === 1) {
return 'mpay';
}
if (Number(payHupiStatus) === 1) {
return 'hupi';
}
if (Number(payLtzfStatus) === 1) {
return 'ltzf';
}
}
/* get auth info */
async getAuthInfo() {
const { siteName, registerBaseUrl, domain } = await this.getConfigs([
'siteName',
'registerBaseUrl',
'domain',
]);
return { siteName, registerBaseUrl, domain };
}
/* get phone verify config */
async getPhoneVerifyConfig() {
const {
phoneLoginStatus,
aliPhoneAccessKeyId,
aliPhoneAccessKeySecret,
aliPhoneSignName,
aliPhoneTemplateCode,
} = await this.getConfigs([
'phoneLoginStatus',
'aliPhoneAccessKeyId',
'aliPhoneAccessKeySecret',
'aliPhoneSignName',
'aliPhoneTemplateCode',
]);
if (Number(phoneLoginStatus) !== 1) {
throw new HttpException('手机验证码功能暂未开放!', HttpStatus.BAD_REQUEST);
}
return {
accessKeyId: aliPhoneAccessKeyId,
accessKeySecret: aliPhoneAccessKeySecret,
SignName: aliPhoneSignName,
TemplateCode: aliPhoneTemplateCode,
};
}
/* get namespace */
getNamespace() {
return process.env.NAMESPACE || 'AIWeb';
}
/* 获取签名赠送额度 */
async getSignatureGiftConfig() {
const {
signInStatus = 0,
signInModel3Count = 0,
signInModel4Count = 0,
signInMjDrawToken = 0,
} = await this.getConfigs([
'signInStatus',
'signInModel3Count',
'signInModel4Count',
'signInMjDrawToken',
]);
if (Number(signInStatus) !== 1) {
throw new HttpException('签到功能暂未开放!', HttpStatus.BAD_REQUEST);
}
return {
model3Count: Number(signInModel3Count),
model4Count: Number(signInModel4Count),
drawMjCount: Number(signInMjDrawToken),
};
}
async auth() {
const api = '';
const response = await fetch(api, {});
const responseData: any = await response.json();
const { success = true, message } = responseData;
Logger.debug('感谢您使用AIWeb祝您使用愉快~');
}
/* 拿到敏感次配置 都开启优先使用百度云 */
async getSensitiveConfig() {
const { baiduTextStatus = 0, baiduTextAccessToken } = await this.getConfigs([
'baiduTextStatus',
'baiduTextAccessToken',
]);
if (Number(baiduTextStatus) === 1) {
return {
useType: 'baidu',
baiduTextAccessToken,
};
}
return null;
}
/* 保存旧账号的用户列表 */
async saveOldWechatUserList(userList: any) {
try {
const oldWechatUserList = JSON.stringify(userList);
await this.createOrUpdate({
configKey: 'oldWechatUserList',
configVal: oldWechatUserList,
configDescribe: '账号迁移-旧账号的用户列表',
});
this.globalConfigs.oldWechatUserList = oldWechatUserList;
return true;
} catch (error) {
Logger.error(`保存旧账号用户列表失败: ${error.message}`, 'GlobalConfigService');
throw new HttpException('保存旧账号用户列表失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/* 保存OpenID映射关系 */
async saveOpenidMapping(mappingList: any) {
try {
const openidMapping = JSON.stringify(mappingList);
await this.createOrUpdate({
configKey: 'openidMapping',
configVal: openidMapping,
configDescribe: '账号迁移-OpenID映射关系',
});
this.globalConfigs.openidMapping = openidMapping;
return true;
} catch (error) {
Logger.error(`保存OpenID映射关系失败: ${error.message}`, 'GlobalConfigService');
throw new HttpException('保存OpenID映射关系失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/* 获取旧账号的用户列表 */
async getOldWechatUserList() {
try {
const oldWechatUserList = this.globalConfigs.oldWechatUserList;
if (!oldWechatUserList) {
return null;
}
return JSON.parse(oldWechatUserList);
} catch (error) {
Logger.error(`获取旧账号用户列表失败: ${error.message}`, 'GlobalConfigService');
return null;
}
}
/* 获取OpenID映射关系 */
async getOpenidMapping() {
try {
const openidMapping = this.globalConfigs.openidMapping;
if (!openidMapping) {
return null;
}
return JSON.parse(openidMapping);
} catch (error) {
Logger.error(`获取OpenID映射关系失败: ${error.message}`, 'GlobalConfigService');
return null;
}
}
/* 保存UnionID映射关系 */
async saveUnionidMapping(mappingData: any) {
try {
// 获取现有的映射数据
let currentMapping = (await this.getOpenidMapping()) || {};
// 确保有unionid_mapping字段
if (!currentMapping.unionid_mapping) {
currentMapping.unionid_mapping = {};
}
// 合并新的映射数据
currentMapping.unionid_mapping = {
...currentMapping.unionid_mapping,
...mappingData,
};
// 保存更新后的映射数据
const openidMapping = JSON.stringify(currentMapping);
await this.createOrUpdate({
configKey: 'openidMapping',
configVal: openidMapping,
configDescribe: '账号迁移-OpenID和UnionID映射关系',
});
this.globalConfigs.openidMapping = openidMapping;
Logger.log(
`保存了${Object.keys(mappingData).length}个UnionID映射关系`,
'GlobalConfigService',
);
return true;
} catch (error) {
Logger.error(`保存UnionID映射关系失败: ${error.message}`, 'GlobalConfigService');
throw new HttpException('保存UnionID映射关系失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -0,0 +1,55 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
@Injectable()
export class MailerService {
constructor(private globalConfigService: GlobalConfigService) {}
async sendMail(options: { to: string; context: any }): Promise<void> {
try {
const configs = await this.globalConfigService.getConfigs([
'MAILER_HOST',
'MAILER_PORT',
'MAILER_USER',
'MAILER_PASS',
'MAILER_SECURE',
'siteName',
'siteUrl',
]);
// 直接使用字符串拼接构建HTML邮件内容
const html = `
<div style="font-family: Helvetica, Arial, sans-serif; max-width: 500px; margin: auto; padding: 40px; background-color: #ffffff; border-radius: 12px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);">
<h2 style="text-align: center; color: #111; font-weight: 400;">验证您的邮箱</h2>
<hr style="border: none; border-top: 1px solid #eaeaea; margin: 30px 0;">
<div style="text-align: center; margin-bottom: 30px;">
<span style="display: inline-block; font-size: 42px; font-weight: 700; padding: 10px 20px; background-color: #f5f5f5; border-radius: 10px;">${options.context.code}</span>
</div>
<p style="font-size: 16px; color: #111; text-align: center; line-height: 1.5;">此验证码将在 10 分钟后失效,非本人操作请忽略。</p>
<hr style="border: none; border-top: 1px solid #eaeaea; margin: 30px 0;">
<p style="font-size: 14px; color: #999; text-align: center;">点击访问:<a href="${configs.siteUrl}" style="color: #007AFF; text-decoration: none;">${configs.siteName}</a></p>
</div>`;
const transporter = nodemailer.createTransport({
host: configs.MAILER_HOST,
port: configs.MAILER_PORT,
secure: configs.MAILER_SECURE === '1' ? true : false,
auth: {
user: configs.MAILER_USER,
pass: configs.MAILER_PASS,
},
});
await transporter.sendMail({
from: configs.MAILER_USER,
to: options.to,
subject: `验证码${options.context.code}`,
html: html,
});
} catch (error) {
console.error('error: ', error);
throw new HttpException('邮件发送失败!', HttpStatus.BAD_REQUEST);
}
}
}

View File

@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';
export class QueryModelDto {
@ApiProperty({ example: 1, description: '页码', required: true })
page: number;
@ApiProperty({ example: 10, description: '数量', required: true })
size: number;
@ApiProperty({ example: 1, description: '模型类型', required: true })
keyType: number;
@ApiProperty({
example: 'dsadgadaorjoqm',
description: '模型key',
required: true,
})
key: string;
@ApiProperty({
example: true,
description: '是否开启当前key对应的模型',
required: true,
})
status: boolean;
@ApiProperty({
example: 'gpt-3.5',
description: '当前key绑定的模型是多少 需要调用的模型',
required: true,
})
model: string;
}

View File

@@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
export class QueryModelTypeDto {
@ApiProperty({ example: 1, description: '页码', required: true })
page: number;
@ApiProperty({ example: 10, description: '数量', required: true })
size: number;
@ApiProperty({ example: 1, description: '模型类型', required: true })
keyType: number;
@ApiProperty({
example: true,
description: '是否开启当前key对应的模型',
required: true,
})
status: boolean;
}

Some files were not shown because too many files have changed in this diff Show More