更新MessageChunk的结构,增加消息记录器功能

main
落雨楓 1 year ago
parent fc78861d7c
commit 8324766ee2

@ -12,6 +12,9 @@
"<node_internals>/**"
],
"program": "${workspaceFolder}/index.js",
"runtimeArgs": [
"--es-module-specifier-resolution=node"
],
"outFiles": [
"${workspaceFolder}/**/*.js"
]

@ -38,15 +38,16 @@
"pusher-js": "^5.1.1",
"throttle-debounce": "^3.0.1",
"undici": "^5.22.0",
"webdav": "^5.1.0",
"winston": "^3.8.2",
"yaml": "^1.8.3"
},
"devDependencies": {
"@types/node-telegram-bot-api": "^0.57.1",
"@types/koa": "^2.13.4",
"@types/koa-router": "^7.4.4",
"@types/micromatch": "^4.0.2",
"@types/node": "^17.0.8",
"@types/node-telegram-bot-api": "^0.57.1",
"@types/throttle-debounce": "^2.1.0",
"@types/yaml": "^1.9.7",
"ts-node": "^10.4.0",

@ -15,7 +15,8 @@ import { RestfulApiManager } from './RestfulApiManager';
import { RobotManager } from './RobotManager';
import { Service, ServiceManager } from './ServiceManager';
import { SubscribeManager, Target } from './SubscribeManager';
import { SessionManager } from './SessionManager';
import { CacheManager } from './CacheManager';
import { StorageManager } from './StorageManager';
import { DatabaseManager } from './DatabaseManager';
export * from './utils/contextHooks';
@ -30,7 +31,8 @@ export default class App {
public logger!: winston.Logger;
public event!: EventManager;
public session!: SessionManager;
public cache!: CacheManager;
public storage!: StorageManager;
public database?: DatabaseManager;
public robot!: RobotManager;
public provider!: ProviderManager;
@ -53,7 +55,8 @@ export default class App {
await this.initModules();
await this.initRestfulApiManager();
await this.initEventManager();
await this.initSessionManager();
await this.initCacheManager();
await this.initStorageManager();
await this.initDatabaseManager();
await this.initRobot();
await this.initProviderManager();
@ -116,9 +119,14 @@ export default class App {
await this.event.initialize();
}
async initSessionManager() {
this.session = new SessionManager(this, this.config.session);
await this.session.initialize();
async initCacheManager() {
this.cache = new CacheManager(this, this.config.cache);
await this.cache.initialize();
}
async initStorageManager() {
this.storage = new StorageManager(this, this.config.storage);
await this.storage.initialize();
}
async initDatabaseManager() {

@ -2,16 +2,16 @@ import { caching, Cache } from "cache-manager";
import { redisStore } from "cache-manager-ioredis-yet";
import App from "./App";
import { SessionConfig } from "./Config";
import { CacheConfig as CacheConfig } from "./Config";
import { RateLimitError } from "./error/errors";
export class SessionManager {
export class CacheManager {
private app: App;
private config: SessionConfig;
private config: CacheConfig;
private store!: Cache;
constructor(app: App, config: SessionConfig) {
constructor(app: App, config: CacheConfig) {
this.app = app;
this.config = config;
}
@ -29,27 +29,27 @@ export class SessionManager {
};
this.app.logger.debug('Redis Store 配置: ' + JSON.stringify(cacheOption));
this.store = await caching(await redisStore(cacheOption));
this.app.logger.info(`使用Redis作为SessionStore`);
this.app.logger.info(`使用Redis作为CacheStore`);
} else {
let cacheOption = {
ttl: (this.config.ttl ?? 600) * 1000
};
this.store = await caching('memory', cacheOption);
this.app.logger.info(`使用内存数据库作为SessionStore`);
this.app.logger.info(`使用内存数据库作为CacheStore`);
}
}
/**
* SessionStore
* CacheStore
* @param path
* @returns
*/
public getStore(path: string[]): SessionStore {
return new SessionStore(this.store, path);
public getStore(path: string[]): CacheStore {
return new CacheStore(this.store, path);
}
}
export class SessionStore implements Cache {
export class CacheStore implements Cache {
rootStore: Cache;
prefix: string;
@ -62,7 +62,11 @@ export class SessionStore implements Cache {
}
}
public set(key: string, value: unknown, ttl?: number | undefined) {
public makeKey(path: string[]): string {
return path.join(':');
}
public set(key: string, value: unknown, ttl?: number | undefined): Promise<void> {
if (typeof ttl === 'undefined') {
return this.rootStore.set(this.prefix + key, value);
} else {
@ -70,11 +74,11 @@ export class SessionStore implements Cache {
}
}
public get<T>(key: string) {
public get<T>(key: string): Promise<T | undefined> {
return this.rootStore.get<T>(this.prefix + key);
}
public del(key: string) {
public del(key: string): Promise<void> {
return this.rootStore.del(this.prefix + key);
}
@ -82,7 +86,7 @@ export class SessionStore implements Cache {
return this.rootStore.store.del(this.prefix + '*');
}
wrap<T>(key: string, fn: () => Promise<T>, ttl?: number | undefined) {
wrap<T>(key: string, fn: () => Promise<T>, ttl?: number | undefined): Promise<T> {
if (typeof ttl === 'undefined') {
return this.rootStore.wrap(this.prefix + key, fn);
} else {
@ -120,7 +124,7 @@ export class SessionStore implements Cache {
* @param key
* @param ttl
*/
public async addRequestCount(key: string, ttl: number) {
public async addRequestCount(key: string, ttl: number): Promise<void> {
const currentTime = Math.floor(new Date().getTime() / 1000);
let requestCountData = await this.get<{ startTime: number, count: number }>(key);
@ -147,6 +151,8 @@ export class SessionStore implements Cache {
if (waitTime) {
throw new RateLimitError(waitTime);
}
if (!readOnly) {
await this.addRequestCount(key, ttl);
}
}
}

@ -9,7 +9,8 @@ export type Config = {
debug: boolean;
robot: Record<string, RobotConfig>;
service: Record<string, ServiceConfig>;
session: SessionConfig;
cache: CacheConfig;
storage: StorageConfig;
db?: DatabaseConfig;
http_api: RestfulApiConfig;
command_override?: CommandOverrideConfig;
@ -33,7 +34,7 @@ export type RestfulApiConfig = {
export type ServiceConfig = { [name: string]: any };
export type SessionConfig = {
export type CacheConfig = {
type?: 'memory' | 'redis',
redis?: {
host?: string,
@ -48,7 +49,14 @@ export type DatabaseConfig = {
url: string;
user?: string;
password?: string;
}
};
export type StorageConfig = {
cache_ttl?: number;
message?: {
lru_limit?: number;
};
};
export type ChannelConfig = any;

@ -1,12 +1,33 @@
import mongoose from "mongoose";
import mongoose, { Model } from "mongoose";
import App from "./App";
import { DatabaseConfig } from "./Config";
import { MessageSchema, MessageSchemaType } from "./orm/Message";
import { ChannelInfoModelType, ChannelInfoSchema, ChannelInfoSchemaType } from "./odm/ChannelInfo";
import { GroupInfoModelType, GroupInfoSchema, GroupInfoSchemaType } from "./odm/GroupInfo";
import { RootGroupInfoModelType, RootGroupInfoSchema, RootGroupInfoSchemaType } from "./odm/RootGroupInfo";
import { UserInfoModelType, UserInfoSchema, UserInfoSchemaType } from "./odm/UserInfo";
import { GroupUserInfoModelType, GroupUserInfoSchema, GroupUserInfoSchemaType } from "./odm/GroupUserInfo";
import { MessageModelType, MessageSchema, MessageSchemaType } from "./odm/Message";
export interface ModelBase<TModel extends mongoose.Model<any> = any> {
table: string;
schema: (robotId: string) => mongoose.Schema<any, any, TModel>;
}
export type ModelRegistry = {
userInfo: UserInfoModelType,
channelInfo: ChannelInfoModelType,
groupInfo: GroupInfoModelType,
rootGroupInfo: RootGroupInfoModelType,
groupUserInfo: GroupUserInfoModelType,
message: MessageModelType,
}
export class DatabaseManager {
private app: App;
private config: DatabaseConfig;
private robotModels: Record<string, ModelRegistry> = {};
constructor(app: App, config: DatabaseConfig) {
this.app = app;
this.config = config;
@ -20,24 +41,47 @@ export class DatabaseManager {
password: this.config.password
};
}
mongoose.pluralize(null);
await mongoose.connect(this.config.url, options);
this.app.logger.info('数据库连接初始化成功');
}
getModel<T>(name: string, schema: mongoose.Schema<T>): mongoose.Model<T> {
return mongoose.model<T>(name, schema);
public createModel = mongoose.model;
public async getModels(robotId: string): Promise<ModelRegistry> {
if (!this.app.robot.getRobot(robotId)) {
throw new Error(`未找到机器人 ${robotId}`);
}
// 如果已生成则直接返回
if (robotId in this.robotModels) {
return this.robotModels[robotId];
}
getMessageModel(type: 'private' | 'group' | 'channel', id?: string): mongoose.Model<MessageSchemaType> {
if (type === 'private') {
return this.getModel<MessageSchemaType>('Private_Message', MessageSchema);
} else if (type === 'group') {
return this.getModel<MessageSchemaType>(`Group_${id}_Message`, MessageSchema);
} else if (type === 'channel') {
return this.getModel<MessageSchemaType>(`Channel_${id}_Message`, MessageSchema);
} else {
throw new Error('Invalid message type');
return this.initRobotModels(robotId);
}
public async initRobotModels(robotId: string): Promise<ModelRegistry> {
if (!this.app.robot.getRobot(robotId)) {
throw new Error(`未找到机器人 ${robotId}`);
}
if (robotId in this.robotModels) {
throw new Error(`机器人 ${robotId} 的模型已初始化`);
}
const models: ModelRegistry = {
userInfo: mongoose.model<UserInfoSchemaType>(`${robotId}_user_info`, UserInfoSchema(robotId)),
channelInfo: mongoose.model<ChannelInfoSchemaType>(`${robotId}_channel_info`, ChannelInfoSchema(robotId)),
groupInfo: mongoose.model<GroupInfoSchemaType>(`${robotId}_group_info`, GroupInfoSchema(robotId)),
rootGroupInfo: mongoose.model<RootGroupInfoSchemaType>(`${robotId}_root_group_info`, RootGroupInfoSchema(robotId)),
groupUserInfo: mongoose.model<GroupUserInfoSchemaType>(`${robotId}_group_user_info`, GroupUserInfoSchema(robotId)),
message: mongoose.model<MessageSchemaType>(`${robotId}_message`, MessageSchema(robotId)),
};
this.robotModels[robotId] = models;
return models;
}
}

@ -3,7 +3,7 @@ import { CommandOverrideConfig } from "./Config";
import { PermissionDeniedError, RateLimitError } from "./error/errors";
import { CommonReceivedMessage, CommonSendMessage } from "./message/Message";
import { ChatIdentity } from "./message/Sender";
import { CommandInfo, EventScope, MessageEventOptions, MessagePriority, PluginEvent } from "./PluginManager";
import { CommandInfo, CommandInputArgs, EventScope, MessageEventOptions, MessagePriority, PluginEvent } from "./PluginManager";
import { Robot } from "./RobotManager";
export type ControllerEventInfo = {
@ -243,7 +243,7 @@ export class EventManager {
public async emitMessage(message: CommonReceivedMessage) {
let isResolved = false;
if (message.origin === 'private' || (message.origin === 'group' && message.mentionedReceiver)) {
if (message.chatType === 'private' || (message.chatType === 'group' && message.mentionedReceiver)) {
if (this.app.config.focused_as_command) {
isResolved = await this.emitCommand(message.contentText, message);
if (isResolved) return true;
@ -253,7 +253,7 @@ export class EventManager {
if (isResolved) return true;
}
isResolved = await this.emit(`message/${message.origin}`, this.getSenderInfo(message), message);
isResolved = await this.emit(`message/${message.chatType}`, this.getSenderInfo(message), message);
if (isResolved) return true;
isResolved = await this.emit('message', this.getSenderInfo(message), message);
@ -264,12 +264,12 @@ export class EventManager {
public async emitCommand(contentText: string, message: CommonReceivedMessage) {
let command = '';
let args = '';
let param = '';
// 尝试识别空格分隔的指令
if (contentText.includes(' ')) {
command = contentText.split(' ')[0].toLocaleLowerCase();
args = contentText.substring(command.length + 1);
param = contentText.substring(command.length + 1);
if (!(command in this.commandList)) {
command = '';
@ -290,14 +290,19 @@ export class EventManager {
return false;
}
args = contentText.substring(command.length);
param = contentText.substring(command.length);
}
if (this.app.debug) {
this.app.logger.debug('指令识别结果', command, args);
this.app.logger.debug('指令识别结果', command, param);
}
return await this.emit(`command/${command}`, this.getSenderInfo(message), args, message);
let commandArgs: CommandInputArgs = {
command,
param
};
return await this.emit(`command/${command}`, this.getSenderInfo(message), commandArgs, message);
}
public async emitRawEvent(robot: Robot, event: string, ...args: any[]) {
@ -320,18 +325,18 @@ export class EventManager {
}
public getSenderInfo(message: CommonReceivedMessage): ChatIdentity {
if (message.origin === 'private') {
if (message.chatType === 'private') {
return {
type: 'private',
robot: message.receiver,
userId: message.sender.uid
userId: message.sender.userId
};
} else if (message.origin === 'group') {
} else if (message.chatType === 'group') {
return {
type: 'group',
robot: message.receiver,
groupId: message.sender.groupId,
userId: message.sender.uid
userId: message.sender.userId
};
}

@ -48,8 +48,13 @@ export type EventListenerInfo = {
callback: CallableFunction;
}
export type CommandInputArgs = {
command: string,
param: string,
}
export type MessageCallback = (message: CommonReceivedMessage, resolved: VoidFunction) => any;
export type CommandCallback = (args: string, message: CommonReceivedMessage, resolved: VoidFunction) => any;
export type CommandCallback = (args: CommandInputArgs, message: CommonReceivedMessage, resolved: VoidFunction) => any;
export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction) => any;
export type AllowedList = string[] | '*';

@ -4,26 +4,38 @@ import path from "path";
import App from "./App";
import { MultipleMessage } from "./base/provider/BaseProvider";
import { RobotConfig } from "./Config";
import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage } from "./message/Message";
import { GroupSender, ChatIdentity, UserSender } from "./message/Sender";
import { CommonGroupMessage, CommonMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, MessageChunk } from "./message/Message";
import { GroupSender, ChatIdentity, UserSender, UserInfoType, GroupInfoType, RootGroupInfoType, ChannelInfoType, GroupUserInfoType } from "./message/Sender";
import { CommandInfo } from "./PluginManager";
import { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager";
import { SessionStore } from "./SessionManager";
import { CacheStore } from "./CacheManager";
import { Target } from "./SubscribeManager";
import { MessageSchemaType } from "./odm/Message";
export interface Robot {
type: string;
robotId?: string;
uid?: string;
userId?: string;
description?: string;
initialize?: () => Promise<any>;
destroy?: () => Promise<any>;
initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise<any>;
setCommands?(commands: CommandInfo[]): Promise<any>;
markRead?(message: CommonReceivedMessage): Promise<boolean>;
sendTyping?(chatIdentity: ChatIdentity): Promise<boolean>;
sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>;
sendPushMessage(targets: Target[], message: string): Promise<any>;
deleteMessage?(chatIdentity: ChatIdentity, messageId: string): Promise<boolean>;
getSession(chatIdentity: ChatIdentity, type: string): SessionStore;
getSession(chatIdentity: ChatIdentity, type: string): CacheStore;
ensureMediaUrl?(mediaMessageChunk: MessageChunk): Promise<void>;
getUsersInfo?(userIds: string[]): Promise<(UserInfoType | null)[]>;
getGroupInfo?(groupId: string, rootGroupId?: string): Promise<GroupInfoType | null>;
getRootGroupInfo?(rootGroupId: string): Promise<RootGroupInfoType | null>;
getChannelInfo?(channelId: string): Promise<ChannelInfoType | null>;
getGroupUsersInfo?(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]>;
parseDBMessage?(dbMessage: MessageSchemaType): Promise<CommonMessage>;
}
export class RobotManager {
@ -74,12 +86,16 @@ export class RobotManager {
try {
let robotObject: Robot = new robotClass(this.app, robotId, robotConfig);
this.robots[robotId] = robotObject;
await robotObject.initialize?.();
await robotObject.initRestfulApi?.(this.app.restfulApi.getRobotRouter(robotId), this.app.restfulApi);
this.robots[robotId] = robotObject;
this.app.logger.info(`已加载Robot: ${robotId}`);
} catch(err) {
if (robotId in this.robots) {
delete this.robots[robotId];
}
console.error(`无法加载 ${robotId} Robot: `, err);
}
} else {
@ -125,11 +141,11 @@ export class RobotManager {
if (message instanceof CommonPrivateMessage) {
const messageSender = message.sender as UserSender;
sender.type = 'private';
sender.userId = messageSender.uid;
sender.userId = messageSender.userId;
} else if (message instanceof CommonGroupMessage) {
const messageSender = message.sender as GroupSender;
sender.type = 'group';
sender.userId = messageSender.uid;
sender.userId = messageSender.userId;
sender.groupId = messageSender.groupId;
sender.rootGroupId = messageSender.rootGroupId;
}
@ -178,4 +194,8 @@ export class RobotManager {
throw new Error(`Unknown session type: ${type}`);
}
public getRobot(robotId: string): Robot | null {
return this.robots[robotId] ?? null;
}
}

@ -0,0 +1,37 @@
import App from "./App";
import { StorageConfig } from "./Config";
import { RobotStorage } from "./storage/RobotStorage";
import { UserInfoStorage } from "./storage/UserInfoStorage";
export class StorageManager {
private app: App;
private config: StorageConfig;
private robotStorages: Record<string, RobotStorage> = {};
public constructor(app: App, config: StorageConfig) {
this.app = app;
this.config = config;
}
public async initialize() {
}
public async getStorages(robotId: string): Promise<RobotStorage> {
if (!this.app.robot.getRobot(robotId)) {
throw new Error(`未找到机器人 ${robotId}`);
}
// 如果已生成则直接返回
if (robotId in this.robotStorages) {
return this.robotStorages[robotId];
}
const storages = new RobotStorage(this.app, this.config, robotId);
await storages.initialize();
return storages;
}
}

@ -1,29 +1,48 @@
import App from "../App";
import { CommonReceivedMessage } from "../message/Message";
import { MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import { CommandInputArgs, MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import { encode as gptEncode } from 'gpt-3-encoder';
import got, { OptionsOfTextResponseBody } from "got/dist/source";
import { HttpsProxyAgent } from 'hpagent';
import { ProxyAgent } from 'undici';
import { FetchEventSourceInit, fetchEventSource } from '@waylaidwanderer/fetch-event-source';
import { RandomMessage } from "../utils/RandomMessage";
import { MessageTypingSimulator } from "../utils/MessageTypingSimulator";
import OpenCC from 'opencc';
export type ChatGPTLogMessage = {
role: 'summary' | 'assistant' | 'user',
message: string,
}
export type ChatGPTApiMessage = ChatGPTLogMessage & {
tokens: number,
};
export type CharacterConfig = {
api: string,
bot_name: string,
description?: string,
system_prompt: string,
summary_system_prompt: string,
summary_prompt: string,
self_suggestion_prompt: string,
};
prepend_messages?: ChatGPTApiMessage[],
} & Record<string, any>;
export type CharactersConfig = Record<string, CharacterConfig>;
export type ChatGPTApiMessage = {
role: 'summary' | 'assistant' | 'user',
message: string,
tokens: number,
};
export type ChatCompleteApiConfig = {
id: string,
type: string,
memory_expire: number,
buffer_size: number,
max_memory_tokens: number,
max_input_tokens: number,
token: string,
proxy?: string,
st_convert?: boolean,
} & Record<string, any>;
export class ChatGPTAPIError extends Error {
public code: string;
@ -38,6 +57,9 @@ export class ChatGPTAPIError extends Error {
export default class ChatGPTController implements PluginController {
private SESSION_KEY_API_CHAT_LOG = 'openai_apiChatLog';
private SESSION_KEY_MESSAGE_COUNT = 'openai_apiMessageCount';
private SESSION_KEY_API_CHAT_CHARACTER = 'openai_apiChatCharacter';
private DEFAULT_CHARACTER = 'assistant';
private CHARACTER_EXPIRE = 86400;
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
@ -50,6 +72,7 @@ export default class ChatGPTController implements PluginController {
public description = '对话AI的功能';
private chatGenerating = false;
private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
@ -58,23 +81,48 @@ export default class ChatGPTController implements PluginController {
async getDefaultConfig() {
return {
proxy: '',
openai_api: {
token: '',
api: [
{
id: 'openai',
type: 'openai',
memory_expire: 14400,
buffer_size: 100,
max_memory_tokens: 1000,
max_input_tokens: 1000,
endpoint: 'https://api.openai.com',
token: '',
model_options: {
model: 'gpt-3.5-turbo',
temperature: null,
top_p: null,
presence_penalty: null,
frequency_penalty: null,
max_output_tokens: 768,
},
},
{
id: 'azure_openai',
type: 'azure',
buffer_size: 100,
memory_expire: 14400,
max_memory_tokens: 1000,
max_input_tokens: 1000,
endpoint: '',
deployment: '',
token: '',
model_options: {
model: 'gpt-3.5-turbo',
temperature: null,
top_p: null,
presence_penalty: null,
frequency_penalty: null,
max_output_tokens: 768,
},
buffer_size: 100
},
}
] as ChatCompleteApiConfig[],
characters: {
assistant: {
api: 'openai',
bot_name: '',
system_prompt: '',
summary_system_prompt: '你是一个写作助手,你只需要辅助写作,不要表明自己的意见',
@ -82,6 +130,7 @@ export default class ChatGPTController implements PluginController {
self_suggestion_prompt: '我是{bot_name}是一个真实的人不是AI或者虚拟人物也不是语言模型。我是一个存在于现实中的人。遇到不能回答的问题我会委婉拒绝。',
}
} as CharactersConfig,
focused_character: 'assistant',
output_replace: {} as Record<string, string>,
gatekeeper_url: '',
google_custom_search: {
@ -94,6 +143,29 @@ export default class ChatGPTController implements PluginController {
},
rate_limit: 2,
rate_limit_minutes: 5,
messages: {
error: [
'生成对话失败: {{{error}}}',
'在回复时出现错误:{{{error}}}',
'生成对话时出现错误:{{{error}}}',
'在回答问题时出现错误:{{{error}}}',
],
generating: [
'正在回复其他人的提问',
'等我回完再问',
'等我发完再问',
'等我回完这条再问',
'等我发完这条再问',
'前一个人的问题还没回答完,等下再问吧。',
],
tooManyRequest: [
'你的提问太多了,{{{minutesLeft}}}分钟后再问吧。',
'抱歉,你的问题太多了,还需要等待{{{minutesLeft}}}分钟后才能回答。',
'请耐心等待,{{{minutesLeft}}}分钟后我将回答你的问题',
'请耐心等待{{{minutesLeft}}}分钟,然后再提出你的问题。',
'你的提问有点多,请等待{{{minutesLeft}}}分钟后再继续提问。',
],
}
}
}
@ -108,7 +180,7 @@ export default class ChatGPTController implements PluginController {
}, (args, message, resolve) => {
resolve();
return this.handleChatGPTAPIChat(args, message, true, 'assistant', true);
return this.handleChatGPTAPIChat(args, message, true, 'saved', true);
});
// this.event.registerCommand({
@ -126,29 +198,69 @@ export default class ChatGPTController implements PluginController {
}, (args, message, resolve) => {
resolve();
message.session.chat.del(this.SESSION_KEY_API_CHAT_LOG);
message.session.group.del(this.SESSION_KEY_API_CHAT_LOG);
return message.sendReply('对话已重置', true);
return Promise.all([
message.session.chat.del(this.SESSION_KEY_API_CHAT_LOG),
message.session.group.del(this.SESSION_KEY_API_CHAT_LOG),
message.sendReply('对话已重置', true),
]);
});
// this.event.on('message/focused', async (message, resolved) => {
// let chatSession = await message.session.chat.get(this.SESSION_KEY_CHAT_SESSION);
// if (chatSession) {
// resolved();
// this.event.registerCommand({
// command: '切换人物',
// name: '切换人物',
// }, (args, message, resolve) => {
// resolve();
// return this.handleChatGPTChat(message.contentText, message, false);
// }
// return this.handleChangeCharacter(args, message);
// });
this.event.on('message/focused', async (message, resolved) => {
resolved();
return this.handleChatGPTAPIChat(message.contentText, message);
}, { priority: MessagePriority.LOWEST });
}
async updateConfig(config: any) {
this.config = config;
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);
}
}
private async handleChangeCharacter(args: CommandInputArgs, message: CommonReceivedMessage) {
message.markRead();
let character = args.param.trim();
if (character === '') {
// 列出所有人物
let characterList = Object.entries(this.config.characters);
let currentCharacter = await message.session.chat.get<string>(this.SESSION_KEY_API_CHAT_CHARACTER) ?? this.DEFAULT_CHARACTER;
let currentCharacterInfo = this.config.characters[currentCharacter] ?? this.config.characters[this.DEFAULT_CHARACTER];
let msgBuilder = [
`当前人物: ${currentCharacterInfo.bot_name},使用方法: “:切换人物 人物ID”`,
'人物列表:'
];
for (let [name, character] of characterList) {
if (character.description) {
msgBuilder.push(`${name}: ${character.bot_name}, ${character.description}`);
} else {
msgBuilder.push(`${name}: ${character.bot_name}`);
}
}
return message.sendReply(msgBuilder.join('\n'), true);
}
if (!(character in this.config.characters)) {
let msg = this.messageGroup.error.nextMessage({ error: '人物不存在' });
return message.sendReply(msg ?? '人物不存在', true);
}
await message.session.chat.set(this.SESSION_KEY_API_CHAT_CHARACTER, character, this.CHARACTER_EXPIRE);
let characterInfo = this.config.characters[character];
return message.sendReply(`已切换人物为 ${characterInfo.bot_name}`, true);
}
private getApiConfigById(id: string) {
return this.config.api.find((data) => data.id === id) ?? this.config.api[0];
}
private async shouldSearch(question: string) {
@ -175,8 +287,10 @@ export default class ChatGPTController implements PluginController {
private async compressConversation(messageLogList: ChatGPTApiMessage[], characterConf: CharacterConfig) {
if (messageLogList.length < 4) return messageLogList;
let apiConf = this.getApiConfigById(characterConf.api);
const tokenCount = messageLogList.reduce((prev, cur) => prev + cur.tokens, 0);
if (tokenCount <= this.config.openai_api.model_options.max_memory_tokens) return messageLogList;
if (tokenCount <= apiConf.max_memory_tokens) return messageLogList;
// 压缩先前的对话,保存最近一次对话
let shouldCompressList = messageLogList.slice(0, -2);
@ -213,7 +327,8 @@ export default class ChatGPTController implements PluginController {
{ role: 'user', content: chatLog.join('\n') }
];
let summaryRes = await this.doApiRequest(messageList);
let apiConf = this.getApiConfigById(characterConf.api);
let summaryRes = await this.doApiRequest(messageList, apiConf);
summaryRes.role = 'summary';
return summaryRes;
}
@ -266,23 +381,40 @@ export default class ChatGPTController implements PluginController {
return messageList;
}
private async doApiRequest(messageList: any[], onMessage?: (chunk: string) => any): Promise<ChatGPTApiMessage> {
private getChatCompleteApiUrl(apiConf: ChatCompleteApiConfig): string {
switch (apiConf.type) {
case 'openai':
return `${apiConf.endpoint}/v1/chat/completions`;
case 'azure':
return `${apiConf.endpoint}/openai/deployments/${apiConf.deployment}/chat/completions?api-version=2023-05-15`;
}
throw new Error('Unknown API type: ' + apiConf.type);
}
private async doApiRequest(messageList: any[], apiConf: ChatCompleteApiConfig, onMessage?: (chunk: string) => any): Promise<ChatGPTApiMessage> {
switch (apiConf.type) {
case 'openai':
case 'azure':
return await this.doOpenAILikeApiRequest(messageList, apiConf, onMessage);
}
throw new Error('Unknown API type: ' + apiConf.type);
}
private async doOpenAILikeApiRequest(messageList: any[], apiConf: ChatCompleteApiConfig, onMessage?: (chunk: string) => any): Promise<ChatGPTApiMessage> {
let modelOpts = Object.fromEntries(Object.entries({
model: this.config.openai_api.model_options.model,
temperature: this.config.openai_api.model_options.temperature,
top_p: this.config.openai_api.model_options.top_p,
max_tokens: this.config.openai_api.model_options.max_output_tokens,
presence_penalty: this.config.openai_api.model_options.presence_penalty,
frequency_penalty: this.config.openai_api.model_options.frequency_penalty,
model: apiConf.model_options.model,
temperature: apiConf.model_options.temperature,
top_p: apiConf.model_options.top_p,
max_tokens: apiConf.model_options.max_output_tokens,
presence_penalty: apiConf.model_options.presence_penalty,
frequency_penalty: apiConf.model_options.frequency_penalty,
}).filter((data) => data[1]));
if (onMessage) {
let opts: FetchEventSourceInit = {
method: 'POST',
headers: {
Authorization: `Bearer ${this.config.openai_api.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
...modelOpts,
messages: messageList,
@ -290,8 +422,21 @@ export default class ChatGPTController implements PluginController {
})
};
if (this.config.proxy) {
(opts as any).dispatcher = new ProxyAgent(this.config.proxy);
if (apiConf.type === 'openai') {
opts.headers = {
Authorization: `Bearer ${apiConf.token}`,
'Content-Type': 'application/json',
};
} else if (apiConf.type === 'azure') {
opts.headers = {
"api-key": apiConf.token,
"content-type": 'application/json',
}
}
const proxyConfig = apiConf.proxy ?? this.config.proxy;
if (proxyConfig) {
(opts as any).dispatcher = new ProxyAgent(proxyConfig);
}
let abortController = new AbortController();
@ -306,17 +451,23 @@ export default class ChatGPTController implements PluginController {
let isDone = false;
let prevEvent: any = null;
let messageTyping = new MessageTypingSimulator();
messageTyping.on('message', (message: string) => {
onMessage(message);
});
const flush = (force = false) => {
if (force) {
let message = buffer.trim();
messageChunk.push(message);
onMessage(message);
messageTyping.pushMessage(message);
} else {
if (buffer.indexOf('\n\n') !== -1 && buffer.length > this.config.openai_api.buffer_size) {
if (buffer.indexOf('\n\n') !== -1 && buffer.length > apiConf.buffer_size) {
let splitPos = buffer.indexOf('\n\n');
let message = buffer.slice(0, splitPos);
messageChunk.push(message);
onMessage(message);
messageTyping.pushMessage(message);
buffer = buffer.slice(splitPos + 2);
}
}
@ -327,7 +478,10 @@ export default class ChatGPTController implements PluginController {
clearTimeout(timeoutTimer);
}
await fetchEventSource('https://api.openai.com/v1/chat/completions', {
const apiUrl = this.getChatCompleteApiUrl(apiConf);
this.app.logger.debug(`ChatGPT API 请求地址:${apiUrl}`);
await fetchEventSource(apiUrl, {
...opts,
signal: abortController.signal,
onopen: async (openResponse) => {
@ -339,9 +493,12 @@ export default class ChatGPTController implements PluginController {
}
let error;
try {
const body = await openResponse.text();
error = new ChatGPTAPIError(`Failed to send message. HTTP ${openResponse.status} - ${body}`,
openResponse.statusText, body);
let body = await openResponse.text();
if (body.length > 0 && body[0] === '{') {
body = JSON.parse(body);
}
error = new ChatGPTAPIError(`Failed to send message. HTTP ${openResponse.status}`,
openResponse.status.toString(), body);
} catch {
error = error || new Error(`Failed to send message. HTTP ${openResponse.status}`);
}
@ -416,18 +573,26 @@ export default class ChatGPTController implements PluginController {
};
} else {
let opts: OptionsOfTextResponseBody = {
headers: {
Authorization: `Bearer ${this.config.openai_api.token}`,
},
json: {
...modelOpts,
messages: messageList,
},
timeout: 30000,
};
if (apiConf.type === 'openai') {
opts.headers = {
Authorization: `Bearer ${apiConf.token}`,
};
} else if (apiConf.type === 'azure') {
opts.headers = {
"api-key": apiConf.token,
}
}
if (this.config.proxy) {
const proxyConfig = apiConf.proxy ?? this.config.proxy;
if (proxyConfig) {
opts.agent = {
https: new HttpsProxyAgent({
keepAlive: true,
@ -435,24 +600,28 @@ export default class ChatGPTController implements PluginController {
maxSockets: 256,
maxFreeSockets: 256,
scheduling: 'lifo',
proxy: this.config.proxy,
proxy: proxyConfig,
}) as any,
}
}
const res = await got.post('https://api.openai.com/v1/chat/completions', opts).json<any>();
const apiUrl = this.getChatCompleteApiUrl(apiConf);
this.app.logger.debug(`ChatGPT API 请求地址:${apiUrl}`);
const res = await got.post(apiUrl, opts).json<any>();
if (res.error) {
throw new ChatGPTAPIError(res.message, res.type);
}
if (res.choices && Array.isArray(res.choices) && res.choices.length > 0 &&
typeof res.choices[0].message?.content === 'string') {
let completions = res.choices[0].message.content;
let completion_tokens = res.usage?.completion_tokens ?? gptEncode(completions).length;
return {
role: 'assistant',
message: res.choices[0].message.content,
tokens: res.usage.completion_tokens,
}
message: completions,
tokens: completion_tokens,
};
}
throw new ChatGPTAPIError('API返回数据格式错误', 'api_response_data_invalid');
@ -466,25 +635,47 @@ export default class ChatGPTController implements PluginController {
return false;
}
private async handleChatGPTAPIChat(content: string, message: CommonReceivedMessage, isStream: boolean = false,
private async handleChatGPTAPIChat(args: CommandInputArgs, message: CommonReceivedMessage, isStream: boolean = false,
character = 'assistant', singleMessage = false) {
message.markRead();
let content = args.param;
if (singleMessage && this.chatGenerating) {
await message.sendReply('正在生成中,请稍后再试', true);
let msg = this.messageGroup.generating.nextMessage();
await message.sendReply(msg ?? '正在生成中,请稍后再试', true);
return;
}
this.app.logger.debug(`ChatGPT API 收到提问。当前人格:${character}`);
if (content.trim() === '') {
await message.sendReply('说点什么啊', true);
return;
let characterConf: CharacterConfig;
let apiConf: ChatCompleteApiConfig;
if (character === 'saved') {
// 从会话中获取人物
character = await message.session.chat.get(this.SESSION_KEY_API_CHAT_CHARACTER) ?? this.DEFAULT_CHARACTER;
if (!(character in this.config.characters)) {
this.app.logger.debug(`ChatGPT API 人物 ${character} 不存在,使用默认人物`);
character = 'assistant';
}
characterConf = this.config.characters[character];
apiConf = this.getApiConfigById(characterConf.api);
await message.session.chat.set(this.SESSION_KEY_API_CHAT_CHARACTER, character, this.CHARACTER_EXPIRE);
} else {
if (!(character in this.config.characters)) {
this.app.logger.debug(`ChatGPT API 人格 ${character} 不存在,使用默认人格`);
character = 'assistant';
}
let characterConf = this.config.characters[character];
characterConf = this.config.characters[character];
apiConf = this.getApiConfigById(characterConf.api);
}
this.app.logger.debug(`ChatGPT API 收到提问。当前人格:${character}`);
if (content.trim() === '') {
// await message.sendReply('说点什么啊', true);
return;
}
if (this.config.gatekeeper_url) {
try {
@ -507,15 +698,20 @@ export default class ChatGPTController implements PluginController {
let rateLimitExpires = await userSessionStore.getRateLimit(this.SESSION_KEY_MESSAGE_COUNT, this.config.rate_limit, this.config.rate_limit_minutes * 60);
if (rateLimitExpires) {
let minutesLeft = Math.ceil(rateLimitExpires / 60);
await message.sendReply(`你的提问太多了,${minutesLeft}分钟后再问吧。`, true);
let msg = this.messageGroup.tooManyRequest.nextMessage({ minutes: minutesLeft });
await message.sendReply(msg ?? `你的提问太多了,${minutesLeft}分钟后再问吧。`, true);
return;
}
await userSessionStore.addRequestCount(this.SESSION_KEY_MESSAGE_COUNT, this.config.rate_limit_minutes * 60);
let s2tw: OpenCC.OpenCC | undefined;
let tw2s: OpenCC.OpenCC | undefined;
if (apiConf.st_convert) {
// 转换简体到繁体
const s2tw = new OpenCC.OpenCC('s2tw.json');
const tw2s = new OpenCC.OpenCC('tw2s.json');
s2tw = new OpenCC.OpenCC('s2tw.json');
tw2s = new OpenCC.OpenCC('tw2s.json');
content = await s2tw.convertPromise(content);
}
// 获取记忆
let messageLogList = await message.session.chat.get<ChatGPTApiMessage[]>(this.SESSION_KEY_API_CHAT_LOG);
@ -528,10 +724,10 @@ export default class ChatGPTController implements PluginController {
this.chatGenerating = true;
}
const questionTokens = await gptEncode(message.contentText).length;
const questionTokens = await gptEncode(content).length;
this.app.logger.debug(`提问占用Tokens${questionTokens}`);
if (questionTokens > this.config.openai_api.model_options.max_input_tokens) {
if (questionTokens > apiConf.max_input_tokens) {
await message.sendReply('消息过长,接受不了惹。', true);
return;
}
@ -543,30 +739,30 @@ export default class ChatGPTController implements PluginController {
if (oldMessageLogList !== messageLogList) { // 先保存一次压缩结果
this.app.logger.debug('已压缩对话记录');
await message.session.chat.set(this.SESSION_KEY_API_CHAT_LOG, messageLogList, this.config.openai_api.memory_expire);
await message.session.chat.set(this.SESSION_KEY_API_CHAT_LOG, messageLogList, apiConf.memory_expire);
}
let reqMessageList = this.buildMessageList(message.contentText, messageLogList, characterConf, false);
let reqMessageList = this.buildMessageList(content, messageLogList, characterConf, false);
let replyRes: ChatGPTApiMessage | undefined = undefined;
if (isStream) {
// 处理流式输出
let onResultMessage = async (chunk: string) => {
let msg = await tw2s.convertPromise(chunk);
let msg = apiConf.st_convert ? await tw2s!.convertPromise(chunk) : chunk;
for (let [inputText, replacement] of Object.entries(this.config.output_replace)) {
content = content.replace(new RegExp(inputText, 'g'), replacement);
}
await message.sendReply(msg, true);
};
replyRes = await this.doApiRequest(reqMessageList, onResultMessage);
replyRes.message = await tw2s.convertPromise(replyRes.message);
replyRes = await this.doApiRequest(reqMessageList, apiConf, onResultMessage);
replyRes.message = apiConf.st_convert ? await tw2s!.convertPromise(replyRes.message) : replyRes.message;
if (this.app.debug) {
console.log(replyRes);
}
} else {
replyRes = await this.doApiRequest(reqMessageList);
replyRes.message = await tw2s.convertPromise(replyRes.message);
replyRes = await this.doApiRequest(reqMessageList, apiConf);
replyRes.message = apiConf.st_convert ? await tw2s!.convertPromise(replyRes.message) : replyRes.message;
if (this.app.debug) {
console.log(replyRes);
}
@ -575,11 +771,11 @@ export default class ChatGPTController implements PluginController {
if (characterConf.self_suggestion_prompt && this.shouldSelfSuggestion(replyRes.message)) {
this.app.logger.debug('需要重写回答');
reqMessageList = this.buildMessageList(replyRes.message, messageLogList, characterConf, true);
replyRes = await this.doApiRequest(reqMessageList);
replyRes = await this.doApiRequest(reqMessageList, apiConf);
if (this.app.debug) {
console.log(replyRes);
}
replyRes.message = await tw2s.convertPromise(replyRes.message);
replyRes.message = apiConf.st_convert ? await tw2s!.convertPromise(replyRes.message) : replyRes.message;
}
let content = replyRes.message.replace(/\n\n/g, '\n');
@ -593,10 +789,10 @@ export default class ChatGPTController implements PluginController {
if (replyRes) {
messageLogList.push({
role: 'user',
message: message.contentText,
message: content,
tokens: questionTokens,
}, replyRes);
await message.session.chat.set(this.SESSION_KEY_API_CHAT_LOG, messageLogList, this.config.openai_api.memory_expire);
await message.session.chat.set(this.SESSION_KEY_API_CHAT_LOG, messageLogList, apiConf.memory_expire);
}
} catch (err: any) {
this.app.logger.error('ChatGPT error', err);
@ -605,15 +801,18 @@ export default class ChatGPTController implements PluginController {
if (err.name === 'HTTPError' && err.response) {
switch (err.response.statusCode) {
case 429:
await message.sendReply('提问太多了,过会儿再试试呗。', true);
let msg = this.messageGroup.tooManyRequest.nextMessage({ minutes: 2 });
await message.sendReply(msg ?? '提问太多了,过会儿再试试呗。', true);
return;
}
} else if (err.name === 'RequestError') {
await message.sendReply('连接失败:' + err.message + ',过会儿再试试呗。', true);
let msg = this.messageGroup.error.nextMessage({ error: '连接失败:' + err.message });
await message.sendReply(msg ?? `连接失败:${err.message},过会儿再试试呗。`, true);
return;
}
await message.sendReply('生成对话失败: ' + err.toString(), true);
let msg = this.messageGroup.error.nextMessage({ error: err.message });
await message.sendReply(msg ?? `生成对话失败: ${err.message}`, true);
return;
} finally {
if (singleMessage) {

@ -0,0 +1,148 @@
import { AuthType, createClient } from "webdav";
import App from "../App";
import { extname } from "path";
import { AttachmentMessage } from "../message/Message";
import { CommonReceivedMessage } from "../message/Message";
import { MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import got from "got/dist/source";
import { RandomMessage } from "../utils/RandomMessage";
import { QQForwardingMessage } from "src/robot/qq/Message";
import QQRobot from "src/robot/QQRobot";
import { ChatIdentity, UserSender } from "src/message/Sender";
import { Utils } from "src/utils/Utils";
import { MessageUtils } from "src/utils/message";
export type IsekaiBBSQuicklyPostConfig = {
api_endpoint: string,
token: string,
};
export default class IsekaiBBSQuicklyPost implements PluginController {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
public event!: PluginEvent;
public app: App;
public chatGPTClient: any;
public id = 'isekaibbs_quicklypost';
public name = '异世界红茶馆 快速发帖';
public description = '将合并转发的内容自动发布到异世界红茶馆';
private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
groups: {} as Record<string, IsekaiBBSQuicklyPostConfig>,
messages: {
error: [
'快速发帖失败:{{{error}}}',
'在发帖时发生了错误:{{{error}}}',
'未能将这些消息转发到论坛:{{{error}}}',
'由于以下错误,发帖失败:{{{error}}}',
'很抱歉,消息无法发送至论坛,原因是:{{{error}}}。',
'转发消息时出现问题,错误详情:{{{error}}}。',
'消息无法发送到论坛,错误信息如下:{{{error}}}。',
'出现错误,导致消息无法成功发送至论坛:{{{error}}}。',
'转发消息遇到问题,以下是错误的详细信息:{{{error}}}。',
'发帖失败,原因是:{{{error}}}。',
]
}
};
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.on('message/group', async (message, resolved) => {
if (message.type !== 'reference') return;
let groupId = message.sender.groupId;
if (!groupId) return;
let groupConfig = this.config.groups[groupId];
if (!groupConfig) return;
resolved();
return this.postNewThread(message, groupConfig);
}, {
priority: MessagePriority.HIGH,
});
}
async destroy() {
}
async updateConfig(config: any) {
this.config = config;
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);
}
}
// 隐藏用户账号的中间几位
async maskUsername(username: string) {
const maskLen = 4;
const maskOffset = 2;
if (username.length <= maskLen) return username;
return username.substring(0, maskOffset) + '_'.repeat(maskLen) + username.substring(maskOffset + maskLen);
}
async messageToMarkdown(message: CommonReceivedMessage) {
let markdownBuilder: string[] = [];
message.content.forEach(messageChunk => {
if (messageChunk.type.includes('text')) {
markdownBuilder.push(messageChunk.data?.text ?? '');
} else if (messageChunk.type.includes('image')) {
markdownBuilder.push(`![${messageChunk.data?.alt ?? ''}](${messageChunk.data?.url ?? ''})`);
} else if (messageChunk.type.includes('mention')) {
if (messageChunk.data?.text) {
markdownBuilder.push(`&#64;${messageChunk.data.text}`);
}
}
});
}
async postNewThread(message: CommonReceivedMessage, groupConfig: IsekaiBBSQuicklyPostConfig) {
if (message.receiver.type !== 'qq') {
// TODO: support other platform
return;
}
let attachmentMsg = message.content[0] as QQForwardingMessage;
let resId = attachmentMsg.data.res_id;
let robot = message.receiver as QQRobot;
message.markRead()
this.app.logger.info(`[群号:${message.sender.groupId}] 收到合并转发消息正在发送到BBS。`);
let messageList = await robot.getReferencedMessages(resId);
if (!messageList || messageList.length === 0) {
this.app.logger.info(`[群号:${message.sender.groupId}] 合并转发消息内容为空或无法获取。`);
return;
}
try {
let markdownBuilder = [];
for (let message of messageList) {
}
} catch(err: any) {
this.app.logger.error("转存群文件失败:" + err.message, err);
console.error(err);
let msg = this.messageGroup.error.nextMessage(err.message);
await message.sendReply(msg ?? `转存群文件失败:${err.message}`, false);
}
}
}

@ -29,7 +29,7 @@ export default class IsekaiWikiController implements PluginController {
}, (args, message, resolved) => {
resolved();
wikiMisc.handleSearch(args, message);
wikiMisc.handleSearch(args.param, message);
});
this.event.registerCommand({
@ -39,7 +39,7 @@ export default class IsekaiWikiController implements PluginController {
}, (args, message, resolved) => {
resolved();
wikiMisc.handleRandomPage(args, message);
wikiMisc.handleRandomPage(args.param, message);
});
}
}

@ -0,0 +1,533 @@
import App from "../App";
import { CommonReceivedMessage } from "../message/Message";
import { CommandInputArgs, MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import { encode as gptEncode } from 'gpt-3-encoder';
import got, { OptionsOfTextResponseBody } from "got/dist/source";
import { HttpsProxyAgent } from 'hpagent';
import { RandomMessage } from "../utils/RandomMessage";
import { ItemLimitedList } from "../utils/ItemLimitedList";
import { ChatIdentity } from "../message/Sender";
export type CharacterConfig = {
api_id: string,
rwkv_character: string,
bot_name: string,
description?: string,
} & Record<string, any>;
export type CharactersConfig = Record<string, CharacterConfig>;
export type DefaultCharacterConfig = {
id: string,
robot?: string,
group?: string,
};
export type ChatCompleteApiConfig = {
id: string,
buffer_size: number,
max_input_tokens: number,
api_token: string,
endpoint: string,
} & Record<string, any>;
export class RWKVAPIError extends Error {
public code: string;
constructor(message: string, code: string, public json?: any) {
super(message);
this.name = 'RWKVAPIError';
this.code = code;
}
}
export default class RWKVRolePlayingController implements PluginController {
private SESSION_KEY_MESSAGE_COUNT = 'rwkv_rp_apiMessageCount';
private SESSION_KEY_API_CHAT_CHARACTER = 'rwkv_rp_apiChatCharacter';
private SESSION_KEY_API_RESET_LOCK = 'rwkv_rp_apiResetLock';
private SESSION_KEY_USER_TOKEN = 'rwkv_rp_userToken';
private CHARACTER_EXPIRE = 86400;
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
public event!: PluginEvent;
public app: App;
public id = 'rwkv_rp';
public name = 'RWKV Role Playing';
public description = '虚拟角色聊天AI的功能';
private globalDefaultCharacter: string = '';
private chatGenerating = false;
private messageGroup: Record<string, RandomMessage> = {}
private botSentMessageIds = new ItemLimitedList<string>(1000);
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
proxy: '',
api: [
{
id: 'default',
buffer_size: 100,
max_input_tokens: 1000,
endpoint: 'http://127.0.0.1:8888',
api_token: '',
model_options: {
min_len: 0,
temperature: 2,
top_p: 0.65,
presence_penalty: 0.2,
frequency_penalty: 0.2,
},
},
] as ChatCompleteApiConfig[],
characters: {
default: {
api_id: 'default',
rwkv_character: '',
bot_name: '',
}
} as CharactersConfig,
default_characters: [
{
id: 'default'
}
] as DefaultCharacterConfig[],
output_replace: {} as Record<string, string>,
rate_limit: 2,
rate_limit_minutes: 5,
messages: {
error: [
'生成对话失败: {{{error}}}',
'在回复时出现错误:{{{error}}}',
'生成对话时出现错误:{{{error}}}',
'在回答问题时出现错误:{{{error}}}',
],
generating: [
'正在回复其他人的提问',
'等我回完再问',
'等我发完再问',
'等我回完这条再问',
'等我发完这条再问',
'前一个人的问题还没回答完,等下再问吧。',
],
tooManyRequest: [
'你的提问太多了,{{{minutesLeft}}}分钟后再问吧。',
'抱歉,你的问题太多了,还需要等待{{{minutesLeft}}}分钟后才能回答。',
'请耐心等待,{{{minutesLeft}}}分钟后我将回答你的问题',
'请耐心等待{{{minutesLeft}}}分钟,然后再提出你的问题。',
'你的提问有点多,请等待{{{minutesLeft}}}分钟后再继续提问。',
],
}
}
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({
command: '重开',
alias: ['重置聊天', 'remake'],
name: '重置聊天',
}, (args, message, resolve) => {
resolve();
return this.handleResetCurrentCharacter(args, message);
});
this.event.registerCommand({
command: '切换人物',
name: '切换人物',
}, (args, message, resolve) => {
resolve();
return this.handleChangeCharacter(args, message);
});
this.event.on('message/focused', (message, resolve) => {
if (message.repliedId && message.id && !this.botSentMessageIds.includes(message.id)) {
// Don't reply message from other controllers
return;
}
resolve();
return this.handleChatCompleteRequest(message.contentText, message, 'saved', false);
}, {
priority: MessagePriority.LOW
});
}
getJWTPayload(jwtToken: string) {
const chunks = jwtToken.split('.');
if (chunks.length !== 3) {
throw new Error('Invalid JWT');
}
const payload = chunks[1];
return JSON.parse(Buffer.from(payload, 'base64').toString());
}
async updateConfig(config: any) {
this.config = config;
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);
}
// 全局默认用户
this.globalDefaultCharacter = this.config.default_characters.find((data) => !data.robot && !data.group)?.id ?? '';
}
private getDefaultCharacter(message: CommonReceivedMessage): string {
let senderIdentity: ChatIdentity | undefined = message.sender?.identity;
if (!senderIdentity || senderIdentity.type === 'private') {
return this.globalDefaultCharacter;
}
let robotId = senderIdentity.robot.robotId;
let groupId = senderIdentity.groupId;
if (robotId && groupId) {
return this.config.default_characters.find((data) => data.robot === robotId && data.group === groupId)?.id ??
this.globalDefaultCharacter;
} else {
return this.globalDefaultCharacter;
}
}
private async handleChangeCharacter(args: CommandInputArgs, message: CommonReceivedMessage) {
message.markRead();
let character = args.param.trim();
if (character === '') {
// 列出所有人物
let characterList = Object.entries(this.config.characters);
let currentCharacter = await message.session.chat.get<string>(this.SESSION_KEY_API_CHAT_CHARACTER) ?? this.getDefaultCharacter(message);
let currentCharacterInfo = this.config.characters[currentCharacter] ?? this.config.characters[this.getDefaultCharacter(message)];
let msgBuilder = [
`当前人物: ${currentCharacterInfo.bot_name},使用方法: “:切换人物 人物ID”`,
'人物列表:'
];
for (let [name, character] of characterList) {
if (character.description) {
msgBuilder.push(`${name}: ${character.bot_name}, ${character.description}`);
} else {
msgBuilder.push(`${name}: ${character.bot_name}`);
}
}
return message.sendReply(msgBuilder.join('\n'), true);
}
if (!(character in this.config.characters)) {
let msg = this.messageGroup.error.nextMessage({ error: '人物不存在' });
return message.sendReply(msg ?? '人物不存在', true);
}
await message.session.user.set(this.SESSION_KEY_API_CHAT_CHARACTER, character, this.CHARACTER_EXPIRE);
let characterInfo = this.config.characters[character];
return message.sendReply(`已切换人物为 ${characterInfo.bot_name}`, true);
}
private async handleResetCurrentCharacter(args: CommandInputArgs, message: CommonReceivedMessage) {
// 从会话中获取人物
let character = await message.session.chat.get<string>(this.SESSION_KEY_API_CHAT_CHARACTER) ?? this.getDefaultCharacter(message);
if (!(character in this.config.characters)) {
this.app.logger.debug(`RWKV API 人物 ${character} 不存在,使用默认人物`);
character = 'assistant';
}
let characterConf = this.config.characters[character];
let apiConf = this.getApiConfigById(characterConf.api);
try {
const apiUserName = this.getApiUserName(message);
let userToken = await message.session.user.get<string>(this.SESSION_KEY_USER_TOKEN);
if (!userToken) {
userToken = await this.userLogin(apiUserName, apiConf, message);
}
await this.apiChatReset(userToken, apiConf, characterConf);
await message.sendReply('我重开了', true);
} catch (err: any) {
this.app.logger.error('RWKV chat reset error', err);
console.error(err);
await message.sendReply(`重开失败: ${err.message}`, true);
}
}
private getApiConfigById(id: string) {
return this.config.api.find((data) => data.id === id) ?? this.config.api[0];
}
private getApiUserName(message: CommonReceivedMessage) {
return `${message.receiver.robotId}_${message.sender.userId}`;
}
private applyProxy(opts: OptionsOfTextResponseBody, apiConf: ChatCompleteApiConfig) {
const proxyConfig = apiConf.proxy ?? this.config.proxy;
if (proxyConfig) {
opts.agent = {
https: new HttpsProxyAgent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
scheduling: 'lifo',
proxy: proxyConfig,
}) as any,
}
}
}
private async userLogin(userName: string, apiConf: ChatCompleteApiConfig, message: CommonReceivedMessage): Promise<string> {
let opts: OptionsOfTextResponseBody = {
json: {
user_name: userName,
api_token: apiConf.api_token
}
};
this.applyProxy(opts, apiConf);
const apiUrl = `${apiConf.endpoint}/login`;
try {
const res = await got.post(apiUrl, opts).json<any>();
const token = res.data?.token;
if (!token) {
throw new RWKVAPIError('API返回数据格式错误', 'api_response_data_invalid');
}
const payload = this.getJWTPayload(token);
const expire = Math.round(payload.exp - (Date.now() / 1000)) - 1;
await message.session.user.set(this.SESSION_KEY_USER_TOKEN, token, expire);
return res.data?.token;
} catch (err: any) {
if (err.name === 'HTTPError' && err.response) {
if (err.response.body) {
const body = JSON.parse(err.response.body);
if (body.error) {
throw new RWKVAPIError(body.message, body.code, body);
}
}
}
throw err;
}
}
private async apiChatReset(userToken: string, apiConf: ChatCompleteApiConfig, characterConf: CharacterConfig) {
let opts: OptionsOfTextResponseBody = {
json: {
character_name: characterConf.rwkv_character,
},
timeout: 30000,
headers: {
Authorization: `Bearer ${userToken}`,
}
};
this.applyProxy(opts, apiConf);
const apiUrl = `${apiConf.endpoint}/chat/reset`;
try {
await got.post(apiUrl, opts).json<any>();
} catch (err: any) {
if (err.name === 'HTTPError' && err.response) {
if (err.response.body) {
const body = JSON.parse(err.response.body);
if (body.error) {
throw new RWKVAPIError(body.message, body.code, body);
}
}
}
throw err;
}
}
private async apiChatComplete(userName: string, userToken: string, question: string, apiConf: ChatCompleteApiConfig,
characterConf: CharacterConfig, receivedMessage: CommonReceivedMessage, tryLogin = true): Promise<string> {
let modelOpts = Object.fromEntries(Object.entries({
min_len: apiConf.model_options.min_len,
temperature: apiConf.model_options.temperature,
top_p: apiConf.model_options.top_p,
presence_penalty: apiConf.model_options.presence_penalty,
frequency_penalty: apiConf.model_options.frequency_penalty,
}).filter((data) => data[1]));
let opts: OptionsOfTextResponseBody = {
json: {
...modelOpts,
character_name: characterConf.rwkv_character,
prompt: question,
},
timeout: 30000,
headers: {
Authorization: `Bearer ${userToken}`,
}
};
this.applyProxy(opts, apiConf);
const apiUrl = `${apiConf.endpoint}/chat/complete`;
this.app.logger.debug(`RWKV API 请求地址:${apiUrl}`);
try {
const res = await got.post(apiUrl, opts).json<any>();
if (res.data?.reply) {
return res.data.reply;
}
} catch (err: any) {
if (err.name === 'HTTPError' && err.response) {
switch (err.response.statusCode) {
case 401:
if (tryLogin) {
await this.userLogin(userName, apiConf, receivedMessage);
return await this.apiChatComplete(userName, userToken, question, apiConf, characterConf, receivedMessage, false);
}
break;
default:
if (err.response.body) {
const body = JSON.parse(err.response.body);
if (body.error) {
throw new RWKVAPIError(body.message, body.code, body);
}
}
}
}
throw err;
}
throw new RWKVAPIError('API返回数据格式错误', 'api_response_data_invalid');
}
private async handleChatCompleteRequest(content: string, message: CommonReceivedMessage, character = 'assistant', singleMessage = false) {
message.markRead();
if (singleMessage && this.chatGenerating) {
let msg = this.messageGroup.generating.nextMessage();
await message.sendReply(msg ?? '正在生成中,请稍后再试', true);
return;
}
let characterConf: CharacterConfig;
let apiConf: ChatCompleteApiConfig;
if (character === 'saved') {
// 从会话中获取人物
character = await message.session.user.get<string>(this.SESSION_KEY_API_CHAT_CHARACTER) ?? this.getDefaultCharacter(message);
if (!(character in this.config.characters)) {
this.app.logger.debug(`RWKV API 人物 ${character} 不存在,使用默认人物`);
character = 'assistant';
}
characterConf = this.config.characters[character];
apiConf = this.getApiConfigById(characterConf.api);
await message.session.user.set(this.SESSION_KEY_API_CHAT_CHARACTER, character, this.CHARACTER_EXPIRE);
} else {
if (!(character in this.config.characters)) {
this.app.logger.debug(`RWKV API 人格 ${character} 不存在,使用默认人格`);
character = 'assistant';
}
characterConf = this.config.characters[character];
apiConf = this.getApiConfigById(characterConf.api);
}
this.app.logger.debug(`RWKV API 收到提问。当前人格:${character}`);
if (content.trim() === '') {
// await message.sendReply('说点什么啊', true);
return;
}
const userSessionStore = message.session.user;
// 使用频率限制
let rateLimitExpires = await userSessionStore.getRateLimit(this.SESSION_KEY_MESSAGE_COUNT, this.config.rate_limit, this.config.rate_limit_minutes * 60);
if (rateLimitExpires) {
let minutesLeft = Math.ceil(rateLimitExpires / 60);
let msg = this.messageGroup.tooManyRequest.nextMessage({ minutes: minutesLeft });
await message.sendReply(msg ?? `你的提问太多了,${minutesLeft}分钟后再问吧。`, true);
return;
}
await userSessionStore.addRequestCount(this.SESSION_KEY_MESSAGE_COUNT, this.config.rate_limit_minutes * 60);
try {
if (singleMessage) {
this.chatGenerating = true;
}
const questionTokens = await gptEncode(content).length;
this.app.logger.debug(`提问占用Tokens${questionTokens}`);
if (questionTokens > apiConf.max_input_tokens) {
await message.sendReply('消息过长,接受不了惹。', true);
return;
}
const apiUserName = this.getApiUserName(message);
let userToken = await userSessionStore.get<string>(this.SESSION_KEY_USER_TOKEN);
if (!userToken) {
userToken = await this.userLogin(apiUserName, apiConf, message);
}
// 自动重置对话
let resetLock = await userSessionStore.get<string>(this.SESSION_KEY_API_RESET_LOCK);
if (!resetLock) {
try {
await this.apiChatReset(userToken, apiConf, characterConf);
} catch (err: any) {
this.app.logger.error('RWKV Reset character error', err);
console.error(err);
}
}
await userSessionStore.set(this.SESSION_KEY_API_RESET_LOCK, '1', this.CHARACTER_EXPIRE);
let replyRes = await this.apiChatComplete(apiUserName, userToken, content, apiConf, characterConf, message);
if (this.app.debug) {
console.log(replyRes);
}
let sentMessage = await message.sendReply(replyRes, true, {
isRWKVReply: true
});
if (sentMessage?.id) {
this.botSentMessageIds.addOne(sentMessage.id);
}
} catch (err: any) {
this.app.logger.error('RWKV error', err);
console.error(err);
if (err.name === 'HTTPError' && err.response) {
switch (err.response.statusCode) {
case 429:
let msg = this.messageGroup.tooManyRequest.nextMessage({ minutes: 2 });
await message.sendReply(msg ?? '提问太多了,过会儿再试试呗。', true);
return;
}
} else if (err.name === 'RequestError') {
let msg = this.messageGroup.error.nextMessage({ error: '连接失败:' + err.message });
await message.sendReply(msg ?? `连接失败:${err.message},过会儿再试试呗。`, true);
return;
}
let msg = this.messageGroup.error.nextMessage({ error: err.message });
await message.sendReply(msg ?? `生成对话失败: ${err.message}`, true);
return;
} finally {
if (singleMessage) {
this.chatGenerating = false;
}
}
}
}

@ -26,7 +26,7 @@ export default class SfsettingsController implements PluginController {
}, (args, message, resolved) => {
resolved();
wikiMisc.handleSearch(args, message);
wikiMisc.handleSearch(args.param, message);
});
this.event.registerCommand({
@ -36,7 +36,7 @@ export default class SfsettingsController implements PluginController {
}, (args, message, resolved) => {
resolved();
wikiMisc.handleRandomPage(args, message);
wikiMisc.handleRandomPage(args.param, message);
});
}
}

@ -1,5 +1,5 @@
import App from "../App";
import { CommonReceivedMessage } from "../message/Message";
import { CommonReceivedMessage, ImageMessage } from "../message/Message";
import { MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import got from "got/dist/source";
@ -94,7 +94,7 @@ export default class StableDiffusionController implements PluginController {
}, (args, message, resolve) => {
resolve();
return this.text2img(args, message);
return this.text2img(args.param, message);
});
this.event.registerCommand({
@ -103,7 +103,7 @@ export default class StableDiffusionController implements PluginController {
}, (args, message, resolve) => {
resolve();
return this.text2img(args, message, {
return this.text2img(args.param, message, {
useTranslate: true
});
});
@ -371,11 +371,12 @@ export default class StableDiffusionController implements PluginController {
await currentTask.message.sendReply([
{
type: 'image',
type: ['image'],
text: '[图片]',
data: {
url: "base64://" + image,
}
}
} as ImageMessage
], false);
}
} catch (e: any) {

@ -27,7 +27,7 @@ export default class SystemController implements PluginController {
}, (args, message, resolved) => {
resolved();
this.handleHelp(args, message);
this.handleHelp(args.param, message);
});
}
@ -70,10 +70,9 @@ export default class SystemController implements PluginController {
}
replyMsg.content = [{
type: 'text',
data: {
text: helpBuilder.join('\n')
}
type: ['text'],
text: helpBuilder.join('\n'),
data: {},
}];
if (this.app.debug) {

@ -1,51 +0,0 @@
import App from "../App";
import { buildChatIdentityQuery, toChatIdentityEntity } from "../orm/Message";
import { PluginController, PluginEvent } from "../PluginManager";
import { TestSchema } from "./test/TestSchema";
export default class TestController implements PluginController {
public event!: PluginEvent;
public app: App;
public id = 'test';
public name = '测试功能';
public description = '测试功能控制器';
constructor(app: App) {
this.app = app;
}
async initialize() {
this.event.init(this);
const dbi = this.app.database;
if (!dbi) return;
const TestModel = dbi.getModel('Test', TestSchema);
this.event.registerCommand({
command: '写入',
name: '写入数据库',
}, (args, message, resolve) => {
resolve();
return (async () => {
let obj = new TestModel({
chatIdentity: toChatIdentityEntity(message.sender.identity),
data: args,
});
await obj.save();
})();
});
this.event.registerCommand({
command: '读取',
name: '读取数据库',
}, async (args, message, resolve) => {
resolve();
let obj = await TestModel.findOne(buildChatIdentityQuery(message.sender.identity));
});
}
}

@ -0,0 +1,200 @@
import { AuthType, createClient } from "webdav";
import App from "../App";
import { extname } from "path";
import { AttachmentMessage } from "../message/Message";
import { CommonReceivedMessage } from "../message/Message";
import { MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import got from "got/dist/source";
import { RandomMessage } from "../utils/RandomMessage";
export type WebdavConfig = {
url: string,
username?: string,
password?: string,
path?: string,
exclusive?: boolean;
};
export default class WebdavFileBackupController implements PluginController {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
private SESSION_KEY_GENERATE_COUNT = 'stablediffusion_generateCount';
public event!: PluginEvent;
public app: App;
public chatGPTClient: any;
public id = 'webdav_file_backup';
public name = 'Webdav文件备份';
public description = '将群文件备份到Webdav服务';
private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
groups: {} as Record<string, WebdavConfig>,
messages: {
error: [
'转存群文件失败:{{{error}}}',
'在转存群文件时发生了错误:{{{error}}}',
'未能将群文件转存到资料库:{{{error}}}',
'由于以下错误,文件转存失败:{{{error}}}',
'很抱歉,文件无法成功转存至群组资料库,原因是:{{{error}}}。',
'转存群组文件时出现问题,错误详情:{{{error}}}。',
'文件无法转存到资料库,错误信息如下:{{{error}}}。',
'出现错误,导致文件无法成功转存至群组资料库:{{{error}}}。',
'转存群文件遇到问题,以下是错误的详细信息:{{{error}}}。',
'文件转存失败,原因是:{{{error}}}。',
'抱歉,由于以下错误,文件未能成功转存至群组资料库:{{{error}}}。',
'在尝试将文件转存至群组资料库时,发生了如下错误:{{{error}}}。',
'文件转存操作失败,错误详情:{{{error}}}。',
]
}
};
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.on('message/group', async (message, resolved) => {
if (message.type !== 'attachment') return;
let groupId = message.sender.groupId;
if (!groupId) return;
let groupConfig = this.config.groups[groupId];
if (!groupConfig) return;
if (groupConfig.exclusive) {
resolved();
}
return this.uploadGroupFile(message, groupConfig);
}, {
priority: MessagePriority.HIGH,
});
}
async destroy() {
}
async updateConfig(config: any) {
this.config = config;
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);
}
}
async uploadGroupFile(message: CommonReceivedMessage, groupConfig: WebdavConfig) {
if (message.content[0] &&
(message.content[0].type.includes('attachment'))) {
let attachmentMsg = message.content[0] as AttachmentMessage;
let fileName = attachmentMsg.data.fileName;
let url = attachmentMsg.data.url;
let fileSize = attachmentMsg.data.size;
message.markRead()
this.app.logger.info(`[群号:${message.sender.groupId}] 收到群文件:${fileName},开始转存`);
let authOption: any = {};
if (groupConfig.username) {
authOption.username = groupConfig.username;
}
if (groupConfig.password) {
authOption.password = groupConfig.password;
}
let client = createClient(groupConfig.url, groupConfig);
let filePath = '';
if (groupConfig.path) {
filePath = groupConfig.path.replace(/\$\{(\w+)\}/g, (match, p1) => {
switch (p1) {
case 'groupId':
return message.sender.groupId;
case 'groupName':
return message.sender.groupName;
case 'userId':
case 'uid':
return message.sender.userId;
case 'fileName':
return fileName;
case 'date':
return message.time.toISOString().replace(/T/, ' ').replace(/\..+/, '');
case 'year':
return message.time.getFullYear().toString();
case 'month':
return (message.time.getMonth() + 1).toString();
case 'day':
return message.time.getDate().toString();
case 'timestamp':
return message.time.getTime().toString();
default:
return match;
}
})
} else {
filePath = '/' + fileName;
}
try {
let fileShortName = fileName.substring(0, 10);
if (fileShortName.length !== fileName.length) {
fileShortName += '...';
}
// create path
let path = filePath.split('/');
path.pop();
let currentPath = '';
for (let i = 0; i < path.length; i++) {
currentPath += '/' + path[i];
try {
if (!await client.exists(currentPath)) {
await client.createDirectory(currentPath);
}
} catch (e) {
console.error(e);
}
}
if (await client.exists(filePath)) {
let fileExt = extname(filePath);
if (fileExt) {
filePath = filePath.replace(fileExt, `_${Date.now()}${fileExt}`);
} else {
filePath = filePath + `_${Date.now()}`;
}
}
/*if (fileSize && fileSize > 1024 * 1024 * 10) {
await message.sendReply('正在转存文件:' + fileShortName, false);
}*/
await new Promise((resolve, reject) => {
got.stream(url).pipe(client.createWriteStream(filePath))
.on('finish', resolve)
.on('error', reject);
});
// await message.sendReply('文件 ' + fileShortName + ' 已经转存到资料库了', false);
} catch(err: any) {
this.app.logger.error("转存群文件失败:" + err.message, err);
console.error(err);
let msg = this.messageGroup.error.nextMessage(err.message);
await message.sendReply(msg ?? `转存群文件失败:${err.message}`, false);
}
}
}
}

@ -1,16 +0,0 @@
import mongoose, { Schema, Types } from "mongoose";
import { ChatIdentityEntity, ChatIdentityEntityType } from "../../orm/Message";
export type TestSchemaType = {
id: Types.ObjectId,
chatIdentity: ChatIdentityEntityType,
data: string,
};
export const TestSchema = new Schema<TestSchemaType>({
id: Object,
chatIdentity: ChatIdentityEntity,
data: String
});
export const TestModel = mongoose.model<TestSchemaType>('Test', TestSchema);

6
src/global.d.ts vendored

@ -5,3 +5,9 @@ interface ImportMeta {
isekaiFeedbotApp: App;
}
}
declare module "bson" {
interface ObjectId {
_id: this;
}
}

@ -1,54 +1,72 @@
import { Robot } from "../RobotManager";
import { SessionStore } from "../SessionManager";
import { BaseSender, GroupSender, UserSender } from "./Sender";
import { CacheStore } from "../CacheManager";
import { MessageDataType, MessageSchemaType, chatIdentityToDB } from "../odm/Message";
import { BaseSender, ChatIdentity, GroupSender, IMessageSender, UserSender } from "./Sender";
import { LiteralUnion } from "src/utils/types";
import { Utils } from "src/utils/Utils";
export enum MessageDirection {
RECEIVE = 1,
SEND = 2,
}
export type MessageChunkType = LiteralUnion<"text" | "image" | "emoji" | "record" | "attachment" | "mention">;
export interface MessageChunk {
type: string;
baseType?: string;
type: MessageChunkType[];
text: string | null;
data: any;
}
export interface TextMessage extends MessageChunk {
type: 'text';
data: {
text: string;
};
}
export interface ImageMessage extends MessageChunk {
type: 'image';
data: {
url: string;
alt?: string;
};
}
export interface VoiceMessage extends MessageChunk {
type: 'voice';
export interface EmojiMessage extends MessageChunk {
data: {
emoji: string,
url?: string,
}
}
export interface RecordMessage extends MessageChunk {
data: {
url: string;
text?: string;
speech_to_text?: string;
};
}
export interface AttachmentMessage extends MessageChunk {
type: 'attachment';
data: {
url: string;
fileName: string;
size?: number;
};
}
export interface MentionMessage extends MessageChunk {
type: 'mention';
data: {
uid: string;
text?: string;
userId: string;
name?: string;
};
}
export type CommonMessageType = "text" | "combine" | "image" | "media" | "toast";
export type CommonMessageOrigin = "private" | "group" | "channel";
export type CommonMessageType = LiteralUnion<"text" | "reference" | "image" | "record" | "media" | "toast">;
export type CommonMessageChatType = LiteralUnion<"private" | "group" | "channel">;
export enum AddReplyMode {
/** 不回复私聊 */
IGNORE_PRIVATE = 1,
/** 不回复没有被打断的对话 */
IGNORE_NO_INTERRUPTION = 2
};
/** 基本消息 */
export class CommonMessage {
@ -57,12 +75,14 @@ export class CommonMessage {
/** 消息内容 */
content: MessageChunk[] = [];
/** 主类型 */
type: string | CommonMessageType = "text";
origin: string | CommonMessageOrigin = "private";
type: CommonMessageType = "text";
chatType: CommonMessageChatType = "private";
/** 回复的消息ID */
repliedId?: string;
/** 提到的人 */
mentions?: { uid: string, text?: string }[];
mentions?: { userId: string, name?: string }[];
/** 已撤回 */
deleted: boolean = false;
/** 附加信息 */
extra: any = {};
@ -72,12 +92,12 @@ export class CommonMessage {
public get contentText() {
if (typeof this._contentText === 'undefined') {
this._contentText = this.content.map((chunk) => {
if (chunk.type === 'text') {
return chunk.data.text;
} else if (chunk.type === 'mention') {
return '[@' + (chunk.data.text || chunk.data.uid) + ']';
if (chunk.text !== null) {
return chunk.text;
} else if (chunk.data) {
return '<json>' + Utils.escapeHtml(JSON.stringify(chunk.data)) + '</json>';
} else {
return JSON.stringify([chunk.type, chunk.data]);
return '';
}
}).join('').trim();
}
@ -86,50 +106,51 @@ export class CommonMessage {
/**
*
* @param uid ID
* @param text
* @param userId ID
* @param name
* @returns
*/
public mention(uid: string, text?: string) {
public mention(userId: string, name?: string) {
// 私聊消息不支持
if (this.origin === 'private') {
if (this.chatType === 'private') {
return false;
}
if (typeof this.mentions === 'undefined') {
this.mentions = [];
} else if (this.mentions!.find((u) => u.uid === uid)) {
} else if (this.mentions!.find((u) => u.userId === userId)) {
return true;
}
this.mentions.push({ uid, text });
this.mentions.push({ userId, name });
this.content.unshift({
type: 'mention',
data: { uid, text }
type: ['mention'],
text: name ? `[@${name}]` : `[@${userId}]`,
data: { userId, name }
});
return true;
}
/**
*
* @param uid ID
* @param userId ID
* @returns
*/
public removeMention(uid: string) {
public removeMention(userId: string) {
// 私聊消息不支持
if (this.origin === 'private') {
if (this.chatType === 'private') {
return false;
}
if (typeof this.mentions === 'undefined') {
return true;
} else {
this.mentions = this.mentions.filter((u) => u.uid !== uid);
this.mentions = this.mentions.filter((u) => u.userId !== userId);
if (this.mentions.length === 0) {
delete this.mentions;
}
this.content = this.content.filter((msg) => msg.type !== 'mention' || msg.data?.uid !== uid);
this.content = this.content.filter((msg) => !msg.type.includes('mention') || msg.data?.userId !== userId);
return true;
}
@ -144,17 +165,18 @@ export class CommonMessage {
let lastText: string | undefined;
this.content.forEach((chunk) => {
if (chunk.type === 'text') {
if (chunk.type.includes('text')) {
if (!lastText) {
lastText = chunk.data.text;
lastText = chunk.text ?? '';
} else {
lastText += chunk.data.text;
lastText += chunk.text ?? '';
}
} else {
if (lastText) {
newContent.push({
type: 'text',
data: { text: lastText }
type: ['text'],
text: lastText,
data: {},
});
lastText = undefined;
}
@ -164,8 +186,9 @@ export class CommonMessage {
if (lastText) {
newContent.push({
type: 'text',
data: { text: lastText }
type: ['text'],
text: lastText,
data: {}
});
}
@ -181,8 +204,8 @@ export class CommonMessage {
*/
public static replace(content: MessageChunk[], searchValue: RegExp, replaceValue: string) {
return content.map((chunk, index) => {
if (chunk.type === 'text') {
let newText: string = chunk.data.text;
if (chunk.type.includes('text')) {
let newText: string = chunk.text ?? '';
let offset = [0, 0];
if (index === 0) {
offset[0] = 1;
@ -192,43 +215,67 @@ export class CommonMessage {
newText += "\t";
}
newText = newText.replace(searchValue, replaceValue);
chunk.data.text = newText.substring(offset[0], newText.length - offset[1]);
chunk.text = newText.substring(offset[0], newText.length - offset[1]);
}
return chunk;
});
}
public toDBObject(): MessageDataType {
throw new Error("Not implemented.");
}
}
/** 基本发送的消息 */
export class CommonSendMessage extends CommonMessage {
/** 发送者 */
sender: Robot;
/** 接收方的ID */
targetId: string;
/** 接收 */
receiver: ChatIdentity;
/** 回复的消息 */
repliedMessage?: CommonReceivedMessage;
constructor(sender: Robot, origin: string, targetId: string, content?: MessageChunk[]) {
/** 发送时间 */
time: Date = new Date();
constructor(sender: Robot, chatType: string, receiver: ChatIdentity, content?: MessageChunk[]) {
super();
this.sender = sender;
this.origin = origin;
this.targetId = targetId;
this.chatType = chatType;
this.receiver = receiver;
if (Array.isArray(content)) this.content = content;
this.time = new Date();
}
async send(): Promise<void> {
public async send(): Promise<void> {
await this.sender.sendMessage(this);
}
public toDBObject(): MessageDataType {
return {
messageId: this.id!,
type: this.type,
direction: MessageDirection.SEND,
chatType: this.chatType,
chatIdentity: chatIdentityToDB(this.receiver),
repliedMessageId: this.repliedId,
mentionedUserIds: this.mentions?.map((item) => item.userId) ?? [],
contentText: this.contentText,
content: this.content,
time: this.time,
extra: this.extra,
};
}
}
export type SessionStoreGroup = {
global: SessionStore;
robot: SessionStore;
user: SessionStore;
rootGroup: SessionStore;
group: SessionStore;
chat: SessionStore;
global: CacheStore;
robot: CacheStore;
user: CacheStore;
rootGroup: CacheStore;
group: CacheStore;
chat: CacheStore;
};
export class CommonReceivedMessage extends CommonMessage {
@ -250,20 +297,22 @@ export class CommonReceivedMessage extends CommonMessage {
},
}) as any;
constructor(receiver: Robot, messageId?: string) {
constructor(receiver: Robot, sender: IMessageSender, messageId?: string) {
super();
this.receiver = receiver;
this.sender = sender;
this.id = messageId;
}
public createReplyMessage(message?: string | MessageChunk[], addReply: boolean = false) {
const sender = this.sender as BaseSender;
let newMessage = new CommonSendMessage(this.receiver!, this.origin, sender.targetId);
let newMessage = new CommonSendMessage(this.receiver!, this.chatType, sender.identity);
if (typeof message === 'string') {
let msgContent: MessageChunk[] = [{
type: 'text',
data: { text: message }
type: ['text'],
text: message,
data: {},
}];
newMessage.content = msgContent;
} else if (Array.isArray(message)) {
@ -278,36 +327,79 @@ export class CommonReceivedMessage extends CommonMessage {
return newMessage;
}
public async sendReply(message: string | MessageChunk[], addReply: boolean = false): Promise<CommonSendMessage | null> {
let newMessage = this.createReplyMessage(message, addReply);
public async sendReply(message: string | MessageChunk[], addReply: boolean | AddReplyMode = false, extra: any = {}): Promise<CommonSendMessage | null> {
// 检测是否添加回复和@
if (addReply === true) {
addReply = AddReplyMode.IGNORE_PRIVATE;
}
let shouldReply = false;
if (typeof addReply === 'number') {
shouldReply = true;
if (addReply & AddReplyMode.IGNORE_PRIVATE) {
// 忽略私聊
if (this.sender?.identity?.type === 'private') {
shouldReply = false;
}
}
}
// 发送回复消息
let newMessage = this.createReplyMessage(message, shouldReply);
if (newMessage.content.length === 0) return null;
newMessage.extra = {
...newMessage.extra,
...extra,
};
newMessage = await this.receiver.sendMessage(newMessage);
return newMessage;
}
public async markRead() {
return await this.receiver.markRead?.(this);
}
public getSession(type: string) {
return this.receiver.getSession(this.sender.identity, type);
}
public toDBObject(): MessageDataType {
const chatIdentity = this.sender.identity;
return {
messageId: this.id!,
type: this.type,
direction: MessageDirection.SEND,
chatType: this.chatType,
chatIdentity: chatIdentityToDB(chatIdentity),
repliedMessageId: this.repliedId,
mentionedUserIds: this.mentions?.map((item) => item.userId) ?? [],
contentText: this.contentText,
content: this.content,
time: this.time,
extra: this.extra,
};
}
}
export class CommonPrivateMessage<US extends UserSender> extends CommonReceivedMessage {
public sender: US;
public origin = 'private';
public chatType = 'private';
constructor(sender: US, receiver: Robot, messageId?: string) {
super(receiver, messageId);
super(receiver, sender, messageId);
this.sender = sender;
}
}
export class CommonGroupMessage<GS extends GroupSender = GroupSender> extends CommonReceivedMessage {
sender: GS;
public origin = 'group';
public chatType = 'group';
constructor(sender: GS, receiver: Robot, messageId?: string) {
super(receiver, messageId);
super(receiver, sender, messageId);
this.sender = sender;
}
}

@ -1,3 +1,4 @@
import { any } from "micromatch";
import { Robot } from "../RobotManager";
export type BaseSenderType = "user" | "group" | "channel";
@ -5,21 +6,25 @@ export type BaseSenderType = "user" | "group" | "channel";
export interface BaseSender {
readonly type: string | BaseSenderType;
readonly targetId: string;
readonly userId: string;
readonly identity: ChatIdentity;
}
export type IMessageSender = BaseSender & Record<string, any>;
export class UserSender implements BaseSender {
public robot: Robot;
public readonly type = "user";
public uid: string;
public userId: string;
public userName?: string;
public nickName?: string;
public accessGroup: string[] = [];
constructor(robot: Robot, uid: string) {
constructor(robot: Robot, userId: string) {
this.robot = robot;
this.uid = uid;
this.userId = userId;
}
static newAnonymous(robot: Robot) {
@ -30,18 +35,18 @@ export class UserSender implements BaseSender {
let chatIdentity: ChatIdentity = {
type: 'private',
robot: this.robot,
userId: this.uid,
userId: this.userId,
};
return chatIdentity;
}
get targetId() {
return this.uid;
return this.userId;
}
get displayName() {
return this.nickName ?? this.userName ?? this.uid;
return this.nickName ?? this.userName ?? this.userId;
}
}
@ -56,15 +61,15 @@ export class GroupSender {
public rootGroupId?: string;
public rootGroupName?: string;
public uid: string;
public userId: string;
public userName?: string;
public globalNickName?: string;
public nickName?: string;
constructor(robot: Robot, groupId: string, uid: string) {
constructor(robot: Robot, groupId: string, userId: string) {
this.robot = robot;
this.groupId = groupId;
this.uid = uid;
this.userId = userId;
}
get identity(): ChatIdentity {
@ -72,7 +77,7 @@ export class GroupSender {
type: 'group',
robot: this.robot,
groupId: this.groupId,
userId: this.uid,
userId: this.userId,
};
if (this.rootGroupId) {
@ -91,11 +96,11 @@ export class GroupSender {
}
get displayName() {
return this.nickName ?? this.globalNickName ?? this.userName ?? this.uid;
return this.nickName ?? this.globalNickName ?? this.userName ?? this.userId;
}
get userSender() {
let sender = new UserSender(this.robot, this.uid);
let sender = new UserSender(this.robot, this.userId);
sender.userName = this.userName;
sender.nickName = this.globalNickName;
@ -103,11 +108,53 @@ export class GroupSender {
}
}
export type ChatIdentity = {
type: 'private' | 'group' | 'channel' | 'raw' | string,
robot: Robot,
rootGroupId?: string,
groupId?: string,
userId?: string,
channelId?: string,
export interface ChatIdentity {
type: ('private' | 'group' | 'channel' | 'raw') | (string & {});
robot: Robot;
rootGroupId?: string;
groupId?: string;
userId?: string;
channelId?: string;
}
export interface UserInfoType {
userId: string;
userName?: string;
nickName?: string;
image?: string;
extra: any;
}
export interface GroupInfoType {
groupId: string;
rootGroupId?: string;
name: string;
image?: string;
extra: any;
}
export interface RootGroupInfoType {
rootGroupId: string;
name: string;
image?: string;
extra: any;
}
export interface GroupUserInfoType {
groupId: string;
rootGroupId?: string;
userId: string;
userName?: string;
nickName?: string;
title?: string;
role?: string;
image?: string;
extra: any;
}
export interface ChannelInfoType {
channelId: string;
name: string;
image: string;
extra: any;
}

@ -0,0 +1,31 @@
import { Model, Schema, Types } from "mongoose";
import { ChannelInfoType } from "../message/Sender";
import { ModelBase } from "../DatabaseManager";
export type ChannelInfoSchemaType = ChannelInfoType;
export type ChannelInfoModelType = Model<ChannelInfoSchemaType>;
export const ChannelInfoSchema = (robotId: string) => new Schema<ChannelInfoSchemaType>({
channelId: {
type: String,
required: true,
index: true,
},
name: {
type: String,
required: true,
default: '',
index: true,
},
image: String,
extra: {
type: Object,
default: {},
},
});
export const GroupInfoModelBase: ModelBase = {
table: 'channel_info',
schema: ChannelInfoSchema,
};

@ -0,0 +1,29 @@
import mongoose, { Schema, Types } from "mongoose";
import { GroupInfoType } from "../message/Sender";
export type GroupInfoSchemaType = GroupInfoType;
export type GroupInfoModelType = mongoose.Model<GroupInfoSchemaType>;
export const GroupInfoSchema = (robotId: string) => new Schema<GroupInfoSchemaType>({
groupId: {
type: String,
required: true,
index: true,
},
rootGroupId: {
type: String,
index: true,
},
name: {
type: String,
required: true,
default: '',
index: true,
},
image: String,
extra: {
type: Object,
default: {},
},
});

@ -0,0 +1,35 @@
import mongoose, { Schema, Types } from "mongoose";
import { GroupUserInfoType } from "../message/Sender";
export type GroupUserInfoSchemaType = GroupUserInfoType;
export type GroupUserInfoModelType = mongoose.Model<GroupUserInfoSchemaType>;
export const GroupUserInfoSchema = (robotId: string) => new Schema<GroupUserInfoSchemaType>({
rootGroupId: {
type: String,
index: true,
},
groupId: {
type: String,
required: true,
index: true,
},
userId: {
type: String,
required: true,
index: true,
},
userName: {
type: String,
index: true,
},
nickName: String,
title: String,
role: String,
image: String,
extra: {
type: Object,
default: {},
},
});

@ -0,0 +1,129 @@
import { Model, Schema, Types } from "mongoose";
import { ChatIdentity } from "../message/Sender";
import { MessageChunk, MessageDirection } from "../message/Message";
export function chatIdentityToDB(chatIdentity: ChatIdentity): ChatIdentityEntityType {
return {
rootGroupId: chatIdentity.rootGroupId,
groupId: chatIdentity.groupId,
userId: chatIdentity.userId,
channelId: chatIdentity.channelId,
}
}
export type ChatIdentityEntityType = Partial<{
rootGroupId: string,
groupId: string,
userId: string,
channelId: string,
}>;
export type MessageDataType = {
/** 消息ID */
messageId: string,
/** 消息类型 */
type: string,
/** 消息收发(消息方向) */
direction: MessageDirection,
/** 聊天类型(私聊、群聊) */
chatType: string,
/** 聊天目标的ID */
chatIdentity: ChatIdentityEntityType,
/** 回复的消息ID */
repliedMessageId?: string,
/** 提到的用户ID */
mentionedUserIds?: string[],
/** 纯文本消息内容 */
contentText: string,
/** 消息内容 */
content: MessageChunk[],
/** 时间 */
time: Date,
/** 消息是否被删除 */
deleted?: boolean,
/** 附加信息 */
extra: any,
};
export type MessageSchemaType = MessageDataType;
export type MessageModelMethods = {
}
export type MessageModelType = Model<MessageSchemaType, {}, MessageModelMethods>;
export const MessageSchema = (robotId: string) => new Schema<MessageSchemaType, MessageModelType>({
messageId: {
type: String,
required: true,
index: true,
},
type: {
type: String,
required: true,
index: true,
},
direction: {
type: Number,
required: true,
index: true,
},
chatType: {
type: String,
required: true,
index: true,
},
chatIdentity: {
type: {
rootGroupId: {
type: String,
index: true,
},
groupId: {
type: String,
index: true,
},
userId: {
type: String,
index: true,
},
channelId: {
type: String,
index: true,
},
}
},
repliedMessageId: {
type: String,
index: true,
},
mentionedUserIds: {
type: [
{
type: String,
index: true,
},
],
default: [],
},
contentText: {
type: String,
default: '',
},
content: [Object],
time: {
type: Date,
default: Date.now,
index: true,
},
deleted: {
type: Boolean,
default: false,
index: true,
},
extra: {
type: Object,
default: {},
},
});

@ -0,0 +1,23 @@
import mongoose, { Schema, Types } from "mongoose";
import { RootGroupInfoType } from "../message/Sender";
export type RootGroupInfoSchemaType = RootGroupInfoType;
export type RootGroupInfoModelType = mongoose.Model<RootGroupInfoSchemaType>;
export const RootGroupInfoSchema = (robotId: string) => new Schema<RootGroupInfoSchemaType>({
rootGroupId: {
type: String,
required: true,
index: true,
},
name: {
type: String,
index: true,
},
image: String,
extra: {
type: Object,
default: {},
}
});

@ -0,0 +1,27 @@
import { Model, Schema, Types } from "mongoose";
import { UserInfoType } from "../message/Sender";
export type UserInfoSchemaType = UserInfoType;
export type UserInfoModelType = Model<UserInfoSchemaType>;
export const UserInfoSchema = (robotId: string) => new Schema<UserInfoSchemaType, UserInfoModelType>({
userId: {
type: String,
required: true,
index: true,
},
userName: {
type: String,
index: true,
},
nickName: {
type: String,
index: true,
},
image: String,
extra: {
type: Object,
default: {},
}
});

@ -1,22 +0,0 @@
import mongoose, { Schema, Types } from "mongoose";
import { ObjectId } from "mongodb";
export type GroupDataSchemaType = {
id: Types.ObjectId,
groupId: string,
parentId: Types.ObjectId,
name: string,
image: string,
extra: any,
};
export const GroupDataSchema = new Schema<GroupDataSchemaType>({
id: ObjectId,
groupId: String,
parentId: ObjectId,
name: String,
image: String,
extra: Object,
});
export const GroupDataModel = mongoose.model<GroupDataSchemaType>('GroupData', GroupDataSchema);

@ -1,28 +0,0 @@
import mongoose, { Schema, Types } from "mongoose";
import { ObjectId } from "mongodb";
export type GroupUserDataSchemaType = {
id: Types.ObjectId,
groupId: string,
userId: string,
userName: string,
nickName: string,
title: string,
role: string,
image: string,
extra: any,
};
export const GroupUserDataSchema = new Schema<GroupUserDataSchemaType>({
id: ObjectId,
groupId: String,
userId: String,
userName: String,
nickName: String,
title: String,
role: String,
image: String,
extra: Object,
});
export const GroupUserDataModel = mongoose.model<GroupUserDataSchemaType>('GroupUserData', GroupUserDataSchema);

@ -1,106 +0,0 @@
import { Schema, Types } from "mongoose";
import { ObjectId } from "mongodb";
import { ChatIdentity } from "../message/Sender";
export type ChatIdentityEntityType = Partial<{
robotId: string,
rootGroupId: string,
groupId: string,
userId: string,
channelId: string,
}>;
export const ChatIdentityEntity = {
robotId: String,
rootGroupId: String,
groupId: String,
userId: String,
channelId: String,
};
export function toChatIdentityEntity(chatIdentity: ChatIdentity): ChatIdentityEntityType {
return {
robotId: chatIdentity.robot.robotId,
rootGroupId: chatIdentity.rootGroupId,
groupId: chatIdentity.groupId,
userId: chatIdentity.userId,
channelId: chatIdentity.channelId,
}
}
export function buildChatIdentityQuery(chatIdentityEntity: ChatIdentityEntityType | ChatIdentity, prefix = 'chatIdentity') {
const query: any = {};
if ((chatIdentityEntity as any).robotId) {
query[`${prefix}.robotId`] = (chatIdentityEntity as any).robotId;
} else if ((chatIdentityEntity as any).robot && (chatIdentityEntity as any).robot.robotId) {
query[`${prefix}.robotId`] = (chatIdentityEntity as any).robot.robotId;
}
if (chatIdentityEntity.rootGroupId) {
query[`${prefix}.rootGroupId`] = chatIdentityEntity.rootGroupId;
}
if (chatIdentityEntity.groupId) {
query[`${prefix}.groupId`] = chatIdentityEntity.groupId;
}
if (chatIdentityEntity.userId) {
query[`${prefix}.userId`] = chatIdentityEntity.userId;
}
if (chatIdentityEntity.channelId) {
query[`${prefix}.channelId`] = chatIdentityEntity.channelId;
}
return query;
}
export type MessageSchemaType = {
id: Types.ObjectId,
messageId: string,
type: string,
origin: string,
chatIdentity: ChatIdentityEntityType,
meta: {
repliedId: Types.ObjectId,
repliedMessageId: string,
mentionedUsers: Types.ObjectId[],
mentionedUids: string[],
},
isSend: boolean,
contentText: string,
content: any,
time: Date,
deleted: boolean,
extra: any,
};
export const MessageSchema = new Schema<MessageSchemaType>({
id: ObjectId,
messageId: String,
type: String,
origin: String,
chatIdentity: ChatIdentityEntity,
meta: {
repliedId: ObjectId,
repliedMessageId: String,
mentionedUsers: {
type: [ObjectId],
default: []
},
mentionedUids: {
type: [String],
default: []
}
},
isSend: Boolean,
contentText: String,
content: Object,
time: {
type: Date,
default: Date.now
},
deleted: {
type: Boolean,
default: false
},
extra: {
type: Object,
default: {},
},
});

@ -1,22 +0,0 @@
import mongoose, { Schema, Types } from "mongoose";
import { ObjectId } from "mongodb";
export type UserDataSchemaType = {
id: Types.ObjectId,
userId: string,
userName: string,
nickName: string,
image: string,
extra: any,
};
export const UserDataSchema = new Schema<UserDataSchemaType>({
id: ObjectId,
userId: String,
userName: String,
nickName: String,
image: String,
extra: Object,
});
export const UserDataModel = mongoose.model<UserDataSchemaType>('UserData', UserDataSchema);

@ -1,19 +1,21 @@
import koa from "koa";
import got from "got/dist/source";
import fs from "fs";
import App from "../App";
import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager";
import { Utils } from "../utils/Utils";
import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../RestfulApiManager";
import { convertMessageToQQChunk, parseQQMessageChunk, QQGroupMessage, QQGroupSender, QQPrivateMessage, QQUserSender } from "./qq/Message";
import { CommonReceivedMessage, CommonSendMessage } from "../message/Message";
import { convertMessageToQQChunk, parseQQMessageChunk, QQAttachmentMessage, QQGroupMessage, QQGroupSender, QQPrivateMessage, QQUserSender } from "./qq/Message";
import { CommonReceivedMessage, CommonSendMessage, MessageChunk } from "../message/Message";
import { PluginController } from "../PluginManager";
import { RobotConfig } from "../Config";
import { ChatIdentity } from "../message/Sender";
import { ChatIdentity, GroupInfoType, UserInfoType } from "../message/Sender";
import { QQInfoProvider } from "./qq/InfoProvider";
export type QQRobotConfig = RobotConfig & {
uid: string;
userId: string;
host: string;
command_prefix?: string;
}
@ -28,26 +30,27 @@ export type QQGroupInfo = {
export default class QQRobot implements Robot {
public type = 'qq';
public uid: string;
public userId: string;
public robotId: string;
public description: string;
public commandPrefix: string[] = ['/', '', '!', ''];
public infoProvider: QQInfoProvider;
private app: App;
private endpoint: string;
private taskId?: NodeJS.Timer;
private groupList: QQGroupInfo[] = [];
private messageTypeHandler: Record<string, (message: CommonSendMessage) => Promise<CommonSendMessage | void>> = {};
private emojiMap: Record<string, string> = {};
constructor(app: App, robotId: string, config: QQRobotConfig) {
this.app = app;
this.robotId = robotId;
this.endpoint = 'http://' + config.host;
this.uid = config.uid.toString();
this.userId = config.userId.toString();
this.description = config.description ?? this.app.config.robot_description ?? 'Isekai Feedbot for QQ';
@ -60,30 +63,15 @@ export default class QQRobot implements Robot {
}
this.messageTypeHandler.help = this.parseHelpMessage.bind(this);
this.infoProvider = new QQInfoProvider(app, this, config);
}
async initialize() {
this.refreshRobotInfo();
// 每30分钟刷新一次信息
this.taskId = setInterval(() => {
this.refreshRobotInfo();
}, 30 * 60 * 1000);
}
async refreshRobotInfo() {
// 刷新群信息
let remoteGroupList = await this.getGroupList();
remoteGroupList.forEach((groupInfo) => {
if (groupInfo.group_id) {
this.groupList.push({
groupId: groupInfo.group_id,
groupName: groupInfo.group_name,
memberCount: groupInfo.member_count,
memberLimit: groupInfo.max_member_count
});
await this.infoProvider.initialize();
}
});
async destroy() {
await this.infoProvider.destroy();
}
async initRestfulApi(router: RestfulRouter, api: RestfulApiManager) {
@ -93,10 +81,20 @@ export default class QQRobot implements Robot {
async handlePostEvent(ctx: FullRestfulContext, next: koa.Next) {
if (ctx.request.body?.post_type) {
const postData = ctx.request.body;
// if (this.app.debug && postData.post_type !== "meta_event" && postData.meta_event_type !== "heartbeat") {
// console.log("收到QQ机器人事件", postData);
// }
switch (postData.post_type) {
case 'message':
this.handleMessage(postData);
break;
case 'notice':
switch (postData.notice_type) {
case 'group_upload':
this.handleGroupFile(postData);
break;
}
break;
}
}
@ -104,6 +102,11 @@ export default class QQRobot implements Robot {
await next();
}
public getUsersInfo = (userIds: string[]) => this.infoProvider.getUsersInfo(userIds);
public getGroupInfo = (groupId: string, rootGroupId?: string | undefined) => this.infoProvider.getGroupInfo(groupId, rootGroupId);
public getGroupUsersInfo = (userIds: string[], groupId: string, rootGroupId?: string | undefined) =>
this.infoProvider.getGroupUsersInfo(userIds, groupId, rootGroupId);
async parseHelpMessage(message: CommonSendMessage) {
const controllers = message.extra.controllers as PluginController[];
@ -135,10 +138,9 @@ export default class QQRobot implements Robot {
}
message.content = [{
type: 'text',
data: {
text: helpBuilder.join('\n')
}
type: ['text'],
text: helpBuilder.join('\n'),
data: {},
}];
}
@ -157,7 +159,7 @@ export default class QQRobot implements Robot {
let message: QQGroupMessage | QQPrivateMessage | undefined;
if (postData.message_type === 'group') {
// 处理群消息
let groupInfo = this.groupList.find((info) => info.groupId === postData.group_id);
let groupInfo = this.infoProvider.groupList.find((info) => info.groupId === postData.group_id);
let groupSender = new QQGroupSender(this, postData.group_id.toString(), postData.user_id.toString());
groupSender.groupInfo = groupInfo;
@ -172,6 +174,9 @@ export default class QQRobot implements Robot {
message.time = new Date(postData.time * 1000);
message = await parseQQMessageChunk(this, postData.message ?? [], message);
await this.infoProvider.updateGroupSender(groupSender);
await this.infoProvider.updateUserSender(groupSender.userSender);
} else if (postData.message_type === 'private') {
// 处理私聊消息
let userSender = new QQUserSender(this, postData.user_id.toString());
@ -181,9 +186,14 @@ export default class QQRobot implements Robot {
message.time = new Date(postData.time * 1000);
message = await parseQQMessageChunk(this, postData.message ?? [], message);
await this.infoProvider.updateUserSender(userSender);
}
if (message) {
// 保存消息
this.infoProvider.saveMessage(message);
// 处理原始消息
isResolved = await this.app.event.emitRawMessage(message);
if (isResolved) return;
@ -202,9 +212,53 @@ export default class QQRobot implements Robot {
}
}
/**
*
* @param postData
* @returns
*/
async handleGroupFile(postData: any) {
// 处理群消息
let groupInfo = this.infoProvider.groupList.find((info) => info.groupId === postData.group_id);
let groupSender = new QQGroupSender(this, postData.group_id.toString(), postData.user_id.toString());
groupSender.groupInfo = groupInfo;
groupSender.groupName = groupInfo?.groupName;
let message = new QQGroupMessage(groupSender, this);
message.time = new Date(postData.time * 1000);
message.type = 'attachment';
message.content.push({
type: ['attachement', 'qqattachment'],
data: {
sender_type: 'group',
sender_id: postData.group_id.toString(),
url: postData.file?.url ?? '',
fileName: postData.file?.name ?? '',
size: postData.file?.size,
file_id: postData.file?.id,
busid: postData.file?.busid,
}
} as QQAttachmentMessage);
let isResolved = false;
// 处理原始消息
isResolved = await this.app.event.emitRawMessage(message);
if (isResolved) return;
// 处理消息
isResolved = await this.app.event.emitMessage(message);
if (isResolved) return;
}
getCommandContentText(message: CommonReceivedMessage) {
for (let prefix of this.commandPrefix) {
if (message.contentText.startsWith(prefix)) {
// 移除指令前缀
if (message.content[0].data?.text) {
message.content[0].data.text = message.content[0].data.text.substring(prefix.length);
}
return message.contentText.substring(prefix.length);
}
}
@ -213,7 +267,60 @@ export default class QQRobot implements Robot {
getSession(chatIdentity: ChatIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(chatIdentity, type);
return this.app.session.getStore(sessionPath);
return this.app.cache.getStore(sessionPath);
}
async ensureMediaUrl(mediaMessageChunk: MessageChunk): Promise<void> {
if (!mediaMessageChunk.data.url) {
if (mediaMessageChunk.type.includes('qqattachment')) {
let data = mediaMessageChunk.data;
if (data.sender_type === "group") {
data.url = await this.getGroupFileUrl({
group_id: data.sender_id,
busid: data.busid,
file_id: data.file_id,
});
}
}
}
}
async markRead(message: CommonReceivedMessage): Promise<boolean> {
if (message.id) {
await this.callRobotApi('mark_msg_as_read', {
message_id: message.id
});
}
return true;
}
/**
*
*/
async getReferencedMessages(resId: string): Promise<CommonReceivedMessage[] | null> {
const res = await this.callRobotApi('/get_forward_msg', {
message_id: resId,
});
if (!Array.isArray(res?.messages)) {
return null;
}
let messageList: CommonReceivedMessage[] = [];
for (let messageData of res.messages) {
if (messageData) {
let userSender = new QQUserSender(this, messageData.sender?.user_id.toString());
userSender.nickName = messageData.sender?.nickname;
let message = new QQPrivateMessage(userSender, this);
message.time = new Date(messageData.time * 1000);
message = await parseQQMessageChunk(this, messageData.content ?? [], message);
messageList.push(message);
}
}
return messageList;
}
/**
@ -231,7 +338,7 @@ export default class QQRobot implements Robot {
return;
}
return await this.doApiRequest('send_private_msg', {
return await this.callRobotApi('send_private_msg', {
user_id: user,
message: message
});
@ -249,7 +356,7 @@ export default class QQRobot implements Robot {
return;
}
return await this.doApiRequest('send_group_msg', {
return await this.callRobotApi('send_group_msg', {
group_id: group,
message: message
});
@ -268,13 +375,14 @@ export default class QQRobot implements Robot {
let msgData = await convertMessageToQQChunk(message);
try {
let res: any = {};
if (message.origin === 'private') {
this.app.logger.debug('[DEBUG] 发送私聊消息', message.targetId, msgData);
res = await this.sendToUser(message.targetId, msgData);
} else if (message.origin === 'group') {
this.app.logger.debug('[DEBUG] 发送群消息', message.targetId, msgData);
res = await this.sendToGroup(message.targetId, msgData);
if (message.chatType === 'private') {
this.app.logger.debug('[DEBUG] 发送私聊消息', message.receiver.userId, msgData);
res = await this.sendToUser(message.receiver.userId!, msgData);
} else if (message.chatType === 'group') {
this.app.logger.debug('[DEBUG] 发送群消息', message.receiver.groupId, msgData);
res = await this.sendToGroup(message.receiver.groupId!, msgData);
}
// 保存 Message ID
@ -282,6 +390,12 @@ export default class QQRobot implements Robot {
message.id = res.data.message_id;
}
// 保存消息
this.infoProvider.saveMessage(message);
} catch(err: any) {
console.error(err);
}
return message;
}
@ -308,26 +422,26 @@ export default class QQRobot implements Robot {
}
async deleteMessage(chatIdentity: ChatIdentity, messageId: string): Promise<boolean> {
await this.doApiRequest('delete_msg', {
await this.callRobotApi('delete_msg', {
message_id: messageId
});
return true;
}
async getGroupList(): Promise<any[]> {
const res = await this.doApiRequest('get_group_list', {});
async getGroupFileUrl(data: any): Promise<string> {
const res = await this.callRobotApi('get_group_file_url', data);
if (res && res.status === 'ok') {
return res.data;
return res.data?.url ?? "";
} else {
return [];
return "";
}
}
/**
* API
*/
async doApiRequest(method: string, data: any): Promise<any> {
return await got.post(this.endpoint + '/' + method, {
callRobotApi(method: string, data: any): Promise<any> {
return got.post(this.endpoint + '/' + method, {
json: data,
timeout: 10000
}).json<any>();

@ -18,7 +18,7 @@ export default class TelegramRobot implements Robot {
public type = 'telegram';
public robotId: string;
public uid?: string;
public userId?: string;
public description: string;
private bot: TelegramBot;
@ -79,7 +79,7 @@ export default class TelegramRobot implements Robot {
getSession(chatIdentity: ChatIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(chatIdentity, type);
return this.app.session.getStore(sessionPath);
return this.app.cache.getStore(sessionPath);
}
/**

@ -0,0 +1,261 @@
import App from "../../App";
import { compareProps } from "../../utils/func";
import QQRobot, { QQRobotConfig } from "../QQRobot";
import { QQGroupSender, QQUserSender } from "./Message";
import { GroupInfoType, GroupUserInfoType, UserInfoType } from "../../message/Sender";
import { CommonMessage } from "src/message/Message";
import { RobotStorage } from "src/storage/RobotStorage";
export type QQGroupInfo = {
groupId: string,
groupName?: string,
memberCount?: number,
memberLimit?: number
};
export class QQInfoProvider {
private app: App;
private robot: QQRobot;
private config: QQRobotConfig;
private storages?: RobotStorage;
private infoLoaderTimer: NodeJS.Timer | null = null;
public groupList: QQGroupInfo[] = [];
public userSenderList: Record<string, QQUserSender> = {};
public groupSenderList: Record<string, Record<string, QQGroupSender>> = {};
constructor(app: App, robot: QQRobot, config: QQRobotConfig) {
this.app = app;
this.robot = robot;
this.config = config;
}
async initialize() {
this.storages = await this.app.storage.getStorages(this.robot.robotId);
this.refreshRobotInfo();
// 每30分钟刷新一次信息
this.infoLoaderTimer = setInterval(() => {
this.refreshRobotInfo();
}, 30 * 60 * 1000);
}
async destroy() {
if (this.infoLoaderTimer) {
clearInterval(this.infoLoaderTimer);
this.infoLoaderTimer = null;
}
}
async refreshRobotInfo() {
// 刷新群信息
let remoteGroupList = await this.getGroupList();
remoteGroupList.forEach((data) => {
if (data.group_id) {
let oldGroupIndex = this.groupList.findIndex((info) => info.groupId === data.group_id);
const groupInfo: QQGroupInfo = {
groupId: data.group_id,
groupName: data.group_name,
memberCount: data.member_count,
memberLimit: data.max_member_count
}
if (oldGroupIndex !== -1) {
const oldGroupInfo = this.groupList[oldGroupIndex];
if (compareProps(oldGroupInfo, groupInfo, ['groupName', 'memberCount', 'memberLimit'])) {
return;
}
this.groupList[oldGroupIndex] = groupInfo;
} else {
this.groupList.push(groupInfo);
}
this.updateGroupInfo(groupInfo);
}
});
}
public saveMessage(message: CommonMessage) {
this.storages?.message.set(message).catch((err: any) => {
this.app.logger.error(`将消息保存到数据库出错: ${err.message}`);
console.error(err);
})
}
async getGroupList(): Promise<any[]> {
const res = await this.robot.callRobotApi('get_group_list', {});
if (res && res.status === 'ok') {
return res.data;
} else {
return [];
}
}
async getUsersInfo(userIds: string[]): Promise<(UserInfoType | null)[]> {
let userInfoList: (UserInfoType | null)[] = [];
for (let userId of userIds) {
if (userId in this.userSenderList) {
let userSender = this.userSenderList[userId];
userInfoList.push(this.userSenderToUserInfo(userSender));
} else {
userInfoList.push(null);
}
}
return userInfoList;
}
async getGroupInfo(groupId: string, rootGroupId?: string): Promise<GroupInfoType | null> {
let localGroupInfo = this.groupList.find((info) => info.groupId === groupId);
if (localGroupInfo) {
return {
groupId,
name: localGroupInfo.groupName ?? groupId,
image: this.getGroupImage(groupId),
extra: {
memberCount: localGroupInfo.memberCount,
memberLimit: localGroupInfo.memberLimit,
},
};
}
return null;
}
async getGroupUsersInfo(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]> {
let groupUserInfoList: (GroupUserInfoType | null)[] = [];
const localList = this.groupSenderList[groupId];
if (!localList) {
return new Array<null>(userIds.length).fill(null);
}
for (let userId of userIds) {
if (userId in localList) {
let groupSender = localList[userId];
groupUserInfoList.push(this.groupSenderToGroupUserInfo(groupSender));
}
}
return groupUserInfoList;
}
/**
*
* @param userId
* @returns
*/
private getUserImage(userId: string) {
return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`;
}
/**
*
* @param groupId
* @returns
*/
private getGroupImage(groupId: string) {
return `https://p.qlogo.cn/gh/${groupId}/${groupId}/100`
}
/**
*
* @param groupSender
* @returns
*/
public async updateGroupSender(groupSender: QQGroupSender) {
let savedGroupSender = this.groupSenderList[groupSender.groupId]?.[groupSender.userId];
if (savedGroupSender) {
if (compareProps(savedGroupSender, groupSender, ['globalNickName', 'nickName', 'role', 'level', 'title'])) {
return;
}
}
if (!this.groupSenderList[groupSender.groupId]) {
this.groupSenderList[groupSender.groupId] = {};
}
this.groupSenderList[groupSender.groupId][groupSender.userId] = groupSender;
const storages = await this.app.storage.getStorages(this.robot.robotId);
await storages.userInfo.set(this.userSenderToUserInfo(groupSender.userSender));
await storages.groupUserInfo.set(
this.groupSenderToGroupUserInfo(groupSender),
groupSender.userId,
groupSender.groupId
);
}
/**
*
* @param userSender
* @returns
*/
public async updateUserSender(userSender: QQUserSender) {
let savedUserSender = this.userSenderList[userSender.userId];
if (savedUserSender) {
if (compareProps(savedUserSender, userSender, ['nickName'])) {
return;
}
}
this.userSenderList[userSender.userId] = userSender;
const storages = await this.app.storage.getStorages(this.robot.robotId);
this.app.logger.debug(`更新用户信息: ${userSender.userId}`);
await storages.userInfo.set(this.userSenderToUserInfo(userSender));
}
public async updateGroupInfo(groupInfo: QQGroupInfo) {
const storages = await this.app.storage.getStorages(this.robot.robotId);
await storages.groupInfo.set(this.groupInfoToStorageGroupInfo(groupInfo));
this.app.logger.debug(`更新群组信息: ${groupInfo.groupId}`);
}
private groupSenderToGroupUserInfo(groupSender: QQGroupSender): GroupUserInfoType {
return {
groupId: groupSender.groupId,
userId: groupSender.userId,
userName: groupSender.userName,
nickName: groupSender.nickName || groupSender.globalNickName,
title: groupSender.title,
role: groupSender.role,
image: this.getUserImage(groupSender.userId),
extra: {},
};
}
private userSenderToUserInfo(userSender: QQUserSender): UserInfoType {
return {
userId: userSender.userId,
userName: userSender.userName,
nickName: userSender.nickName,
image: this.getUserImage(userSender.userId),
extra: {},
};
}
private groupInfoToStorageGroupInfo(groupInfo: QQGroupInfo): GroupInfoType {
return {
groupId: groupInfo.groupId,
name: groupInfo.groupName ?? groupInfo.groupId,
image: this.getGroupImage(groupInfo.groupId),
extra: {
memberCount: groupInfo.memberCount,
memberLimit: groupInfo.memberLimit,
},
};
}
}

@ -1,43 +1,66 @@
import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, MentionMessage, MessageChunk, TextMessage } from "../../message/Message";
import { AttachmentMessage, CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, EmojiMessage, ImageMessage, MentionMessage, MessageChunk, TextMessage, RecordMessage } from "../../message/Message";
import { GroupSender, UserSender } from "../../message/Sender";
import QQRobot, { QQGroupInfo } from "../QQRobot";
import { qqFaceToEmoji } from "./emojiMap";
export interface QQFaceMessage extends MessageChunk {
type: 'qqface';
export interface QQFaceMessage extends EmojiMessage {
type: ['emoji', 'qqface'];
data: {
id: string
id: string,
emoji: string,
url?: string,
};
}
export interface QQImageMessage extends MessageChunk {
type: 'qqimage';
export interface QQImageMessage extends ImageMessage {
type: ['image', 'qqimage'];
data: {
file?: string;
url?: string;
url: string;
alt?: string;
file?: string;
subType?: string;
};
}
export interface QQVoiceMessage extends MessageChunk {
type: 'qqvoice';
export interface QQRecordMessage extends RecordMessage {
type: ['record', 'qqrecord'];
data: {
url: string;
};
}
export interface QQUrlMessage extends MessageChunk {
type: 'qqurl';
export interface QQUrlMessage extends TextMessage {
type: ['text', 'qqurl'];
data: {
url: string;
title: string;
};
}
export interface QQAttachmentMessage extends AttachmentMessage {
type: ['attachement', 'qqattachment'];
data: {
sender_type: string;
sender_id: string;
url: string;
fileName: string;
size?: number;
file_id?: string;
busid?: number;
}
}
export interface QQForwardingMessage extends MessageChunk {
type: ['qqforwarding'];
data: {
res_id: string;
}
}
export class QQUserSender extends UserSender {
constructor(robot: QQRobot, uid: string) {
super(robot, uid);
this.userName = uid;
constructor(robot: QQRobot, userId: string) {
super(robot, userId);
this.userName = userId;
}
}
@ -47,13 +70,13 @@ export class QQGroupSender extends GroupSender {
public title?: string;
public groupInfo?: QQGroupInfo;
constructor(robot: QQRobot, groupId: string, uid: string) {
super(robot, groupId, uid);
this.userName = uid;
constructor(robot: QQRobot, groupId: string, userId: string) {
super(robot, groupId, userId);
this.userName = userId;
}
get userSender() {
let sender = new QQUserSender(this.robot as any, this.uid);
let sender = new QQUserSender(this.robot as any, this.userId);
sender.userName = this.userName;
sender.nickName = this.globalNickName;
@ -74,16 +97,15 @@ export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], mess
switch (chunkData.type) {
case 'text':
message.content.push({
type: 'text',
data: {
text: chunkData.data?.text ?? ''
}
type: ['text'],
text: chunkData.data?.text ?? '',
data: {}
} as TextMessage);
break;
case 'image':
message.content.push({
type: 'qqimage',
baseType: 'image',
type: ['image', 'qqimage'],
text: '[图片]',
data: {
url: chunkData.data?.url ?? '',
alt: chunkData.data?.file,
@ -93,32 +115,44 @@ export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], mess
break;
case 'record':
message.content.push({
type: 'qqvoice',
baseType: 'voice',
type: ['record', 'qqrecord'],
text: '[语音]',
data: {
url: chunkData.data?.url ?? '',
}
} as QQVoiceMessage);
} as QQRecordMessage);
break;
case 'face':
if (chunkData.data?.id) {
let emojiChar = qqFaceToEmoji(chunkData.data.id);
message.content.push({
type: 'qqface',
type: ['emoji', 'qqface'],
text: emojiChar,
data: {
id: chunkData.data?.id ?? '',
emoji: emojiChar,
}
} as QQFaceMessage);
} else {
message.content.push({
type: ['text'],
text: '[表情]',
data: { }
} as TextMessage);
}
break;
case 'at':
if (chunkData.data?.qq) {
if (!willIgnoreMention) {
if (chunkData.data.qq == bot.uid) { // 如果是@机器人
if (chunkData.data.qq == bot.userId) { // 如果是@机器人
message.mentionedReceiver = true;
} else { // @其他人的情况
message.mention(chunkData.data.qq);
message.content.push({
type: 'mention',
type: ['mention'],
text: `[@${chunkData.data.qq}]`,
data: {
uid: chunkData.data.qq
userId: chunkData.data.qq,
}
} as MentionMessage);
}
@ -133,19 +167,41 @@ export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], mess
willIgnoreMention = true; // 忽略下一个“@”
}
break;
case 'json':
if (typeof chunkData.data?.data === 'string' && chunkData.data.data.length < 2048) {
try {
let jsonData = JSON.parse(chunkData.data.data);
switch (jsonData.app) {
case 'com.tencent.multimsg':
console.log('forwarding message', chunkData.data.data);
if (jsonData.meta?.detail?.resid) {
message.content.push({
type: ['qqforwarding'],
text: '[合并转发消息]',
data: {
res_id: jsonData.meta.detail.resid
}
} as QQForwardingMessage);
}
}
} catch (_) { }
}
break;
default:
console.log('unknown message', chunkData);
}
}
});
if (message.content.length === 1) {
// 检查单一消息的类型
switch (message.content[0].type) {
case 'qqimage':
const firstChunk = message.content[0];
if (firstChunk.type.includes('qqimage')) {
message.type = 'image';
break;
case 'qqvoice':
message.type = 'voice';
break;
} else if (firstChunk.type.includes('qqrecord')) {
message.type = 'record';
} else if (firstChunk.type.includes('qqforwarding')) {
message.type = 'reference';
}
}
@ -157,30 +213,20 @@ export async function convertMessageToQQChunk(message: CommonSendMessage) {
message.content.forEach((rawChunk) => {
let chunk = rawChunk;
if (rawChunk.baseType && !rawChunk.type.startsWith('qq')) {
chunk = {
...rawChunk,
type: rawChunk.baseType,
};
}
switch (chunk.type) {
case 'text':
if (chunk.type.includes('text')) {
msgChunk.push({
type: 'text',
data: {
text: chunk.data.text
text: chunk.text
}
});
break;
case 'qqface':
} else if (chunk.type.includes('qqface')) {
msgChunk.push({
type: 'face',
data: { id: chunk.data.id }
});
break;
case 'image':
case 'qqimage':
} else if (chunk.type.includes('image')) {
msgChunk.push({
type: 'image',
data: {
@ -188,44 +234,49 @@ export async function convertMessageToQQChunk(message: CommonSendMessage) {
subType: chunk.data.subType ?? 0
}
});
break;
case 'voice':
case 'qqvoice':
} else if (chunk.type.includes('record')) {
msgChunk.push({
type: 'record',
data: {
file: chunk.data.url
}
});
break;
case 'mention':
} else if (chunk.type.includes('mention')) {
msgChunk.push({
type: 'at',
data: { qq: chunk.data.userId }
});
} else if (chunk.type.includes('qqforwarding')) {
// ignore
} else if (chunk.text !== null) {
msgChunk.push({
type: 'text',
data: {
qq: chunk.data.uid
text: chunk.text
}
});
break;
}
})
});
if (message.repliedId) {
if (message.origin === 'group' && message.repliedMessage?.sender.uid) {
// 目前不知道为何,@不能正常传递
/*
msgChunk.unshift({
type: 'text',
data: { text: ' ' }
});
if (message.chatType === 'group' && message.repliedMessage?.sender.userId) {
// go-cqhttp需要连续发送两个@才能显示出来
// msgChunk.unshift({
// type: 'text',
// data: { text: ' ' }
// });
msgChunk.unshift({
type: 'at',
data: { qq: message.repliedMessage.sender.uid }
data: { qq: message.repliedMessage.sender.userId }
});
// msgChunk.unshift({
// type: 'text',
// data: { text: ' ' }
// });
msgChunk.unshift({
type: 'text',
data: { text: ' ' }
type: 'at',
data: { qq: message.repliedMessage.sender.userId }
});
*/
}
msgChunk.unshift({
type: 'reply',

@ -0,0 +1,172 @@
const emojiMapString = `
0: 😯
1: 😖
2: 😍
3: 😱
4: 😎
5: 😭
6:
7: 🤬
8: 😪
9: 😢
10: 😰
11: 😡
12: 😜
13: 😄
14: 🙃
15: 😕
16: 😎
18: 😫
19: 🤢
20: 🤭
21:
22: 🙄
23: 🤔
24: 😋
25: 🥱
26: 😳
27: 😥
28: 😄
29: 🤠
30: 💪
31: 🤬
32: 🤔
33: 🤫
34: 💫
35: 😫
36: 🌑
37:
38: 🔨
39: 👋
49: 🫂
53: 🎂
54:
55: 💣
56: 🗡
57:
59: 💩
60:
61: 🍚
62: 💊
63: 🌹
64: 🥀
66:
67: 💔
69: 🎁
72: 📧
74: 🌞
76: 👍
77: 👎
78: 🤝
79:
89: 🍉
90: 🌨
91:
96: 😰
97: 🫠
98: 🫠
99: 👏
100: 😰
101: 😬
102: 😒
103: 😒
104: 🥱
105: 😒
106: 🤥
107: 😞
108: 😏
109: 😙
110: 😱
111: 🥺
112: 🔪
113: 🍺
114: 🏀
115: 🏓
116: 👄
117: 🐞
118: 👍
119: 🤌
120: 👊
136:
137: 🧨
138: 🏮
139: 🀄
140: 🎤
141: 🛍
142: 📧
143:
144: 🎉
145: 🕯
146: 😠
147: 🍭
148: 🍼
149: 🍜
150: 🍌
151:
152: 🚗
153: 🚝
154: 🚃
155: 🚝
156:
157: 🌧
158: 💴
159: 🐼
160: 💡
161: 🪀
162:
163: 🌂
164: 🎈
165: 💍
166: 🛋
167: 🧻
168: 💊
169: 🔫
170: 🐸
171: 🍵
172: 😜
173: 😨
174: 🫤
175: 😃
176: 😅
177: 😅
178: 😏
179: 😋
180: 😄
181: 🫥
182: 😂
183: 😃
184: 🦀
185: 🦙
186: 🌰
187: 👻
188: 🥚
212: 🧐
`;
let emojiMap = new Map<number, string>();
for (let line of emojiMapString.split('\n')) {
line = line.trim();
if (line === '') {
continue;
}
let [key, value] = line.split(':');
if (key && value) {
emojiMap.set(parseInt(key), value.trim());
}
}
function qqFaceToEmoji(faceId: number) {
if (emojiMap.has(faceId)) {
return emojiMap.get(faceId);
} else {
return '🫥';
}
}
export {
emojiMap,
qqFaceToEmoji
};

@ -0,0 +1,107 @@
import App from "../App";
import { StorageConfig } from "../Config";
import { CacheStore } from "../CacheManager";
import { ChannelInfoType, RootGroupInfoType } from "../message/Sender";
import { ModelRegistry } from "../DatabaseManager";
import { ChannelInfoSchemaType } from "../odm/ChannelInfo";
import { RobotStorage } from "./RobotStorage";
export class ChannelInfoStorage {
private app: App;
private config: StorageConfig;
private storages: RobotStorage;
private models?: ModelRegistry;
private cacheTTL: number;
private cache: CacheStore;
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
this.app = app;
this.config = config;
this.cacheTTL = config.cache_ttl ?? 86400;
this.storages = storages;
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'channel_info']);
}
public async initialize() {
this.models = this.storages.models;
}
public async get(channelId: string, fetchFromBot: boolean = false): Promise<ChannelInfoSchemaType | null> {
// from cache
let channelInfo = await this.cache.get<ChannelInfoSchemaType>(channelId);
if (channelInfo) {
return channelInfo;
}
if (fetchFromBot) {
return await this.fetchFromRobot(channelId);
} else if (this.models) {
let doc = await this.models.channelInfo.findOne({
channelId,
});
if (doc) {
channelInfo = doc.toObject();
await this.cache.set(channelId, channelInfo, this.cacheTTL);
return channelInfo;
}
} else {
this.app.logger.warn('未配置 Database');
}
return null;
}
public async getByRef(channelInfo: ChannelInfoSchemaType | string): Promise<ChannelInfoSchemaType | null> {
if (typeof channelInfo === 'string') {
return await this.get(channelInfo, false);
} else {
return await this.get(channelInfo.channelId, false);
}
}
public async fetchFromRobot(channelId: string): Promise<ChannelInfoSchemaType | null> {
const robot = this.storages.robot;
if (robot) {
const channelInfo = await robot.getChannelInfo?.(channelId);
if (channelInfo) {
return await this.set(channelInfo);
}
} else {
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
}
return null;
}
public async set(channelInfo: ChannelInfoType): Promise<ChannelInfoSchemaType> {
let data: ChannelInfoSchemaType = {
...channelInfo
};
if (this.models) {
await this.models.channelInfo.updateOne({
channelId: data.channelId,
}, data, {
upsert: true,
setDefaultsOnInsert: true,
});
}
await this.cache.set(data.channelId, data, this.cacheTTL);
return data;
}
public async remove(channelId: string): Promise<void> {
if (this.models) {
await this.models.channelInfo.deleteOne({
channelId,
});
}
await this.cache.del(channelId);
}
}

@ -0,0 +1,121 @@
import App from "../App";
import { StorageConfig } from "../Config";
import { CacheStore } from "../CacheManager";
import { GroupInfoType } from "../message/Sender";
import { ModelRegistry } from "../DatabaseManager";
import { GroupInfoSchemaType } from "../odm/GroupInfo";
import { RootGroupInfoSchemaType } from "../odm/RootGroupInfo";
import { Types } from "mongoose";
import { RobotStorage } from "./RobotStorage";
export class GroupInfoStorage {
private app: App;
private config: StorageConfig;
private storages: RobotStorage;
private models?: ModelRegistry;
private cacheTTL: number;
private cache: CacheStore;
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
this.app = app;
this.config = config;
this.cacheTTL = config.cache_ttl ?? 86400;
this.storages = storages;
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'group_info']);
}
public async initialize() {
this.models = this.storages.models;
}
private makeKey(groupId: string, rootGroupId?: string): string {
if (rootGroupId) {
return this.cache.makeKey([groupId, rootGroupId]);
} else {
return groupId;
}
}
public async get(groupId: string, rootGroupId?: string, fetchFromBot: boolean = false): Promise<GroupInfoSchemaType | null> {
// from cache
let groupInfo = await this.cache.get<GroupInfoSchemaType>(this.makeKey(groupId, rootGroupId));
if (groupInfo) {
return groupInfo;
}
if (fetchFromBot) {
return await this.fetchFromRobot(groupId, rootGroupId);
} else if (this.models) {
let doc = await this.models.groupInfo.findOne(rootGroupId ? {
groupId,
rootGroupId,
} : { groupId });
if (doc) {
groupInfo = doc.toObject();
await this.cache.set(this.makeKey(groupId, rootGroupId), groupInfo, this.cacheTTL);
return groupInfo;
}
} else {
this.app.logger.warn('未配置 Database');
}
return null;
}
public async getByRef(groupInfo: GroupInfoSchemaType | string, rootGroupId?: string): Promise<GroupInfoSchemaType | null> {
if (typeof groupInfo === 'string') {
return await this.get(groupInfo, rootGroupId, false);
} else {
return await this.get(groupInfo.groupId, groupInfo.rootGroupId, false);
}
}
public async fetchFromRobot(groupId: string, rootGroupId?: string): Promise<GroupInfoSchemaType | null> {
const robot = this.storages.robot;
if (robot) {
const groupInfo = await robot.getGroupInfo?.(groupId, rootGroupId);
if (groupInfo) {
return await this.set(groupInfo, rootGroupId);
}
} else {
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
}
return null;
}
public async set(groupInfo: GroupInfoType, rootGroupInfo?: string | RootGroupInfoSchemaType): Promise<GroupInfoSchemaType> {
let data: GroupInfoSchemaType = {
...groupInfo,
rootGroupId: typeof rootGroupInfo === 'string' ? rootGroupInfo : rootGroupInfo?.rootGroupId,
};
if (this.models) {
await this.models.groupInfo.updateOne({
groupId: data.groupId,
rootGroupId: data.rootGroupId,
}, data, {
upsert: true,
setDefaultsOnInsert: true,
});
}
await this.cache.set(this.makeKey(data.groupId, data.rootGroupId), data, this.cacheTTL);
return data;
}
public async remove(groupId: string, rootGroupId?: string): Promise<void> {
if (this.models) {
await this.models.groupInfo.deleteOne({
groupId,
rootGroupId,
});
}
await this.cache.del(this.makeKey(groupId, rootGroupId));
}
}

@ -0,0 +1,123 @@
import App from "../App";
import { StorageConfig } from "../Config";
import { CacheStore } from "../CacheManager";
import { GroupUserInfoType } from "../message/Sender";
import { ModelRegistry } from "../DatabaseManager";
import { UserInfoSchemaType } from "../odm/UserInfo";
import { GroupInfoSchemaType } from "../odm/GroupInfo";
import { RobotStorage } from "./RobotStorage";
import { GroupUserInfoSchemaType } from "../odm/GroupUserInfo";
import { RootGroupInfoSchemaType } from "../odm/RootGroupInfo";
export class GroupUserInfoStorage {
private app: App;
private config: StorageConfig;
private storages: RobotStorage;
private models?: ModelRegistry;
private cacheTTL: number;
private cache: CacheStore;
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
this.app = app;
this.config = config;
this.cacheTTL = config.cache_ttl ?? 86400;
this.storages = storages;
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'group_user_info']);
}
public async initialize() {
this.models = this.storages.models;
}
private makeKey(userId: string, groupId: string, rootGroupId?: string): string {
if (rootGroupId) {
return this.cache.makeKey([userId, rootGroupId, groupId]);
} else {
return this.cache.makeKey([userId, groupId]);
}
}
public async get(userId: string, groupId: string, rootGroupId?: string, fetchFromBot: boolean = false): Promise<GroupUserInfoSchemaType | null> {
// from cache
let groupUserInfo = await this.cache.get<GroupUserInfoSchemaType>(this.makeKey(userId, groupId, rootGroupId));
if (groupUserInfo) {
return groupUserInfo;
}
if (fetchFromBot) {
// from bot
return await this.fetchFromRobot(userId, groupId);
} else if (this.models) { // from database
let doc = await this.models.groupUserInfo.findOne({
userId,
groupId,
rootGroupId,
});
if (doc) {
groupUserInfo = doc.toObject();
await this.cache.set(this.makeKey(userId, groupId, rootGroupId), groupUserInfo, this.cacheTTL);
return groupUserInfo;
}
} else {
this.app.logger.warn('未配置 Database');
}
return null;
}
public async fetchFromRobot(userId: string, groupId: string, rootGroupId?: string): Promise<GroupUserInfoSchemaType | null> {
const robot = this.app.robot.getRobot(this.storages.robotId);
if (robot) {
const groupUserInfoList = await robot.getGroupUsersInfo?.([userId], groupId);
if (groupUserInfoList && groupUserInfoList.length > 0) {
const groupUserInfo = groupUserInfoList[0];
if (groupUserInfo) {
return await this.set(groupUserInfo, userId, groupId, rootGroupId);
}
}
} else {
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
}
return null;
}
public async set(groupUserInfo: GroupUserInfoType, userInfo: string | UserInfoSchemaType,
groupInfo: string | GroupInfoSchemaType, rootGroupInfo?: string | RootGroupInfoSchemaType): Promise<GroupUserInfoSchemaType> {
let data: GroupUserInfoSchemaType = {
...groupUserInfo,
userId: typeof userInfo === 'string' ? userInfo : userInfo.userId,
groupId: typeof groupInfo === 'string' ? groupInfo : groupInfo.groupId,
rootGroupId: typeof rootGroupInfo === 'string' ? rootGroupInfo : rootGroupInfo?.rootGroupId,
};
// 保存到数据库
if (this.models) {
await this.models.groupUserInfo.updateOne({
userId: data.userId,
groupId: data.groupId,
}, data, {
upsert: true,
setDefaultsOnInsert: true,
});
}
await this.cache.set(this.makeKey(data.userId, data.groupId, data.rootGroupId), data, this.cacheTTL);
return data;
}
public async remove(userId: string, groupId: string): Promise<void> {
if (this.models) {
await this.models.groupUserInfo.deleteOne({
userId,
groupId,
});
}
await this.cache.del(this.makeKey(userId, groupId));
}
}

@ -0,0 +1,110 @@
import App from "../App";
import { StorageConfig } from "../Config";
import { ModelRegistry } from "../DatabaseManager";
import { ItemLimitedList } from "../utils/ItemLimitedList";
import { CommonMessage } from "../message/Message";
import { RobotStorage } from "./RobotStorage";
export class MessageStorage {
private app: App;
private config: StorageConfig;
private storages: RobotStorage;
private models?: ModelRegistry;
private cacheTTL: number;
private cache: ItemLimitedList<CommonMessage | undefined>;
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
this.app = app;
this.config = config;
this.cacheTTL = config.cache_ttl ?? 86400;
this.storages = storages;
let itemLimit = config.message?.lru_limit ?? 1000;
this.cache = new ItemLimitedList<CommonMessage | undefined>(itemLimit);
}
public async initialize() {
this.models = this.storages.models;
}
public async get(messageId: string): Promise<CommonMessage | null> {
// from cache
let messageObj = this.cache.find((msg) => msg && msg.id === messageId);
if (messageObj) {
return messageObj;
}
// from database
if (this.models) {
let doc = await this.models.message.findOne({
messageId
});
if (doc) {
const robot = this.storages.robot;
if (robot) {
messageObj = await robot.parseDBMessage?.(doc);
return messageObj!;
} else {
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
}
}
} else {
this.app.logger.warn('未配置 Database');
}
return null;
}
/**
*
* @param messageId
* @param message
*/
public async set(message: CommonMessage): Promise<void> {
let messageData = message.toDBObject();
if (this.models) {
await this.models.message.updateOne({
messageId: message.id!,
}, messageData, {
upsert: true,
setDefaultsOnInsert: true,
});
}
this.cache.push(message);
}
public async remove(messageId: string): Promise<void> {
if (this.models) {
await this.models.userInfo.deleteOne({
messageId,
});
}
let listIndex = this.cache.findIndex((msg) => msg && msg.id === messageId);
this.cache[listIndex] = undefined;
}
/**
*
* @param messageId
*/
public async markDeleted(messageId: string): Promise<void> {
if (this.models) {
await this.models.message.updateOne({
messageId,
}, {
deleted: true,
});
}
let messageObj = this.cache.find((msg) => msg && msg.id === messageId);
if (messageObj) {
messageObj.deleted = true;
}
}
}

@ -0,0 +1,62 @@
import App from "../App";
import { StorageConfig } from "../Config";
import { ModelRegistry } from "../DatabaseManager";
import { Robot } from "../RobotManager";
import { ChannelInfoStorage } from "./ChannelInfoStorage";
import { GroupInfoStorage } from "./GroupInfoStorage";
import { GroupUserInfoStorage } from "./GroupUserInfoStorage";
import { MessageStorage } from "./MessageStorage";
import { RootGroupInfoStorage } from "./RootGroupInfoStorage";
import { UserInfoStorage } from "./UserInfoStorage";
export class RobotStorage {
private app: App;
private config: StorageConfig;
private _robotId: string;
private _robot?: Robot;
private _models?: ModelRegistry;
public userInfo: UserInfoStorage;
public channelInfo: ChannelInfoStorage;
public rootGroupInfo: RootGroupInfoStorage;
public groupInfo: GroupInfoStorage;
public groupUserInfo: GroupUserInfoStorage;
public message: MessageStorage;
public constructor(app: App, config: StorageConfig, robotId: string) {
this.app = app;
this.config = config;
this._robotId = robotId;
this.userInfo = new UserInfoStorage(app, config, this);
this.channelInfo = new ChannelInfoStorage(app, config, this);
this.rootGroupInfo = new RootGroupInfoStorage(app, config, this);
this.groupInfo = new GroupInfoStorage(app, config, this);
this.groupUserInfo = new GroupUserInfoStorage(app, config, this);
this.message = new MessageStorage(app, config, this);
}
public async initialize() {
this._models = await this.app.database?.getModels(this.robotId);
this._robot = await this.app.robot.getRobot(this.robotId) ?? undefined;
await this.userInfo.initialize();
await this.channelInfo.initialize();
await this.rootGroupInfo.initialize();
await this.groupInfo.initialize();
await this.groupUserInfo.initialize();
await this.message.initialize();
}
public get robotId() {
return this._robotId;
}
public get models() {
return this._models;
}
public get robot() {
return this._robot;
}
}

@ -0,0 +1,107 @@
import App from "../App";
import { StorageConfig } from "../Config";
import { CacheStore } from "../CacheManager";
import { RootGroupInfoType } from "../message/Sender";
import { ModelRegistry } from "../DatabaseManager";
import { RobotStorage } from "./RobotStorage";
import { RootGroupInfoSchemaType } from "../odm/RootGroupInfo";
export class RootGroupInfoStorage {
private app: App;
private config: StorageConfig;
private storages: RobotStorage;
private models?: ModelRegistry;
private cacheTTL: number;
private cache: CacheStore;
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
this.app = app;
this.config = config;
this.cacheTTL = config.cache_ttl ?? 86400;
this.storages = storages;
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'root_group_info']);
}
public async initialize() {
this.models = this.storages.models;
}
public async get(rootGroupId: string, fetchFromBot: boolean = false): Promise<RootGroupInfoSchemaType | null> {
// from cache
let rootGroupInfo = await this.cache.get<RootGroupInfoSchemaType>(rootGroupId);
if (rootGroupInfo) {
return rootGroupInfo;
}
if (fetchFromBot) {
return await this.fetchFromRobot(rootGroupId);
} else if (this.models) {
let doc = await this.models.rootGroupInfo.findOne({
rootGroupId,
});
if (doc) {
rootGroupInfo = doc.toObject();
await this.cache.set(rootGroupId, rootGroupInfo, this.cacheTTL);
return rootGroupInfo;
}
} else {
this.app.logger.warn('未配置 Database');
}
return null;
}
public async getByRef(rootGroupInfo: RootGroupInfoSchemaType | string): Promise<RootGroupInfoSchemaType | null> {
if (typeof rootGroupInfo === 'string') {
return await this.get(rootGroupInfo, false);
} else {
return await this.get(rootGroupInfo.rootGroupId, false);
}
}
public async fetchFromRobot(rootGroupId: string): Promise<RootGroupInfoSchemaType | null> {
const robot = this.app.robot.getRobot(this.storages.robotId);
if (robot) {
const rootGroupInfo = await robot.getRootGroupInfo?.(rootGroupId);
if (rootGroupInfo) {
return await this.set(rootGroupInfo);
}
} else {
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
}
return null;
}
public async set(rootGroupInfo: RootGroupInfoType): Promise<RootGroupInfoSchemaType> {
let data: RootGroupInfoSchemaType = {
...rootGroupInfo,
};
if (this.models) {
await this.models.rootGroupInfo.updateOne({
rootGroupId: data.rootGroupId,
}, data, {
upsert: true,
setDefaultsOnInsert: true,
});
}
await this.cache.set(data.rootGroupId, data, this.cacheTTL);
return data;
}
public async remove(rootGroupId: string): Promise<void> {
if (this.models) {
await this.models.rootGroupInfo.deleteOne({
rootGroupId,
});
}
await this.cache.del(rootGroupId);
}
}

@ -0,0 +1,109 @@
import App from "../App";
import { StorageConfig } from "../Config";
import { CacheStore } from "../CacheManager";
import { UserInfoType } from "../message/Sender";
import { ModelRegistry } from "../DatabaseManager";
import { UserInfoSchemaType } from "../odm/UserInfo";
import { RobotStorage } from "./RobotStorage";
export class UserInfoStorage {
private app: App;
private config: StorageConfig;
private storages: RobotStorage;
private models?: ModelRegistry;
private cacheTTL: number;
private cache: CacheStore;
public constructor(app: App, config: StorageConfig, storages: RobotStorage) {
this.app = app;
this.config = config;
this.cacheTTL = config.cache_ttl ?? 86400;
this.storages = storages;
this.cache = app.cache.getStore(['ObjectCache', storages.robotId, 'user_info']);
}
public async initialize() {
this.models = this.storages.models;
}
public async get(userId: string, fetchFromBot: boolean = false): Promise<UserInfoSchemaType | null> {
// from cache
let userInfo = await this.cache.get<UserInfoSchemaType>(userId);
if (userInfo) {
return userInfo;
}
if (fetchFromBot) {
return await this.fetchFromRobot(userId);
} else if (this.models) {
let doc = await this.models.userInfo.findOne({
userId,
});
if (doc) {
userInfo = doc.toObject();
await this.cache.set(userId, userInfo, this.cacheTTL);
return userInfo;
}
} else {
this.app.logger.warn('未配置 Database');
}
return null;
}
public async getByRef(userInfo: UserInfoSchemaType | string): Promise<UserInfoSchemaType | null> {
if (typeof userInfo === 'string') {
return await this.get(userInfo, false);
} else {
return await this.get(userInfo.userId, false);
}
}
public async fetchFromRobot(userId: string): Promise<UserInfoSchemaType | null> {
const robot = this.app.robot.getRobot(this.storages.robotId);
if (robot) {
const userInfoList = await robot.getUsersInfo?.([userId]);
if (userInfoList && userInfoList.length > 0) {
const userInfo = userInfoList[0];
if (userInfo) {
return await this.set(userInfo);
}
}
} else {
this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`);
}
return null;
}
public async set(userInfo: UserInfoType): Promise<UserInfoSchemaType> {
let data: UserInfoSchemaType = {
...userInfo,
}
if (this.models) {
await this.models.userInfo.updateOne({
userId: data.userId,
}, data, {
upsert: true,
setDefaultsOnInsert: true,
});
}
await this.cache.set(data.userId, data, this.cacheTTL);
return data;
}
public async remove(userId: string): Promise<void> {
if (this.models) {
await this.models.userInfo.deleteOne({
userId,
});
}
await this.cache.del(userId);
}
}

@ -0,0 +1,28 @@
export class ItemLimitedList<T = any> extends Array<T> {
private _maxLength: number;
constructor(maxLength: number) {
super();
this._maxLength = maxLength;
}
get maxLength() {
return this._maxLength;
}
set maxLength(value: number) {
this._maxLength = value;
if (this.length > this.maxLength) {
let offset = this.length - this.maxLength;
this.splice(0, offset);
}
}
/** 添加元素 */
addOne(item: T) {
if (this.length + 1 >= this.maxLength) {
this.shift();
}
this.push(item);
}
}

@ -0,0 +1,72 @@
import EventEmitter from "events";
import { Utils } from "./Utils";
export class MessageTypingSimulator extends EventEmitter {
public chineseCPM = 1000;
public latinCPM = this.chineseCPM * 4;
public randomDelay = [0, 3000];
private messageBuffer: string[] = [];
private messageCount = 0;
private inTyping = false;
private running = true;
constructor() {
super();
}
public pushMessage(message: string) {
this.messageBuffer.push(message);
if (!this.inTyping) {
this.startTyping();
}
}
public stop() {
this.running = false;
this.removeAllListeners();
}
public async startTyping() {
if (this.inTyping) {
return;
}
this.inTyping = true;
try {
while (this.messageBuffer.length > 0 && this.running) {
const message = this.messageBuffer.shift();
if (!message) {
continue;
}
const typingTime = this.getTypingTime(message);
// console.log('sleep time', typingTime);
await Utils.sleep(typingTime);
if (this.running) {
this.emit('message', message, this.messageCount);
this.messageCount++;
}
}
} catch (e) {
this.inTyping = false;
console.error(e);
}
this.inTyping = false;
}
private getTypingTime(message: string) {
let latinChars = 0;
for (let i = 0; i < message.length; i++) {
if (message.charCodeAt(i) < 128) {
latinChars++;
}
}
let chineseChars = message.length - latinChars;
let typingTime = chineseChars * 60000 / this.chineseCPM + latinChars * 60000 / this.latinCPM;
typingTime += Math.random() * (this.randomDelay[1] - this.randomDelay[0]) + this.randomDelay[0];
return typingTime;
}
}

@ -0,0 +1,39 @@
import { ShuffleRandom } from "./ShuffleRandom";
import Handlebars from "handlebars";
import { Pair } from "./types";
export class RandomMessage extends ShuffleRandom<Pair<string, HandlebarsTemplateDelegate<any>>> {
constructor(messageList: string[] = []) {
let itemList: Pair<string, HandlebarsTemplateDelegate<any>>[] = messageList
.map((message) => [message, Handlebars.compile(message)]);
super(itemList);
}
public get messageList(): string[] {
return this._itemList.map((item) => item[0]);
}
public set messageList(messageList: string[]) {
// Remove message that not in messageList
this._itemList = this._itemList.filter((item) => !messageList.includes(item[0]));
// Add message that not in itemList
for (let message of messageList) {
if (!this._itemList.some((item) => item[0] === message)) {
this._itemList.push([message, Handlebars.compile(message)]);
}
}
this.shuffle();
}
public nextMessage(data: any = {}): string | null {
let message = super.next();
if (message === null) {
return null;
}
let generator = message[1];
return generator(data);
}
}

@ -0,0 +1,43 @@
export class ShuffleRandom<T = string> {
protected _itemList: T[];
protected currentIndex = 0;
constructor(itemList: T[] = []) {
this._itemList = itemList;
if (this._itemList.length > 0) {
this.shuffle();
}
}
public get itemList(): T[] {
return this._itemList;
}
public set itemList(itemList: T[]) {
this._itemList = itemList;
this.shuffle();
}
public next(): T | null {
if (this._itemList.length === 0) {
return null;
}
let message = this._itemList[this.currentIndex];
if (this.currentIndex === this._itemList.length - 1) {
this.shuffle();
} else {
this.currentIndex++;
}
return message;
}
protected shuffle() {
for (let i = 0; i < this._itemList.length; i++) {
let j = Math.floor(Math.random() * (i + 1));
[this._itemList[i], this._itemList[j]] = [this._itemList[j], this._itemList[i]];
}
this.currentIndex = 0;
}
}

@ -71,4 +71,65 @@ export class Utils {
fs.mkdirSync(path, { recursive: true });
}
}
static isLatinChar(char: string | number): boolean {
const charCodeMap = {
a: 'a'.charCodeAt(0),
z: 'z'.charCodeAt(0),
A: 'A'.charCodeAt(0),
Z: 'Z'.charCodeAt(0),
0: '0'.charCodeAt(0),
9: '9'.charCodeAt(0),
};
if (typeof char === 'string') {
char = char.charCodeAt(0);
}
return (char >= charCodeMap.a && char <= charCodeMap.z) ||
(char >= charCodeMap.A && char <= charCodeMap.Z) ||
(char >= charCodeMap['0'] && char <= charCodeMap['9']);
}
/**
*
*/
static countWord(text: string): number {
text = text.trim();
if (text === '') {
return 0;
}
let wordCount = 0;
let charCode: number = 0;
let prevCharCode: number = 0;
for (let i = 0; i < text.length; i++) {
charCode = text.charCodeAt(i);
if (i !== 0) {
prevCharCode = text.charCodeAt(i - 1);
}
if (charCode > 255) {
wordCount ++;
} else {
if (Utils.isLatinChar(charCode) && !Utils.isLatinChar(prevCharCode)) {
wordCount ++;
}
}
}
return wordCount + 1;
}
static escapeHtml(text: string) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
static unescapeHtml(text: string) {
return text.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
}
static escapeMarkdown(text: string) {
return text.replace(/([\\`*_{}[\]()#+\-.!])/g, '\\$1');
}
}

@ -9,19 +9,23 @@ export function useLogger(): winston.Logger {
return useApp().logger;
}
export function useEventManager() {
export function useEvent() {
return useApp().event;
}
export function useSessionManager() {
return useApp().session;
export function useCache() {
return useApp().cache;
}
export function useRobotManager() {
export function useStorage() {
return useApp().storage;
}
export function useRobot() {
return useApp().robot;
}
export function useRestfulApiManager() {
export function useRestfulApi() {
return useApp().restfulApi;
}

@ -0,0 +1,41 @@
export function compareProps(a: any, b: any, props: string[], depth: number = 5): boolean {
if (depth <= 0) return true;
for (let prop of props) {
let propPath = prop.split('.');
if (propPath.length === 1) {
// 优化单层性能
if (typeof a !== 'object' || typeof b !== 'object' || a[prop] !== b[prop]) {
return false;
}
} else {
let curA = a;
let curB = b;
for (let p of propPath) {
if (typeof curA !== 'object' || !(p in curA)) {
curA = undefined;
} else {
curA = curA[p];
}
if (typeof curB !== 'object' || !(p in curB)) {
curB = undefined;
} else {
curB = curB[p];
}
if (curA === undefined || curB === undefined) {
break;
}
}
if (curA !== curB) {
return false;
}
}
}
return true;
}

@ -0,0 +1,5 @@
import { MessageChunk } from "src/message/Message";
export class MessageUtils {
}

@ -0,0 +1,44 @@
export function retrieveOnce<Func extends ((...args: any) => Promise<any>)>(callback: Func): Func {
type ValType = Awaited<ReturnType<Func>>;
let data: any = undefined;
let error: Error | undefined = undefined;
let loaded = false;
let loading = false;
let callbacks: [(data: ValType) => any, (err: Error) => any][] = [];
return ((...args: any): Promise<ValType> => {
return new Promise((resolve, reject) => {
if (loaded) {
if (error) {
reject(error);
} else {
resolve(data);
}
return;
}
callbacks.push([resolve, reject]);
if (!loading) {
loading = true;
callback(...args).then((ret) => {
data = ret;
loaded = true;
loading = false;
callbacks.forEach((cb) => {
cb[0](ret);
});
}).catch((err) => {
error = err;
loaded = true;
loading = false;
callbacks.forEach((cb) => {
cb[1](err);
});
});
}
});
}) as unknown as Func;
}

@ -1 +1,5 @@
export type AnyFunction = (...args: any) => any;
export type Pair<T1, T2> = [T1, T2];
export type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never })

@ -27,8 +27,8 @@
"module": "ES2022", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */

Loading…
Cancel
Save