支持ESM,增加Session和Controller config

main
落雨楓 2 years ago
parent f7e53aa87f
commit 5a08de26d9

1
.gitignore vendored

@ -3,3 +3,4 @@ subscribe.yml
node_modules/ node_modules/
dist/ dist/
*.zip *.zip
configs/

@ -1,7 +1,4 @@
const Promise = require('bluebird'); import 'node-telegram-bot-api';
Promise.config({ import App from './dist/App';
cancellation: true,
});
var App = require('./dist/App').default; new App("./config.yml");
new App(__dirname + "/config.yml");

1334
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -3,9 +3,10 @@
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module",
"scripts": { "scripts": {
"start": "node index.js", "start": "node --es-module-specifier-resolution=node index.js",
"dev": "tsc && node index.js", "dev": "tsc && node --es-module-specifier-resolution=node index.js",
"build": "tsc" "build": "tsc"
}, },
"author": { "author": {
@ -16,8 +17,13 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node-telegram-bot-api": "^0.57.1", "@types/node-telegram-bot-api": "^0.57.1",
"@waylaidwanderer/chatgpt-api": "file:../node-chatgpt-api",
"cache-manager": "^5.1.7",
"cache-manager-ioredis-yet": "^1.1.0",
"cache-manager-redis-yet": "^4.1.1",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"decoders": "^1.25.3", "decoders": "^1.25.3",
"got": "^11.8.3",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"koa": "^2.13.4", "koa": "^2.13.4",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",
@ -28,18 +34,15 @@
"node-telegram-bot-api": "^0.58.0", "node-telegram-bot-api": "^0.58.0",
"pusher": "^3.0.1", "pusher": "^3.0.1",
"pusher-js": "^5.1.1", "pusher-js": "^5.1.1",
"request": "^2.88.2",
"request-promise": "^4.2.5",
"throttle-debounce": "^3.0.1", "throttle-debounce": "^3.0.1",
"winston": "^3.8.2",
"yaml": "^1.8.3" "yaml": "^1.8.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bluebird-global": "^3.5.13",
"@types/koa": "^2.13.4", "@types/koa": "^2.13.4",
"@types/koa-router": "^7.4.4", "@types/koa-router": "^7.4.4",
"@types/micromatch": "^4.0.2", "@types/micromatch": "^4.0.2",
"@types/node": "^17.0.8", "@types/node": "^17.0.8",
"@types/request-promise": "^4.1.48",
"@types/throttle-debounce": "^2.1.0", "@types/throttle-debounce": "^2.1.0",
"@types/yaml": "^1.9.7", "@types/yaml": "^1.9.7",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",

@ -1,27 +1,33 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import winston from 'winston';
import Yaml from 'yaml'; import Yaml from 'yaml';
import { BaseProvider, MultipleMessage } from './base/provider/BaseProvider'; import { fileURLToPath } from 'url';
import path from 'path';
import { BaseProvider, MultipleMessage } from './base/provider/BaseProvider';
import { Setup } from './Setup';
import { ChannelManager } from './ChannelManager'; import { ChannelManager } from './ChannelManager';
import { ChannelConfig, Config } from './Config'; import { ChannelConfig, Config } from './Config';
import { EventManager } from './EventManager'; import { EventManager } from './EventManager';
import { CommonSendMessage } from './message/Message';
import { PluginManager } from './PluginManager'; import { PluginManager } from './PluginManager';
import { ProviderManager } from './ProviderManager'; import { ProviderManager } from './ProviderManager';
import { RestfulApiManager } from './RestfulApiManager'; import { RestfulApiManager } from './RestfulApiManager';
import { RobotManager } from './RobotManager'; import { RobotManager } from './RobotManager';
import { Service, ServiceManager } from './ServiceManager'; import { Service, ServiceManager } from './ServiceManager';
import { Setup } from './Setup';
import { SubscribeManager, Target } from './SubscribeManager'; import { SubscribeManager, Target } from './SubscribeManager';
import { SessionManager } from './SessionManager';
export default class App { export default class App {
public config: Config; public config: Config;
public srcPath: string = __dirname;
public srcPath: string = path.dirname(fileURLToPath(import.meta.url));
public basePath: string = path.dirname(this.srcPath);
public debug: boolean = false; public debug: boolean = false;
public logger!: winston.Logger;
public event!: EventManager; public event!: EventManager;
public session!: SessionManager;
public robot!: RobotManager; public robot!: RobotManager;
public provider!: ProviderManager; public provider!: ProviderManager;
public service!: ServiceManager; public service!: ServiceManager;
@ -33,7 +39,7 @@ export default class App {
constructor(configFile: string) { constructor(configFile: string) {
this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' })); this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' }));
this.debug = this.config.debug; this.debug = this.config.debug;
this.initialize(); this.initialize();
} }
@ -41,17 +47,56 @@ export default class App {
await this.initModules(); await this.initModules();
await this.initRestfulApiManager(); await this.initRestfulApiManager();
await this.initEventManager(); await this.initEventManager();
await this.initSessionManager();
await this.initRobot(); await this.initRobot();
await this.initProviderManager(); await this.initProviderManager();
await this.initServiceManager(); await this.initServiceManager();
await this.initSubscribeManager(); await this.initSubscribeManager();
await this.initChannelManager(); await this.initChannelManager();
await this.initPluginManager(); await this.initPluginManager();
console.log('初始化完成,正在接收消息');
this.logger.info('初始化完成,正在接收消息');
} }
async initModules() { async initModules() {
await Setup.initHandlebars(); await Setup.initHandlebars();
// 创建Logger
const loggerFormat = winston.format.printf(({ level, message, timestamp }) => {
return `${timestamp} [${level}]: ${message}`;
});
this.logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
});
if (this.debug) {
this.logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.colorize(),
winston.format.simple(),
loggerFormat,
winston.format.metadata()
),
level: 'debug'
})
);
} else {
this.logger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.colorize(),
winston.format.simple(),
loggerFormat
),
level: 'info',
})
);
}
} }
async initRestfulApiManager() { async initRestfulApiManager() {
@ -64,6 +109,11 @@ export default class App {
await this.event.initialize(); await this.event.initialize();
} }
async initSessionManager() {
this.session = new SessionManager(this, this.config.session);
await this.session.initialize();
}
async initRobot() { async initRobot() {
this.robot = new RobotManager(this, this.config.robot); this.robot = new RobotManager(this, this.config.robot);
await this.robot.initialize(); await this.robot.initialize();
@ -91,7 +141,7 @@ export default class App {
} }
async initPluginManager() { async initPluginManager() {
this.plugin = new PluginManager(this, this.config.plugin_path); this.plugin = new PluginManager(this, this.config.plugin_path, this.config.plugin_config_path);
await this.plugin.initialize(); await this.plugin.initialize();
} }
@ -119,11 +169,7 @@ export default class App {
* @returns * @returns
*/ */
async sendPushMessage(channelId: string, messages: MultipleMessage): Promise<void> { async sendPushMessage(channelId: string, messages: MultipleMessage): Promise<void> {
console.log(`[${channelId}] 消息: `, messages); this.logger.info(`[${channelId}] 消息: `, messages);
this.robot.sendPushMessage(channelId, messages); this.robot.sendPushMessage(channelId, messages);
} }
require(file: string): any {
return require(path.join(this.srcPath, file));
}
} }

@ -96,7 +96,6 @@ export class ChannelManager extends EventEmitter {
this.channelName[channelId] = config?.name; this.channelName[channelId] = config?.name;
} else { } else {
if (BaseProvider.checkConfig(config)) { if (BaseProvider.checkConfig(config)) {
// console.log(`正在加载Channel: ${channelId}`);
// 处理channel // 处理channel
let providerName = this.getProviderName(config); let providerName = this.getProviderName(config);
let isReload = false; let isReload = false;
@ -114,10 +113,10 @@ export class ChannelManager extends EventEmitter {
this.channelName[channelId] = config?.name; this.channelName[channelId] = config?.name;
if (isReload) { if (isReload) {
this.emit('reload', channelId); this.emit('reload', channelId);
console.log(`已重载Channel: ${channelId}`); this.app.logger.info(`已重载Channel: ${channelId}`);
} else { } else {
this.emit('add', channelId); this.emit('add', channelId);
console.log(`已加载Channel: ${channelId}`); this.app.logger.info(`已加载Channel: ${channelId}`);
} }
} }
} else { } else {
@ -148,7 +147,7 @@ export class ChannelManager extends EventEmitter {
delete this.channels[channelId]; delete this.channels[channelId];
delete this.channelName[channelId]; delete this.channelName[channelId];
this.emit('remove', channelId); this.emit('remove', channelId);
console.log("已移除Channel: ", this.getChannelFullName(channelId)); this.app.logger.info("已移除Channel: ", this.getChannelFullName(channelId));
} }
} }
} }

