feat: content check

This commit is contained in:
archer 2023-05-21 22:12:02 +08:00
parent 98444fd04b
commit 51a5d450b7
No known key found for this signature in database
GPG Key ID: 166CA6BF2383B2BB
15 changed files with 310 additions and 65 deletions

View File

@ -16,6 +16,8 @@ aliTemplateCode=SMS_xxx
TOKEN_KEY=xxx TOKEN_KEY=xxx
# root key, 最高权限 # root key, 最高权限
ROOT_KEY=xxx ROOT_KEY=xxx
# 是否进行安全校验(1: 开启0: 关闭)
SENSITIVE_CHECK=1
# openai # openai
# OPENAI_BASE_URL=https://api.openai.com/v1 # OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_BASE_URL_AUTH=可选的安全凭证(不需要的时候,记得去掉) # OPENAI_BASE_URL_AUTH=可选的安全凭证(不需要的时候,记得去掉)

View File

@ -52,6 +52,8 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs USER nextjs
ENV PORT=3000
EXPOSE 3000 EXPOSE 3000
CMD ["node", "server.js"] CMD ["node", "server.js"]

View File

@ -52,6 +52,10 @@ services:
- aliTemplateCode=SMS_xxxx - aliTemplateCode=SMS_xxxx
# token加密凭证随便填作为登录凭证 # token加密凭证随便填作为登录凭证
- TOKEN_KEY=xxxx - TOKEN_KEY=xxxx
# root key, 最高权限
- ROOT_KEY=xxx
# 是否进行安全校验(1: 开启0: 关闭)
- SENSITIVE_CHECK=1
# 和上方mongo镜像的username,password对应 # 和上方mongo镜像的username,password对应
- MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin - MONGODB_URI=mongodb://username:password@0.0.0.0:27017/?authSource=admin
- MONGODB_NAME=fastgpt - MONGODB_NAME=fastgpt

View File

@ -10,34 +10,38 @@
# proxy可选 # proxy可选
AXIOS_PROXY_HOST=127.0.0.1 AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT=7890 AXIOS_PROXY_PORT=7890
# openai 中转连接(可选) # 是否开启队列任务。 1-开启0-关闭请求parentUrl去执行任务,单机时直接填1
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_BASE_URL_AUTH=可选的安全凭证
# 是否开启队列任务。 1-开启0-关闭(请求 parentUrl 去执行任务,单机时直接填1
queueTask=1 queueTask=1
parentUrl=https://hostname/api/openapi/startEvents parentUrl=https://hostname/api/openapi/startEvents
# 发送邮箱验证码配置。用的是 QQ 邮箱。参考 nodeMail 获取MAILE_CODE自行百度。 # email
MY_MAIL=xxxx@qq.com MY_MAIL=xxx@qq.com
MAILE_CODE=xxxx MAILE_CODE=xxx
# 阿里短信服务(邮箱和短信至少二选一) # ali ems
aliAccessKeyId=xxxx aliAccessKeyId=xxx
aliAccessKeySecret=xxxx aliAccessKeySecret=xxx
aliSignName=xxxxx aliSignName=xxx
aliTemplateCode=SMS_xxxx aliTemplateCode=SMS_xxx
# token加密凭证随便填作为登录凭证 # token
TOKEN_KEY=xxxx TOKEN_KEY=xxx
queueTask=1 # root key, 最高权限
parentUrl=https://hostname/api/openapi/startEvents ROOT_KEY=xxx
# 和mongo镜像的username,password对应 # 是否进行安全校验(1: 开启0: 关闭)
MONGODB_URI=mongodb://username:passsword@0.0.0.0:27017/?authSource=admin SENSITIVE_CHECK=1
MONGODB_NAME=xxx # openai
# OPENAI_BASE_URL=https://api.openai.com/v1
# OPENAI_BASE_URL_AUTH=可选的安全凭证(不需要的时候,记得去掉)
OPENAIKEY=sk-xxx
GPT4KEY=sk-xxx
# claude
CLAUDE_BASE_URL=calude模型请求地址
CLAUDE_KEY=CLAUDE_KEY
# db
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/test?authSource=admin
PG_HOST=0.0.0.0 PG_HOST=0.0.0.0
PG_PORT=8100 PG_PORT=8100
# 和PG镜像对应. PG_USER=xxx
PG_USER=fastgpt # POSTGRES_USER PG_PASSWORD=xxx
PG_PASSWORD=1234 # POSTGRES_PASSWORD PG_DB_NAME=xxx
PG_DB_NAME=fastgpt # POSTGRES_DB
OPENAIKEY=sk-xxxxx
``` ```
## 运行 ## 运行

View File

