generated from tailored/app-template
temp
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
import { app } from './app.ts';
|
||||
import { useConfig } from '@kevisual/use-config/env';
|
||||
const config = useConfig();
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'auth',
|
||||
key: 'auth',
|
||||
id: 'auth',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
ctx.state.tokenUser = {
|
||||
id: 'abcdefff',
|
||||
username: 'root',
|
||||
};
|
||||
ctx.query.token = config.ROOT_TEST_TOKEN;
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
@@ -11,6 +27,4 @@ app
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
console.log('run demo: http://localhost:' + config.PORT + '/api/router?path=demo&key=demo');
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useConfig } from '@kevisual/use-config/env';
|
||||
import { useContextKey } from '@kevisual/use-config/context';
|
||||
import { Redis } from 'ioredis';
|
||||
export const redis = useContextKey('redis', () => {
|
||||
return new Redis();
|
||||
});
|
||||
import { app } from './index.ts'; // 开发环境
|
||||
import { app } from './app.ts';
|
||||
import './demo-route.ts';
|
||||
import './routes/index.ts';
|
||||
|
||||
const config = useConfig();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app } from './app.ts';
|
||||
import './demo-route.ts';
|
||||
import './routes/index.ts';
|
||||
|
||||
export { app };
|
||||
export { app };
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
import { useContextKey } from '@kevisual/use-config/context';
|
||||
import { Redis } from 'ioredis';
|
||||
export const redis = useContextKey('redis', () => {
|
||||
const redis = new Redis({
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
});
|
||||
return redis;
|
||||
});
|
||||
|
||||
export const redis = useContextKey('redis');
|
||||
const checkConnection = async () => {
|
||||
const res = await redis.ping();
|
||||
console.log('redis ping', res);
|
||||
};
|
||||
|
||||
// checkConnection();
|
||||
|
||||
12
src/modules/query.ts
Normal file
12
src/modules/query.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Query } from '@kevisual/query/query';
|
||||
import { QueryConfig } from '@kevisual/query-config';
|
||||
import { config } from './config.ts';
|
||||
|
||||
const baseURL = new URL(config.path, config.host);
|
||||
export const query = new Query({
|
||||
url: baseURL.toString(),
|
||||
});
|
||||
|
||||
export const queryConfig = new QueryConfig({
|
||||
query: query as any,
|
||||
});
|
||||
30
src/modules/sequelize.ts
Normal file
30
src/modules/sequelize.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Sequelize } from 'sequelize';
|
||||
import { useConfig } from '@kevisual/use-config/env';
|
||||
export const config = useConfig() as any;
|
||||
|
||||
export type PostgresConfig = {
|
||||
postgres: {
|
||||
username: string;
|
||||
password: string;
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
};
|
||||
};
|
||||
if (!config.POSTGRES_PASSWORD || !config.POSTGRES_USER) {
|
||||
console.error('postgres config is required password and user');
|
||||
process.exit(1);
|
||||
}
|
||||
const postgresConfig = {
|
||||
username: config.POSTGRES_USER,
|
||||
password: config.POSTGRES_PASSWORD,
|
||||
host: config.POSTGRES_HOST || 'localhost',
|
||||
port: parseInt(config.POSTGRES_PORT || '5432'),
|
||||
database: config.POSTGRES_DB || 'postgres',
|
||||
};
|
||||
// connect to db
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: 'postgres',
|
||||
...postgresConfig,
|
||||
// logging: false,
|
||||
});
|
||||
@@ -1,7 +1,4 @@
|
||||
import OpenAI from 'openai';
|
||||
import { APIPromise } from 'openai/core.mjs';
|
||||
import { ChatCompletionChunk } from 'openai/resources.mjs';
|
||||
import { Stream } from 'openai/streaming.mjs';
|
||||
|
||||
export type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
||||
export type ChatMessageOptions = Partial<OpenAI.Chat.Completions.ChatCompletionCreateParams>;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
// 加密函数
|
||||
export function encryptAES(plainText: string, secretKey: string) {
|
||||
return AES.encrypt(plainText, secretKey).toString();
|
||||
return CryptoJS.AES.encrypt(plainText, secretKey).toString();
|
||||
}
|
||||
|
||||
// 解密函数
|
||||
export function decryptAES(cipherText: string, secretKey: string) {
|
||||
const bytes = AES.decrypt(cipherText, secretKey);
|
||||
return bytes.toString(enc.Utf8);
|
||||
const bytes = CryptoJS.AES.decrypt(cipherText, secretKey);
|
||||
return bytes.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
type AIModel = {
|
||||
@@ -25,9 +25,13 @@ type AIModel = {
|
||||
*/
|
||||
group: string;
|
||||
/**
|
||||
* 每日限制
|
||||
* 每日请求频率限制
|
||||
*/
|
||||
dayLimit?: number;
|
||||
/**
|
||||
* 总的token限制
|
||||
*/
|
||||
tokenLimit?: number;
|
||||
};
|
||||
|
||||
type SecretKey = {
|
||||
@@ -56,6 +60,7 @@ export type ProviderResult = {
|
||||
group: string;
|
||||
apiKey: string;
|
||||
dayLimit?: number;
|
||||
tokenLimit?: number;
|
||||
baseURL?: string;
|
||||
/**
|
||||
* 解密密钥
|
||||
@@ -68,6 +73,12 @@ export type AIConfig = {
|
||||
description?: string;
|
||||
models: AIModel[];
|
||||
secretKeys: SecretKey[];
|
||||
filter?: {
|
||||
objectKey: string;
|
||||
type: 'array' | 'object';
|
||||
operate: 'removeAttribute' | 'remove';
|
||||
attribute: string[];
|
||||
}[];
|
||||
};
|
||||
export class AIConfigParser {
|
||||
private config: AIConfig;
|
||||
|
||||
@@ -1,11 +1,165 @@
|
||||
import { app } from '@/app.ts';
|
||||
import { ChatServices } from './services/chat-services.ts';
|
||||
import { ChatConfigServices } from './services/chat-config-srevices.ts';
|
||||
import { AiChatHistoryModel } from './models/ai-chat-history.ts';
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'ai',
|
||||
key: 'chat',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async () => {
|
||||
//
|
||||
.define(async (ctx) => {
|
||||
const data = ctx.query.data || {};
|
||||
const { id, messages = [], options = {}, title, hook, getFull = false } = data;
|
||||
let { username, model, group } = ctx.query;
|
||||
const tokenUser = ctx.state.tokenUser || {};
|
||||
const tokenUsername = tokenUser.username;
|
||||
let aiChatHistory: AiChatHistoryModel;
|
||||
if (id) {
|
||||
aiChatHistory = await AiChatHistoryModel.findByPk(id);
|
||||
if (!aiChatHistory) {
|
||||
ctx.throw(400, 'aiChatHistory not found');
|
||||
}
|
||||
if (aiChatHistory.uid !== tokenUser.uid) {
|
||||
ctx.throw(403, 'not permission');
|
||||
}
|
||||
username = username || aiChatHistory.username;
|
||||
model = model || aiChatHistory.model;
|
||||
group = group || aiChatHistory.group;
|
||||
} else {
|
||||
username = username || tokenUsername;
|
||||
}
|
||||
if (!Array.isArray(messages)) {
|
||||
ctx.throw(400, 'chat messages is not array');
|
||||
}
|
||||
|
||||
// 初始化服务
|
||||
const chatServices = await ChatServices.createServices({
|
||||
owner: username,
|
||||
model,
|
||||
group,
|
||||
username: tokenUsername,
|
||||
});
|
||||
const chatConfigServices = new ChatConfigServices(username, tokenUsername);
|
||||
await chatConfigServices.checkUserCanChat(tokenUsername);
|
||||
await chatServices.checkCanChat();
|
||||
const pickMessages = await chatServices.chatMessagePick(messages);
|
||||
if (pickMessages.length === 0) {
|
||||
ctx.throw(400, 'chat messages is empty');
|
||||
}
|
||||
const res = await chatServices.chat(pickMessages, options);
|
||||
if (!aiChatHistory) {
|
||||
aiChatHistory = await AiChatHistoryModel.create({
|
||||
username,
|
||||
model,
|
||||
group,
|
||||
title,
|
||||
});
|
||||
if (!title) {
|
||||
// TODO: 创建标题
|
||||
}
|
||||
}
|
||||
const message = res.choices[0].message;
|
||||
const newMessage = await chatServices.createNewMessage([...messages, message]);
|
||||
|
||||
const usage = chatServices.chatProvider.getChatUsage();
|
||||
await chatServices.updateChatLimit(usage.total_tokens);
|
||||
await chatConfigServices.updateUserChatLimit(tokenUsername, usage.total_tokens);
|
||||
|
||||
const needUpdateData: any = {
|
||||
messages: newMessage,
|
||||
prompt_tokens: aiChatHistory.prompt_tokens + usage.prompt_tokens,
|
||||
completion_tokens: aiChatHistory.completion_tokens + usage.completion_tokens,
|
||||
total_tokens: aiChatHistory.total_tokens + usage.total_tokens,
|
||||
};
|
||||
if (hook) {
|
||||
needUpdateData.data = {
|
||||
...aiChatHistory.data,
|
||||
hook,
|
||||
};
|
||||
}
|
||||
await AiChatHistoryModel.update(needUpdateData, { where: { id: aiChatHistory.id } });
|
||||
ctx.body = {
|
||||
message: newMessage[newMessage.length - 1],
|
||||
aiChatHistory: getFull || !id ? aiChatHistory : undefined,
|
||||
};
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
// http://localhost:4010/api/router?path=ai&key=question&question="1 and 1 equals"
|
||||
app
|
||||
.route({
|
||||
path: 'ai',
|
||||
key: 'question',
|
||||
middleware: ['auth'],
|
||||
isDebug: true,
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const data = ctx.query;
|
||||
const model = data.model || 'qwq:latest';
|
||||
const group = data.group || 'ollama';
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const chatServices = await ChatServices.createServices({
|
||||
owner: data.username || 'root',
|
||||
model,
|
||||
group,
|
||||
username: tokenUser.username,
|
||||
});
|
||||
const res = await chatServices.chat([
|
||||
{
|
||||
role: 'user',
|
||||
content: data.question,
|
||||
},
|
||||
]);
|
||||
ctx.body = res;
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
// http://localhost:4010/api/router?path=ai&key=get-model-list
|
||||
app
|
||||
.route({
|
||||
path: 'ai',
|
||||
key: 'get-model-list',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const username = ctx.query.username || 'root';
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const isSameUser = username === tokenUser.username;
|
||||
const configObject: Record<string, any> = {};
|
||||
const services = new ChatConfigServices(username, tokenUser.username);
|
||||
const res = await services.getChatConfig(true, ctx.query.token);
|
||||
configObject[username] = res;
|
||||
if (!isSameUser) {
|
||||
const selfServices = new ChatConfigServices(tokenUser.username, tokenUser.username);
|
||||
const selfRes = await selfServices.getChatConfig(true, ctx.query.token);
|
||||
configObject['self'] = selfRes;
|
||||
} else {
|
||||
configObject['self'] = res;
|
||||
}
|
||||
ctx.body = configObject;
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'ai',
|
||||
key: 'get-chat-usage',
|
||||
description: '获取chat使用情况',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser || {};
|
||||
const username = tokenUser.username;
|
||||
const services = new ChatConfigServices('root', username);
|
||||
const chatServices = await ChatServices.createServices({ owner: username, username });
|
||||
const rootUsage = await services.getUserChatLimit(username);
|
||||
const selfUsage = await chatServices.getChatLimit();
|
||||
|
||||
ctx.body = {
|
||||
rootUsage,
|
||||
selfUsage,
|
||||
};
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
23
src/routes/ai-chat/list.ts
Normal file
23
src/routes/ai-chat/list.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { app } from '@/app.ts';
|
||||
import { AiChatHistoryModel } from './models/ai-chat-history.ts';
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'ai',
|
||||
key: 'get-chat-list',
|
||||
description: '获取chat列表',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const aiChatList = await AiChatHistoryModel.findAll({
|
||||
where: {
|
||||
uid: tokenUser.uid,
|
||||
},
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
ctx.body = {
|
||||
list: aiChatList,
|
||||
};
|
||||
})
|
||||
.addTo(app);
|
||||
99
src/routes/ai-chat/models/ai-chat-history.ts
Normal file
99
src/routes/ai-chat/models/ai-chat-history.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { sequelize } from '@/modules/sequelize.ts';
|
||||
import { DataTypes, Model } from 'sequelize';
|
||||
|
||||
export type AiChatHistory = Partial<InstanceType<typeof AiChatHistoryModel>>;
|
||||
|
||||
export type ChastHistoryMessage = {
|
||||
role: string;
|
||||
content: string;
|
||||
name: string;
|
||||
id?: string;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
hide?: boolean;
|
||||
noUse?: boolean;
|
||||
};
|
||||
type AiChatHistoryData = {
|
||||
hook?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
export class AiChatHistoryModel extends Model {
|
||||
declare id: string;
|
||||
declare username: string;
|
||||
declare model: string;
|
||||
declare group: string;
|
||||
|
||||
declare title: string;
|
||||
|
||||
declare messages: ChastHistoryMessage[];
|
||||
declare uid: string;
|
||||
declare data: AiChatHistoryData;
|
||||
declare prompt_tokens: number;
|
||||
declare total_tokens: number;
|
||||
declare completion_tokens: number;
|
||||
|
||||
declare createdAt: Date;
|
||||
declare updatedAt: Date;
|
||||
}
|
||||
|
||||
AiChatHistoryModel.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
model: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
group: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
messages: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
defaultValue: [],
|
||||
},
|
||||
prompt_tokens: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
},
|
||||
total_tokens: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
},
|
||||
completion_tokens: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
},
|
||||
uid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: 'kv_ai_chat_history',
|
||||
paranoid: false,
|
||||
},
|
||||
);
|
||||
|
||||
AiChatHistoryModel.sync({ alter: true, logging: false }).catch((e) => {
|
||||
console.error('AiChatHistoryModel sync', e);
|
||||
});
|
||||
128
src/routes/ai-chat/services/chat-config-srevices.ts
Normal file
128
src/routes/ai-chat/services/chat-config-srevices.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { AIConfig } from '@/provider/utils/parse-config.ts';
|
||||
import { redis } from '@/modules/db.ts';
|
||||
import { CustomError } from '@kevisual/router';
|
||||
import { queryConfig } from '@/modules/query.ts';
|
||||
export class ChatConfigServices {
|
||||
cachePrefix = 'ai:chat:config';
|
||||
// 使用谁的模型
|
||||
owner: string;
|
||||
// 使用者
|
||||
username: string;
|
||||
/**
|
||||
* username 是使用的模型的用户名,使用谁的模型
|
||||
* @param username
|
||||
*/
|
||||
constructor(owner: string, username: string, token?: string) {
|
||||
this.owner = owner;
|
||||
this.username = username;
|
||||
}
|
||||
getKey() {
|
||||
return `${this.cachePrefix}:${this.owner}`;
|
||||
}
|
||||
/**
|
||||
* 获取chat配置
|
||||
* @param needClearSecret 是否需要清除secret 默认false
|
||||
* @returns
|
||||
*/
|
||||
async getChatConfig(needClearSecret = false, token?: string) {
|
||||
const key = this.getKey();
|
||||
const cache = await redis.get(key);
|
||||
let modelConfig = null;
|
||||
if (cache) {
|
||||
modelConfig = JSON.parse(cache);
|
||||
}
|
||||
if (!modelConfig) {
|
||||
if (this.owner !== this.username) {
|
||||
throw new CustomError(`the owner [${this.owner}] config, [${this.username}] not permission to init config, only owner can init config, place connect owner`);
|
||||
} else {
|
||||
const res = await queryConfig.getConfigByKey('ai.json', { token });
|
||||
if (res.code === 200 && res.data?.data) {
|
||||
modelConfig = res.data.data;
|
||||
} else {
|
||||
throw new CustomError(400, 'get config failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!modelConfig) {
|
||||
throw new CustomError(`${this.owner} modelConfig is null`);
|
||||
}
|
||||
if (!cache) {
|
||||
const cacheTime = 60 * 60 * 24 * 40; // 1天
|
||||
await redis.set(key, JSON.stringify(modelConfig), 'EX', cacheTime);
|
||||
}
|
||||
if (needClearSecret) {
|
||||
modelConfig = this.filterApiKey(modelConfig);
|
||||
}
|
||||
return modelConfig;
|
||||
}
|
||||
async filterApiKey(chatConfig: AIConfig) {
|
||||
// 过滤掉secret中的所有apiKey,移除掉并返回chatConfig
|
||||
const { secretKeys, ...rest } = chatConfig;
|
||||
return {
|
||||
...rest,
|
||||
secretKeys: secretKeys.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
apiKey: undefined,
|
||||
decryptKey: undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 获取和检测当前用户的额度, 当使用 root 账号的时候,才需要检测
|
||||
* username是当前使用用户
|
||||
* @param username
|
||||
*/
|
||||
async checkUserCanChat(username: string) {
|
||||
if (this.owner !== 'root') return true;
|
||||
const maxToken = 100000;
|
||||
const userCacheKey = `${this.cachePrefix}:root:chat-limit:${username}`;
|
||||
const cache = await redis.get(userCacheKey);
|
||||
if (cache) {
|
||||
const cacheData = JSON.parse(cache);
|
||||
if (cacheData.token >= maxToken) {
|
||||
throw new CustomError(400, 'use root account token limit exceeded');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 获取用户的使用情况
|
||||
* username是当前使用用户
|
||||
* @param username
|
||||
* @returns
|
||||
*/
|
||||
async getUserChatLimit(username: string) {
|
||||
if (this.owner !== 'root') return;
|
||||
const userCacheKey = `${this.cachePrefix}:root:chat-limit:${username}`;
|
||||
const cache = await redis.get(userCacheKey);
|
||||
if (cache) {
|
||||
const cacheData = JSON.parse(cache);
|
||||
return cacheData;
|
||||
}
|
||||
return {
|
||||
token: 0,
|
||||
day: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户的使用情况
|
||||
* username是当前使用用户
|
||||
* @param username
|
||||
* @param token
|
||||
*/
|
||||
async updateUserChatLimit(username: string, token: number) {
|
||||
if (this.owner !== 'root') return;
|
||||
const userCacheKey = `${this.cachePrefix}:root:chat-limit:${username}`;
|
||||
const cache = await redis.get(userCacheKey);
|
||||
if (cache) {
|
||||
const cacheData = JSON.parse(cache);
|
||||
cacheData.token = cacheData.token + token;
|
||||
await redis.set(userCacheKey, JSON.stringify(cacheData), 'EX', 60 * 60 * 24 * 30); // 30天
|
||||
} else {
|
||||
await redis.set(userCacheKey, JSON.stringify({ token }), 'EX', 60 * 60 * 24 * 30); // 30天
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
import { AIConfigParser, ProviderResult } from '@/provider/utils/parse-config.ts';
|
||||
import { ProviderManager, ChatMessage, BaseChat } from '@/provider/index.ts';
|
||||
import { getChatConfig } from '@/modules/chat-config.ts';
|
||||
import { ProviderManager, ChatMessage, BaseChat, ChatMessageOptions } from '@/provider/index.ts';
|
||||
import { redis } from '@/modules/db.ts';
|
||||
import { CustomError } from '@kevisual/router';
|
||||
import { ChatConfigServices } from './chat-config-srevices.ts';
|
||||
import { pick } from 'lodash-es';
|
||||
import { ChastHistoryMessage } from '../models/ai-chat-history.ts';
|
||||
import { nanoid } from '@/utils/uuid.ts';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export type ChatServicesConfig = {
|
||||
username: string;
|
||||
owner: string;
|
||||
model: string;
|
||||
group: string;
|
||||
decryptKey?: string;
|
||||
@@ -13,7 +19,7 @@ export class ChatServices {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
username: string;
|
||||
owner: string;
|
||||
/**
|
||||
* 模型
|
||||
*/
|
||||
@@ -32,7 +38,7 @@ export class ChatServices {
|
||||
modelConfig?: ProviderResult;
|
||||
chatProvider?: BaseChat;
|
||||
constructor(opts: ChatServicesConfig) {
|
||||
this.username = opts.username;
|
||||
this.owner = opts.owner;
|
||||
this.model = opts.model;
|
||||
this.group = opts.group;
|
||||
this.decryptKey = opts.decryptKey;
|
||||
@@ -41,8 +47,8 @@ export class ChatServices {
|
||||
* 初始化
|
||||
* @returns
|
||||
*/
|
||||
async init() {
|
||||
const config = await this.getConfig();
|
||||
async init(username: string) {
|
||||
const config = await this.getConfig(username);
|
||||
const aiConfigParser = new AIConfigParser(config);
|
||||
const model = this.model;
|
||||
const group = this.group;
|
||||
@@ -52,24 +58,30 @@ export class ChatServices {
|
||||
const apiKey = await aiConfigParser.getSecretKey({
|
||||
getCache: async (key) => {
|
||||
const cache = await redis.get(that.wrapperKey(key));
|
||||
return cache;
|
||||
return cache ? JSON.parse(cache) : null;
|
||||
},
|
||||
setCache: async (key, value) => {
|
||||
await redis.set(that.wrapperKey(key), value);
|
||||
await redis.set(that.wrapperKey(key), JSON.stringify(value), 'EX', 60 * 60 * 24 * 1); // 1天
|
||||
},
|
||||
});
|
||||
that.modelConfig = { ...providerResult, apiKey };
|
||||
return that.modelConfig;
|
||||
}
|
||||
async wrapperKey(key: string) {
|
||||
const username = this.username;
|
||||
return `${this.cachePrefix}${username}:${key}`;
|
||||
/**
|
||||
* 包装key , 默认了username
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
wrapperKey(key: string) {
|
||||
const owner = this.owner;
|
||||
return `${this.cachePrefix}${owner}:${key}`;
|
||||
}
|
||||
async getConfig() {
|
||||
return getChatConfig();
|
||||
async getConfig(username: string) {
|
||||
const services = new ChatConfigServices(this.owner, username);
|
||||
return services.getChatConfig();
|
||||
}
|
||||
|
||||
async chat(messages: ChatMessage[]) {
|
||||
async chat(messages: ChatMessage[], options?: ChatMessageOptions) {
|
||||
const { model, provider, apiKey, baseURL } = this.modelConfig;
|
||||
const providerManager = await ProviderManager.createProvider({
|
||||
provider: provider,
|
||||
@@ -78,14 +90,111 @@ export class ChatServices {
|
||||
baseURL: baseURL,
|
||||
});
|
||||
this.chatProvider = providerManager;
|
||||
const result = await providerManager.chat(messages);
|
||||
const result = await providerManager.chat(messages, options);
|
||||
return result;
|
||||
}
|
||||
static async createServices(opts: Partial<ChatServicesConfig>) {
|
||||
const username = opts.username || 'root';
|
||||
const model = opts.model || 'deepseek-r1-250120';
|
||||
async createTitle(messages: ChastHistoryMessage[]) {
|
||||
return nanoid();
|
||||
}
|
||||
/**
|
||||
* 过滤消息,只保留对话需要的内容,name,role,content
|
||||
* @param messages
|
||||
* @returns
|
||||
*/
|
||||
async chatMessagePick(messages: ChastHistoryMessage[]) {
|
||||
let newMessages = messages.filter((item) => !item.hide && !item.noUse);
|
||||
return newMessages.map((item) => pick(item, ['role', 'content', 'name'])) as ChatMessage[];
|
||||
}
|
||||
async createNewMessage(messages: ChastHistoryMessage[]) {
|
||||
return messages.map((item) => {
|
||||
if (!item.id) {
|
||||
item.id = 'chat' + nanoid();
|
||||
item.createdAt = Date.now();
|
||||
item.updatedAt = Date.now();
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
static async createServices(opts: Partial<ChatServicesConfig> & { username: string }) {
|
||||
const owner = opts.owner || 'root';
|
||||
const model = opts.model || 'deepseek-chat';
|
||||
const group = opts.group || 'deepseek';
|
||||
const decryptKey = opts.decryptKey;
|
||||
return new ChatServices({ username, model, group, decryptKey });
|
||||
const chatServices = new ChatServices({ owner, model, group, decryptKey });
|
||||
await chatServices.init(opts.username);
|
||||
return chatServices;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型的余量
|
||||
* @returns
|
||||
*/
|
||||
async checkCanChat() {
|
||||
const { modelConfig } = this;
|
||||
const { tokenLimit, dayLimit, group, model } = modelConfig;
|
||||
const key = this.wrapperKey(`chat-limit`);
|
||||
const cache = await redis.get(key);
|
||||
if (cache) {
|
||||
const cacheData = JSON.parse(cache);
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
const current = cacheData.find((item) => item.group === group && item.model === model);
|
||||
const day = current[today] || 0;
|
||||
const token = current.token || 0;
|
||||
if (tokenLimit && token >= tokenLimit) {
|
||||
throw new CustomError(400, 'token limit exceeded');
|
||||
}
|
||||
if (dayLimit && day >= dayLimit) {
|
||||
throw new CustomError(400, 'day limit exceeded');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 获取模型的使用情况
|
||||
* @returns
|
||||
*/
|
||||
async getChatLimit() {
|
||||
const { modelConfig } = this;
|
||||
const { group, model } = modelConfig;
|
||||
const key = this.wrapperKey(`chat-limit`);
|
||||
const cache = await redis.get(key);
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
if (cache) {
|
||||
const cacheData = JSON.parse(cache);
|
||||
return cacheData;
|
||||
}
|
||||
return [
|
||||
{
|
||||
group: group,
|
||||
model: model,
|
||||
token: 0,
|
||||
[today]: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
/**
|
||||
* 更新模型的使用情况
|
||||
* @param token
|
||||
*/
|
||||
async updateChatLimit(token: number) {
|
||||
const { modelConfig } = this;
|
||||
const { group, model } = modelConfig;
|
||||
const key = this.wrapperKey(`chat-limit`);
|
||||
const cache = await redis.get(key);
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
if (cache) {
|
||||
const cacheData = JSON.parse(cache);
|
||||
const current = cacheData.find((item) => item.group === group && item.model === model);
|
||||
if (current) {
|
||||
const day = current[today] || 0;
|
||||
current[today] = day + 1;
|
||||
current.token = current.token + token;
|
||||
} else {
|
||||
cacheData.push({ group, model, token: token, [today]: 1 });
|
||||
}
|
||||
await redis.set(key, JSON.stringify(cacheData), 'EX', 60 * 60 * 24 * 30); // 30天
|
||||
} else {
|
||||
await redis.set(key, JSON.stringify({ group, model, token: token, [today]: 1 }), 'EX', 60 * 60 * 24 * 30); // 30天
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
src/routes/index.ts
Normal file
2
src/routes/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import './ai-chat/index.ts';
|
||||
import './ai-chat/list.ts';
|
||||
9
src/test/encrypt/index.ts
Normal file
9
src/test/encrypt/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { encryptAES, decryptAES } from '../../provider/utils/parse-config.ts';
|
||||
|
||||
const plainx = process.env.API_KEY;
|
||||
const decryptKey = process.env.DECRYPT_KEY;
|
||||
const encrypt = encryptAES(plainx, decryptKey);
|
||||
console.log('encrypt', encrypt);
|
||||
|
||||
const decrypt = decryptAES(encrypt, decryptKey);
|
||||
console.log(decrypt);
|
||||
8
src/utils/uuid.ts
Normal file
8
src/utils/uuid.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
export const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
export const nanoid = customAlphabet(alphabet, 16);
|
||||
|
||||
export function uuid() {
|
||||
return nanoid();
|
||||
}
|
||||
Reference in New Issue
Block a user