diff --git a/channels/isekaibbs/newPost.yml b/channels/isekaibbs/newPost.yml index 41f33c0..c646f94 100644 --- a/channels/isekaibbs/newPost.yml +++ b/channels/isekaibbs/newPost.yml @@ -6,6 +6,10 @@ source: type: newPost tpl: default: |- - {{{postUser.displayname}}}「{{{postUser.username}}}」发布了帖子《{{{topic.titleRaw}}}》: - {{{excerpt post.content 100}}} - {{{topic.url}}} + {{{postUser.displayname}}}「{{{postUser.username}}}」发布了帖子《{{{topic.titleRaw}}}》: + {{{excerpt post.content 100}}} + + # default: |- + # {{{postUser.displayname}}}「{{{postUser.username}}}」发布了帖子《{{{topic.titleRaw}}}》: + # {{{excerpt post.content 100}}} + # {{{topic.url}}} diff --git a/channels/isekaibbs/newReply.yml b/channels/isekaibbs/newReply.yml index c2d78bf..9f362c6 100644 --- a/channels/isekaibbs/newReply.yml +++ b/channels/isekaibbs/newReply.yml @@ -8,4 +8,5 @@ tpl: default: |- {{{postUser.displayname}}}「{{{postUser.username}}}」回复了 {{{topicUser.displayname}}}「{{{topicUser.username}}}」的帖子《{{topic.titleRaw}}》: {{{excerpt post.content 100}}} - {{{post.url}}} \ No newline at end of file + + # {{{post.url}}} \ No newline at end of file diff --git a/channels/isekaiwiki/editPage.yml b/channels/isekaiwiki/editPage.yml index d7a2cae..f3c8b76 100644 --- a/channels/isekaiwiki/editPage.yml +++ b/channels/isekaiwiki/editPage.yml @@ -8,4 +8,3 @@ tpl: default: |- {{{author}}} 更新了页面《{{{title}}}》: {{{summary}}} - {{{url}}} diff --git a/channels/isekaiwiki/newPage.yml b/channels/isekaiwiki/newPage.yml index c6eab36..bc66fa4 100644 --- a/channels/isekaiwiki/newPage.yml +++ b/channels/isekaiwiki/newPage.yml @@ -8,5 +8,3 @@ tpl: default: |- {{{author}}} 创建了新页面《{{{title}}}》: {{{summary}}} - - {{{url}}} diff --git a/package.json b/package.json index 9bf07f6..8f778d1 100644 --- a/package.json +++ b/package.json @@ -52,5 +52,8 @@ "@types/yaml": "^1.9.7", "ts-node": "^10.4.0", "typescript": "^4.5.4" + }, + "imports": { + "#ibot/*": "./dist/*" } } diff --git a/src/EventManager.ts b/src/EventManager.ts index 29ea67c..43fe518 100644 --- a/src/EventManager.ts +++ b/src/EventManager.ts @@ -1,10 +1,11 @@ import App from "./App"; import { CommandOverrideConfig } from "./Config"; import { PermissionDeniedError, RateLimitError } from "./error/errors"; -import { CommonReceivedMessage, CommonSendMessage } from "./message/Message"; +import { CommonReceivedMessage } from "./message/Message"; import { ChatIdentity } from "./message/Sender"; import { CommandInfo, CommandInputArgs, EventScope, MessageEventOptions, MessagePriority, PluginEvent } from "./PluginManager"; -import { Robot } from "./RobotManager"; +import { Robot } from "./robot/Robot"; +import { Reactive } from "./utils/reactive"; export type ControllerEventInfo = { priority: number; @@ -143,7 +144,7 @@ export class EventManager { public async emit(eventName: string, senderInfo?: ChatIdentity | null, ...args: any[]) { if (this.app.debug) { - if (args[0] instanceof CommonReceivedMessage) { + if (typeof args[0] === 'object' && args[0].chatType) { this.app.logger.debug(`触发事件 ${eventName} ${args[0].contentText}`); } else { this.app.logger.debug(`触发事件 ${eventName}`); @@ -166,8 +167,8 @@ export class EventManager { console.error(error); for (let arg of args) { - if (arg instanceof CommonReceivedMessage) { - const msg = arg; + if (typeof arg === 'object' && arg.chatType) { + const msg: CommonReceivedMessage = arg; if (error instanceof RateLimitError) { const retryAfterMinutes = Math.ceil(error.retryAfter / 60); msg.sendReply(`使用太多了,${retryAfterMinutes}分钟后再试吧`); @@ -240,7 +241,7 @@ export class EventManager { return isResolved; } - public async emitMessage(message: CommonReceivedMessage) { + public async emitMessage(message: Reactive) { let isResolved = false; if (message.chatType === 'private' || (message.chatType === 'group' && message.mentionedReceiver)) { @@ -262,7 +263,7 @@ export class EventManager { return false; } - public async emitCommand(contentText: string, message: CommonReceivedMessage) { + public async emitCommand(contentText: string, message: Reactive) { let command = ''; let param = ''; @@ -309,7 +310,7 @@ export class EventManager { return await this.emit(`raw/${robot.type}/${event}`, { type: 'raw', robot: robot }, event); } - public async emitRawMessage(message: CommonReceivedMessage) { + public async emitRawMessage(message: Reactive) { let isResolved = false; await this.emit(`filter/message`, null, message); @@ -320,7 +321,7 @@ export class EventManager { return await this.emit('raw/message', this.getSenderInfo(message), message); } - public async emitFilterSendMessage(message: CommonSendMessage) { + public async emitFilterSendMessage(message: Reactive) { } diff --git a/src/PluginManager.ts b/src/PluginManager.ts index 7422914..ff9576a 100644 --- a/src/PluginManager.ts +++ b/src/PluginManager.ts @@ -1,6 +1,5 @@ import { EventManager } from "./EventManager"; import { CommonReceivedMessage } from "./message/Message"; -import { Robot } from "./RobotManager"; import fs from 'fs'; import fsAsync from 'fs/promises'; import chokidar from 'chokidar'; @@ -10,6 +9,8 @@ import EventEmitter from "events"; import path from "path"; import { ChatIdentity } from "./message/Sender"; import { Utils } from "./utils/Utils"; +import { Robot } from "./robot/Robot"; +import { Reactive } from "./utils/reactive"; export const MessagePriority = { LOWEST: 0, @@ -53,8 +54,8 @@ export type CommandInputArgs = { param: string, } -export type MessageCallback = (message: CommonReceivedMessage, resolved: VoidFunction) => any; -export type CommandCallback = (args: CommandInputArgs, message: CommonReceivedMessage, resolved: VoidFunction) => any; +export type MessageCallback = (message: Reactive, resolved: VoidFunction) => any; +export type CommandCallback = (args: CommandInputArgs, message: Reactive, resolved: VoidFunction) => any; export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction) => any; export type AllowedList = string[] | '*'; diff --git a/src/RobotManager.ts b/src/RobotManager.ts index a13e022..6025d8c 100644 --- a/src/RobotManager.ts +++ b/src/RobotManager.ts @@ -4,39 +4,9 @@ import path from "path"; import App from "./App"; import { MultipleMessage } from "./base/provider/BaseProvider"; import { RobotConfig } from "./Config"; -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 { CacheStore } from "./CacheManager"; -import { Target } from "./SubscribeManager"; -import { MessageSchemaType } from "./odm/Message"; - -export interface Robot { - type: string; - robotId?: string; - userId?: string; - description?: string; - initialize?: () => Promise; - destroy?: () => Promise; - initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise; - setCommands?(commands: CommandInfo[]): Promise; - markRead?(message: CommonReceivedMessage): Promise; - sendTyping?(chatIdentity: ChatIdentity): Promise; - sendMessage(message: CommonSendMessage): Promise; - sendPushMessage(targets: Target[], message: string): Promise; - deleteMessage?(chatIdentity: ChatIdentity, messageId: string): Promise; - getSession(chatIdentity: ChatIdentity, type: string): CacheStore; - ensureMediaUrl?(mediaMessageChunk: MessageChunk): Promise; - - getUsersInfo?(userIds: string[]): Promise<(UserInfoType | null)[]>; - getGroupInfo?(groupId: string, rootGroupId?: string): Promise; - getRootGroupInfo?(rootGroupId: string): Promise; - getChannelInfo?(channelId: string): Promise; - getGroupUsersInfo?(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]>; - - parseDBMessage?(dbMessage: MessageSchemaType): Promise; -} +import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage } from "./message/Message"; +import { GroupSender, ChatIdentity, UserSender } from "./message/Sender"; +import { Robot, RobotAdapter } from "./robot/Robot"; export class RobotManager { private app: App; @@ -54,7 +24,7 @@ export class RobotManager { } public async initialize() { - const ROBOT_PATH = path.join(this.app.srcPath, "robot"); + const ROBOT_PATH = path.join(this.app.srcPath, "robot/adapter"); for (let file of fs.readdirSync(ROBOT_PATH)) { let robotFile = `${ROBOT_PATH}/${file}`; @@ -84,12 +54,12 @@ export class RobotManager { if (robotType in this.robotClasses) { let robotClass = this.robotClasses[robotType]; try { - let robotObject: Robot = new robotClass(this.app, robotId, robotConfig); - - this.robots[robotId] = robotObject; + let robotAdapter: RobotAdapter = new robotClass(this.app, robotId, robotConfig); + let robotWrapper: Robot = new Robot(this.app, robotId, robotAdapter); - await robotObject.initialize?.(); - await robotObject.initRestfulApi?.(this.app.restfulApi.getRobotRouter(robotId), this.app.restfulApi); + this.robots[robotId] = robotWrapper; + + await robotWrapper.initialize(); this.app.logger.info(`已加载Robot: ${robotId}`); } catch(err) { diff --git a/src/controller/IsekaiBBSQuicklyPost.ts b/src/controller/IsekaiBBSQuicklyPostController.ts similarity index 53% rename from src/controller/IsekaiBBSQuicklyPost.ts rename to src/controller/IsekaiBBSQuicklyPostController.ts index 166cf0d..c154920 100644 --- a/src/controller/IsekaiBBSQuicklyPost.ts +++ b/src/controller/IsekaiBBSQuicklyPostController.ts @@ -1,22 +1,42 @@ -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 "../robot/qq/Message"; -import QQRobot from "../robot/QQRobot"; -import { ChatIdentity, UserSender } from "../message/Sender"; -import { Utils } from "../utils/Utils"; -import { MessageUtils } from "../utils/message"; +import { QQForwardingMessage } from "../robot/adapter/qq/Message"; +import QQRobot from "../robot/adapter/QQRobot"; +import { GroupSender } from "../message/Sender"; +import { Robot } from "#ibot/robot/Robot"; export type IsekaiBBSQuicklyPostConfig = { api_endpoint: string, token: string, }; +export type IsekaiQuicklyPostMessageData = { + /** 发布账号 */ + account: string, + /** 发布账号的昵称(QQ昵称、邮箱姓名) */ + nickname?: string, + /** 头像 */ + avatar?: string, + /** 文章内容(Markdown) */ + content: string, + /** 消息ID,用于建立回复树 */ + id?: string, + /** 回复的消息ID */ + replyId?: string, +} + +export type IsekaiQuicklyPostBody = { + /** 发布来源 */ + srcType: string, + /** 文章标题 */ + title?: string, + /** 消息列表 */ + messages: IsekaiQuicklyPostMessageData[], +}; + export default class IsekaiBBSQuicklyPost implements PluginController { private config!: Awaited>; @@ -101,48 +121,87 @@ export default class IsekaiBBSQuicklyPost implements PluginController { let markdownBuilder: string[] = []; message.content.forEach(messageChunk => { if (messageChunk.type.includes('text')) { - markdownBuilder.push(messageChunk.data?.text ?? ''); + markdownBuilder.push(messageChunk.text ?? ''); } else if (messageChunk.type.includes('image')) { markdownBuilder.push(`![${messageChunk.data?.alt ?? ''}](${messageChunk.data?.url ?? ''})`); + } else if (messageChunk.type.includes('emoji')) { + markdownBuilder.push(messageChunk.text ?? '🫥'); + } else if (messageChunk.type.includes('record')) { + markdownBuilder.push('[语音消息]'); } else if (messageChunk.type.includes('mention')) { if (messageChunk.data?.text) { - markdownBuilder.push(`@${messageChunk.data.text}`); + markdownBuilder.push(`**@${messageChunk.data.text}**`); } } }); + return markdownBuilder.join(''); } - async postNewThread(message: CommonReceivedMessage, groupConfig: IsekaiBBSQuicklyPostConfig) { - if (message.receiver.type !== 'qq') { + async postNewThread(refMessage: CommonReceivedMessage, groupConfig: IsekaiBBSQuicklyPostConfig) { + if (refMessage.receiver.type !== 'qq') { // TODO: support other platform return; } - let attachmentMsg = message.content[0] as QQForwardingMessage; + let attachmentMsg = refMessage.content[0] as QQForwardingMessage; let resId = attachmentMsg.data.res_id; - let robot = message.receiver as QQRobot; + let robot = refMessage.receiver as Robot; + let referenceSender = refMessage.sender as GroupSender; - message.markRead() + refMessage.markRead() - this.app.logger.info(`[群号:${message.sender.groupId}] 收到合并转发消息,正在发送到BBS。`); + this.app.logger.info(`[群号:${refMessage.sender.groupId}] 收到合并转发消息 ${resId},正在发送到BBS。`); - let messageList = await robot.getReferencedMessages(resId); + let messageList = await robot.adapter.getReferencedMessages(resId); if (!messageList || messageList.length === 0) { - this.app.logger.info(`[群号:${message.sender.groupId}] 合并转发消息内容为空或无法获取。`); + this.app.logger.info(`[群号:${refMessage.sender.groupId}] 合并转发消息内容为空或无法获取。`); return; } try { - let markdownBuilder = []; + let markdownBuilder: string[] = []; for (let message of messageList) { - + const sender = message.sender as GroupSender; + const content = await this.messageToMarkdown(message); + + markdownBuilder.push('**' + (sender.displayName ?? sender.userName ?? sender.userId) + ':** '); + markdownBuilder.push(content); + markdownBuilder.push('\n'); + } + + let postData = { + srcType: 'qq', + messages: [{ + account: referenceSender.userId, + nickname: referenceSender.displayName, + avatar: robot.adapter.infoProvider.getUserImage(referenceSender.userId), + content: markdownBuilder.join('\n'), + }], + } as IsekaiQuicklyPostBody; + + const res = await got.post(groupConfig.api_endpoint, { + json: postData, + headers: { + authorization: `Bearer ${groupConfig.token}`, + } + }).json(); + + if (res.error) { + throw new Error(res.message); } + + // 保存threadId到消息 + refMessage.extra['isekai_bbs_quicklypost'] = { + threadId: res.tid, + }; } catch(err: any) { - this.app.logger.error("转存群文件失败:" + err.message, err); + this.app.logger.error("转发消息到BBS失败:" + err.message, err); console.error(err); - let msg = this.messageGroup.error.nextMessage(err.message); - await message.sendReply(msg ?? `转存群文件失败:${err.message}`, false); + let msg = this.messageGroup.error.nextMessage({ + error: err.message, + }); + await refMessage.sendReply(msg ?? `转发失败:${err.message}`, false); } } } \ No newline at end of file diff --git a/src/message/Chat.ts b/src/message/Chat.ts index 5cc68c8..6f63784 100644 --- a/src/message/Chat.ts +++ b/src/message/Chat.ts @@ -1,5 +1,5 @@ +import { Robot } from "#ibot/robot/Robot"; import App from "../App"; -import { Robot } from "../RobotManager"; export class ChatThread { private app: App; diff --git a/src/message/Message.ts b/src/message/Message.ts index 7a18ea6..bfa4939 100644 --- a/src/message/Message.ts +++ b/src/message/Message.ts @@ -1,9 +1,10 @@ -import { Robot } from "../RobotManager"; import { CacheStore } from "../CacheManager"; -import { MessageDataType, MessageSchemaType, chatIdentityToDB } from "../odm/Message"; +import { MessageDataType, chatIdentityToDB } from "../odm/Message"; import { BaseSender, ChatIdentity, GroupSender, IMessageSender, UserSender } from "./Sender"; import { LiteralUnion } from "../utils/types"; import { Utils } from "../utils/Utils"; +import { Robot } from "#ibot/robot/Robot"; +import { Reactive, reactive } from "#ibot/utils/reactive"; export enum MessageDirection { RECEIVE = 1, @@ -85,7 +86,7 @@ export class CommonMessage { deleted: boolean = false; /** 附加信息 */ - extra: any = {}; + extra: any = reactive({}); private _contentText?: string; @@ -234,7 +235,7 @@ export class CommonSendMessage extends CommonMessage { receiver: ChatIdentity; /** 回复的消息 */ - repliedMessage?: CommonReceivedMessage; + repliedMessage?: Reactive; /** 发送时间 */ time: Date = new Date(); @@ -290,6 +291,10 @@ export class CommonReceivedMessage extends CommonMessage { /** Session存储 */ session: SessionStoreGroup = new Proxy({} as any, { get: (target, p) => { + if (p.toString().startsWith('_')) { + return undefined; + } + if (!target[p]) { target[p] = this.getSession(p as string); } @@ -321,7 +326,6 @@ export class CommonReceivedMessage extends CommonMessage { if (addReply) { newMessage.repliedId = this.id; - newMessage.repliedMessage = this; } return newMessage; @@ -348,10 +352,10 @@ export class CommonReceivedMessage extends CommonMessage { let newMessage = this.createReplyMessage(message, shouldReply); if (newMessage.content.length === 0) return null; - newMessage.extra = { + newMessage.extra = reactive({ ...newMessage.extra, ...extra, - }; + }); newMessage = await this.receiver.sendMessage(newMessage); diff --git a/src/message/Sender.ts b/src/message/Sender.ts index 541e39a..2df122c 100644 --- a/src/message/Sender.ts +++ b/src/message/Sender.ts @@ -1,5 +1,6 @@ +import { Robot } from "#ibot/robot/Robot"; +import { LiteralUnion } from "#ibot/utils/types"; import { any } from "micromatch"; -import { Robot } from "../RobotManager"; export type BaseSenderType = "user" | "group" | "channel"; @@ -92,11 +93,11 @@ export class GroupSender { } get groupDisplayName() { - return this.groupName ?? this.groupId; + return this.groupName || this.groupId; } get displayName() { - return this.nickName ?? this.globalNickName ?? this.userName ?? this.userId; + return this.nickName || this.globalNickName || this.userName || this.userId; } get userSender() { @@ -109,7 +110,7 @@ export class GroupSender { } export interface ChatIdentity { - type: ('private' | 'group' | 'channel' | 'raw') | (string & {}); + type: LiteralUnion<'private' | 'group' | 'channel' | 'raw'>; robot: Robot; rootGroupId?: string; groupId?: string; diff --git a/src/robot/Robot.ts b/src/robot/Robot.ts new file mode 100644 index 0000000..0d682c8 --- /dev/null +++ b/src/robot/Robot.ts @@ -0,0 +1,275 @@ +import { Reactive, reactive } from "#ibot/utils/reactive"; +import App from "../App"; +import { CacheStore } from "../CacheManager"; +import { CommandInfo } from "../PluginManager"; +import { RestfulRouter } from "../RestfulApiManager"; +import { Target } from "../SubscribeManager"; +import { CommonReceivedMessage, CommonSendMessage, MessageChunk, CommonMessage, MessageDirection } from "../message/Message"; +import { ChatIdentity, UserInfoType, GroupInfoType, RootGroupInfoType, ChannelInfoType, GroupUserInfoType, UserSender, BaseSender, GroupSender } from "../message/Sender"; +import { MessageDataType, MessageSchemaType } from "../odm/Message"; +import { RobotStorage } from "../storage/RobotStorage"; + +export interface RobotAdapter { + type: string; + robotId?: string; + userId?: string; + description?: string; + initialize?: (wrapper: Robot) => Promise; + destroy?: () => Promise; + setCommands?(commands: CommandInfo[]): Promise; + markRead?(message: CommonReceivedMessage): Promise; + sendTyping?(chatIdentity: ChatIdentity, typing: boolean): Promise; + sendMessage(message: CommonSendMessage): Promise; + sendPushMessage(targets: Target[], message: string): Promise; + deleteMessage?(chatIdentity: ChatIdentity, messageId: string): Promise; + retrieveMediaUrl?(mediaMessageChunk: MessageChunk): Promise; + + getUsersInfo?(userIds: string[]): Promise<(UserInfoType | null)[]>; + getGroupInfo?(groupId: string, rootGroupId?: string): Promise; + getRootGroupInfo?(rootGroupId: string): Promise; + getChannelInfo?(channelId: string): Promise; + getGroupUsersInfo?(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]>; + + parseDBMessage?(dbMessage: MessageSchemaType): Promise; +} + +export class Robot { + public type: string; + public robotId: string; + public description: string = ''; + + // 机器人配置项 + public commandPrefix: string[] = ['/']; + public account: string = ''; + + public adapter: Adapter; + public storages?: RobotStorage; + public restfulRouter!: RestfulRouter; + + private app: App; + + constructor(app: App, robotId: string, adapter: Adapter) { + this.app = app; + + this.robotId = robotId; + this.adapter = adapter; + this.type = adapter.type; + this.description = adapter.description || ''; + } + + async initialize() { + // Restful API + this.restfulRouter = this.app.restfulApi.getRobotRouter(this.robotId); + + // Storages + this.storages = await this.app.storage.getStorages(this.robotId); + + await this.adapter.initialize?.(this); + } + + async destroy() { + await this.adapter.destroy?.(); + } + + public getSession(chatIdentity: ChatIdentity, type: string) { + const sessionPath = this.app.robot.getSessionPath(chatIdentity, type); + return this.app.cache.getStore(sessionPath); + } + + /** + * 设置机器人指令信息(用于某些协议在聊天窗口显示机器人指令列表) + * @param commands + * @returns + */ + public setCommands(commands: CommandInfo[]): Promise { + return this.adapter.setCommands ? this.adapter.setCommands(commands) : Promise.resolve(); + } + + /** + * 标记消息已读 + * @param message + * @returns + */ + public markRead(message: CommonReceivedMessage): Promise { + return this.adapter.markRead ? this.adapter.markRead(message) : Promise.resolve(false); + } + + /** + * 发送正在输入状态 + * @param chatIdentity + * @param typing + * @returns + */ + public sendTyping(chatIdentity: ChatIdentity, typing: boolean): Promise { + return this.adapter.sendTyping ? this.adapter.sendTyping(chatIdentity, typing) : Promise.resolve(false); + } + + /** + * 在函数执行时保持输入状态 + * @param chatIdentity + * @param callback + */ + async wrapTyping(chatIdentity: ChatIdentity, callback: () => Promise): Promise { + let timer = setInterval(() => { + this.sendTyping(chatIdentity, true); + }); + + this.sendTyping(chatIdentity, true); + + try { + await callback(); + clearInterval(timer); + this.sendTyping(chatIdentity, false); + } catch(err) { + clearInterval(timer); + this.sendTyping(chatIdentity, false); + throw err; + } + } + + /** + * 发送消息 + * @param message + * @returns + */ + public sendMessage(message: CommonSendMessage): Promise { + return this.adapter.sendMessage ? this.adapter.sendMessage(message) : Promise.resolve(message); + } + + public sendPushMessage(targets: Target[], message: string): Promise { + return this.adapter.sendPushMessage ? this.adapter.sendPushMessage(targets, message) : Promise.resolve(); + } + + /** + * 删除消息 + * @param chatIdentity + * @param messageId + * @returns + */ + public deleteMessage(chatIdentity: ChatIdentity, messageId: string): Promise { + return this.adapter.deleteMessage ? this.adapter.deleteMessage(chatIdentity, messageId) : Promise.resolve(false); + } + + /** + * 获取消息的媒体文件URL,并更新消息内容 + * @param mediaMessageChunk + * @returns + */ + public retrieveMediaUrl(mediaMessageChunk: MessageChunk): Promise { + return this.adapter.retrieveMediaUrl ? this.adapter.retrieveMediaUrl(mediaMessageChunk) : Promise.resolve(); + } + + /** + * 解析数据库中的消息 + * @param dbMessage + * @returns + */ + public async parseDBMessage(dbMessage: MessageSchemaType): Promise | null> { + let parsedMessage: CommonMessage | null = null; + + if (this.adapter.parseDBMessage) { + parsedMessage = await this.adapter.parseDBMessage(dbMessage); + } else { + const chatIdentity: ChatIdentity = { + ...dbMessage.chatIdentity, + robot: this, + type: dbMessage.chatType, + }; + + if (dbMessage.direction === MessageDirection.RECEIVE) { + let sender: UserSender | GroupSender | null = null; + if (dbMessage.chatType === 'private' && dbMessage.chatIdentity.userId) { + sender = new UserSender(this, dbMessage.chatIdentity.userId); + let userInfo = await this.storages?.userInfo.get(dbMessage.chatIdentity.userId); + + if (userInfo) { + sender.userName = userInfo.userName; + sender.nickName = userInfo.nickName; + } + } else if (dbMessage.chatType === 'group' && dbMessage.chatIdentity.userId && dbMessage.chatIdentity.groupId) { + sender = new GroupSender(this, dbMessage.chatIdentity.groupId, dbMessage.chatIdentity.userId); + let userInfo = await this.storages?.userInfo.get(dbMessage.chatIdentity.userId); + let rootGroupInfo = dbMessage.chatIdentity.rootGroupId ? + await this.storages?.rootGroupInfo.get(dbMessage.chatIdentity.rootGroupId) : + null; + let groupInfo = await this.storages?.groupInfo.get(dbMessage.chatIdentity.groupId, dbMessage.chatIdentity.rootGroupId); + let groupUserInfo = await this.storages?.groupUserInfo.get(dbMessage.chatIdentity.userId, dbMessage.chatIdentity.groupId); + + if (userInfo) { + sender.userName = userInfo.userName; + sender.globalNickName = userInfo.nickName; + } + + if (rootGroupInfo) { + sender.rootGroupName = rootGroupInfo.name; + } + + if (groupInfo) { + sender.groupName = groupInfo.name; + } + + if (groupUserInfo) { + sender.userName = groupUserInfo.userName; + sender.nickName = groupUserInfo.nickName; + } + } + + if (sender) { + let message = new CommonReceivedMessage(this, sender, dbMessage.messageId); + + message.content = dbMessage.content; + message.repliedId = dbMessage.repliedMessageId; + message.mentions = dbMessage.mentionedUserIds?.map((userId) => { + return { + userId, + }; + }) ?? []; + message.time = dbMessage.time; + } + } else if (dbMessage.direction === MessageDirection.SEND) { + let message = new CommonSendMessage(this, dbMessage.chatType, chatIdentity, dbMessage.content); + message.id = dbMessage.messageId; + message.repliedId = dbMessage.repliedMessageId; + message.mentions = dbMessage.mentionedUserIds?.map((userId) => { + return { + userId, + }; + }) ?? []; + message.time = dbMessage.time; + message.extra = reactive(dbMessage.extra); + + parsedMessage = message; + } + } + + if (parsedMessage) { + if (this.storages) { + return this.storages.message.reactive(parsedMessage); + } else { + return reactive(parsedMessage); + } + } + + return null; + } + + getUsersInfo(userIds: string[]): Promise<(UserInfoType | null)[]> { + return this.adapter.getUsersInfo ? this.adapter.getUsersInfo(userIds) : Promise.resolve([]); + } + + getGroupInfo(groupId: string, rootGroupId?: string): Promise { + return this.adapter.getGroupInfo ? this.adapter.getGroupInfo(groupId, rootGroupId) : Promise.resolve(null); + } + + getRootGroupInfo(rootGroupId: string): Promise { + return this.adapter.getRootGroupInfo ? this.adapter.getRootGroupInfo(rootGroupId) : Promise.resolve(null); + } + + getChannelInfo(channelId: string): Promise { + return this.adapter.getChannelInfo ? this.adapter.getChannelInfo(channelId) : Promise.resolve(null); + } + + getGroupUsersInfo(userIds: string[], groupId: string, rootGroupId?: string): Promise<(GroupUserInfoType | null)[]> { + return this.adapter.getGroupUsersInfo ? this.adapter.getGroupUsersInfo(userIds, groupId, rootGroupId) : Promise.resolve([]); + } +} \ No newline at end of file diff --git a/src/robot/QQRobot.ts b/src/robot/adapter/QQRobot.ts similarity index 81% rename from src/robot/QQRobot.ts rename to src/robot/adapter/QQRobot.ts index 8096744..95e411f 100644 --- a/src/robot/QQRobot.ts +++ b/src/robot/adapter/QQRobot.ts @@ -1,17 +1,16 @@ 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 App from "../../App"; +import { Robot, RobotAdapter } from "../Robot"; +import { Target } from "../../SubscribeManager"; +import { Utils } from "../../utils/Utils"; +import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../../RestfulApiManager"; 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, GroupInfoType, UserInfoType } from "../message/Sender"; +import { CommonReceivedMessage, CommonSendMessage, MessageChunk } from "../../message/Message"; +import { PluginController } from "../../PluginManager"; +import { RobotConfig } from "../../Config"; +import { ChatIdentity } from "../../message/Sender"; import { QQInfoProvider } from "./qq/InfoProvider"; export type QQRobotConfig = RobotConfig & { @@ -27,18 +26,19 @@ export type QQGroupInfo = { memberLimit?: number }; -export default class QQRobot implements Robot { +export default class QQRobot implements RobotAdapter { public type = 'qq'; public userId: string; public robotId: string; public description: string; - public commandPrefix: string[] = ['/', '!', '!', '/']; - public infoProvider: QQInfoProvider; + private config: QQRobotConfig; + private app: App; + private wrapper!: Robot; private endpoint: string; private taskId?: NodeJS.Timer; @@ -49,24 +49,30 @@ export default class QQRobot implements Robot { constructor(app: App, robotId: string, config: QQRobotConfig) { this.app = app; this.robotId = robotId; + this.config = config; this.endpoint = 'http://' + config.host; this.userId = config.userId.toString(); this.description = config.description ?? this.app.config.robot_description ?? 'Isekai Feedbot for QQ'; - if (config.command_prefix) { - if (Array.isArray(config.command_prefix)) { - this.commandPrefix = config.command_prefix; - } else if (typeof config.command_prefix === 'string') { - this.commandPrefix = [config.command_prefix]; - } - } - this.messageTypeHandler.help = this.parseHelpMessage.bind(this); this.infoProvider = new QQInfoProvider(app, this, config); } - async initialize() { + async initialize(wrapper: Robot) { + this.wrapper = wrapper; + + if (this.config.command_prefix) { + if (Array.isArray(this.config.command_prefix)) { + this.wrapper.commandPrefix = this.config.command_prefix; + } else if (typeof this.config.command_prefix === 'string') { + this.wrapper.commandPrefix = [this.config.command_prefix]; + } + } + + this.wrapper.account = this.userId; + + await this.initRestfulApi(this.wrapper.restfulRouter); await this.infoProvider.initialize(); } @@ -74,7 +80,7 @@ export default class QQRobot implements Robot { await this.infoProvider.destroy(); } - async initRestfulApi(router: RestfulRouter, api: RestfulApiManager) { + async initRestfulApi(router: RestfulRouter) { router.post(`/event`, this.handlePostEvent.bind(this)); } @@ -116,10 +122,10 @@ export default class QQRobot implements Robot { } helpBuilder.push( - '可用的指令前缀:"' + this.commandPrefix.join('"、"') + '"', + '可用的指令前缀:"' + this.wrapper.commandPrefix.join('"、"') + '"', '功能列表:' ); - const mainCommandPrefix = this.commandPrefix[0]; + const mainCommandPrefix = this.wrapper.commandPrefix[0]; for (let controller of controllers) { helpBuilder.push(`【${controller.name}】`); @@ -151,7 +157,7 @@ export default class QQRobot implements Robot { async handleMessage(postData: any) { let isResolved = false; if (postData.type) { - isResolved = await this.app.event.emitRawEvent(this, postData.type, postData); + isResolved = await this.app.event.emitRawEvent(this.wrapper, postData.type, postData); if (isResolved) return; } @@ -161,7 +167,7 @@ export default class QQRobot implements Robot { // 处理群消息 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()); + let groupSender = new QQGroupSender(this.wrapper, postData.group_id.toString(), postData.user_id.toString()); groupSender.groupInfo = groupInfo; groupSender.groupName = groupInfo?.groupName; groupSender.globalNickName = postData.sender?.nickname; @@ -170,7 +176,7 @@ export default class QQRobot implements Robot { groupSender.level = postData.sender?.level; groupSender.title = postData.sender?.title; - message = new QQGroupMessage(groupSender, this, postData.message_id.toString()); + message = new QQGroupMessage(groupSender, this.wrapper, postData.message_id.toString()); message.time = new Date(postData.time * 1000); message = await parseQQMessageChunk(this, postData.message ?? [], message); @@ -179,10 +185,10 @@ export default class QQRobot implements Robot { await this.infoProvider.updateUserSender(groupSender.userSender); } else if (postData.message_type === 'private') { // 处理私聊消息 - let userSender = new QQUserSender(this, postData.user_id.toString()); + let userSender = new QQUserSender(this.wrapper, postData.user_id.toString()); userSender.nickName = postData.sender?.nickname; - message = new QQPrivateMessage(userSender, this, postData.message_id.toString()); + message = new QQPrivateMessage(userSender, this.wrapper, postData.message_id.toString()); message.time = new Date(postData.time * 1000); message = await parseQQMessageChunk(this, postData.message ?? [], message); @@ -192,21 +198,21 @@ export default class QQRobot implements Robot { if (message) { // 保存消息 - this.infoProvider.saveMessage(message); + let messageRef = this.infoProvider.saveMessage(message); // 处理原始消息 - isResolved = await this.app.event.emitRawMessage(message); + isResolved = await this.app.event.emitRawMessage(messageRef); if (isResolved) return; // 处理指令 - let commandText = this.getCommandContentText(message); + let commandText = this.getCommandContentText(messageRef); if (commandText) { - await this.app.event.emitCommand(commandText, message); + await this.app.event.emitCommand(commandText, messageRef); return; } // 处理消息 - isResolved = await this.app.event.emitMessage(message); + isResolved = await this.app.event.emitMessage(messageRef); if (isResolved) return; } } @@ -221,11 +227,11 @@ export default class QQRobot implements Robot { // 处理群消息 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()); + let groupSender = new QQGroupSender(this.wrapper, postData.group_id.toString(), postData.user_id.toString()); groupSender.groupInfo = groupInfo; groupSender.groupName = groupInfo?.groupName; - let message = new QQGroupMessage(groupSender, this); + let message = new QQGroupMessage(groupSender, this.wrapper); message.time = new Date(postData.time * 1000); message.type = 'attachment'; @@ -242,18 +248,20 @@ export default class QQRobot implements Robot { } } as QQAttachmentMessage); + let messageRef = await this.infoProvider.saveMessage(message); + let isResolved = false; // 处理原始消息 - isResolved = await this.app.event.emitRawMessage(message); + isResolved = await this.app.event.emitRawMessage(messageRef); if (isResolved) return; // 处理消息 - isResolved = await this.app.event.emitMessage(message); + isResolved = await this.app.event.emitMessage(messageRef); if (isResolved) return; } getCommandContentText(message: CommonReceivedMessage) { - for (let prefix of this.commandPrefix) { + for (let prefix of this.wrapper.commandPrefix) { if (message.contentText.startsWith(prefix)) { // 移除指令前缀 if (message.content[0].data?.text) { @@ -265,12 +273,7 @@ export default class QQRobot implements Robot { return null; } - getSession(chatIdentity: ChatIdentity, type: string) { - const sessionPath = this.app.robot.getSessionPath(chatIdentity, type); - return this.app.cache.getStore(sessionPath); - } - - async ensureMediaUrl(mediaMessageChunk: MessageChunk): Promise { + async retrieveMediaUrl(mediaMessageChunk: MessageChunk): Promise { if (!mediaMessageChunk.data.url) { if (mediaMessageChunk.type.includes('qqattachment')) { let data = mediaMessageChunk.data; @@ -298,22 +301,34 @@ export default class QQRobot implements Robot { * 获取合并转发的原消息列表 */ async getReferencedMessages(resId: string): Promise { - const res = await this.callRobotApi('/get_forward_msg', { + const res = await this.callRobotApi('get_forward_msg', { message_id: resId, }); - if (!Array.isArray(res?.messages)) { + if (!Array.isArray(res?.data?.messages)) { return null; } let messageList: CommonReceivedMessage[] = []; - for (let messageData of res.messages) { + for (let messageData of res.data.messages) { if (messageData) { - let userSender = new QQUserSender(this, messageData.sender?.user_id.toString()); + messageData.content ??= []; + + let userSender = new QQUserSender(this.wrapper, messageData.sender?.user_id.toString()); userSender.nickName = messageData.sender?.nickname; - let message = new QQPrivateMessage(userSender, this); + let message = new QQPrivateMessage(userSender, this.wrapper); + + // 生成消息ID + message.id = `ref:${userSender.userId}:${messageData.time}`; message.time = new Date(messageData.time * 1000); + // 修改回复消息的指向 + messageData.content.forEach((chunk: any) => { + if (chunk?.type === 'reply' && chunk.data?.qq && chunk.data?.time) { + chunk.data.id = `ref:${chunk.data.qq}:${chunk.data.time}`; + } + }) + message = await parseQQMessageChunk(this, messageData.content ?? [], message); messageList.push(message); diff --git a/src/robot/TelegramRobot.ts b/src/robot/adapter/TelegramRobot.ts similarity index 83% rename from src/robot/TelegramRobot.ts rename to src/robot/adapter/TelegramRobot.ts index ef0766c..34c34d6 100644 --- a/src/robot/TelegramRobot.ts +++ b/src/robot/adapter/TelegramRobot.ts @@ -1,19 +1,19 @@ import TelegramBot from "node-telegram-bot-api"; -import App from "../App"; -import { RobotConfig } from "../Config"; -import { CommonSendMessage } from "../message/Message"; -import { ChatIdentity } from "../message/Sender"; -import { CommandInfo } from "../PluginManager"; -import { Robot } from "../RobotManager"; -import { Target } from "../SubscribeManager"; -import { Utils } from "../utils/Utils"; +import App from "../../App"; +import { RobotConfig } from "../../Config"; +import { CommonSendMessage } from "../../message/Message"; +import { ChatIdentity } from "../../message/Sender"; +import { CommandInfo } from "../../PluginManager"; +import { Target } from "../../SubscribeManager"; +import { Utils } from "../../utils/Utils"; +import { RobotAdapter } from "../Robot"; export type TelegramRobotConfig = RobotConfig & { token: string; proxy?: string; } -export default class TelegramRobot implements Robot { +export default class TelegramRobot implements RobotAdapter { private app: App; public type = 'telegram'; @@ -77,11 +77,6 @@ export default class TelegramRobot implements Robot { */ } - getSession(chatIdentity: ChatIdentity, type: string) { - const sessionPath = this.app.robot.getSessionPath(chatIdentity, type); - return this.app.cache.getStore(sessionPath); - } - /** * 发送群消息 */ diff --git a/src/robot/qq/InfoProvider.ts b/src/robot/adapter/qq/InfoProvider.ts similarity index 90% rename from src/robot/qq/InfoProvider.ts rename to src/robot/adapter/qq/InfoProvider.ts index f2375b6..42a27de 100644 --- a/src/robot/qq/InfoProvider.ts +++ b/src/robot/adapter/qq/InfoProvider.ts @@ -1,10 +1,11 @@ -import App from "../../App"; -import { compareProps } from "../../utils/func"; -import QQRobot, { QQRobotConfig } from "../QQRobot"; +import App from "../../../App"; +import { compareProps } from "../../../utils/func"; import { QQGroupSender, QQUserSender } from "./Message"; -import { GroupInfoType, GroupUserInfoType, UserInfoType } from "../../message/Sender"; -import { CommonMessage } from "../../message/Message"; -import { RobotStorage } from "../../storage/RobotStorage"; +import { GroupInfoType, GroupUserInfoType, UserInfoType } from "../../../message/Sender"; +import { CommonMessage } from "../../../message/Message"; +import { RobotStorage } from "../../../storage/RobotStorage"; +import { Reactive, reactive } from "../../../utils/reactive"; +import QQRobot, { QQRobotConfig } from "../QQRobot"; export type QQGroupInfo = { groupId: string, @@ -79,11 +80,17 @@ export class QQInfoProvider { }); } - public saveMessage(message: CommonMessage) { - this.storages?.message.set(message).catch((err: any) => { - this.app.logger.error(`将消息保存到数据库出错: ${err.message}`); - console.error(err); - }) + public saveMessage(message: T): Reactive { + if (this.storages) { + this.storages.message.set(message).catch((err: any) => { + this.app.logger.error(`将消息保存到数据库出错: ${err.message}`); + console.error(err); + }); + + return this.storages.message.reactive(message); + } else { + return reactive(message); + } } async getGroupList(): Promise { @@ -152,7 +159,7 @@ export class QQInfoProvider { * @param userId * @returns */ - private getUserImage(userId: string) { + public getUserImage(userId: string) { return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`; } @@ -161,7 +168,7 @@ export class QQInfoProvider { * @param groupId * @returns */ - private getGroupImage(groupId: string) { + public getGroupImage(groupId: string) { return `https://p.qlogo.cn/gh/${groupId}/${groupId}/100` } diff --git a/src/robot/qq/Message.ts b/src/robot/adapter/qq/Message.ts similarity index 74% rename from src/robot/qq/Message.ts rename to src/robot/adapter/qq/Message.ts index af11285..2f016a0 100644 --- a/src/robot/qq/Message.ts +++ b/src/robot/adapter/qq/Message.ts @@ -1,5 +1,18 @@ -import { AttachmentMessage, CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage, EmojiMessage, ImageMessage, MentionMessage, MessageChunk, TextMessage, RecordMessage } from "../../message/Message"; -import { GroupSender, UserSender } from "../../message/Sender"; +import { Robot } from "#ibot/robot/Robot"; +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"; @@ -51,14 +64,14 @@ export interface QQAttachmentMessage extends AttachmentMessage { } export interface QQForwardingMessage extends MessageChunk { - type: ['qqforwarding']; + type: ['reference', 'qqforwarding']; data: { res_id: string; } } export class QQUserSender extends UserSender { - constructor(robot: QQRobot, userId: string) { + constructor(robot: Robot, userId: string) { super(robot, userId); this.userName = userId; } @@ -70,7 +83,7 @@ export class QQGroupSender extends GroupSender { public title?: string; public groupInfo?: QQGroupInfo; - constructor(robot: QQRobot, groupId: string, userId: string) { + constructor(robot: Robot, groupId: string, userId: string) { super(robot, groupId, userId); this.userName = userId; } @@ -176,13 +189,54 @@ export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], mess console.log('forwarding message', chunkData.data.data); if (jsonData.meta?.detail?.resid) { message.content.push({ - type: ['qqforwarding'], + type: ['reference', 'qqforwarding'], text: '[合并转发消息]', data: { res_id: jsonData.meta.detail.resid } } as QQForwardingMessage); } + break; + case 'com.tencent.miniapp_01': + if (jsonData.meta?.detail_1?.qqdocurl) { + message.content.push({ + type: ['text', 'qqurl'], + text: jsonData.meta.detail_1.qqdocurl, + data: { + url: jsonData.meta.detail_1.qqdocurl, + title: jsonData.meta.detail_1.desc, + } + } as QQUrlMessage); + } else if (jsonData.meta?.detail_1?.url) { + message.content.push({ + type: ['text', 'qqurl'], + text: jsonData.meta.detail_1.url, + data: { + url: jsonData.meta.detail_1.url, + title: jsonData.meta.detail_1.desc, + } + } as QQUrlMessage); + } + break; + case 'com.tencent.structmsg': + if (jsonData.meta) { + for (let item of Object.values(jsonData.meta)) { + if (item?.jumpUrl || item?.url) { + message.content.push({ + type: ['text', 'qqurl'], + text: item.jumpUrl ?? item.url, + data: { + url: item.jumpUrl ?? item.url, + title: item.title ?? item.desc, + } + } as QQUrlMessage); + break; + } + } + } + break; + default: + console.log('unknown message', chunkData); } } catch (_) { } } diff --git a/src/robot/qq/emojiMap.ts b/src/robot/adapter/qq/emojiMap.ts similarity index 100% rename from src/robot/qq/emojiMap.ts rename to src/robot/adapter/qq/emojiMap.ts diff --git a/src/storage/MessageStorage.ts b/src/storage/MessageStorage.ts index f7b860f..6536b51 100644 --- a/src/storage/MessageStorage.ts +++ b/src/storage/MessageStorage.ts @@ -4,6 +4,8 @@ import { ModelRegistry } from "../DatabaseManager"; import { ItemLimitedList } from "../utils/ItemLimitedList"; import { CommonMessage } from "../message/Message"; import { RobotStorage } from "./RobotStorage"; +import { Reactive, reactive } from "../utils/reactive"; +import { debounce } from "throttle-debounce"; export class MessageStorage { private app: App; @@ -31,7 +33,7 @@ export class MessageStorage { public async get(messageId: string): Promise { // from cache - let messageObj = this.cache.find((msg) => msg && msg.id === messageId); + let messageObj: CommonMessage | null | undefined = this.cache.find((msg) => msg && msg.id === messageId); if (messageObj) { return messageObj; } @@ -46,7 +48,7 @@ export class MessageStorage { const robot = this.storages.robot; if (robot) { messageObj = await robot.parseDBMessage?.(doc); - return messageObj!; + return messageObj; } else { this.app.logger.error(`无法找到机器人配置:${this.storages.robotId}`); } @@ -78,6 +80,29 @@ export class MessageStorage { this.cache.push(message); } + /** + * 将消息转换为Reactive对象(自动更新数据库) + * @param message + */ + public reactive(message: T): Reactive { + const messageRef = reactive(message); + + // debounce + const onDataChanged = debounce(this.cacheTTL * 1000, async () => { + this.app.logger.debug(`Reactive 更新消息: ${message.id}`); + this.set(message).catch((err) => { + this.app.logger.error(`更新消息 ${message.id} 失败:${err.message}`, err); + console.error(err); + }); + }); + + messageRef._on('change', (key: string, val: Reactive) => { + onDataChanged(); + }); + + return messageRef; + } + public async remove(messageId: string): Promise { if (this.models) { await this.models.userInfo.deleteOne({ diff --git a/src/storage/RobotStorage.ts b/src/storage/RobotStorage.ts index 3a64674..93c8dc1 100644 --- a/src/storage/RobotStorage.ts +++ b/src/storage/RobotStorage.ts @@ -1,7 +1,7 @@ +import { Robot } from "#ibot/robot/Robot"; 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"; diff --git a/src/utils/reactive.ts b/src/utils/reactive.ts new file mode 100644 index 0000000..3b4e4ad --- /dev/null +++ b/src/utils/reactive.ts @@ -0,0 +1,65 @@ +import EventEmitter from "events"; + +export type Reactive = T & { + _on: EventEmitter['on']; + _once: EventEmitter['once']; + _off: EventEmitter['off']; + _isReactive: true; + _value: T; + updated: () => void; +}; + +export function reactive(obj: T): Reactive { + const eventEmitter = new EventEmitter(); + + // 递归监听子对象 + for (let key of Object.getOwnPropertyNames(obj)) { + if (key.startsWith('_')) continue; + + const value = obj[key as keyof T]; + if (typeof value === 'object' && (value as Reactive<{}>)._isReactive) { + (value as Reactive<{}>)._on('change', (childKey: string, newValue: any) => { + eventEmitter.emit('change', key, value); + eventEmitter.emit(`change:${key}`, value); + }); + } + } + + return new Proxy(obj, { + set: (target, key, value) => { + if (!key.toString().startsWith('_') && typeof value === 'object') { + // 递归监听子对象 + if (!value._isReactive) { + value = reactive(value); + } + + value._on('change', (childKey: string, newValue: any) => { + eventEmitter.emit('change', key, value); + eventEmitter.emit(`change:${key.toString()}`, value); + }); + } + target[key as keyof T] = value; + return true; + }, + get: (target, key): any => { + switch (key) { + case '_on': + return eventEmitter.on.bind(eventEmitter); + case '_once': + return eventEmitter.once.bind(eventEmitter); + case '_off': + return eventEmitter.off.bind(eventEmitter); + case '_isReactive': + return true; + case '_value': + return target; + case 'updated': + return () => { + eventEmitter.emit('change', null, null); + }; + default: + return target[key as keyof T]; + } + } + }) as unknown as Reactive; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c9f2a0c..c15fed0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,8 +27,10 @@ "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": { + "#ibot/*": ["./src/*"], + }, /* 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. */