@ -2,3 +2,13 @@ export enum SplitTextTypEnum {
'qa' = 'qa', 'qa' = 'qa',
'subsection' = 'subsection' 'subsection' = 'subsection'
} }
export enum PluginTypeEnum {
LLM = 'LLM',
Text = 'Text',
Function = 'Function'
}
export enum PluginParamsTypeEnum {
'Text' = 'text'
}

View File

@ -10,6 +10,7 @@ import { resStreamResponse } from '@/service/utils/chat';
import { searchKb } from '@/service/plugins/searchKb'; import { searchKb } from '@/service/plugins/searchKb';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
import { BillTypeEnum } from '@/constants/user'; import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '@/service/api/text';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -44,6 +45,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 读取对话内容 // 读取对话内容
const prompts = [...content, prompt]; const prompts = [...content, prompt];
let systemPrompts: {
obj: ChatRoleEnum;
value: string;
}[] = [];
// 使用了知识库搜索 // 使用了知识库搜索
if (model.chat.relatedKbs.length > 0) { if (model.chat.relatedKbs.length > 0) {
@ -60,15 +65,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.send(searchPrompts[0]?.value); return res.send(searchPrompts[0]?.value);
} }
prompts.splice(prompts.length - 3, 0, ...searchPrompts); systemPrompts = searchPrompts;
} else { } else if (model.chat.systemPrompt) {
// 没有用知识库搜索,仅用系统提示词 systemPrompts = [
model.chat.systemPrompt && {
prompts.splice(prompts.length - 3, 0, {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: model.chat.systemPrompt value: model.chat.systemPrompt
});
} }
];
}
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
// content check
await sensitiveCheck({
input: [...systemPrompts, prompt].map((item) => item.value).join('')
});
// 计算温度 // 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed( const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(

View File

