支持ESM,增加Session和Controller config

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

1
.gitignore vendored

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

@ -1,7 +1,4 @@
const Promise = require('bluebird');
Promise.config({
cancellation: true,
});
import 'node-telegram-bot-api';
import App from './dist/App';
var App = require('./dist/App').default;
new App(__dirname + "/config.yml");
new App("./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",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "tsc && node index.js",
"start": "node --es-module-specifier-resolution=node index.js",
"dev": "tsc && node --es-module-specifier-resolution=node index.js",
"build": "tsc"
},
"author": {
@ -16,8 +17,13 @@
"license": "MIT",
"dependencies": {
"@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",
"decoders": "^1.25.3",
"got": "^11.8.3",
"handlebars": "^4.7.7",
"koa": "^2.13.4",
"koa-body": "^6.0.1",
@ -28,18 +34,15 @@
"node-telegram-bot-api": "^0.58.0",
"pusher": "^3.0.1",
"pusher-js": "^5.1.1",
"request": "^2.88.2",
"request-promise": "^4.2.5",
"throttle-debounce": "^3.0.1",
"winston": "^3.8.2",
"yaml": "^1.8.3"
},
"devDependencies": {
"@types/bluebird-global": "^3.5.13",
"@types/koa": "^2.13.4",
"@types/koa-router": "^7.4.4",
"@types/micromatch": "^4.0.2",
"@types/node": "^17.0.8",
"@types/request-promise": "^4.1.48",
"@types/throttle-debounce": "^2.1.0",
"@types/yaml": "^1.9.7",
"ts-node": "^10.4.0",

@ -1,27 +1,33 @@
import fs from 'fs';
import path from 'path';
import winston from 'winston';
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 { ChannelConfig, Config } from './Config';
import { EventManager } from './EventManager';
import { CommonSendMessage } from './message/Message';
import { PluginManager } from './PluginManager';
import { ProviderManager } from './ProviderManager';
import { RestfulApiManager } from './RestfulApiManager';
import { RobotManager } from './RobotManager';
import { Service, ServiceManager } from './ServiceManager';
import { Setup } from './Setup';
import { SubscribeManager, Target } from './SubscribeManager';
import { SessionManager } from './SessionManager';
export default class App {
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 logger!: winston.Logger;
public event!: EventManager;
public session!: SessionManager;
public robot!: RobotManager;
public provider!: ProviderManager;
public service!: ServiceManager;
@ -41,17 +47,56 @@ export default class App {
await this.initModules();
await this.initRestfulApiManager();
await this.initEventManager();
await this.initSessionManager();
await this.initRobot();
await this.initProviderManager();
await this.initServiceManager();
await this.initSubscribeManager();
await this.initChannelManager();
await this.initPluginManager();
console.log('初始化完成,正在接收消息');
this.logger.info('初始化完成,正在接收消息');
}
async initModules() {
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() {
@ -64,6 +109,11 @@ export default class App {
await this.event.initialize();
}
async initSessionManager() {
this.session = new SessionManager(this, this.config.session);
await this.session.initialize();
}
async initRobot() {
this.robot = new RobotManager(this, this.config.robot);
await this.robot.initialize();
@ -91,7 +141,7 @@ export default class App {
}
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();
}
@ -119,11 +169,7 @@ export default class App {
* @returns
*/
async sendPushMessage(channelId: string, messages: MultipleMessage): Promise<void> {
console.log(`[${channelId}] 消息: `, messages);
this.logger.info(`[${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;
} else {
if (BaseProvider.checkConfig(config)) {
// console.log(`正在加载Channel: ${channelId}`);
// 处理channel
let providerName = this.getProviderName(config);
let isReload = false;
@ -114,10 +113,10 @@ export class ChannelManager extends EventEmitter {
this.channelName[channelId] = config?.name;
if (isReload) {
this.emit('reload', channelId);
console.log(`已重载Channel: ${channelId}`);
this.app.logger.info(`已重载Channel: ${channelId}`);
} else {
this.emit('add', channelId);
console.log(`已加载Channel: ${channelId}`);
this.app.logger.info(`已加载Channel: ${channelId}`);
}
}
} else {
@ -148,7 +147,7 @@ export class ChannelManager extends EventEmitter {
delete this.channels[channelId];
delete this.channelName[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 = {
channel_config_path: string;
plugin_config_path: string;
plugin_path: string;
subscribe_config: string;
debug: boolean;
robot: Record<string, RobotConfig>;
service: Record<string, ServiceConfig>;
session: SessionConfig;
http_api: RestfulApiConfig;
command_override: CommandOverrideConfig;
focused_as_command: true;
command_override?: CommandOverrideConfig;
focused_as_command?: true;
robot_description?: string;
};
export type RobotConfig = {
type: string;
baseId: string;
description?: string;
};
export type RestfulApiConfig = {
@ -26,6 +32,17 @@ export type RestfulApiConfig = {
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 GeneratorConfig = {

@ -1,34 +1,57 @@
import App from "./App";
import { CommandOverrideConfig } from "./Config";
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";
export type PluginControllerListenerInfo = {
export type ControllerEventInfo = {
priority: number;
callback: CallableFunction;
controllerEvent: PluginEvent;
eventScope: PluginEvent;
}
export type PluginControllerCommandInfo = {
export type SessionEventInfo = {
activeTime: Date;
eventScope: EventScope;
}
export type ControllerCommandInfo = {
commandInfo: CommandInfo;
controllerEvent: PluginEvent;
eventScope: PluginEvent;
}
export class EventManager {
private app: App;
/** 事件排序的debounce */
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) {
this.app = app;
this.commandOverride = app.config.command_override ?? {};
}
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)) {
this.eventList[event] = [];
}
@ -48,7 +71,7 @@ export class EventManager {
const eventInfo = {
callback: callback,
priority: options.priority!,
controllerEvent
eventScope
};
this.eventList[event].push(eventInfo);
@ -56,62 +79,73 @@ export class EventManager {
this.sortEvent(event);
}
public off(event: string, controllerEvent: PluginEvent, callback: CallableFunction): void
public off(controllerEvent: PluginEvent): void
public off(event: string, eventScope: PluginEvent, callback: CallableFunction): void
public off(eventScope: PluginEvent): void
public off(...args: any): void {
if (typeof args[0] === 'string') {
let [event, controller, callback] = args;
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') {
let controller = args[0];
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 = {
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)) {
commandInfo.alias.forEach((alias) => {
this.commandList[alias] = data;
this.commandList[alias.toLocaleLowerCase()] = data;
});
}
}
public removeCommand(commandInfo: CommandInfo): void
public removeCommand(controllerEvent: PluginEvent): void
public removeCommand(eventScope: PluginEvent): void
public removeCommand(...args: any): void {
if ('command' in 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)) {
commandInfo.alias.forEach((alias) => {
delete this.commandList[alias];
delete this.commandList[alias.toLocaleLowerCase()];
});
}
} 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) {
if (this.commandList[command].controllerEvent.controller?.id === controllerEvent.controller?.id) {
if (this.commandList[command].eventScope.controller?.id === eventScope.controller?.id) {
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 (args[0] instanceof CommonReceivedMessage) {
console.log(`[DEBUG] 触发事件 ${eventName} ${args[0].contentText}`);
this.app.logger.debug(`触发事件 ${eventName} ${args[0].contentText}`);
} else {
console.log(`[DEBUG] 触发事件 ${eventName}`);
this.app.logger.debug(`触发事件 ${eventName}`);
}
}
@ -126,43 +160,49 @@ export class EventManager {
isResolved = true;
};
let subscribeList: string[] = [];
let [subscribedControllers, disabledControllers] = this.getControllerSubscribe(senderInfo);
if (senderInfo) {
// 获取订阅列表
let targetType = '';
let targetId = '';
for (let eventInfo of eventList) {
if (!isFilter && senderInfo) {
switch (senderInfo.type) {
case 'private':
targetType = 'user';
targetId = senderInfo.userId!;
if (!eventInfo.eventScope.allowPrivate) {
continue;
}
if (!eventInfo.eventScope.isAllowSubscribe(senderInfo)) {
continue;
}
break;
case 'group':
targetType = 'group';
targetId = senderInfo.groupId!;
if (!eventInfo.eventScope.allowGroup) {
continue;
}
break;
case 'channel':
targetType = 'channel';
targetId = senderInfo.channelId!;
break;
if (!eventInfo.eventScope.allowChannel) {
continue;
}
subscribeList = this.app.subscribe.getSubscribedList(senderInfo.robot.robotId!, targetType, targetId, 'controller');
break;
}
for (let eventInfo of eventList) {
if (!isFilter && senderInfo) {
if (eventInfo.controllerEvent.autoSubscribe) {
if (!eventInfo.controllerEvent.isAllowSubscribe(senderInfo)) {
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 if (senderInfo.type !== 'private') {
if (!eventInfo.controllerEvent.controller || !subscribeList.includes(eventInfo.controllerEvent.controller.id)) {
}
} else {
// 检测控制器是否已启用
if (!eventInfo.eventScope.controller || !subscribedControllers.includes(eventInfo.eventScope.controller.id)) {
continue;
}
}
}
}
try {
await eventInfo.callback(...args, resolved);
@ -170,7 +210,7 @@ export class EventManager {
break;
}
} catch(err: any) {
console.error(`事件 ${eventName} 处理失败`);
this.app.logger.error(`事件 ${eventName} 处理失败: `, err);
console.error(err);
}
}
@ -206,7 +246,7 @@ export class EventManager {
// 尝试识别空格分隔的指令
if (contentText.includes(' ')) {
command = contentText.split(' ')[0];
command = contentText.split(' ')[0].toLocaleLowerCase();
args = contentText.substring(command.length + 1);
if (!(command in this.commandList)) {
@ -232,7 +272,7 @@ export class EventManager {
}
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);
@ -257,7 +297,7 @@ export class EventManager {
}
public getSenderInfo(message: CommonReceivedMessage): ControllerSubscribeSource {
public getSenderInfo(message: CommonReceivedMessage): SenderIdentity {
if (message.origin === 'private') {
return {
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) {
if (this.eventSortDebounce[eventName]) {
return;

@ -1,21 +1,27 @@
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';
import Yaml from 'yaml';
import App from "./App";
import EventEmitter from "events";
import path from "path";
import { SenderIdentity } from "./message/Sender";
import { Utils } from "./utils/Utils";
export const MessagePriority = {
LOWEST: 0,
LOW: 20,
DEFAULT: 40,
HIGH: 50,
/**
*
*
*
*
*/
TEMP_HANDLER: 60,
HIGH: 80,
SESSION_HANDLER: 60,
HIGHER: 70,
/**
*
*/
@ -48,29 +54,26 @@ export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction
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 {
private app: App;
private pluginPath: string;
private configPath: string;
private watcher!: chokidar.FSWatcher;
private configWatcher!: chokidar.FSWatcher;
public controllers: 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();
this.app = app;
this.pluginPath = pluginPath;
this.pluginPath = path.resolve(pluginPath);
this.configPath = path.resolve(configPath);
this.controllers = {};
this.fileControllers = {};
this.configControllers = {};
}
/**
@ -82,10 +85,15 @@ export class PluginManager extends EventEmitter {
ignorePermissionErrors: true,
persistent: true
});
this.watcher.on('add', this.loadController.bind(this));
this.watcher.on('change', this.loadController.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) {
@ -111,16 +119,19 @@ export class PluginManager extends EventEmitter {
this.fileControllers[file] = controllerInstance;
if (isReload) {
console.log(`已重新加载Controller: ${file}`);
this.app.logger.info(`已重新加载Controller: ${file}`);
this.emit('controllerReloaded', controllerInstance);
} else {
console.log(`已加载Controller: ${file}`);
this.app.logger.info(`已加载Controller: ${file}`);
this.emit('controllerLoaded', controllerInstance);
}
const pluginMisc = new PluginEvent(this.app.event);
controllerInstance.event = pluginMisc;
await controllerInstance.initialize();
const pluginEvent = new PluginEvent(this.app);
controllerInstance.event = pluginEvent;
const controllerConfig = await this.loadControllerConfig('standalone', controllerInstance);
await controllerInstance.initialize(controllerConfig);
} else {
throw new Error('PluginController ID is not defined.');
}
@ -136,76 +147,174 @@ export class PluginManager extends EventEmitter {
async removeController(file: string, isReload = false) {
const controller = this.fileControllers[file];
if (controller) {
const configFile = this.getConfigFile('standalone', controller);
await controller.event.destroy();
await controller.destroy?.();
delete this.controllers[file];
delete this.fileControllers[file];
if (configFile in this.configControllers) {
delete this.configControllers[configFile];
}
this.emit('controllerRemoved', controller);
if (!isReload) {
console.log(`已移除Controller: ${controller.id}`);
this.app.logger.info(`已移除Controller: ${controller.id}`);
}
}
}
}
export interface PluginController {
id: string;
name: string;
description?: string;
getConfigFile(pluginId: string, controller: PluginController) {
return path.resolve(this.configPath, pluginId, controller.id + '.yml');
}
event: PluginEvent;
async loadControllerConfig(pluginId: string, controller: PluginController) {
const configFile = this.getConfigFile(pluginId, controller);
try {
if (configFile in this.configControllers) { // 防止保存时触发重载
delete this.configControllers[configFile];
}
initialize: () => Promise<void>;
destroy?: () => Promise<void>;
}
const defaultConfig = await controller.getDefaultConfig?.() ?? {};
let config: any = defaultConfig;
let shouldFill: boolean = false;
export class PluginEvent {
private eventManager: EventManager;
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;
}
public controller?: PluginController;
if (shouldFill) {
Utils.prepareDir(path.dirname(configFile));
await fsAsync.writeFile(configFile, Yaml.stringify(config));
}
public autoSubscribe = false;
public forceSubscribe = false;
public showInSubscribeList = true;
setTimeout(() => {
this.configControllers[configFile] = controller;
}, 1000);
public allowPrivate = true;
public allowGroup = true;
public allowChannel = true;
return config;
} catch(err: any) {
this.app.logger.error(`加载Controller配置失败: ${configFile}`, err);
console.error(err);
}
}
public allowedRobotTypeList: AllowedList = '*';
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);
}
}
}
private commandList: CommandInfo[] = [];
private eventList: Record<string, EventListenerInfo[]> = {};
/**
*
* @param senderInfo
* @returns
*/
public getSubscribedControllers(senderInfo: SenderIdentity): PluginController[] {
let [subscribedControllers, disabledControllers] = this.app.event.getControllerSubscribe(senderInfo);
constructor(eventManager: EventManager) {
this.eventManager = eventManager;
}
return Object.values(this.controllers).filter((controller) => {
if (controller.event.commandList.length === 0) return false;
public isAllowSubscribe: (source: ControllerSubscribeSource) => boolean = (source) => {
if (this.allowedRobotTypeList !== '*' && !this.allowedRobotTypeList.includes(source.robot.type)) {
switch (senderInfo.type) {
case 'private':
if (!controller.event.allowPrivate) {
return false;
}
switch (source.type) {
case 'private':
if (!this.allowPrivate) {
if (!controller.event.isAllowSubscribe(senderInfo)) {
return false;
}
break;
case 'group':
if (!this.allowGroup) {
if (!controller.event.allowGroup) {
return false;
}
break;
case 'channel':
if (!this.allowChannel) {
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 {
id: string;
name: string;
description?: string;
event: PluginEvent;
initialize: (config: any) => Promise<void>;
destroy?: () => Promise<void>;
getDefaultConfig?: () => Promise<any>;
updateConfig?: (config: any) => Promise<void>;
}
export class EventScope {
protected app: App;
protected eventManager: EventManager;
public commandList: CommandInfo[] = [];
public eventList: Record<string, EventListenerInfo[]> = {};
public eventSorted: Record<string, boolean> = {};
constructor(app: App) {
this.app = app;
this.eventManager = app.event;
}
/**
@ -295,8 +404,9 @@ export class PluginEvent {
priority: options.priority!
};
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 {
@ -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.
* @param command
* @param name
* @param callback
* @param options
* @param command Command
* @param name Command name
* @param callback Callback function
* @param options Command options
*/
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(...args: any[]): void {
// 处理传入参数
@ -341,18 +492,62 @@ export class PluginEvent {
this.on(`command/${commandInfo.command}`, callback, options);
if (Array.isArray(commandInfo.alias)) { // Add event for alias
commandInfo.alias.forEach((cmd) => {
this.on(`command/${cmd}`, callback, options);
this.on(`command/${cmd.toLocaleLowerCase()}`, callback, options);
});
}
this.eventManager.addCommand(commandInfo as any, this);
this.afterAddCommand(commandInfo as any);
}
/**
* Initialize plugin controller.
*/
public async initialize() {
protected afterAddEventListener(event: string, callback: CallableFunction, options?: MessageEventOptions): void {
}
protected afterRemoveEventListener(event: string, callback: CallableFunction): void { }
protected afterAddCommand(commandInfo: CommandInfo): void { }
}
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 = {};
}
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 { ChannelConfig } from './Config';
const PROVIDER_PATH = __dirname + "/provider";
export class ProviderManager {
private app: App;
private providerClasses: { [key: string]: any }
@ -16,20 +14,22 @@ export class ProviderManager {
}
async initialize() {
const PROVIDER_PATH = path.join(this.app.srcPath, "provider");
for (let file of fs.readdirSync(PROVIDER_PATH)) {
let providerFile = `${PROVIDER_PATH}/${file}`;
if (providerFile.match(/\.(js|mjs)$/)) {
// 加载js文件
let providerName = path.basename(providerFile).replace(/Provider\.(js|mjs)$/gi, "").toLocaleLowerCase();
try {
let provider = require(providerFile)?.default;
if (!provider) {
let provider = await import(providerFile);
if (!provider || !provider.default) {
throw new Error("provider is empty");
}
this.providerClasses[providerName] = provider;
console.log(`已加载Provider: ${providerName}`);
this.providerClasses[providerName] = provider.default;
this.app.logger.info(`已加载Provider: ${providerName}`);
} 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 Router from "koa-router";
import { makeRoutes } from "./restful/routes";
import koaBody from "koa-body";
import { koaBody } from "koa-body";
export interface RestfulContext {
feedbot: App,
@ -46,7 +46,7 @@ export class RestfulApiManager {
return new Promise((resolve) => {
this.koa.use(this.router.routes());
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();
});
});

@ -4,22 +4,24 @@ import path from "path";
import App from "./App";
import { MultipleMessage } from "./base/provider/BaseProvider";
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 { RestfulApiManager, RestfulContext, RestfulRouter } from "./RestfulApiManager";
import { SessionStore } from "./SessionManager";
import { Target } from "./SubscribeManager";
const ROBOT_PATH = __dirname + "/robot";
export interface Robot {
type: string;
robotId?: string;
uid?: string;
description?: string;
initialize?: () => Promise<any>;
initRestfulApi?: (router: RestfulRouter, api: RestfulApiManager) => Promise<any>;
setCommands?(commands: CommandInfo[]): Promise<any>;
sendMessage(message: CommonSendMessage): Promise<CommonSendMessage>;
sendPushMessage(targets: Target[], message: string): Promise<any>;
getSession(senderIdentity: SenderIdentity, type: string): SessionStore;
}
export class RobotManager {
@ -37,20 +39,22 @@ export class RobotManager {
this.robots = {};
}
async initialize() {
public async initialize() {
const ROBOT_PATH = path.join(this.app.srcPath, "robot");
for (let file of fs.readdirSync(ROBOT_PATH)) {
let robotFile = `${ROBOT_PATH}/${file}`;
if (robotFile.match(/\.m?js$/)) {
// 加载js文件
let robotName = path.basename(robotFile).replace(/Robot\.m?js$/gi, "").toLocaleLowerCase();
try {
let robotClass = require(robotFile)?.default;
if (!robotClass) {
let robotClass = await import(robotFile);
if (!robotClass || !robotClass.default) {
throw new Error("robot api is empty");
}
this.robotClasses[robotName] = robotClass;
this.robotClasses[robotName] = robotClass.default;
} 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 robotType: string = robotConfig.type;
if (!robotType) {
console.error("无法加载 " + robotId + " Robot: 配置文件中未定义 'type'");
this.app.logger.error("无法加载 " + robotId + " Robot: 配置文件中未定义 'type'");
continue;
}
robotType = robotType.toLocaleLowerCase();
@ -72,7 +76,7 @@ export class RobotManager {
await robotObject.initRestfulApi?.(this.app.restfulApi.getRobotRouter(robotId), this.app.restfulApi);
this.robots[robotId] = robotObject;
console.log(`已加载Robot: ${robotId}`);
this.app.logger.info(`已加载Robot: ${robotId}`);
} catch(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 { ServiceConfig } from './Config';
const SERVICE_PATH = __dirname + "/service";
export interface Service {
initialize(): Promise<void>;
destory(): Promise<void>;
@ -35,20 +33,22 @@ export class ServiceManager {
this.services = {};
}
async initialize() {
public async initialize() {
const SERVICE_PATH = path.join(this.app.srcPath, "service");
for (let file of fs.readdirSync(SERVICE_PATH)) {
let serviceFile = `${SERVICE_PATH}/${file}`;
if (serviceFile.match(/\.(js|mjs)$/)) {
// 加载js文件
let serviceName = path.basename(serviceFile).replace(/Service\.(js|mjs)$/gi, "").toLocaleLowerCase();
try {
let serviceClass = require(serviceFile)?.default;
if (!serviceClass) {
let serviceClass = await import(serviceFile);
if (!serviceClass || !serviceClass.default) {
throw new Error("service is empty");
}
this.serviceClasses[serviceName] = serviceClass;
this.serviceClasses[serviceName] = serviceClass.default;
} 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);
await serviceObject.initialize();
this.services[serviceName] = serviceObject;
console.log(`已加载Service: ${serviceName}`);
this.app.logger.info(`已加载Service: ${serviceName}`);
} catch(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.subscribeList = {};
this.rebuildTree();
console.log('已重载Subscribe');
this.app.logger.info('已重载Subscribe');
}
/**
@ -80,14 +80,10 @@ export class SubscribeManager {
let targetTypeConf = targetConf[targetType];
for (let targetId in targetTypeConf) {
let subscribeList = targetTypeConf[targetId];
if (subscribeList.channel) {
for (let sourceId in subscribeList.channel) {
this.addSubscribe(robotId, targetType, targetId, 'channel:' + sourceId);
}
}
if (subscribeList.controller) {
for (let controllerId in subscribeList.controller) {
this.addSubscribe(robotId, targetType, targetId, 'controller:' + controllerId);
for (let sourceType in subscribeList) {
let sourceList = subscribeList[sourceType];
for (let sourceId of sourceList) {
this.addSubscribe(robotId, targetType, targetId, sourceType + ':' + sourceId);
}
}
}

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

@ -1,4 +1,5 @@
import { Robot } from "../RobotManager";
import { SessionStore } from "../SessionManager";
import { BaseSender, GroupSender, UserSender } from "./Sender";
export interface MessageChunk {
@ -215,8 +216,21 @@ export class CommonSendMessage extends CommonMessage {
this.targetId = targetId;
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 {
/** 接收时间 */
time: Date = new Date();
@ -226,6 +240,15 @@ export class CommonReceivedMessage extends CommonMessage {
sender: any;
/** 接收者是否被提到 */
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) {
super();
@ -234,7 +257,7 @@ export class CommonReceivedMessage extends CommonMessage {
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;
let newMessage = new CommonSendMessage(this.receiver!, this.origin, sender.targetId);
if (typeof message === 'string') {
@ -245,8 +268,6 @@ export class CommonReceivedMessage extends CommonMessage {
newMessage.content = msgContent;
} else if (Array.isArray(message)) {
newMessage.content = message;
} else {
return null;
}
if (addReply) {
@ -254,10 +275,21 @@ export class CommonReceivedMessage extends CommonMessage {
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);
return newMessage;
}
public getSession(type: string) {
return this.receiver.getSession(this.sender.identity, type);
}
}
export class CommonPrivateMessage<US extends UserSender> extends CommonReceivedMessage {

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

1
src/module.d.ts vendored

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

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

@ -1,15 +1,19 @@
import koa from "koa";
import got from "got/dist/source";
import App from "../App";
import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager";
import request from "request-promise";
import { Utils } from "../utils/Utils";
import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../RestfulApiManager";
import koa from "koa";
import { convertMessageToQQChunk, parseQQMessageChunk, QQFaceMessage, QQGroupMessage, QQGroupSender, QQImageMessage, QQPrivateMessage, QQUserSender, QQVoiceMessage } from "./qq/Message";
import { CommonReceivedMessage, CommonSendMessage, MentionMessage, TextMessage } from "../message/Message";
export type QQRobotConfig = {
user: string;
import { convertMessageToQQChunk, parseQQMessageChunk, QQGroupMessage, QQGroupSender, QQPrivateMessage, QQUserSender } from "./qq/Message";
import { CommonReceivedMessage, CommonSendMessage } from "../message/Message";
import { PluginController } from "../PluginManager";
import { RobotConfig } from "../Config";
import { SenderIdentity } from "../message/Sender";
export type QQRobotConfig = RobotConfig & {
uid: string;
host: string;
command_prefix?: string;
}
@ -26,6 +30,7 @@ export default class QQRobot implements Robot {
public uid: string;
public robotId: string;
public description: string;
public commandPrefix: string[] = ['/', '', '!', ''];
@ -36,11 +41,15 @@ export default class QQRobot implements Robot {
private groupList: QQGroupInfo[] = [];
private messageTypeHandler: Record<string, (message: CommonSendMessage) => Promise<CommonSendMessage | void>> = {};
constructor(app: App, robotId: string, config: QQRobotConfig) {
this.app = app;
this.robotId = robotId;
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 (Array.isArray(config.command_prefix)) {
@ -49,6 +58,8 @@ export default class QQRobot implements Robot {
this.commandPrefix = [config.command_prefix];
}
}
this.messageTypeHandler.help = this.parseHelpMessage.bind(this);
}
async initialize() {
@ -93,6 +104,44 @@ export default class QQRobot implements Robot {
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
@ -110,7 +159,7 @@ export default class QQRobot implements Robot {
// 处理群消息
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.groupName = groupInfo?.groupName;
groupSender.globalNickName = postData.sender?.nickname;
@ -125,7 +174,7 @@ export default class QQRobot implements Robot {
message = await parseQQMessageChunk(this, postData.message ?? [], message);
} 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;
message = new QQPrivateMessage(userSender, this, postData.message_id.toString());
@ -162,6 +211,11 @@ export default class QQRobot implements Robot {
return null;
}
getSession(senderIdentity: SenderIdentity, type: string) {
const sessionPath = this.app.robot.getSessionPath(senderIdentity, type);
return this.app.session.getStore(sessionPath);
}
/**
*
* @param user - QQ
@ -206,15 +260,19 @@ export default class QQRobot implements Robot {
* @param message
*/
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);
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);
} 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);
}
@ -256,12 +314,9 @@ export default class QQRobot implements Robot {
* API
*/
async doApiRequest(method: string, data: any): Promise<any> {
return await request({
method: 'POST',
uri: this.endpoint + '/' + method,
body: data,
json: true,
return await got.post(this.endpoint + '/' + method, {
json: data,
timeout: 10000
});
}).json<any>();
}
}

@ -1,26 +1,33 @@
import TelegramBot from "node-telegram-bot-api";
import App from "../App";
import { RobotConfig } from "../Config";
import { CommonSendMessage } from "../message/Message";
import { SenderIdentity } from "../message/Sender";
import { CommandInfo } from "../PluginManager";
import { Robot } from "../RobotManager";
import { Target } from "../SubscribeManager";
import { Utils } from "../utils/Utils";
export type TelegramRobotConfig = {
export type TelegramRobotConfig = RobotConfig & {
token: string;
baseId?: string;
proxy?: string;
}
export default class TelegramRobot implements Robot {
private app: App;
public type = 'telegram';
public robotId: string;
public uid?: string;
public description: string;
private bot: TelegramBot;
constructor(app: App, robotId: string, config: TelegramRobotConfig) {
this.app = app;
this.robotId = robotId;
this.description = config.description ?? app.config.robot_description ?? 'Isekai Feedbot for Telegram';
let botOptions: any = {
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 {
constructor(uid: string) {
super(uid);
constructor(robot: QQRobot, uid: string) {
super(robot, uid);
this.userName = uid;
}
}
@ -46,13 +46,13 @@ export class QQGroupSender extends GroupSender {
public title?: string;
public groupInfo?: QQGroupInfo;
constructor(groupId: string, uid: string) {
super(groupId, uid);
constructor(robot: QQRobot, groupId: string, uid: string) {
super(robot, groupId, uid);
this.userName = uid;
}
get userSender() {
let sender = new QQUserSender(this.uid);
let sender = new QQUserSender(this.robot as any, this.uid);
sender.userName = this.userName;
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 {
static dictJoin(dict: { [key: string]: any }, d1: string = ": ", d2: string = "\n"): string {
let lines: string[] = [];
@ -37,4 +39,36 @@ export class Utils {
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. */
/* 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. */
// "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. */
// "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. */

Loading…
Cancel
Save