@ -11,6 +11,8 @@ import { ChatIdentity } from "./message/Sender";
import { Utils } from "./utils/Utils" ;
import { Utils } from "./utils/Utils" ;
import { Robot } from "./robot/Robot" ;
import { Robot } from "./robot/Robot" ;
import { Reactive } from "./utils/reactive" ;
import { Reactive } from "./utils/reactive" ;
import { PluginController } from "#ibot-api/PluginController" ;
import { PluginApiBridge } from "./plugin/PluginApiBridge" ;
export const MessagePriority = {
export const MessagePriority = {
LOWEST : 0 ,
LOWEST : 0 ,
@ -60,6 +62,19 @@ export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction
export type AllowedList = string [ ] | '*' ;
export type AllowedList = string [ ] | '*' ;
export type SubscribedPluginInfo = {
id : string ,
controller : PluginController ,
eventGroups : PluginEvent [ ] ,
}
export type PluginInstance = {
id : string ,
path : string ,
bridge : PluginApiBridge ,
controller : PluginController ,
}
export class PluginManager extends EventEmitter {
export class PluginManager extends EventEmitter {
private app : App ;
private app : App ;
private pluginPath : string ;
private pluginPath : string ;
@ -67,9 +82,9 @@ export class PluginManager extends EventEmitter {
private watcher ! : chokidar . FSWatcher ;
private watcher ! : chokidar . FSWatcher ;
private configWatcher ! : chokidar . FSWatcher ;
private configWatcher ! : chokidar . FSWatcher ;
public controllers : Record < string , PluginController > ;
public fileControllers: Record < string , PluginController > ;
public pluginInstanceMap: Record < string , PluginInstance > = { } ;
public config Controllers: Record < string , PluginController > ;
public config PluginMap: Record < string , string > = { } ;
constructor ( app : App , pluginPath : string , configPath : string ) {
constructor ( app : App , pluginPath : string , configPath : string ) {
super ( ) ;
super ( ) ;
@ -77,67 +92,102 @@ export class PluginManager extends EventEmitter {
this . app = app ;
this . app = app ;
this . pluginPath = path . resolve ( pluginPath ) ;
this . pluginPath = path . resolve ( pluginPath ) ;
this . configPath = path . resolve ( configPath ) ;
this . configPath = path . resolve ( configPath ) ;
this . controllers = { } ;
this . pluginInstanceMap = { } ;
this . fileControllers = { } ;
this . configControllers = { } ;
}
}
/ * *
/ * *
* 加 载 所 有 Controllers
* 加 载 所 有 Controllers
* /
* /
async initialize() {
async initialize() {
this . watcher = chokidar . watch ( this . pluginPath , {
// this.watcher = chokidar.watch(this.pluginPath + "/**/*.js", {
ignored : '*.bak' ,
// ignorePermissionErrors: true,
ignorePermissionErrors : true ,
// persistent: true,
persistent : true
// followSymlinks: true,
} ) ;
// depth: 1
this . watcher . on ( 'add' , this . loadController . bind ( this ) ) ;
// });
this . watcher . on ( 'change' , this . loadController . bind ( this ) ) ;
// this.watcher.on('add', this.onPluginFileAdded.bind(this));
this . watcher . on ( 'unlink' , this . removeController . bind ( this ) ) ;
// this.watcher.on('change', this.onPluginFileChanged.bind(this));
// this.watcher.on('unlink', this.onPluginFileRemoved.bind(this));
for ( let folder of fs . readdirSync ( this . pluginPath ) ) {
if ( folder . startsWith ( '.' ) ) continue ;
let pluginPath = path . join ( this . pluginPath , folder ) ;
if ( ! fs . statSync ( pluginPath ) . isDirectory ( ) ) continue ;
await this . loadPlugin ( pluginPath ) ;
}
this . configWatcher = chokidar . watch ( this . configPath + '/**/*.yml' , {
this . configWatcher = chokidar . watch ( this . configPath + '/ plugin/*.ya ml', {
ignorePermissionErrors : true ,
ignorePermissionErrors : true ,
persistent : true
persistent : true
} ) ;
} ) ;
this . configWatcher . on ( 'change' , this . reloadConfig . bind ( this ) ) ;
this . configWatcher . on ( 'change' , this . reloadConfig . bind ( this ) ) ;
}
}
async loadController ( file : string ) {
async loadPlugin ( folder : string ) {
if ( ! file . match ( /Controller\.m?js$/ ) ) return ;
folder = path . resolve ( folder ) ;
this . app . logger . debug ( '尝试从 ' + folder + ' 加载插件' ) ;
let module Name = path . resolve ( file ) . replace ( /\\/g , '/' ) . replace ( /\.m?js$/ , '' ) ;
const pluginIndexFile = path . join ( folder , 'plugin.yaml' ) ;
if ( ! fs . existsSync ( pluginIndexFile ) ) return ;
let pluginId = '' ;
try {
try {
const controller = await import ( module Name ) ;
const pluginIndex = Yaml . parse ( await fsAsync . readFile ( pluginIndexFile , 'utf-8' ) ) ;
if ( ! pluginIndex || typeof pluginIndex . controller !== "string" ) {
this . app . logger . error ( '插件 ' + folder + ' 没有指定主文件' ) ;
return ;
}
if ( ! pluginIndex . controller . endsWith ( '.js' ) ) {
pluginIndex . controller += '.js' ;
}
const controllerFile = path . join ( folder , pluginIndex . controller ) ;
if ( ! fs . existsSync ( controllerFile ) ) {
this . app . logger . error ( '插件 ' + folder + ' 控制器 ' + controllerFile + ' 不存在' ) ;
return ;
}
const controller = await import ( controllerFile ) ;
if ( controller ) {
if ( controller ) {
const controllerClass = controller . default ? ? controller ;
const controllerClass : typeof PluginController = controller . default ? ? controller ;
const controllerInstance : PluginController = new controllerClass ( this . app ) ;
if ( controllerClass . id ) {
if ( controllerInstance . id && controllerInstance . id !== '' ) {
pluginId = controllerClass . id ;
const controllerId = controllerInstance . id ;
const pluginApiBridge = new PluginApiBridge ( this . app , pluginId ) ;
const controllerInstance : PluginController = new controllerClass ( this . app , pluginApiBridge ) ;
const pluginInstance : PluginInstance = {
id : pluginId ,
path : folder ,
bridge : pluginApiBridge ,
controller : controllerInstance
} ;
pluginApiBridge . setController ( controllerInstance ) ;
let isReload = false ;
let isReload = false ;
if ( controllerId in this . controllers ) {
if ( pluginId in this . pluginInstanceMap ) {
// Reload plugin
// Reload plugin
isReload = true ;
isReload = true ;
await this . removeController ( file , true ) ;
await this . unloadPlugin( pluginId , true ) ;
}
}
this . controllers [ controllerId ] = controllerInstance ;
this . fileControllers [ file ] = controllerInstance ;
this . pluginInstanceMap[ pluginId ] = plugin Instance;
if ( isReload ) {
if ( isReload ) {
this . app . logger . info ( ` 已重新加载Controller: ${ file } ` ) ;
this . app . logger . info ( ` 已重新加载 插件: ${ pluginId } ` ) ;
this . emit ( ' controller Reloaded', controllerInstance ) ;
this . emit ( ' plugin Reloaded', controllerInstance ) ;
} else {
} else {
this . app . logger . info ( ` 已加载 Controller: ${ file } ` ) ;
this . app . logger . info ( ` 已加载 插件: ${ pluginId } ` ) ;
this . emit ( ' controller Loaded', controllerInstance ) ;
this . emit ( ' plugin Loaded', controllerInstance ) ;
}
}
const pluginEvent = new PluginEvent ( this . app ) ;
const controllerConfig = await this . loadMainConfig ( pluginId , controllerInstance ) ;
controllerInstance . event = pluginEvent ;
const controllerConfig = await this . loadControllerConfig ( 'standalone' , controllerInstance ) ;
await controllerInstance . _initialize ( controllerConfig ) ;
await controllerInstance . initialize ( controllerConfig ) ;
} else {
} else {
throw new Error ( 'PluginController ID is not defined.' ) ;
throw new Error ( 'PluginController ID is not defined.' ) ;
}
}
@ -145,41 +195,64 @@ export class PluginManager extends EventEmitter {
throw new Error ( 'PluginController does not have an export.' ) ;
throw new Error ( 'PluginController does not have an export.' ) ;
}
}
} catch ( err : any ) {
} catch ( err : any ) {
console . error ( ` 加载 Controller失败: ${ file } ` ) ;
console . error ( ` 加载 插件失败: ${ folder } ` ) ;
console . error ( err ) ;
console . error ( err ) ;
if ( pluginId && this . pluginInstanceMap [ pluginId ] ) {
delete this . pluginInstanceMap [ pluginId ] ;
}
}
}
}
}
async removeController ( file : string , isReload = false ) {
async unloadPlugin ( pluginId : string , isReload = false ) {
const controller = this . fileControllers [ file ] ;
const instance = this . pluginInstanceMap [ pluginId ] ;
if ( controller ) {
if ( instance ) {
const configFile = this . getConfigFile ( 'standalone' , controller ) ;
const configFile = this . getConfigFile ( pluginId ) ;
await instance . bridge . destroy ( ) ;
await instance . controller . destroy ? . ( ) ;
await controller . event . destroy ( ) ;
delete this . pluginInstanceMap [ pluginId ] ;
await controller . destroy ? . ( ) ;
delete this . controllers [ file ] ;
if ( configFile in this . configPluginMap ) {
delete this . fileControllers [ file ] ;
delete this . configPluginMap [ configFile ] ;
if ( configFile in this . configControllers ) {
delete this . configControllers [ configFile ] ;
}
}
this . emit ( ' controllerRemoved', controller ) ;
this . emit ( 'pluginUnloaded' , instance ) ;
if ( ! isReload ) {
if ( ! isReload ) {
this . app . logger . info ( ` 已 移除Controller: ${ controller . i d} ` ) ;
this . app . logger . info ( ` 已 关闭插件: ${ pluginI d} ` ) ;
}
}
}
}
}
}
getConfigFile ( pluginId : string , controller : PluginController ) {
async reloadPlugin ( pluginId : string ) {
return path . resolve ( this . configPath , pluginId , controller . id + '.yml' ) ;
let pluginInstance = this . pluginInstanceMap [ pluginId ] ;
if ( ! pluginInstance ) return ;
await this . loadPlugin ( pluginInstance . path ) ;
}
getPluginPathFromFile ( filePath : string ) {
if ( filePath . startsWith ( this . pluginPath ) ) {
return filePath . substring ( this . pluginPath . length + 1 ) . split ( path . sep ) [ 0 ] ;
} else {
return null
}
}
onPluginFileChanged ( filePath : string ) {
// Unfinished
}
}
async loadControllerConfig ( pluginId : string , controller : PluginController ) {
getConfigFile ( pluginId : string ) {
const configFile = this . getConfigFile ( pluginId , controller ) ;
return path . resolve ( this . configPath , "plugin" , pluginId + '.yaml' ) ;
}
async loadMainConfig ( pluginId : string , controller : PluginController ) {
const configFile = this . getConfigFile ( pluginId ) ;
try {
try {
if ( configFile in this . configControllers ) { // 防止保存时触发重载
if ( configFile in this . config PluginMap ) { // 防止保存时触发重载
delete this . configControllers [ configFile ] ;
delete this . config PluginMap [ configFile ] ;
}
}
const defaultConfig = await controller . getDefaultConfig ? . ( ) ? ? { } ;
const defaultConfig = await controller . getDefaultConfig ? . ( ) ? ? { } ;
@ -203,124 +276,128 @@ export class PluginManager extends EventEmitter {
}
}
setTimeout ( ( ) = > {
setTimeout ( ( ) = > {
this . config Controllers[ configFile ] = controller ;
this . config PluginMap[ configFile ] = pluginId ;
} , 1000 ) ;
} , 1000 ) ;
return config ;
return config ;
} catch ( err : any ) {
} catch ( err : any ) {
this . app . logger . error ( ` 加载 Controller配置 失败: ${ configFile } ` , err ) ;
this . app . logger . error ( ` 加载 插件主配置文件 失败: ${ configFile } ` , err ) ;
console . error ( err ) ;
console . error ( err ) ;
}
}
}
}
async reloadConfig ( file : string ) {
async reloadConfig ( file : string ) {
this . app . logger . info ( ` 配置文件已更新: ${ file } ` ) ;
this . app . logger . info ( ` 配置文件已更新: ${ file } ` ) ;
if ( file in this . configControllers ) {
if ( file in this . configPluginMap ) {
const pluginId = this . configPluginMap [ file ] ;
try {
try {
const controller = this . configControllers [ file ] ;
const pluginInstance = this . pluginInstanceMap [ pluginId ] ;
if ( controller . updateConfig ) { // 如果控制器支持重载配置,则直接调用
if ( pluginInstance ) {
const localConfig = Yaml . parse ( await fsAsync . readFile ( file , 'utf-8' ) ) ;
const ctor = pluginInstance . controller . constructor as typeof PluginController ;
await controller . updateConfig ( localConfig ) ;
if ( ctor . reloadWhenConfigUpdated ) { // 重载整个控制器
this . app . logger . info ( ` 已重载Controller配置: ${ controller . id } ` ) ;
await this . reloadPlugin ( pluginId ) ;
} else { // 重载整个控制器
return ;
let controllerFile : string = '' ;
for ( let [ file , c ] of Object . entries ( this . fileControllers ) ) {
if ( c === controller ) {
controllerFile = file ;
break ;
}
}
if ( controllerFile ) {
await this . loadController ( controllerFile ) ;
}
}
const localConfig = Yaml . parse ( await fsAsync . readFile ( file , 'utf-8' ) ) ;
await pluginInstance . controller . _setConfig ( localConfig ) ;
this . app . logger . info ( ` 已重载插件配置文件: ${ pluginId } ` ) ;
}
}
} catch ( err : any ) {
} catch ( err : any ) {
this . app . logger . error ( ` 重载Controller配置失败: ${ file } ` , err ) ;
this . app . logger . error ( ` 重载插件 [ ${ pluginId } ] 配置失败: ${ file } ` , err ) ;
console . error ( err ) ;
console . error ( err ) ;
}
}
}
}
}
}
/ * *
/ * *
* 获 取 订 阅 的 控 制 器
* 获 取 订 阅 的 控 制 器 和 事 件 组
* @param senderInfo
* @param senderInfo
* @returns
* @returns
* /
* /
public getSubscribed Controllers ( senderInfo : ChatIdentity ) : PluginController [ ] {
public getSubscribed ( senderInfo : ChatIdentity ) : SubscribedPluginInfo [ ] {
let [ subscribed Controllers, disabledControllers ] = this . app . event . getController Subscribe( senderInfo ) ;
let [ subscribed Scopes, disabledScopes ] = this . app . event . getPlugin Subscribe( senderInfo ) ;
return Object . values ( this . controllers ) . filter ( ( controller ) = > {
let subscribed : SubscribedPluginInfo [ ] = [ ] ;
if ( controller . event . commandList . length === 0 ) return false ;
for ( let pluginInstance of Object . values ( this . pluginInstanceMap ) ) {
let eventGroups : PluginEvent [ ] = [ ] ;
for ( let scopeName in pluginInstance . bridge . scopedEvent ) {
let eventGroup = pluginInstance . bridge . scopedEvent [ scopeName ] ;
switch ( senderInfo . type ) {
if ( eventGroup . commandList . length === 0 ) continue ;
case 'private' :
if ( ! controller . event . allowPrivate ) {
switch ( senderInfo . type ) {
return false ;
case 'private' :
}
if ( ! eventGroup . allowPrivate ) {
if ( ! controller . event . isAllowSubscribe ( senderInfo ) ) {
continue ;
return false ;
}
}
if ( ! eventGroup . isAllowSubscribe ( senderInfo ) ) {
break ;
continue ;
case 'group' :
}
if ( ! controller . event . allowGroup ) {
break ;
return false ;
case 'group' :
}
if ( ! eventGroup . allowGroup ) {
break ;
continue ;
case 'channel' :
}
if ( ! controller . event . allowChannel ) {
break ;
return false ;
case 'channel' :
}
if ( ! eventGroup . allowChannel ) {
break ;
continue ;
}
}
break ;
}
if ( senderInfo . type !== 'private' ) { // 私聊消息不存在订阅,只判断群消息和频道消息
if ( senderInfo . type !== 'private' ) { // 私聊消息不存在订阅,只判断群消息和频道消息
if ( controller . event . autoSubscribe ) {
if ( eventGroup . autoSubscribe ) {
if ( ! controller . event . isAllowSubscribe ( senderInfo ) ) {
if ( ! eventGroup . isAllowSubscribe ( senderInfo ) ) {
return false ;
continue ;
} else {
// 检测控制器是否已禁用
if ( this . app . event . isPluginScopeInList ( pluginInstance . id , scopeName , disabledScopes ) ) {
continue ;
}
}
} else {
} else {
// 检测控制器是否已禁用
// 检测控制器是否已 启 用
if ( disabledControllers . includes ( controller . id ) ) {
if ( ! this . app . event . isPluginScopeInList ( pluginInstance . id , scopeName , subscribedScopes ) ) {
return false ;
continu e;
}
}
}
}
} else {
// 检测控制器是否已启用
if ( ! subscribedControllers . includes ( controller . id ) ) {
return false ;
}
}
}
}
return true ;
eventGroups . push ( eventGroup ) ;
} ) ;
}
}
}
export interface PluginController {
id : string ;
name : string ;
description? : string ;
event : PluginEvent ;
initialize : ( config : any ) = > Promise < void > ;
if ( eventGroups . length > 0 ) {
destroy ? : ( ) = > Promise < void > ;
subscribed . push ( {
id : pluginInstance.id ,
controller : pluginInstance.controller ,
eventGroups : eventGroups
} ) ;
}
}
getDefaultConfig ? : ( ) = > Promise < any > ;
return subscribed ;
updateConfig ? : ( config : any ) = > Promise < void > ;
}
}
}
export class EventScope {
export class EventScope {
protected app : App ;
protected app : App ;
protected eventManager : EventManager ;
protected eventManager : EventManager ;
public pluginId : string ;
public scopeName : string ;
public commandList : CommandInfo [ ] = [ ] ;
public commandList : CommandInfo [ ] = [ ] ;
public eventList : Record < string , EventListenerInfo [ ] > = { } ;
public eventList : Record < string , EventListenerInfo [ ] > = { } ;
public eventSorted : Record < string , boolean > = { } ;
public eventSorted : Record < string , boolean > = { } ;
constructor ( app : App ) {
constructor ( app : App , pluginId : string , scopeName : string ) {
this . app = app ;
this . app = app ;
this . eventManager = app . event ;
this . eventManager = app . event ;
this . pluginId = pluginId ;
this . scopeName = scopeName ;
}
}
/ * *
/ * *
@ -515,8 +592,6 @@ export class EventScope {
}
}
export class PluginEvent extends EventScope {
export class PluginEvent extends EventScope {
public controller? : PluginController ;
public autoSubscribe = false ;
public autoSubscribe = false ;
public forceSubscribe = false ;
public forceSubscribe = false ;
public showInSubscribeList = true ;
public showInSubscribeList = true ;
@ -552,10 +627,6 @@ export class PluginEvent extends EventScope {
return true ;
return true ;
}
}
public init ( controller : PluginController ) {
this . controller = controller ;
}
/ * *
/ * *
* Destroy eventGroup .
* Destroy eventGroup .
* Will remove all event listeners .
* Will remove all event listeners .