@ -10,6 +10,7 @@ import { resStreamResponse } from '@/service/utils/chat';
import { searchKb } from '@/service/plugins/searchKb'; import { searchKb } from '@/service/plugins/searchKb';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
import { BillTypeEnum } from '@/constants/user'; import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '@/service/api/text';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -41,6 +42,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
let systemPrompts: {
obj: ChatRoleEnum;
value: string;
}[] = [];
// 使用了知识库搜索 // 使用了知识库搜索
if (model.chat.relatedKbs.length > 0) { if (model.chat.relatedKbs.length > 0) {
const { code, searchPrompts } = await searchKb({ const { code, searchPrompts } = await searchKb({
@ -56,15 +62,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.send(searchPrompts[0]?.value); return res.send(searchPrompts[0]?.value);
} }
prompts.splice(prompts.length - 3, 0, ...searchPrompts); systemPrompts = searchPrompts;
} else { } else if (model.chat.systemPrompt) {
// 没有用知识库搜索,仅用系统提示词 systemPrompts = [
model.chat.systemPrompt && {
prompts.splice(prompts.length - 3, 0, {
obj: ChatRoleEnum.System, obj: ChatRoleEnum.System,
value: model.chat.systemPrompt value: model.chat.systemPrompt
});
} }
];
}
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
// content check
await sensitiveCheck({
input: [...systemPrompts, prompts[prompts.length - 1]].map((item) => item.value).join('')
});
// 计算温度 // 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed( const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(

View File

@ -10,6 +10,7 @@ import { searchKb } from '@/service/plugins/searchKb';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
import { withNextCors } from '@/service/utils/tools'; import { withNextCors } from '@/service/utils/tools';
import { BillTypeEnum } from '@/constants/user'; import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '@/service/api/text';
/* 发送提示词 */ /* 发送提示词 */
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) { export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -62,13 +63,16 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
const modelConstantsData = ChatModelMap[model.chat.chatModel]; const modelConstantsData = ChatModelMap[model.chat.chatModel];
let systemPrompts: {
obj: ChatRoleEnum;
value: string;
}[] = [];
// 使用了知识库搜索 // 使用了知识库搜索
if (model.chat.relatedKbs.length > 0) { if (model.chat.relatedKbs.length > 0) {
const similarity = ModelVectorSearchModeMap[model.chat.searchMode]?.similarity || 0.22;
const { code, searchPrompts } = await searchKb({ const { code, searchPrompts } = await searchKb({
prompts, prompts,
similarity, similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
model, model,
userId userId
}); });
@ -77,18 +81,29 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
if (code === 201) { if (code === 201) {
return isStream return isStream
? res.send(searchPrompts[0]?.value) ? res.send(searchPrompts[0]?.value)
: jsonRes(res, { data: searchPrompts[0]?.value }); : jsonRes(res, {
} data: searchPrompts[0]?.value,
prompts.splice(prompts.length - 3, 0, ...searchPrompts); message: searchPrompts[0]?.value
} else {
// 没有用知识库搜索,仅用系统提示词
model.chat.systemPrompt &&
prompts.splice(prompts.length - 3, 0, {
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}); });
} }
systemPrompts = searchPrompts;
} else if (model.chat.systemPrompt) {
systemPrompts = [
{
obj: ChatRoleEnum.System,
value: model.chat.systemPrompt
}
];
}
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
// content check
await sensitiveCheck({
input: [...systemPrompts, prompts[prompts.length - 1]].map((item) => item.value).join('')
});
// 计算温度 // 计算温度
const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed( const temperature = (modelConstantsData.maxTemperature * (model.chat.temperature / 10)).toFixed(
2 2

View File

@ -0,0 +1,48 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authUser, getSystemOpenAiKey } from '@/service/utils/auth';
import type { TextPluginRequestParams } from '@/types/plugin';
import axios from 'axios';
import { axiosConfig } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (process.env.SENSITIVE_CHECK !== '1') {
return jsonRes(res);
}
await authUser({ req });
const { input } = req.body as TextPluginRequestParams;
const response = await axios({
...axiosConfig(getSystemOpenAiKey()),
method: 'POST',
url: `/moderations`,
data: {
input
}
});
const data = (response.data.results?.[0]?.category_scores as Record<string, number>) || {};
const values = Object.values(data);
for (const val of values) {
if (val > 0.2) {
return jsonRes(res, {
code: 500,
message: '您的内容不合规'
});
}
}
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

110
src/service/api/request.ts Normal file
View File

@ -0,0 +1,110 @@
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
interface ConfigType {
headers?: { [key: string]: string };
hold?: boolean;
}
interface ResponseDataType {
code: number;
message: string;
data: any;
}
/**
*
*/
function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
if (config.headers) {
config.headers.rootkey = process.env.ROOT_KEY;
}
return config;
}
/**
* ,
*/
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
return response;
}
/**
*
*/
function checkRes(data: ResponseDataType) {
if (data === undefined) {
return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data);
}
return data.data;
}
/**
*
*/
function responseError(err: any) {
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
return Promise.reject(err);
}
/* 创建请求实例 */
const instance = axios.create({
timeout: 60000, // 超时时间
headers: {
'content-type': 'application/json'
}
});
/* 请求拦截 */
instance.interceptors.request.use(requestStart, (err) => Promise.reject(err));
/* 响应拦截 */
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
function request(url: string, data: any, config: ConfigType, method: Method): any {
/* 去空 */
for (const key in data) {
if (data[key] === null || data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
baseURL: `http://localhost:${process.env.PORT || 3000}/api`,
url,
method,
data: method === 'GET' ? null : data,
params: method === 'GET' ? data : null, // get请求不携带dataparams放在url上
...config // 用户自定义配置,可以覆盖前面的配置
})
.then((res) => checkRes(res.data))
.catch((err) => responseError(err));
}
/**
* api请求方式
* @param {String} url
* @param {Any} params
* @param {Object} config
* @returns
*/
export function GET<T>(url: string, params = {}, config: ConfigType = {}): Promise<T> {
return request(url, params, config, 'GET');
}
export function POST<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, data, config, 'POST');
}
export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, data, config, 'PUT');
}
export function DELETE<T>(url: string, config: ConfigType = {}): Promise<T> {
return request(url, {}, config, 'DELETE');
}

5
src/service/api/text.ts Normal file
View File

@ -0,0 +1,5 @@
import { POST } from './request';
import type { TextPluginRequestParams } from '@/types/plugin';
export const sensitiveCheck = (data: TextPluginRequestParams) =>
POST('/openapi/text/sensitiveCheck', data);

View File