@ -3,19 +3,25 @@ import { RegexFilterConfig } from "./generator/RegexFilter";
export type Config = { export type Config = {
channel_config_path: string; channel_config_path: string;
plugin_config_path: string;
plugin_path: string; plugin_path: string;
subscribe_config: string; subscribe_config: string;
debug: boolean; debug: boolean;
robot: Record<string, RobotConfig>; robot: Record<string, RobotConfig>;
service: Record<string, ServiceConfig>; service: Record<string, ServiceConfig>;
session: SessionConfig;
http_api: RestfulApiConfig; http_api: RestfulApiConfig;
command_override: CommandOverrideConfig; command_override?: CommandOverrideConfig;
focused_as_command: true; focused_as_command?: true;
robot_description?: string;
}; };
export type RobotConfig = { export type RobotConfig = {
type: string; type: string;
baseId: string; baseId: string;
description?: string;
}; };
export type RestfulApiConfig = { export type RestfulApiConfig = {
@ -26,6 +32,17 @@ export type RestfulApiConfig = {
export type ServiceConfig = { [name: string]: any }; export type ServiceConfig = { [name: string]: any };
export type SessionConfig = {
type?: 'memory' | 'redis',
redis?: {
host?: string,
port?: number,
password?: string,
db?: number,
}
ttl?: number
};
export type ChannelConfig = any; export type ChannelConfig = any;
export type GeneratorConfig = { export type GeneratorConfig = {

@ -1,34 +1,57 @@
import App from "./App"; import App from "./App";
import { CommandOverrideConfig } from "./Config";
import { CommonReceivedMessage, CommonSendMessage } from "./message/Message"; import { CommonReceivedMessage, CommonSendMessage } from "./message/Message";
import { CommandInfo, ControllerSubscribeSource, MessageEventOptions, MessagePriority, PluginController, PluginEvent } from "./PluginManager"; import { SenderIdentity } from "./message/Sender";
import { CommandInfo, EventScope, MessageEventOptions, MessagePriority, PluginEvent } from "./PluginManager";
import { Robot } from "./RobotManager"; import { Robot } from "./RobotManager";
export type PluginControllerListenerInfo = { export type ControllerEventInfo = {
priority: number; priority: number;
callback: CallableFunction; callback: CallableFunction;
controllerEvent: PluginEvent; eventScope: PluginEvent;
} }
export type PluginControllerCommandInfo = { export type SessionEventInfo = {
activeTime: Date;
eventScope: EventScope;
}
export type ControllerCommandInfo = {
commandInfo: CommandInfo; commandInfo: CommandInfo;
controllerEvent: PluginEvent; eventScope: PluginEvent;
} }
export class EventManager { export class EventManager {
private app: App; private app: App;
/** 事件排序的debounce */
private eventSortDebounce: Record<string, NodeJS.Timeout> = {}; private eventSortDebounce: Record<string, NodeJS.Timeout> = {};
private eventList: Record<string, PluginControllerListenerInfo[]> = {};
private commandList: Record<string, PluginControllerCommandInfo> = {}; /** 全局事件列表 */
private eventList: Record<string, ControllerEventInfo[]> = {};
/** 会话事件列表 */
private sessionEventList: Record<string, EventScope> = {};
/** 全局指令列表 */
private commandList: Record<string, ControllerCommandInfo> = {};
/** 指令信息 */
private commandInfoList: ControllerCommandInfo[] = [];
/** 指令信息覆盖配置 */
private commandOverride: CommandOverrideConfig;
constructor(app: App) { constructor(app: App) {
this.app = app; this.app = app;
this.commandOverride = app.config.command_override ?? {};
} }
public async initialize() { public async initialize() {
} }
public on(event: string, controllerEvent: PluginEvent, callback: CallableFunction, options?: MessageEventOptions) { public on(event: string, eventScope: PluginEvent, callback: CallableFunction, options?: MessageEventOptions) {
if (!(event in this.eventList)) { if (!(event in this.eventList)) {
this.eventList[event] = []; this.eventList[event] = [];
} }
@ -48,7 +71,7 @@ export class EventManager {
const eventInfo = { const eventInfo = {
callback: callback, callback: callback,
priority: options.priority!, priority: options.priority!,
controllerEvent eventScope
}; };
this.eventList[event].push(eventInfo); this.eventList[event].push(eventInfo);
@ -56,62 +79,73 @@ export class EventManager {
this.sortEvent(event); this.sortEvent(event);
} }
public off(event: string, controllerEvent: PluginEvent, callback: CallableFunction): void public off(event: string, eventScope: PluginEvent, callback: CallableFunction): void
public off(controllerEvent: PluginEvent): void public off(eventScope: PluginEvent): void
public off(...args: any): void { public off(...args: any): void {
if (typeof args[0] === 'string') { if (typeof args[0] === 'string') {
let [event, controller, callback] = args; let [event, controller, callback] = args;
if (Array.isArray(this.eventList[event])) { if (Array.isArray(this.eventList[event])) {
this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.callback !== callback || eventInfo.controllerEvent !== controller); this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.callback !== callback || eventInfo.eventScope !== controller);
} }
} else if (typeof args[0] !== 'undefined') { } else if (typeof args[0] !== 'undefined') {
let controller = args[0]; let controller = args[0];
for (let event in this.eventList) { for (let event in this.eventList) {
this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.controllerEvent !== controller); this.eventList[event] = this.eventList[event].filter((eventInfo) => eventInfo.eventScope !== controller);
} }
} }
} }
public addCommand(commandInfo: CommandInfo, controllerEvent: PluginEvent) { public addCommand(commandInfo: CommandInfo, eventScope: PluginEvent) {
// 如果配置了Command覆盖则覆盖原本的指令设置
if (commandInfo.command in this.commandOverride) {
commandInfo = {
...commandInfo,
...this.commandOverride[commandInfo.command]
};
}
let data = { let data = {
commandInfo, commandInfo,
controllerEvent: controllerEvent eventScope: eventScope
}; };
this.commandList[commandInfo.command] = data; this.commandInfoList.push(data);
this.commandList[commandInfo.command.toLocaleLowerCase()] = data;
if (Array.isArray(commandInfo.alias)) { if (Array.isArray(commandInfo.alias)) {
commandInfo.alias.forEach((alias) => { commandInfo.alias.forEach((alias) => {
this.commandList[alias] = data; this.commandList[alias.toLocaleLowerCase()] = data;
}); });
} }
} }
public removeCommand(commandInfo: CommandInfo): void public removeCommand(commandInfo: CommandInfo): void
public removeCommand(controllerEvent: PluginEvent): void public removeCommand(eventScope: PluginEvent): void
public removeCommand(...args: any): void { public removeCommand(...args: any): void {
if ('command' in args[0]) { if ('command' in args[0]) {
let commandInfo: CommandInfo = args[0]; let commandInfo: CommandInfo = args[0];
delete this.commandList[commandInfo.command]; this.commandInfoList = this.commandInfoList.filter((commandInfoItem) => commandInfoItem.commandInfo !== commandInfo);
delete this.commandList[commandInfo.command.toLocaleLowerCase()];
if (Array.isArray(commandInfo.alias)) { if (Array.isArray(commandInfo.alias)) {
commandInfo.alias.forEach((alias) => { commandInfo.alias.forEach((alias) => {
delete this.commandList[alias]; delete this.commandList[alias.toLocaleLowerCase()];
}); });
} }
} else if (typeof args[0] !== 'undefined') { } else if (typeof args[0] !== 'undefined') {
let controllerEvent = args[0]; let eventScope = args[0];
this.commandInfoList = this.commandInfoList.filter((commandInfoItem) => commandInfoItem.eventScope !== eventScope);
for (let command in this.commandList) { for (let command in this.commandList) {
if (this.commandList[command].controllerEvent.controller?.id === controllerEvent.controller?.id) { if (this.commandList[command].eventScope.controller?.id === eventScope.controller?.id) {
delete this.commandList[command]; delete this.commandList[command];
} }
} }
} }
} }
public async emit(eventName: string, senderInfo?: ControllerSubscribeSource | null, ...args: any[]) { public async emit(eventName: string, senderInfo?: SenderIdentity | null, ...args: any[]) {
if (this.app.debug) { if (this.app.debug) {
if (args[0] instanceof CommonReceivedMessage) { if (args[0] instanceof CommonReceivedMessage) {
console.log(`[DEBUG] 触发事件 ${eventName} ${args[0].contentText}`); this.app.logger.debug(`触发事件 ${eventName} ${args[0].contentText}`);
} else { } else {
console.log(`[DEBUG] 触发事件 ${eventName}`); this.app.logger.debug(`触发事件 ${eventName}`);
} }
} }
@ -126,40 +160,46 @@ export class EventManager {
isResolved = true; isResolved = true;
}; };
let subscribeList: string[] = []; let [subscribedControllers, disabledControllers] = this.getControllerSubscribe(senderInfo);
if (senderInfo) {
// 获取订阅列表
let targetType = '';
let targetId = '';
switch (senderInfo.type) {
case 'private':
targetType = 'user';
targetId = senderInfo.userId!;
break;
case 'group':
targetType = 'group';
targetId = senderInfo.groupId!;
break;
case 'channel':
targetType = 'channel';
targetId = senderInfo.channelId!;
break;
}
subscribeList = this.app.subscribe.getSubscribedList(senderInfo.robot.robotId!, targetType, targetId, 'controller');
}
for (let eventInfo of eventList) { for (let eventInfo of eventList) {
if (!isFilter && senderInfo) { if (!isFilter && senderInfo) {
if (eventInfo.controllerEvent.autoSubscribe) { switch (senderInfo.type) {
if (!eventInfo.controllerEvent.isAllowSubscribe(senderInfo)) { case 'private':
continue; if (!eventInfo.eventScope.allowPrivate) {
continue;
}
if (!eventInfo.eventScope.isAllowSubscribe(senderInfo)) {
continue;
}
break;
case 'group':
if (!eventInfo.eventScope.allowGroup) {
continue;
}
break;
case 'channel':
if (!eventInfo.eventScope.allowChannel) {
continue;
}
break;
}
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
if (eventInfo.eventScope.autoSubscribe) {
if (!eventInfo.eventScope.isAllowSubscribe(senderInfo)) {
continue;
} else {
// 检测控制器是否已禁用
if (!eventInfo.eventScope.controller || disabledControllers.includes(eventInfo.eventScope.controller.id)) {
continue;
}
}
} else { } else {
// 需要添加订阅检测 // 检测控制器是否已启用
} if (!eventInfo.eventScope.controller || !subscribedControllers.includes(eventInfo.eventScope.controller.id)) {
} else if (senderInfo.type !== 'private') { continue;
if (!eventInfo.controllerEvent.controller || !subscribeList.includes(eventInfo.controllerEvent.controller.id)) { }
continue;
} }
} }
} }
@ -170,7 +210,7 @@ export class EventManager {
break; break;
} }
} catch(err: any) { } catch(err: any) {
console.error(`事件 ${eventName} 处理失败`); this.app.logger.error(`事件 ${eventName} 处理失败: `, err);
console.error(err); console.error(err);
} }
} }
@ -206,7 +246,7 @@ export class EventManager {
// 尝试识别空格分隔的指令 // 尝试识别空格分隔的指令
if (contentText.includes(' ')) { if (contentText.includes(' ')) {
command = contentText.split(' ')[0]; command = contentText.split(' ')[0].toLocaleLowerCase();
args = contentText.substring(command.length + 1); args = contentText.substring(command.length + 1);
if (!(command in this.commandList)) { if (!(command in this.commandList)) {
@ -232,7 +272,7 @@ export class EventManager {
} }
if (this.app.debug) { if (this.app.debug) {
console.log('[DEBUG] 指令识别结果', command, args); this.app.logger.debug('指令识别结果', command, args);
} }
return await this.emit(`command/${command}`, this.getSenderInfo(message), args, message); return await this.emit(`command/${command}`, this.getSenderInfo(message), args, message);
@ -257,7 +297,7 @@ export class EventManager {
} }
public getSenderInfo(message: CommonReceivedMessage): ControllerSubscribeSource { public getSenderInfo(message: CommonReceivedMessage): SenderIdentity {
if (message.origin === 'private') { if (message.origin === 'private') {
return { return {
type: 'private', type: 'private',
@ -279,6 +319,37 @@ export class EventManager {
} }
} }
public getControllerSubscribe(senderInfo?: SenderIdentity | null): [string[], string[]] {
let subscribedCommands: string[] = [];
let disabledCommands: string[] = [];
if (senderInfo) {
let targetType = '';
let targetId = '';
switch (senderInfo.type) {
case 'private':
targetType = 'user';
targetId = senderInfo.userId!;
break;
case 'group':
targetType = 'group';
targetId = senderInfo.groupId!;
break;
case 'channel':
targetType = 'channel';
targetId = senderInfo.channelId!;
break;
}
subscribedCommands = this.app.subscribe.getSubscribedList(senderInfo.robot.robotId!, targetType, targetId, 'controller');
disabledCommands = this.app.subscribe.getSubscribedList(senderInfo.robot.robotId!, targetType, targetId, 'disable_controller');
}
return [
subscribedCommands,
disabledCommands
];
}
private sortEvent(eventName: string) { private sortEvent(eventName: string) {
if (this.eventSortDebounce[eventName]) { if (this.eventSortDebounce[eventName]) {
return; return;

@ -1,21 +1,27 @@
import { EventManager } from "./EventManager"; import { EventManager } from "./EventManager";
import { CommonReceivedMessage } from "./message/Message"; import { CommonReceivedMessage } from "./message/Message";
import { Robot } from "./RobotManager"; import { Robot } from "./RobotManager";
import fs from 'fs';
import fsAsync from 'fs/promises';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import Yaml from 'yaml';
import App from "./App"; import App from "./App";
import EventEmitter from "events"; import EventEmitter from "events";
import path from "path"; import path from "path";
import { SenderIdentity } from "./message/Sender";
import { Utils } from "./utils/Utils";
export const MessagePriority = { export const MessagePriority = {
LOWEST: 0, LOWEST: 0,
LOW: 20, LOW: 20,
DEFAULT: 40, DEFAULT: 40,
HIGH: 50,
/** /**
* *
* *
*/ */
TEMP_HANDLER: 60, SESSION_HANDLER: 60,
HIGH: 80, HIGHER: 70,
/** /**
* *
*/ */
@ -48,29 +54,26 @@ export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction
export type AllowedList = string[] | '*'; export type AllowedList = string[] | '*';
export type ControllerSubscribeSource = {
type: 'private' | 'group' | 'channel' | 'raw' | string,
robot: Robot,
groupId?: string,
userId?: string,
channelId?: string,
}
export class PluginManager extends EventEmitter { export class PluginManager extends EventEmitter {
private app: App; private app: App;
private pluginPath: string; private pluginPath: string;
private configPath: string;
private watcher!: chokidar.FSWatcher; private watcher!: chokidar.FSWatcher;
private configWatcher!: chokidar.FSWatcher;
public controllers: Record<string, PluginController>; public controllers: Record<string, PluginController>;
public fileControllers: Record<string, PluginController>; public fileControllers: Record<string, PluginController>;
public configControllers: Record<string, PluginController>;
constructor(app: App, pluginPath: string) { constructor(app: App, pluginPath: string, configPath: string) {
super(); super();
this.app = app; this.app = app;
this.pluginPath = pluginPath; this.pluginPath = path.resolve(pluginPath);
this.configPath = path.resolve(configPath);
this.controllers = {}; this.controllers = {};
this.fileControllers = {}; this.fileControllers = {};
this.configControllers = {};
} }
/** /**
@ -82,10 +85,15 @@ export class PluginManager extends EventEmitter {
ignorePermissionErrors: true, ignorePermissionErrors: true,
persistent: true persistent: true
}); });
this.watcher.on('add', this.loadController.bind(this)); this.watcher.on('add', this.loadController.bind(this));
this.watcher.on('change', this.loadController.bind(this)); this.watcher.on('change', this.loadController.bind(this));
this.watcher.on('unlink', this.removeController.bind(this)); this.watcher.on('unlink', this.removeController.bind(this));
this.configWatcher = chokidar.watch(this.configPath + '/**/*.yml', {
ignorePermissionErrors: true,
persistent: true
});
this.configWatcher.on('change', this.reloadConfig.bind(this));
} }
async loadController(file: string) { async loadController(file: string) {
@ -111,16 +119,19 @@ export class PluginManager extends EventEmitter {
this.fileControllers[file] = controllerInstance; this.fileControllers[file] = controllerInstance;
if (isReload) { if (isReload) {
console.log(`已重新加载Controller: ${file}`); this.app.logger.info(`已重新加载Controller: ${file}`);
this.emit('controllerReloaded', controllerInstance); this.emit('controllerReloaded', controllerInstance);
} else { } else {
console.log(`已加载Controller: ${file}`); this.app.logger.info(`已加载Controller: ${file}`);
this.emit('controllerLoaded', controllerInstance); this.emit('controllerLoaded', controllerInstance);
} }
const pluginMisc = new PluginEvent(this.app.event); const pluginEvent = new PluginEvent(this.app);
controllerInstance.event = pluginMisc; controllerInstance.event = pluginEvent;
await controllerInstance.initialize();
const controllerConfig = await this.loadControllerConfig('standalone', controllerInstance);
await controllerInstance.initialize(controllerConfig);
} else { } else {
throw new Error('PluginController ID is not defined.'); throw new Error('PluginController ID is not defined.');
} }
@ -136,18 +147,147 @@ export class PluginManager extends EventEmitter {
async removeController(file: string, isReload = false) { async removeController(file: string, isReload = false) {
const controller = this.fileControllers[file]; const controller = this.fileControllers[file];
if (controller) { if (controller) {
const configFile = this.getConfigFile('standalone', controller);
await controller.event.destroy(); await controller.event.destroy();
await controller.destroy?.(); await controller.destroy?.();
delete this.controllers[file]; delete this.controllers[file];
delete this.fileControllers[file]; delete this.fileControllers[file];
if (configFile in this.configControllers) {
delete this.configControllers[configFile];
}
this.emit('controllerRemoved', controller); this.emit('controllerRemoved', controller);
if (!isReload) { if (!isReload) {
console.log(`已移除Controller: ${controller.id}`); this.app.logger.info(`已移除Controller: ${controller.id}`);
} }
} }
} }
getConfigFile(pluginId: string, controller: PluginController) {
return path.resolve(this.configPath, pluginId, controller.id + '.yml');
}
async loadControllerConfig(pluginId: string, controller: PluginController) {
const configFile = this.getConfigFile(pluginId, controller);
try {
if (configFile in this.configControllers) { // 防止保存时触发重载
delete this.configControllers[configFile];
}
const defaultConfig = await controller.getDefaultConfig?.() ?? {};
let config: any = defaultConfig;
let shouldFill: boolean = false;
if (fs.existsSync(configFile)) {
let localConfig = Yaml.parse(await fsAsync.readFile(configFile, 'utf-8'));
config = {...defaultConfig, ...localConfig};
if (!Utils.compare(config, localConfig)) {
shouldFill = true;
this.app.logger.info(`配置文件已生成: ${configFile}`);
}
} else {
shouldFill = true;
}
if (shouldFill) {
Utils.prepareDir(path.dirname(configFile));
await fsAsync.writeFile(configFile, Yaml.stringify(config));
}
setTimeout(() => {
this.configControllers[configFile] = controller;
}, 1000);
return config;
} catch(err: any) {
this.app.logger.error(`加载Controller配置失败: ${configFile}`, err);
console.error(err);
}
}
async reloadConfig(file: string) {
this.app.logger.info(`配置文件已更新: ${file}`);
if (file in this.configControllers) {
try {
const controller = this.configControllers[file];
if (controller.updateConfig) { // 如果控制器支持重载配置,则直接调用
const localConfig = Yaml.parse(await fsAsync.readFile(file, 'utf-8'));
await controller.updateConfig(localConfig);
this.app.logger.info(`已重载Controller配置: ${controller.id}`);
} else { // 重载整个控制器
let controllerFile: string = '';
for (let [file, c] of Object.entries(this.fileControllers)) {
if (c === controller) {
controllerFile = file;
break;
}
}
if (controllerFile) {
await this.loadController(controllerFile);
}
}
} catch(err: any) {
this.app.logger.error(`重载Controller配置失败: ${file}`, err);
console.error(err);
}
}
}
/**
*
* @param senderInfo
* @returns
*/
public getSubscribedControllers(senderInfo: SenderIdentity): PluginController[] {
let [subscribedControllers, disabledControllers] = this.app.event.getControllerSubscribe(senderInfo);
return Object.values(this.controllers).filter((controller) => {
if (controller.event.commandList.length === 0) return false;
switch (senderInfo.type) {
case 'private':
if (!controller.event.allowPrivate) {
return false;
}
if (!controller.event.isAllowSubscribe(senderInfo)) {
return false;
}
break;
case 'group':
if (!controller.event.allowGroup) {
return false;
}
break;
case 'channel':
if (!controller.event.allowChannel) {
return false;
}
break;
}
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
if (controller.event.autoSubscribe) {
if (!controller.event.isAllowSubscribe(senderInfo)) {
return false;
} else {
// 检测控制器是否已禁用
if (disabledControllers.includes(controller.id)) {
return false;
}
}
} else {
// 检测控制器是否已启用
if (!subscribedControllers.includes(controller.id)) {
return false;
}
}
}
return true;
});
}
} }
export interface PluginController { export interface PluginController {
@ -157,55 +297,24 @@ export interface PluginController {
event: PluginEvent; event: PluginEvent;
initialize: () => Promise<void>; initialize: (config: any) => Promise<void>;
destroy?: () => Promise<void>; destroy?: () => Promise<void>;
}
export class PluginEvent {
private eventManager: EventManager;
public controller?: PluginController;
public autoSubscribe = false;
public forceSubscribe = false;
public showInSubscribeList = true;
public allowPrivate = true; getDefaultConfig?: () => Promise<any>;
public allowGroup = true; updateConfig?: (config: any) => Promise<void>;
public allowChannel = true; }
public allowedRobotTypeList: AllowedList = '*';
private commandList: CommandInfo[] = [];
private eventList: Record<string, EventListenerInfo[]> = {};
constructor(eventManager: EventManager) { export class EventScope {
this.eventManager = eventManager; protected app: App;
} protected eventManager: EventManager;
public isAllowSubscribe: (source: ControllerSubscribeSource) => boolean = (source) => { public commandList: CommandInfo[] = [];
if (this.allowedRobotTypeList !== '*' && !this.allowedRobotTypeList.includes(source.robot.type)) { public eventList: Record<string, EventListenerInfo[]> = {};
return false; public eventSorted: Record<string, boolean> = {};
}
switch (source.type) { constructor(app: App) {
case 'private': this.app = app;
if (!this.allowPrivate) { this.eventManager = app.event;
return false;
}
break;
case 'group':
if (!this.allowGroup) {
return false;
}
break;
case 'channel':
if (!this.allowChannel) {
return false;
}
break;
}
return true;
} }
/** /**
@ -295,8 +404,9 @@ export class PluginEvent {
priority: options.priority! priority: options.priority!
}; };
this.eventList[event].push(eventInfo); this.eventList[event].push(eventInfo);
this.eventSorted[event] = false;
this.eventManager.on(event, this, callback, options);
this.afterAddEventListener(event, callback, options);
} }
public off(event: string, callback: CallableFunction): void { public off(event: string, callback: CallableFunction): void {
@ -306,17 +416,58 @@ export class PluginEvent {
}); });
} }
this.eventManager.off(event, this, callback); this.afterRemoveEventListener(event, callback);
}
/**
* Trigger event.
* @param event Event name
* @param args Arguments
* @returns
*/
public async emit(event: string, ...args: any[]) {
let isResolved = false;
const resolved = () => {
isResolved = true;
};
if (event in this.eventList) {
if (!this.eventSorted[event]) { // 如果事件未排序,触发排序
this.eventList[event].sort((a, b) => {
return a.priority - b.priority;
});
}
for (const eventInfo of this.eventList[event]) {
try {
await eventInfo.callback(...args, resolved);
if (isResolved) {
break;
}
} catch (err) {
this.app.logger.error(err);
}
}
}
return isResolved;
} }
/** /**
* Register command. * Register command.
* @param command * @param command Command
* @param name * @param name Command name
* @param callback * @param callback Callback function
* @param options * @param options Command options
*/ */
public registerCommand(command: string, name: string, callback: CommandCallback, options?: MessageEventOptions): void public registerCommand(command: string, name: string, callback: CommandCallback, options?: MessageEventOptions): void
/**
* Register command.
* @param commandInfo Command info
* @param callback Callback function
* @param options Command options
*/
public registerCommand(commandInfo: CommandInfo, callback: CommandCallback, options?: MessageEventOptions): void public registerCommand(commandInfo: CommandInfo, callback: CommandCallback, options?: MessageEventOptions): void
public registerCommand(...args: any[]): void { public registerCommand(...args: any[]): void {
// 处理传入参数 // 处理传入参数
@ -341,18 +492,62 @@ export class PluginEvent {
this.on(`command/${commandInfo.command}`, callback, options); this.on(`command/${commandInfo.command}`, callback, options);
if (Array.isArray(commandInfo.alias)) { // Add event for alias if (Array.isArray(commandInfo.alias)) { // Add event for alias
commandInfo.alias.forEach((cmd) => { commandInfo.alias.forEach((cmd) => {
this.on(`command/${cmd}`, callback, options); this.on(`command/${cmd.toLocaleLowerCase()}`, callback, options);
}); });
} }
this.afterAddCommand(commandInfo as any);
}
protected afterAddEventListener(event: string, callback: CallableFunction, options?: MessageEventOptions): void {
this.eventManager.addCommand(commandInfo as any, this);
} }
/** protected afterRemoveEventListener(event: string, callback: CallableFunction): void { }
* Initialize plugin controller.
*/ protected afterAddCommand(commandInfo: CommandInfo): void { }
public async initialize() { }
export class PluginEvent extends EventScope {
public controller?: PluginController;
public autoSubscribe = false;
public forceSubscribe = false;
public showInSubscribeList = true;
public allowPrivate = true;
public allowGroup = true;
public allowChannel = true;
public allowedRobotTypeList: AllowedList = '*';
public isAllowSubscribe: (source: SenderIdentity) => boolean = (source) => {
if (this.allowedRobotTypeList !== '*' && !this.allowedRobotTypeList.includes(source.robot.type)) {
return false;
}
switch (source.type) {
case 'private':
if (!this.allowPrivate) {
return false;
}
break;
case 'group':
if (!this.allowGroup) {
return false;
}
break;
case 'channel':
if (!this.allowChannel) {
return false;
}
break;
}
return true;
}
public init(controller: PluginController) {
this.controller = controller;
} }
/** /**
@ -365,4 +560,16 @@ export class PluginEvent {
this.eventList = {}; this.eventList = {};
} }
protected override afterAddEventListener(event: string, callback: CallableFunction, options?: MessageEventOptions): void {
this.eventManager.on(event, this, callback, options);
}
protected override afterRemoveEventListener(event: string, callback: CallableFunction): void {
this.eventManager.off(event, this, callback);
}
protected override afterAddCommand(commandInfo: CommandInfo): void {
this.eventManager.addCommand(commandInfo as any, this);
}
} }

@ -4,8 +4,6 @@ import App from './App';
import { BaseProvider } from './base/provider/BaseProvider'; import { BaseProvider } from './base/provider/BaseProvider';
import { ChannelConfig } from './Config'; import { ChannelConfig } from './Config';
const PROVIDER_PATH = __dirname + "/provider";
export class ProviderManager { export class ProviderManager {
private app: App; private app: App;
private providerClasses: { [key: string]: any } private providerClasses: { [key: string]: any }
@ -16,20 +14,22 @@ export class ProviderManager {
} }
async initialize() { async initialize() {
const PROVIDER_PATH = path.join(this.app.srcPath, "provider");
for (let file of fs.readdirSync(PROVIDER_PATH)) { for (let file of fs.readdirSync(PROVIDER_PATH)) {
let providerFile = `${PROVIDER_PATH}/${file}`; let providerFile = `${PROVIDER_PATH}/${file}`;
if (providerFile.match(/\.(js|mjs)$/)) { if (providerFile.match(/\.(js|mjs)$/)) {
// 加载js文件 // 加载js文件
let providerName = path.basename(providerFile).replace(/Provider\.(js|mjs)$/gi, "").toLocaleLowerCase(); let providerName = path.basename(providerFile).replace(/Provider\.(js|mjs)$/gi, "").toLocaleLowerCase();
try { try {
let provider = require(providerFile)?.default; let provider = await import(providerFile);
if (!provider) { if (!provider || !provider.default) {
throw new Error("provider is empty"); throw new Error("provider is empty");
} }
this.providerClasses[providerName] = provider; this.providerClasses[providerName] = provider.default;
console.log(`已加载Provider: ${providerName}`); this.app.logger.info(`已加载Provider: ${providerName}`);
} catch(err) { } catch(err) {
console.log(`无法加载Provider: ${providerName}`, err); this.app.logger.info(`无法加载Provider: ${providerName}`, err);
} }
} }
} }

@ -3,7 +3,7 @@ import Koa from 'koa';
import { RestfulApiConfig } from "./Config"; import { RestfulApiConfig } from "./Config";
import Router from "koa-router"; import Router from "koa-router";
import { makeRoutes } from "./restful/routes"; import { makeRoutes } from "./restful/routes";
import koaBody from "koa-body"; import { koaBody } from "koa-body";
export interface RestfulContext { export interface RestfulContext {
feedbot: App, feedbot: App,
@ -46,7 +46,7 @@ export class RestfulApiManager {
return new Promise((resolve) => { return new Promise((resolve) => {
this.koa.use(this.router.routes()); this.koa.use(this.router.routes());
this.koa.listen(this.config.port, () => { this.koa.listen(this.config.port, () => {
console.log(`Restful API 启动于http://${this.config.host}:${this.config.port}`); this.app.logger.info(`Restful API 启动于http://${this.config.host}:${this.config.port}`);
resolve(); resolve();
}); });
}); });

@ -4,22 +4,24 @@ import path from "path";
import App from "./App"; import App from "./App";
import { MultipleMessage } from "./base/provider/BaseProvider"; import { MultipleMessage } from "./base/provider/BaseProvider";
import { RobotConfig } from "./Config"; import { RobotConfig } from "./Config";
import { CommonSendMessage } from "./message/Message"; import { CommonGroupMessage, CommonPrivateMessage, CommonReceivedMessage, CommonSendMessage } from "./message/Message";
import { GroupSender, SenderIdentity, UserSender } from "./message/Sender";
import { CommandInfo } from "./PluginManager"; import { CommandInfo } from "./PluginManager";
import { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager"; import { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager";
import { SessionStore } from "./SessionManager";
import { Target } from "./SubscribeManager"; import { Target } from "./SubscribeManager";
const ROBOT_PATH = __dirname + "/robot";
export interface Robot { export interface Robot {
type: string; type: string;
robotId?: string; robotId?: string;
uid?: string; uid?: string;
description?: string;
initialize?: () => Promise<any>; initialize?: () => Promise<any>;
initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise<any>; initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise<any>;
setCommands?(commands: CommandInfo[]): Promise<any>; setCommands?(commands: CommandInfo[]): Promise<any>;
sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>; sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>;
sendPushMessage(targets: Target[], message: string): Promise<any>; sendPushMessage(targets: Target[], message: string): Promise<any>;
getSession(senderIdentity: SenderIdentity, type: string): SessionStore;
} }
export class RobotManager { export class RobotManager {
@ -37,20 +39,22 @@ export class RobotManager {
this.robots = {}; this.robots = {};
} }
async initialize() { public async initialize() {
const ROBOT_PATH = path.join(this.app.srcPath, "robot");
for (let file of fs.readdirSync(ROBOT_PATH)) { for (let file of fs.readdirSync(ROBOT_PATH)) {
let robotFile = `${ROBOT_PATH}/${file}`; let robotFile = `${ROBOT_PATH}/${file}`;
if (robotFile.match(/\.m?js$/)) { if (robotFile.match(/\.m?js$/)) {
// 加载js文件 // 加载js文件
let robotName = path.basename(robotFile).replace(/Robot\.m?js$/gi, "").toLocaleLowerCase(); let robotName = path.basename(robotFile).replace(/Robot\.m?js$/gi, "").toLocaleLowerCase();
try { try {
let robotClass = require(robotFile)?.default; let robotClass = await import(robotFile);
if (!robotClass) { if (!robotClass || !robotClass.default) {
throw new Error("robot api is empty"); throw new Error("robot api is empty");
} }
this.robotClasses[robotName] = robotClass; this.robotClasses[robotName] = robotClass.default;
} catch(err) { } catch(err) {
console.log(`无法加载Robot API: ${robotName}`, err); this.app.logger.error(`无法加载Robot API: ${robotName}`, err);
} }
} }
} }
@ -59,7 +63,7 @@ export class RobotManager {
let robotConfig = this.config[robotId]; let robotConfig = this.config[robotId];
let robotType: string = robotConfig.type; let robotType: string = robotConfig.type;
if (!robotType) { if (!robotType) {
console.error("无法加载 " + robotId + " Robot: 配置文件中未定义 'type'"); this.app.logger.error("无法加载 " + robotId + " Robot: 配置文件中未定义 'type'");
continue; continue;
} }
robotType = robotType.toLocaleLowerCase(); robotType = robotType.toLocaleLowerCase();
@ -72,7 +76,7 @@ export class RobotManager {
await robotObject.initRestfulApi?.(this.app.restfulApi.getRobotRouter(robotId), this.app.restfulApi); await robotObject.initRestfulApi?.(this.app.restfulApi.getRobotRouter(robotId), this.app.restfulApi);
this.robots[robotId] = robotObject; this.robots[robotId] = robotObject;
console.log(`已加载Robot: ${robotId}`); this.app.logger.info(`已加载Robot: ${robotId}`);
} catch(err) { } catch(err) {
console.error(`无法加载 ${robotId} Robot: `, err); console.error(`无法加载 ${robotId} Robot: `, err);
} }
@ -110,4 +114,66 @@ export class RobotManager {
} }
} }
} }
public getSenderIdentity(robot: Robot, message: CommonReceivedMessage) {
let sender: SenderIdentity = {
robot: robot,
type: 'raw',
};
if (message instanceof CommonPrivateMessage) {
const messageSender = message.sender as UserSender;
sender.type = 'private';
sender.userId = messageSender.uid;
} else if (message instanceof CommonGroupMessage) {
const messageSender = message.sender as GroupSender;
sender.type = 'group';
sender.userId = messageSender.uid;
sender.groupId = messageSender.groupId;
sender.rootGroupId = messageSender.rootGroupId;
}
return sender;
}
public getSessionPath(sender: SenderIdentity, type: string = 'chat'): string[] {
if (type === 'global') { // 全局Session
return ['global'];
}
let ret: string[] = ['robot', sender.robot.robotId!];
if (type === 'robot') { // 机器人Session
return ret;
}
if (!sender.userId) {
throw new Error("Unknown sender");
}
if (type === 'user' || sender.type === 'private') { // 用户Session
ret.push('user', sender.userId!);
return ret;
}
if (sender.type === 'group') {
if (sender.rootGroupId && sender.groupId) {
ret.push('group', sender.rootGroupId);
if (type === 'rootGroup') return ret;
ret.push(sender.groupId);
if (type === 'group') return ret;
ret.push(sender.userId!);
if (type === 'chat') return ret;
} else if (sender.rootGroupId || sender.groupId) {
ret.push('group', sender.rootGroupId || sender.groupId!);
if (type === 'rootGroup' || type === 'group') return ret;
ret.push(sender.userId!);
if (type === 'chat') return ret;
}
}
throw new Error(`Unknown session type: ${type}`);
}
} }

@ -4,8 +4,6 @@ import path from 'path';
import App from './App'; import App from './App';
import { ServiceConfig } from './Config'; import { ServiceConfig } from './Config';
const SERVICE_PATH = __dirname + "/service";
export interface Service { export interface Service {
initialize(): Promise<void>; initialize(): Promise<void>;
destory(): Promise<void>; destory(): Promise<void>;
@ -35,20 +33,22 @@ export class ServiceManager {
this.services = {}; this.services = {};
} }
async initialize() { public async initialize() {
const SERVICE_PATH = path.join(this.app.srcPath, "service");
for (let file of fs.readdirSync(SERVICE_PATH)) { for (let file of fs.readdirSync(SERVICE_PATH)) {
let serviceFile = `${SERVICE_PATH}/${file}`; let serviceFile = `${SERVICE_PATH}/${file}`;
if (serviceFile.match(/\.(js|mjs)$/)) { if (serviceFile.match(/\.(js|mjs)$/)) {
// 加载js文件 // 加载js文件
let serviceName = path.basename(serviceFile).replace(/Service\.(js|mjs)$/gi, "").toLocaleLowerCase(); let serviceName = path.basename(serviceFile).replace(/Service\.(js|mjs)$/gi, "").toLocaleLowerCase();
try { try {
let serviceClass = require(serviceFile)?.default; let serviceClass = await import(serviceFile);
if (!serviceClass) { if (!serviceClass || !serviceClass.default) {
throw new Error("service is empty"); throw new Error("service is empty");
} }
this.serviceClasses[serviceName] = serviceClass; this.serviceClasses[serviceName] = serviceClass.default;
} catch(err) { } catch(err) {
console.log(`无法加载Service: ${serviceName}`, err); this.app.logger.error(`无法加载Service: ${serviceName}`, err);
} }
} }
} }
@ -67,7 +67,7 @@ export class ServiceManager {
let serviceObject: Service = new serviceClass(this.app, serviceConfig); let serviceObject: Service = new serviceClass(this.app, serviceConfig);
await serviceObject.initialize(); await serviceObject.initialize();
this.services[serviceName] = serviceObject; this.services[serviceName] = serviceObject;
console.log(`已加载Service: ${serviceName}`); this.app.logger.info(`已加载Service: ${serviceName}`);
} catch(err) { } catch(err) {
console.error(`无法加载 ${serviceName} Service: `, err); console.error(`无法加载 ${serviceName} Service: `, err);
} }

@ -0,0 +1,95 @@
import { caching, Cache } from "cache-manager";
import { redisStore } from "cache-manager-redis-yet";
import App from "./App";
import { SessionConfig } from "./Config";
export class SessionManager {
private app: App;
private config: SessionConfig;
private store!: Cache;
constructor(app: App, config: SessionConfig) {
this.app = app;
this.config = config;
}
public async initialize() {
if (this.config.type === 'redis') {
let cacheOption = {
socket: {
host: this.config.redis?.host ?? 'localhost',
port: this.config.redis?.port ?? 6379,
},
password: this.config.redis?.password,
db: this.config.redis?.db ?? 0,
ttl: (this.config.ttl ?? 600) * 1000
};
this.app.logger.debug('Redis Store 配置: ' + JSON.stringify(cacheOption));
this.store = await caching(await redisStore(cacheOption));
this.app.logger.info(`使用Redis作为SessionStore`);
} else {
let cacheOption = {
ttl: (this.config.ttl ?? 600) * 1000
};
this.store = await caching('memory', cacheOption);
this.app.logger.info(`使用内存数据库作为SessionStore`);
}
}
/**
* SessionStore
* @param path
* @returns
*/
public getStore(path: string[]): SessionStore {
return new SessionStore(this.store, path);
}
}
export class SessionStore implements Cache {
rootStore: Cache;
prefix: string;
constructor(rootStore: Cache, path: string[]) {
this.rootStore = rootStore;
if (path.length > 0) {
this.prefix = path.join(':') + ':';
} else {
this.prefix = '';
}
}
public set(key: string, value: unknown, ttl?: number | undefined) {
if (typeof ttl === 'undefined') {
return this.rootStore.set(this.prefix + key, value);
} else {
return this.rootStore.set(this.prefix + key, value, ttl * 1000);
}
}
public get<T>(key: string) {
return this.rootStore.get<T>(this.prefix + key);
}
public del(key: string) {
return this.rootStore.del(this.prefix + key);
}
public reset() {
return this.rootStore.store.del(this.prefix + '*');
}
wrap<T>(key: string, fn: () => Promise<T>, ttl?: number | undefined) {
if (typeof ttl === 'undefined') {
return this.rootStore.wrap(this.prefix + key, fn);
} else {
return this.rootStore.wrap(this.prefix + key, fn, ttl * 1000);
}
}
get store() {
return this.rootStore.store;
}
}

@ -67,7 +67,7 @@ export class SubscribeManager {
this.loadSubscribeFile(); this.loadSubscribeFile();
this.subscribeList = {}; this.subscribeList = {};
this.rebuildTree(); this.rebuildTree();
console.log('已重载Subscribe'); this.app.logger.info('已重载Subscribe');
} }
/** /**
@ -80,14 +80,10 @@ export class SubscribeManager {
let targetTypeConf = targetConf[targetType]; let targetTypeConf = targetConf[targetType];
for (let targetId in targetTypeConf) { for (let targetId in targetTypeConf) {
let subscribeList = targetTypeConf[targetId]; let subscribeList = targetTypeConf[targetId];
if (subscribeList.channel) { for (let sourceType in subscribeList) {
for (let sourceId in subscribeList.channel) { let sourceList = subscribeList[sourceType];
this.addSubscribe(robotId, targetType, targetId, 'channel:' + sourceId); for (let sourceId of sourceList) {
} this.addSubscribe(robotId, targetType, targetId, sourceType + ':' + sourceId);
}
if (subscribeList.controller) {
for (let controllerId in subscribeList.controller) {
this.addSubscribe(robotId, targetType, targetId, 'controller:' + controllerId);
} }
} }
} }

@ -1,6 +1,6 @@
var Channel = require('./Channel'); var Channel = require('./Channel');
class BroadcastChannel extends Channel { export default class BroadcastChannel extends Channel {
constructor(app){ constructor(app){
super(app, {}); super(app, {});
} }
@ -27,5 +27,3 @@ class BroadcastChannel extends Channel {
} }
} }
} }
module.exports = BroadcastChannel;

@ -1,6 +1,6 @@
var utils = require('../Utils'); import { Utils as utils } from '../utils/Utils';
class Channel { export default class Channel {
constructor(app, config){ constructor(app, config){
this.app = app; this.app = app;
this.config = config; this.config = config;
@ -172,5 +172,3 @@ class Channel {
} }
} }
} }
module.exports = Channel;

@ -0,0 +1,241 @@
import App from "../App";
import { CommonReceivedMessage } from "../message/Message";
import { MessagePriority, PluginController, PluginEvent } from "../PluginManager";
import { ChatGPTBrowserClient, ChatGPTClient } from '@waylaidwanderer/chatgpt-api';
export default class ChatGPTController implements PluginController {
private SESSION_KEY_CHAT_SESSION = 'openai_chatSession';
private SESSION_KEY_API_CHAT_SESSION = 'openai_apiChatSession';
private DEFAULT_PROMPT = '';
private config: any = {};
public event!: PluginEvent;
public app: App;
public chatGPTClient: any;
public chatGPTApiClient: any;
public id = 'openai';
public name = 'OpenAI';
public description = '对话AI的功能';
private chatGenerating = false;
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
proxy: '',
browser_api: {
token: '',
cookies: '',
},
openai_api: {
token: '',
bot_name: '',
model_options: {
model: 'gpt-3.5-turbo',
max_tokens: 1000,
}
},
}
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({
command: 'ai',
name: '开始对话',
}, (args, message, resolve) => {
resolve();
this.handleChatGPTChat(args, message).catch(console.error);
});
this.event.registerCommand({
command: '重置对话',
name: '重置对话',
}, (args, message, resolve) => {
resolve();
message.session.chat.del(this.SESSION_KEY_CHAT_SESSION);
message.session.chat.del(this.SESSION_KEY_API_CHAT_SESSION);
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.handleChatGPTChat(message.contentText, message).catch(console.error);
}
});
*/
this.event.on('message/focused', async (message, resolved) => {
resolved();
this.handleChatGPTAPIChat(message.contentText, message).catch(console.error);
}, { priority: MessagePriority.LOWEST });
}
async updateConfig(config: any) {
this.config = config;
const clientOptions = {
accessToken: config.browser_api.token,
cookies: config.browser_api.cookies,
proxy: config.proxy,
};
this.chatGPTClient = new ChatGPTBrowserClient(clientOptions);
const apiClientOptions = {
promptPrefix: config.openai_api.system_prompt,
chatGptLabel: config.openai_api.bot_name,
proxy: config.proxy,
modelOptions: config.openai_api.model_options ? {
model: config.openai_api.model_options.model,
max_tokens: config.openai_api.model_options.max_tokens,
} : undefined,
}
this.chatGPTApiClient = new ChatGPTClient(config.openai_api.token, apiClientOptions);
this.DEFAULT_PROMPT = config.browser_api.prefix_prompt;
}
private async handleChatGPTChat(content: string, message: CommonReceivedMessage) {
if (this.chatGenerating) {
message.sendReply('正在生成另一段对话,请稍后', true);
return;
}
if (content.trim() === '') {
message.sendReply('说点什么啊', true);
return;
}
let response: any;
let isFirstMessage = false;
let chatSession = await message.session.chat.get<any>(this.SESSION_KEY_CHAT_SESSION);
if (!chatSession) {
isFirstMessage = true;
chatSession = {};
}
this.app.logger.debug('ChatGPT chatSession', chatSession);
const lowSpeedTimer = setTimeout(() => {
message.sendReply('生成对话速度较慢,请耐心等待', true);
}, 10 * 1000);
this.chatGenerating = true;
try {
if (!chatSession.conversationId) {
response = await this.chatGPTClient.sendMessage(this.DEFAULT_PROMPT + content);
} else {
response = await this.chatGPTClient.sendMessage(content, chatSession);
}
} catch (err: any) {
this.app.logger.error('ChatGPT error', err);
console.error(err);
if (err?.json?.detail) {
if (err.json.detail === 'Conversation not found') {
message.sendReply('对话已失效,请重新开始', true);
await message.session.chat.del(this.SESSION_KEY_CHAT_SESSION);
return;
}
}
message.sendReply('生成对话失败: ' + err.toString(), true);
return;
} finally {
clearTimeout(lowSpeedTimer);
this.chatGenerating = false;
}
if (this.app.debug) {
this.app.logger.debug('ChatGPT response', JSON.stringify(response));
console.log(response);
}
if (response.response) {
let reply: string = response.response ?? '';
reply = reply.replace(/\n\n/g, '\n');
/*
if (isFirstMessage) {
reply += '\n\n接下来的对话可以直接回复我。';
}
*/
chatSession.conversationId = response.conversationId;
chatSession.parentMessageId = response.messageId;
await message.session.chat.set(this.SESSION_KEY_CHAT_SESSION, chatSession, 600);
message.sendReply(reply, true);
}
}
private async handleChatGPTAPIChat(content: string, message: CommonReceivedMessage) {
if (content.trim() === '') {
message.sendReply('说点什么啊', true);
return;
}
let response: any;
let isFirstMessage = false;
let chatSession = await message.session.chat.get<any>(this.SESSION_KEY_API_CHAT_SESSION);
if (!chatSession) {
isFirstMessage = true;
chatSession = {};
}
this.app.logger.debug('ChatGPT chatSession', chatSession);
try {
if (!chatSession.conversationId) {
response = await this.chatGPTApiClient.sendMessage(content);
} else {
response = await this.chatGPTApiClient.sendMessage(content, chatSession);
}
} catch (err: any) {
this.app.logger.error('ChatGPT error', err);
console.error(err);
if (err?.json?.detail) {
if (err.json.detail === 'Conversation not found') {
message.sendReply('对话已失效,请重新开始', true);
await message.session.chat.del(this.SESSION_KEY_CHAT_SESSION);
return;
}
}
message.sendReply('生成对话失败: ' + err.toString(), true);
return;
}
if (this.app.debug) {
this.app.logger.debug('ChatGPT response', JSON.stringify(response));
console.log(response);
}
if (response.response) {
let reply: string = response.response ?? '';
chatSession.conversationId = response.conversationId;
chatSession.parentMessageId = response.messageId;
await message.session.chat.set(this.SESSION_KEY_API_CHAT_SESSION, chatSession, 3600);
message.sendReply(reply, true);
}
}
}

@ -19,7 +19,7 @@ export default class IsekaiWikiController implements PluginController {
} }
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
this.event.controller = this; this.event.init(this);
const wikiMisc = new WikiMisc(this.app, 'https://www.isekai.cn/api.php'); const wikiMisc = new WikiMisc(this.app, 'https://www.isekai.cn/api.php');

@ -15,7 +15,7 @@ export default class SfsettingsController implements PluginController {
} }
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
this.event.controller = this; this.event.init(this);
const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w/api.php'); const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w/api.php');

@ -0,0 +1,84 @@
import App from "../App";
import { CommonReceivedMessage, CommonSendMessage } from "../message/Message";
import { PluginController, PluginEvent } from "../PluginManager";
export default class SystemController implements PluginController {
public event!: PluginEvent;
public app: App;
public id = 'system';
public name = '系统功能';
public description = '系统功能控制器';
constructor(app: App) {
this.app = app;
}
async initialize() {
this.event.init(this);
this.event.autoSubscribe = true;
this.event.forceSubscribe = true;
this.event.registerCommand({
command: '帮助',
name: '获取帮助',
alias: ['help', '?', ''],
}, (args, message, resolved) => {
resolved();
this.handleHelp(args, message);
});
}
async handleHelp(args: string, message: CommonReceivedMessage) {
const senderInfo = this.app.event.getSenderInfo(message);
const subscribedControllers = this.app.plugin.getSubscribedControllers(senderInfo);
let replyMsg = message.createReplyMessage();
replyMsg.type = 'help';
replyMsg.extra.controllers = subscribedControllers;
let helpBuilder: string[] = [];
let robotDescription = message.receiver.description;
if (robotDescription) {
helpBuilder.push(robotDescription);
helpBuilder.push('');
}
helpBuilder.push('功能列表:');
for (let controller of subscribedControllers) {
helpBuilder.push(`${controller.name}`);
if (controller.event.commandList.length > 0) {
controller.event.commandList.forEach(commandInfo => {
helpBuilder.push(`/${commandInfo.command} - ${commandInfo.name}`);
});
} else {
helpBuilder.push('此功能没有指令');
}
helpBuilder.push('');
}
if (helpBuilder[helpBuilder.length - 1] === '') {
helpBuilder.pop();
}
if (this.app.debug) {
this.app.logger.debug(`收到帮助指令,已找到 ${subscribedControllers.length} 个控制器`);
}
replyMsg.content = [{
type: 'text',
data: {
text: helpBuilder.join('\n')
}
}];
if (this.app.debug) {
this.app.logger.debug('发送帮助信息');
}
await replyMsg.send();
}
}

@ -0,0 +1,59 @@
import App from "../App";
import { PluginController, PluginEvent } from "../PluginManager";
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);
this.event.registerCommand({
command: '写入全局',
name: '写入全局Session',
}, (args, message, resolve) => {
resolve();
message.session.global.set('_test', args);
});
this.event.registerCommand({
command: '写入群组',
name: '写入群组Session',
}, (args, message, resolve) => {
resolve();
message.session.group.set('_test', args);
});
this.event.registerCommand({
command: '写入对话',
name: '写入对话Session',
}, (args, message, resolve) => {
resolve();
message.session.chat.set('_test', args);
});
this.event.registerCommand({
command: '读取',
name: '读取Session',
}, async (args, message, resolve) => {
resolve();
let globalSession = await message.session.global.get('_test');
let groupSession = await message.session.group.get('_test');
let chatSession = await message.session.chat.get('_test');
message.sendReply(`全局Session: ${globalSession}\n群组Session: ${groupSession}\n对话Session: ${chatSession}`);
});
}
}

@ -1,4 +1,4 @@
import request from "request-promise"; import got from "got";
import App from "../../App"; import App from "../../App";
import { CommonReceivedMessage } from "../../message/Message"; import { CommonReceivedMessage } from "../../message/Message";
import { PluginEvent } from "../../PluginManager"; import { PluginEvent } from "../../PluginManager";
@ -9,10 +9,6 @@ export class WikiMisc {
private apiEndpoint: string; private apiEndpoint: string;
public id = 'sfsettings';
public name = '科幻设定百科';
public description = '科幻设定百科的相关功能';
constructor(app: App, apiEndpoint: string) { constructor(app: App, apiEndpoint: string) {
this.app = app; this.app = app;
this.apiEndpoint = apiEndpoint; this.apiEndpoint = apiEndpoint;
@ -20,19 +16,16 @@ export class WikiMisc {
public async handleSearch(args: string, message: CommonReceivedMessage) { public async handleSearch(args: string, message: CommonReceivedMessage) {
try { try {
let res = await request({ let res = await got.post(this.apiEndpoint, {
uri: this.apiEndpoint, form: {
method: 'GET',
qs: {
action: 'opensearch', action: 'opensearch',
search: args, search: args,
limit: 10, limit: 10,
namespace: 0, namespace: 0,
format: 'json', format: 'json',
formatversion: 2, formatversion: 2,
}, }
json: true, }).json<any>();
});
if (res.error) { if (res.error) {
message.sendReply('获取词条列表失败: ' + res.error.info, true); message.sendReply('获取词条列表失败: ' + res.error.info, true);
@ -45,10 +38,8 @@ export class WikiMisc {
} }
// Get page info // Get page info
res = await request({ res = await got.post(this.apiEndpoint, {
uri: this.apiEndpoint, form: {
method: 'GET',
qs: {
action: 'query', action: 'query',
prop: 'info|extracts', prop: 'info|extracts',
inprop: 'url', inprop: 'url',
@ -61,8 +52,7 @@ export class WikiMisc {
formatversion: 2, formatversion: 2,
titles: titles[0], titles: titles[0],
}, },
json: true, }).json<any>();
});
if (res.error) { if (res.error) {
message.sendReply('获取词条详情失败: ' + res.error.info, true); message.sendReply('获取词条详情失败: ' + res.error.info, true);
@ -89,10 +79,8 @@ export class WikiMisc {
public async handleRandomPage(args: string, message: CommonReceivedMessage) { public async handleRandomPage(args: string, message: CommonReceivedMessage) {
try { try {
let res = await request({ let res = await got.post(this.apiEndpoint, {
uri: this.apiEndpoint, form: {
method: 'GET',
qs: {
action: 'query', action: 'query',
prop: 'info|extracts', prop: 'info|extracts',
inprop: 'url', inprop: 'url',
@ -106,8 +94,7 @@ export class WikiMisc {
format: 'json', format: 'json',
formatversion: 2, formatversion: 2,
}, },
json: true, }).json<any>();
});
if (res.error) { if (res.error) {
message.sendReply('获取随机页面失败: ' + res.error.info, true); message.sendReply('获取随机页面失败: ' + res.error.info, true);
@ -115,7 +102,7 @@ export class WikiMisc {
} }
if (this.app.debug) { if (this.app.debug) {
console.log(res); this.app.logger.debug('随机页面信息', res);
} }
let pageTitle = res.query.random?.[0]?.title; let pageTitle = res.query.random?.[0]?.title;
@ -124,10 +111,8 @@ export class WikiMisc {
return; return;
} }
// Get page info // Get page info
res = await request({ res = await got.post(this.apiEndpoint, {
uri: this.apiEndpoint, form: {
method: 'GET',
qs: {
action: 'query', action: 'query',
prop: 'info|extracts', prop: 'info|extracts',
inprop: 'url', inprop: 'url',
@ -140,8 +125,7 @@ export class WikiMisc {
formatversion: 2, formatversion: 2,
titles: pageTitle, titles: pageTitle,
}, },
json: true, }).json();
});
if (res.error) { if (res.error) {
message.sendReply('获取随机页面失败: ' + res.error.info, true); message.sendReply('获取随机页面失败: ' + res.error.info, true);

@ -1,4 +1,5 @@
import { Robot } from "../RobotManager"; import { Robot } from "../RobotManager";
import { SessionStore } from "../SessionManager";
import { BaseSender, GroupSender, UserSender } from "./Sender"; import { BaseSender, GroupSender, UserSender } from "./Sender";
export interface MessageChunk { export interface MessageChunk {
@ -215,8 +216,21 @@ export class CommonSendMessage extends CommonMessage {
this.targetId = targetId; this.targetId = targetId;
if (Array.isArray(content)) this.content = content; if (Array.isArray(content)) this.content = content;
} }
async send(): Promise<void> {
await this.sender.sendMessage(this);
}
} }
export type SessionStoreGroup = {
global: SessionStore;
robot: SessionStore;
user: SessionStore;
rootGroup: SessionStore;
group: SessionStore;
chat: SessionStore;
};
export class CommonReceivedMessage extends CommonMessage { export class CommonReceivedMessage extends CommonMessage {
/** 接收时间 */ /** 接收时间 */
time: Date = new Date(); time: Date = new Date();
@ -226,6 +240,15 @@ export class CommonReceivedMessage extends CommonMessage {
sender: any; sender: any;
/** 接收者是否被提到 */ /** 接收者是否被提到 */
mentionedReceiver: boolean = false; mentionedReceiver: boolean = false;
/** Session存储 */
session: SessionStoreGroup = new Proxy({} as any, {
get: (target, p) => {
if (!target[p]) {
target[p] = this.getSession(p as string);
}
return target[p];
},
}) as any;
constructor(receiver: Robot, messageId?: string) { constructor(receiver: Robot, messageId?: string) {
super(); super();
@ -234,7 +257,7 @@ export class CommonReceivedMessage extends CommonMessage {
this.id = messageId; this.id = messageId;
} }
public async sendReply(message: string | MessageChunk[], addReply: boolean = false): Promise<CommonSendMessage | null> { public createReplyMessage(message?: string | MessageChunk[], addReply: boolean = false) {
const sender = this.sender as BaseSender; const sender = this.sender as BaseSender;
let newMessage = new CommonSendMessage(this.receiver!, this.origin, sender.targetId); let newMessage = new CommonSendMessage(this.receiver!, this.origin, sender.targetId);
if (typeof message === 'string') { if (typeof message === 'string') {
@ -245,8 +268,6 @@ export class CommonReceivedMessage extends CommonMessage {
newMessage.content = msgContent; newMessage.content = msgContent;
} else if (Array.isArray(message)) { } else if (Array.isArray(message)) {
newMessage.content = message; newMessage.content = message;
} else {
return null;
} }
if (addReply) { if (addReply) {
@ -254,10 +275,21 @@ export class CommonReceivedMessage extends CommonMessage {
newMessage.repliedMessage = this; newMessage.repliedMessage = this;
} }
return newMessage;
}
public async sendReply(message: string | MessageChunk[], addReply: boolean = false): Promise<CommonSendMessage | null> {
let newMessage = this.createReplyMessage(message, addReply);
if (newMessage.content.length === 0) return null;
newMessage = await this.receiver.sendMessage(newMessage); newMessage = await this.receiver.sendMessage(newMessage);
return newMessage; return newMessage;
} }
public getSession(type: string) {
return this.receiver.getSession(this.sender.identity, type);
}
} }
export class CommonPrivateMessage<US extends UserSender> extends CommonReceivedMessage { export class CommonPrivateMessage<US extends UserSender> extends CommonReceivedMessage {

@ -1,3 +1,5 @@
import { Robot } from "../RobotManager";
export type BaseSenderType = "user" | "group" | "channel"; export type BaseSenderType = "user" | "group" | "channel";
export interface BaseSender { export interface BaseSender {
@ -6,17 +8,31 @@ export interface BaseSender {
} }
export class UserSender implements BaseSender { export class UserSender implements BaseSender {
public robot: Robot;
public readonly type = "user"; public readonly type = "user";
public uid: string; public uid: string;
public userName?: string; public userName?: string;
public nickName?: string; public nickName?: string;
constructor(uid: string) { constructor(robot: Robot, uid: string) {
this.robot = robot;
this.uid = uid; this.uid = uid;
} }
static newAnonymous() { static newAnonymous(robot: Robot) {
return new UserSender(''); return new UserSender(robot, '');
}
get identity(): SenderIdentity {
let senderIdentity: SenderIdentity = {
type: 'private',
robot: this.robot,
userId: this.uid,
};
return senderIdentity;
} }
get targetId() { get targetId() {
@ -31,19 +47,40 @@ export class UserSender implements BaseSender {
export class GroupSender { export class GroupSender {
public readonly type = "group"; public readonly type = "group";
public robot: Robot;
public groupId: string; public groupId: string;
public groupName?: string; public groupName?: string;
public rootGroupId?: string;
public rootGroupName?: string;
public uid: string; public uid: string;
public userName?: string; public userName?: string;
public globalNickName?: string; public globalNickName?: string;
public nickName?: string; public nickName?: string;
constructor(groupId: string, uid: string) { constructor(robot: Robot, groupId: string, uid: string) {
this.robot = robot;
this.groupId = groupId; this.groupId = groupId;
this.uid = uid; this.uid = uid;
} }
get identity(): SenderIdentity {
let senderIdentity: SenderIdentity = {
type: 'group',
robot: this.robot,
groupId: this.groupId,
userId: this.uid,
};
if (this.rootGroupId) {
senderIdentity.rootGroupId = this.rootGroupId;
}
return senderIdentity;
}
get targetId() { get targetId() {
return this.groupId; return this.groupId;
} }
@ -57,10 +94,19 @@ export class GroupSender {
} }
get userSender() { get userSender() {
let sender = new UserSender(this.uid); let sender = new UserSender(this.robot, this.uid);
sender.userName = this.userName; sender.userName = this.userName;
sender.nickName = this.globalNickName; sender.nickName = this.globalNickName;
return sender; return sender;
} }
}
export type SenderIdentity = {
type: 'private' | 'group' | 'channel' | 'raw' | string,
robot: Robot,
rootGroupId?: string,
groupId?: string,
userId?: string,
channelId?: string,
} }

3
src/module.d.ts vendored

@ -1 +1,2 @@
declare module 'yaml'; declare module 'yaml';
declare module '@waylaidwanderer/chatgpt-api';

@ -3,8 +3,7 @@ import { BaseProvider, MultipleMessage } from "../base/provider/BaseProvider";
import { ChannelConfig } from "../Config"; import { ChannelConfig } from "../Config";
import { ConfigCheckError } from "../error/ConfigCheckError"; import { ConfigCheckError } from "../error/ConfigCheckError";
import PusherService from "../service/PusherService"; import PusherService from "../service/PusherService";
import { string, optional, object, guard } from "decoders";
const { string, optional, object, guard } = require("decoders");
export type PusherProviderConfig = { export type PusherProviderConfig = {
source: { source: {

@ -1,15 +1,19 @@
import koa from "koa";
import got from "got/dist/source";
import App from "../App"; import App from "../App";
import { Robot } from "../RobotManager"; import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager"; import { Target } from "../SubscribeManager";
import request from "request-promise";
import { Utils } from "../utils/Utils"; import { Utils } from "../utils/Utils";
import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../RestfulApiManager"; import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../RestfulApiManager";
import koa from "koa"; import { convertMessageToQQChunk, parseQQMessageChunk, QQGroupMessage, QQGroupSender, QQPrivateMessage, QQUserSender } from "./qq/Message";
import { convertMessageToQQChunk, parseQQMessageChunk, QQFaceMessage, QQGroupMessage, QQGroupSender, QQImageMessage, QQPrivateMessage, QQUserSender, QQVoiceMessage } from "./qq/Message"; import { CommonReceivedMessage, CommonSendMessage } from "../message/Message";
import { CommonReceivedMessage, CommonSendMessage, MentionMessage, TextMessage } from "../message/Message"; import { PluginController } from "../PluginManager";
import { RobotConfig } from "../Config";
export type QQRobotConfig = { import { SenderIdentity } from "../message/Sender";
user: string;
export type QQRobotConfig = RobotConfig & {
uid: string;
host: string; host: string;
command_prefix?: string; command_prefix?: string;
} }
@ -26,6 +30,7 @@ export default class QQRobot implements Robot {
public uid: string; public uid: string;
public robotId: string; public robotId: string;
public description: string;
public commandPrefix: string[] = ['/', '', '!', '']; public commandPrefix: string[] = ['/', '', '!', ''];
@ -36,11 +41,15 @@ export default class QQRobot implements Robot {
private groupList: QQGroupInfo[] = []; private groupList: QQGroupInfo[] = [];
private messageTypeHandler: Record<string, (message: CommonSendMessage) => Promise<CommonSendMessage | void>> = {};
constructor(app: App, robotId: string, config: QQRobotConfig) { constructor(app: App, robotId: string, config: QQRobotConfig) {
this.app = app; this.app = app;
this.robotId = robotId; this.robotId = robotId;
this.endpoint = 'http://' + config.host; this.endpoint = 'http://' + config.host;
this.uid = config.user; this.uid = config.uid.toString();
this.description = config.description ?? this.app.config.robot_description ?? 'Isekai Feedbot for QQ';
if (config.command_prefix) { if (config.command_prefix) {
if (Array.isArray(config.command_prefix)) { if (Array.isArray(config.command_prefix)) {
@ -49,6 +58,8 @@ export default class QQRobot implements Robot {
this.commandPrefix = [config.command_prefix]; this.commandPrefix = [config.command_prefix];
} }
} }
this.messageTypeHandler.help = this.parseHelpMessage.bind(this);
} }
async initialize() { async initialize() {
@ -93,6 +104,44 @@ export default class QQRobot implements Robot {
await next(); await next();
} }
async parseHelpMessage(message: CommonSendMessage) {
const controllers = message.extra.controllers as PluginController[];
let helpBuilder: string[] = [];
if (this.description) {
helpBuilder.push(this.description, '');
}
helpBuilder.push(
'可用的指令前缀:"' + this.commandPrefix.join('"、"') + '"',
'功能列表:'
);
const mainCommandPrefix = this.commandPrefix[0];
for (let controller of controllers) {
helpBuilder.push(`${controller.name}`);
if (controller.event.commandList.length > 0) {
controller.event.commandList.forEach(commandInfo => {
helpBuilder.push(`${mainCommandPrefix}${commandInfo.command} - ${commandInfo.name}`);
});
} else {
helpBuilder.push('此功能没有指令');
}
helpBuilder.push('');
}
if (helpBuilder[helpBuilder.length - 1] === '') {
helpBuilder.pop();
}
message.content = [{
type: 'text',
data: {
text: helpBuilder.join('\n')
}
}];
}
/** /**
* *
* @param postData * @param postData
@ -110,7 +159,7 @@ export default class QQRobot implements Robot {
// 处理群消息 // 处理群消息
let groupInfo = this.groupList.find((info) => info.groupId === postData.group_id); let groupInfo = this.groupList.find((info) => info.groupId === postData.group_id);
let groupSender = new QQGroupSender(postData.group_id.toString(), postData.user_id.toString()); let groupSender = new QQGroupSender(this, postData.group_id.toString(), postData.user_id.toString());
groupSender.groupInfo = groupInfo; groupSender.groupInfo = groupInfo;
groupSender.groupName = groupInfo?.groupName; groupSender.groupName = groupInfo?.groupName;
groupSender.globalNickName = postData.sender?.nickname; groupSender.globalNickName = postData.sender?.nickname;
@ -125,7 +174,7 @@ export default class QQRobot implements Robot {
message = await parseQQMessageChunk(this, postData.message ?? [], message); message = await parseQQMessageChunk(this, postData.message ?? [], message);
} else if (postData.message_type === 'private') { } else if (postData.message_type === 'private') {
// 处理私聊消息 // 处理私聊消息
let userSender = new QQUserSender(postData.user_id.toString()); let userSender = new QQUserSender(this, postData.user_id.toString());
userSender.nickName = postData.sender?.nickname; userSender.nickName = postData.sender?.nickname;
message = new QQPrivateMessage(userSender, this, postData.message_id.toString()); message = new QQPrivateMessage(userSender, this, postData.message_id.toString());
@ -162,6 +211,11 @@ export default class QQRobot implements Robot {
return null; return null;
} }
getSession(senderIdentity: SenderIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(senderIdentity, type);
return this.app.session.getStore(sessionPath);
}
/** /**
* *
* @param user - QQ * @param user - QQ
@ -206,15 +260,19 @@ export default class QQRobot implements Robot {
* @param message * @param message
*/ */
async sendMessage(message: CommonSendMessage): Promise<CommonSendMessage> { async sendMessage(message: CommonSendMessage): Promise<CommonSendMessage> {
if (message.type in this.messageTypeHandler) {
this.app.logger.debug('[DEBUG] 运行消息类型处理器', message.type);
let newMessage = await this.messageTypeHandler[message.type](message);
if (newMessage) message = newMessage;
}
let msgData = await convertMessageToQQChunk(message); let msgData = await convertMessageToQQChunk(message);
if (message.origin === 'private') { if (message.origin === 'private') {
if (this.app.debug) console.log('[DEBUG] 发送私聊消息', message.targetId, msgData); this.app.logger.debug('[DEBUG] 发送私聊消息', message.targetId, msgData);
await this.sendToUser(message.targetId, msgData); await this.sendToUser(message.targetId, msgData);
} else if (message.origin === 'group') { } else if (message.origin === 'group') {
if (this.app.debug) console.log('[DEBUG] 发送群消息', message.targetId, msgData); this.app.logger.debug('[DEBUG] 发送群消息', message.targetId, msgData);
await this.sendToGroup(message.targetId, msgData); await this.sendToGroup(message.targetId, msgData);
} }
@ -256,12 +314,9 @@ export default class QQRobot implements Robot {
* API * API
*/ */
async doApiRequest(method: string, data: any): Promise<any> { async doApiRequest(method: string, data: any): Promise<any> {
return await request({ return await got.post(this.endpoint + '/' + method, {
method: 'POST', json: data,
uri: this.endpoint + '/' + method,
body: data,
json: true,
timeout: 10000 timeout: 10000
}); }).json<any>();
} }
} }

@ -1,26 +1,33 @@
import TelegramBot from "node-telegram-bot-api"; import TelegramBot from "node-telegram-bot-api";
import App from "../App"; import App from "../App";
import { RobotConfig } from "../Config";
import { CommonSendMessage } from "../message/Message"; import { CommonSendMessage } from "../message/Message";
import { SenderIdentity } from "../message/Sender";
import { CommandInfo } from "../PluginManager"; import { CommandInfo } from "../PluginManager";
import { Robot } from "../RobotManager"; import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager"; import { Target } from "../SubscribeManager";
import { Utils } from "../utils/Utils"; import { Utils } from "../utils/Utils";
export type TelegramRobotConfig = { export type TelegramRobotConfig = RobotConfig & {
token: string; token: string;
baseId?: string;
proxy?: string; proxy?: string;
} }
export default class TelegramRobot implements Robot { export default class TelegramRobot implements Robot {
private app: App;
public type = 'telegram'; public type = 'telegram';
public robotId: string; public robotId: string;
public uid?: string; public uid?: string;
public description: string;
private bot: TelegramBot; private bot: TelegramBot;
constructor(app: App, robotId: string, config: TelegramRobotConfig) { constructor(app: App, robotId: string, config: TelegramRobotConfig) {
this.app = app;
this.robotId = robotId; this.robotId = robotId;
this.description = config.description ?? app.config.robot_description ?? 'Isekai Feedbot for Telegram';
let botOptions: any = { let botOptions: any = {
polling: true polling: true
@ -70,6 +77,11 @@ export default class TelegramRobot implements Robot {
*/ */
} }
getSession(senderIdentity: SenderIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(senderIdentity, type);
return this.app.session.getStore(sessionPath);
}
/** /**
* *
*/ */

@ -34,8 +34,8 @@ export interface QQUrlMessage extends MessageChunk {
} }
export class QQUserSender extends UserSender { export class QQUserSender extends UserSender {
constructor(uid: string) { constructor(robot: QQRobot, uid: string) {
super(uid); super(robot, uid);
this.userName = uid; this.userName = uid;
} }
} }
@ -46,13 +46,13 @@ export class QQGroupSender extends GroupSender {
public title?: string; public title?: string;
public groupInfo?: QQGroupInfo; public groupInfo?: QQGroupInfo;
constructor(groupId: string, uid: string) { constructor(robot: QQRobot, groupId: string, uid: string) {
super(groupId, uid); super(robot, groupId, uid);
this.userName = uid; this.userName = uid;
} }
get userSender() { get userSender() {
let sender = new QQUserSender(this.uid); let sender = new QQUserSender(this.robot as any, this.uid);
sender.userName = this.userName; sender.userName = this.userName;
sender.nickName = this.globalNickName; sender.nickName = this.globalNickName;

@ -1,5 +1,3 @@
class HttpQueue { export default class HttpQueue {
} }
module.exports = HttpQueue;

@ -1,3 +1,5 @@
import fs from 'fs';
export class Utils { export class Utils {
static dictJoin(dict: { [key: string]: any }, d1: string = ": ", d2: string = "\n"): string { static dictJoin(dict: { [key: string]: any }, d1: string = ": ", d2: string = "\n"): string {
let lines: string[] = []; let lines: string[] = [];
@ -37,4 +39,36 @@ export class Utils {
return text; return text;
} }
} }
static compare(a: any, b: any, depth: number = 5): boolean {
if (depth <= 0) return true;
if (a === b) return true;
if (a === null || b === null) return false;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!this.compare(a[i], b[i], depth - 1)) return false;
}
return true;
} else {
let keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
for (let i = 0; i < keys.length; i++) {
if (!this.compare(a[keys[i]], b[keys[i]], depth - 1)) return false;
}
return true;
}
} else {
return false;
}
}
static prepareDir(path: string): void {
if (!fs.existsSync(path)) {
fs.mkdirSync(path, { recursive: true });
}
}
} }

@ -24,9 +24,9 @@
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */ /* Modules */
"module": "commonjs", /* Specify what module code is generated. */ "module": "ES2022", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */ "rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ "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. */ // "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. */ // "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. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */

Loading…
Cancel
Save