@ -66,8 +66,8 @@ export const authUser = async ({
return Promise.reject(error); return Promise.reject(error);
} }
}; };
const parseRootKey = async (rootKey?: string, userId?: string) => { const parseRootKey = async (rootKey?: string, userId = '') => {
if (!rootKey || !userId || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) {
return Promise.reject(ERROR_ENUM.unAuthorization); return Promise.reject(ERROR_ENUM.unAuthorization);
} }
return userId; return userId;
@ -104,7 +104,7 @@ export const authUser = async ({
}; };
/* random get openai api key */ /* random get openai api key */
export const getOpenAiKey = () => { export const getSystemOpenAiKey = () => {
// 纯字符串类型 // 纯字符串类型
const keys = process.env.OPENAIKEY?.split(',') || []; const keys = process.env.OPENAIKEY?.split(',') || [];
const i = Math.floor(Math.random() * keys.length); const i = Math.floor(Math.random() * keys.length);
@ -129,7 +129,7 @@ export const getApiKey = async ({
const keyMap = { const keyMap = {
[OpenAiChatEnum.GPT35]: { [OpenAiChatEnum.GPT35]: {
userOpenAiKey: user.openaiKey || '', userOpenAiKey: user.openaiKey || '',
systemAuthKey: getOpenAiKey() as string systemAuthKey: getSystemOpenAiKey() as string
}, },
[OpenAiChatEnum.GPT4]: { [OpenAiChatEnum.GPT4]: {
userOpenAiKey: user.openaiKey || '', userOpenAiKey: user.openaiKey || '',

View File

@ -7,16 +7,14 @@ import { adaptChatItem_openAI } from '@/utils/chat/openai';
import { modelToolMap } from '@/utils/chat'; import { modelToolMap } from '@/utils/chat';
import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index'; import { ChatCompletionType, ChatContextFilter, StreamResponseType } from './index';
import { ChatRoleEnum } from '@/constants/chat'; import { ChatRoleEnum } from '@/constants/chat';
import { getOpenAiKey } from '../auth'; import { getSystemOpenAiKey } from '../auth';
export const getOpenAIApi = (apiKey: string) => { export const getOpenAIApi = () =>
const configuration = new Configuration({ new OpenAIApi(
apiKey, new Configuration({
basePath: process.env.OPENAI_BASE_URL basePath: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'
}); })
);
return new OpenAIApi(configuration);
};
/* 获取向量 */ /* 获取向量 */
export const openaiCreateEmbedding = async ({ export const openaiCreateEmbedding = async ({
@ -28,10 +26,10 @@ export const openaiCreateEmbedding = async ({
userId: string; userId: string;
textArr: string[]; textArr: string[];
}) => { }) => {
const systemAuthKey = getOpenAiKey(); const systemAuthKey = getSystemOpenAiKey();
// 获取 chatAPI // 获取 chatAPI
const chatAPI = getOpenAIApi(userOpenAiKey || systemAuthKey); const chatAPI = getOpenAIApi();
// 把输入的内容转成向量 // 把输入的内容转成向量
const res = await chatAPI const res = await chatAPI
@ -42,7 +40,7 @@ export const openaiCreateEmbedding = async ({
}, },
{ {
timeout: 60000, timeout: 60000,
...axiosConfig() ...axiosConfig(userOpenAiKey || systemAuthKey)
} }
) )
.then((res) => ({ .then((res) => ({
@ -78,7 +76,7 @@ export const chatResponse = async ({
}); });
const adaptMessages = adaptChatItem_openAI({ messages: filterMessages }); const adaptMessages = adaptChatItem_openAI({ messages: filterMessages });
const chatAPI = getOpenAIApi(apiKey); const chatAPI = getOpenAIApi();
const response = await chatAPI.createChatCompletion( const response = await chatAPI.createChatCompletion(
{ {
@ -93,7 +91,7 @@ export const chatResponse = async ({
{ {
timeout: stream ? 60000 : 240000, timeout: stream ? 60000 : 240000,
responseType: stream ? 'stream' : 'json', responseType: stream ? 'stream' : 'json',
...axiosConfig() ...axiosConfig(apiKey)
} }
); );

View File

@ -31,9 +31,11 @@ export const clearCookie = (res: NextApiResponse) => {
}; };
/* openai axios config */ /* openai axios config */
export const axiosConfig = () => ({ export const axiosConfig = (apikey: string) => ({
baseURL: process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
httpsAgent: global.httpsAgent, httpsAgent: global.httpsAgent,
headers: { headers: {
Authorization: `Bearer ${apikey}`,
auth: process.env.OPENAI_BASE_URL_AUTH || '' auth: process.env.OPENAI_BASE_URL_AUTH || ''
} }
}); });

20
src/types/plugin.d.ts vendored
View File

@ -1,10 +1,12 @@
import type { kbSchema } from './mongoSchema'; import type { kbSchema } from './mongoSchema';
import { PluginTypeEnum } from '@/constants/plugin';
/* kb type */ /* kb type */
export interface KbItemType extends kbSchema { export interface KbItemType extends kbSchema {
totalData: number; totalData: number;
tags: string; tags: string;
} }
export interface KbDataItemType { export interface KbDataItemType {
id: string; id: string;
status: 'waiting' | 'ready'; status: 'waiting' | 'ready';
@ -13,3 +15,21 @@ export interface KbDataItemType {
kbId: string; kbId: string;
userId: string; userId: string;
} }
/* plugin */
export interface PluginConfig {
name: string;
desc: string;
url: string;
category: `${PluginTypeEnum}`;
uniPrice: 22; // 1k token
params: [
{
type: '';
}
];
}
export type TextPluginRequestParams = {
input: string;
};