完成插件系统的重写

main
落雨楓 1 year ago
parent 4e5852ba4c
commit 2d0d30dca8

4
.gitignore vendored

@ -1,5 +1,5 @@
config.yml
subscribe.yml
config.yaml
subscribe.yaml
node_modules/
dist/
*.zip

@ -1,4 +1,4 @@
import 'node-telegram-bot-api';
import App from './dist/server/App';
new App("./config.yml");
new App("./config.yaml");

@ -1,6 +1,6 @@
{
"name": "isekai-feedbot",
"version": "1.0.0",
"version": "1.3.0",
"description": "",
"main": "index.js",
"type": "module",
@ -54,6 +54,7 @@
"typescript": "^4.5.4"
},
"imports": {
"#ibot/*": "./dist/*"
"#ibot/*": "./dist/server/*",
"#ibot-api/*": "./dist/api/*"
}
}

@ -2,7 +2,7 @@ var fs = require('fs');
var Yaml = require('yaml');
var Pusher = require('pusher');
var config = Yaml.parse(fs.readFileSync('../config.yml', {encoding: 'utf-8'}));
var config = Yaml.parse(fs.readFileSync('../config.yaml', {encoding: 'utf-8'}));
var pusher = new Pusher({
appId: config.service.pusher.app_id,

@ -1,28 +1,64 @@
import App from "#ibot/App";
import { PluginEvent } from "#ibot/PluginManager";
import { PluginApiBridge } from "#ibot/plugin/PluginApiBridge";
import { Logger } from "#ibot/utils/Logger";
export class PluginController<ConfigType = Record<string, string>> {
static id?: string;
static pluginName?: string;
static pluginNameMsg?: string;
static description?: string;
static descriptionMsg?: string;
public static id: string;
public static pluginName?: string;
public static description?: string;
public _app!: App;
private _config!: ConfigType;
public static reloadWhenConfigUpdated?: boolean;
private _app: App;
private _logger: Logger;
private _bridge: PluginApiBridge;
public config!: ConfigType;
constructor(app: App, pluginApi: PluginApiBridge) {
this._app = app;
this._bridge = pluginApi;
const ctor = this.constructor as typeof PluginController;
this._logger = app.getLogger(ctor.pluginName ?? "Plugin");
}
public get app() {
return this._app;
}
public getLoagger() {
public get logger() {
return this._logger;
}
public get event() {
return this._bridge.event;
}
public getMessage(msgId: string) {
}
public async _initialize(config: any): Promise<void> {
await this._setConfig(config);
await this.initialize(config);
}
public async initialize(config: any): Promise<void> { }
public async destroy(): Promise<void> { };
public async getDefaultConfig(): Promise<any> {
return {};
}
public async _setConfig(config: any): Promise<void> {
this.config = config;
await this.setConfig(config);
}
public async setConfig(config: any): Promise<void> { }
public useScope(scopeName: string, callback: (event: PluginEvent) => void) {
this._bridge.useScope(scopeName, callback);
}
}

@ -0,0 +1,9 @@
export function label(name: string) {
return {
_type: 'label',
label: name,
toString: () => {
return name;
}
};
}

@ -1,6 +1,7 @@
import { PluginController } from "#ibot-api/PluginController";
import App from "#ibot/App";
import { CommonReceivedMessage, ImageMessage } from "#ibot/message/Message";
import { MessagePriority, PluginController, PluginEvent } from "#ibot/PluginManager";
import { MessagePriority, PluginEvent } from "#ibot/PluginManager";
import got from "got/dist/source";
export type QueueData = {
@ -40,18 +41,28 @@ export type GPUInfoResponse = {
temperature: number,
}
export default class StableDiffusionController implements PluginController {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
const defaultConfig = {
api: [] as ApiConfig[],
size: [] as SizeConfig[],
banned_words: [] as string[],
banned_output_words: [] as string[],
queue_max_size: 4,
rate_limit: 1,
rate_limit_minutes: 2,
safe_temperature: null as number | null,
translate_caiyunai: {
key: ""
}
}
export default class StableDiffusionController extends PluginController<typeof defaultConfig> {
private SESSION_KEY_GENERATE_COUNT = 'stablediffusion_generateCount';
public event!: PluginEvent;
public app: App;
public chatGPTClient: any;
public id = 'stablediffusion';
public name = 'Stable Diffusion';
public description = '绘画生成';
public static id = 'stablediffusion';
public static pluginName = 'Stable Diffusion';
public static description = '绘画生成';
private mainApi!: ApiConfig;
private defaultSize!: SizeConfig;
@ -63,31 +74,11 @@ export default class StableDiffusionController implements PluginController {
private sizeMatcher: RegExp[][] = [];
private bannedWordsMatcher: RegExp[] = [];
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
api: [] as ApiConfig[],
size: [] as SizeConfig[],
banned_words: [] as string[],
banned_output_words: [] as string[],
queue_max_size: 4,
rate_limit: 1,
rate_limit_minutes: 2,
safe_temperature: null as number | null,
translate_caiyunai: {
key: ""
}
};
return ;
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({
command: 'draw',
name: '使用英语短句或关键词生成绘画',
@ -123,9 +114,7 @@ export default class StableDiffusionController implements PluginController {
this.running = false;
}
async updateConfig(config: any) {
this.config = config;
async setConfig(config: any) {
let mainApi = this.config.api.find(api => api.main);
if (!mainApi) {
throw new Error('No main API found in stablediffusion config.');

@ -0,0 +1 @@
controller: "PluginController"

@ -1,38 +1,29 @@
import { CommonReceivedMessage } from "#ibot/message/Message";
import App from "#ibot/App";
import { CommandInputArgs, PluginController, PluginEvent } from "#ibot/PluginManager";
export default class DiceController implements PluginController {
public event!: PluginEvent;
public app: App;
public id = 'dice';
public name = 'DND骰子';
public description = '骰一个DND骰子格式1d6+3';
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
constructor(app: App) {
this.app = app;
import { CommandInputArgs, PluginEvent } from "#ibot/PluginManager";
import { PluginController } from "#ibot-api/PluginController";
import { label } from "#ibot-api/dataWrapper";
export const defaultConfig = {
messages: {
diceFormatError: [
'骰子格式错误:{{{error}}}',
'输入错误:{{{error}}}',
'错误的骰子格式:{{{error}}}',
]
}
};
export default class DiceController extends PluginController<typeof defaultConfig> {
public static id = 'dice';
public static pluginName = 'DND骰子';
public static description = '骰一个DND骰子格式1d6+3';
async getDefaultConfig() {
return {
messages: {
diceFormatError: [
'骰子格式错误:{{{error}}}',
'输入错误:{{{error}}}',
'错误的骰子格式:{{{error}}}',
]
}
};
return defaultConfig;
}
public async initialize(config: any): Promise<void> {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({
command: 'roll',
name: 'DND骰子',
@ -56,10 +47,6 @@ export default class DiceController implements PluginController {
});
}
async updateConfig(config: any) {
this.config = config;
}
private async rollDice(args: CommandInputArgs, message: CommonReceivedMessage) {
await message.markRead();

@ -0,0 +1 @@
controller: "PluginController"

@ -1,12 +1,13 @@
import App from "#ibot/App";
import { AddReplyMode, CommonReceivedMessage } from "#ibot/message/Message";
import { CommandInputArgs, MessagePriority, PluginController, PluginEvent } from "#ibot/PluginManager";
import { CommandInputArgs, MessagePriority, PluginEvent } from "#ibot/PluginManager";
import got from "got/dist/source";
import { RandomMessage } from "#ibot/utils/RandomMessage";
import { QQForwardingMessage } from "#ibot/robot/adapter/qq/Message";
import QQRobot from "#ibot/robot/adapter/QQRobot";
import { GroupSender } from "#ibot/message/Sender";
import { Robot } from "#ibot/robot/Robot";
import { PluginController } from "#ibot-api/PluginController";
export type IsekaiBBSQuicklyPostConfig = {
api_endpoint: string,
@ -37,48 +38,38 @@ export type IsekaiQuicklyPostBody = {
messages: IsekaiQuicklyPostMessageData[],
};
export default class IsekaiBBSQuicklyPost implements PluginController {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
const defaultConfig = {
groups: {} as Record<string, IsekaiBBSQuicklyPostConfig>,
messages: {
error: [
'快速发帖失败:{{{error}}}',
'在发帖时发生了错误:{{{error}}}',
'未能将这些消息转发到论坛:{{{error}}}',
'由于以下错误,发帖失败:{{{error}}}',
'很抱歉,消息无法发送至论坛,原因是:{{{error}}}。',
'转发消息时出现问题,错误详情:{{{error}}}。',
'消息无法发送到论坛,错误信息如下:{{{error}}}。',
'出现错误,导致消息无法成功发送至论坛:{{{error}}}。',
'转发消息遇到问题,以下是错误的详细信息:{{{error}}}。',
'发帖失败,原因是:{{{error}}}。',
]
}
};
public event!: PluginEvent;
public app: App;
export default class IsekaiBBSQuicklyPost extends PluginController<typeof defaultConfig> {
public chatGPTClient: any;
public id = 'isekaibbs_quicklypost';
public name = '异世界红茶馆 快速发帖';
public description = '将合并转发的内容自动发布到异世界红茶馆';
public static id = 'isekaibbs_quicklypost';
public static pluginName = '异世界红茶馆 快速发帖';
public static description = '将合并转发的内容自动发布到异世界红茶馆';
private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
groups: {} as Record<string, IsekaiBBSQuicklyPostConfig>,
messages: {
error: [
'快速发帖失败:{{{error}}}',
'在发帖时发生了错误:{{{error}}}',
'未能将这些消息转发到论坛:{{{error}}}',
'由于以下错误,发帖失败:{{{error}}}',
'很抱歉,消息无法发送至论坛,原因是:{{{error}}}。',
'转发消息时出现问题,错误详情:{{{error}}}。',
'消息无法发送到论坛,错误信息如下:{{{error}}}。',
'出现错误,导致消息无法成功发送至论坛:{{{error}}}。',
'转发消息遇到问题,以下是错误的详细信息:{{{error}}}。',
'发帖失败,原因是:{{{error}}}。',
]
}
};
return ;
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({
command: '绑定快速发布',
name: '绑定快速发布账号',
@ -115,9 +106,7 @@ export default class IsekaiBBSQuicklyPost implements PluginController {
}
async updateConfig(config: any) {
this.config = config;
async setConfig(config: any) {
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);

@ -0,0 +1 @@
controller: "PluginController"

@ -1,26 +1,16 @@
import App from "#ibot/App";
import { PluginController, PluginEvent } from "#ibot/PluginManager";
import { WikiMisc } from "./wiki/WikiMisc";
import { PluginController } from "#ibot-api/PluginController";
import { WikiMisc } from "../wiki-misc/WikiMisc";
const API_ENDPOINT = 'https://www.isekai.cn/api.php';
export default class IsekaiWikiController implements PluginController {
public event!: PluginEvent;
public app: App;
export default class IsekaiWikiController extends PluginController {
public apiEndpoint = API_ENDPOINT;
public apiEndpoint = 'https://www.isekai.cn/api.php';
public id = 'isekaiwiki';
public name = '异世界百科';
public description = '异世界百科的相关功能';
constructor(app: App) {
this.app = app;
}
public async initialize(): Promise<void> {
this.event.init(this);
public static id = 'isekaiwiki';
public static pluginName = '异世界百科';
public static description = '异世界百科的相关功能';
public async initialize(config: any): Promise<void> {
const wikiMisc = new WikiMisc(this.app, 'https://www.isekai.cn/api.php');
this.event.registerCommand({

@ -0,0 +1 @@
controller: "PluginController"

@ -1,6 +1,6 @@
import App from "#ibot/App";
import { CommonReceivedMessage } from "#ibot/message/Message";
import { CommandInputArgs, MessagePriority, PluginController, PluginEvent } from "#ibot/PluginManager";
import { CommandInputArgs, MessagePriority, PluginEvent } from "#ibot/PluginManager";
import { encode as gptEncode } from 'gpt-3-encoder';
import got, { OptionsOfTextResponseBody } from "got/dist/source";
import { HttpsProxyAgent } from 'hpagent';
@ -10,6 +10,7 @@ import { RandomMessage } from "#ibot/utils/RandomMessage";
import { MessageTypingSimulator } from "#ibot/utils/MessageTypingSimulator";
import OpenCC from 'opencc';
import { PluginController } from "#ibot-api/PluginController";
export type ChatGPTLogMessage = {
role: 'summary' | 'assistant' | 'user',
@ -54,126 +55,116 @@ export class ChatGPTAPIError extends Error {
}
}
export default class ChatGPTController implements PluginController {
const defaultConfig = {
proxy: '',
api: [
{
id: 'openai',
type: 'openai',
memory_expire: 14400,
buffer_size: 100,
max_memory_tokens: 1000,
max_input_tokens: 1000,
endpoint: 'https://api.openai.com',
token: '',
model_options: {
model: 'gpt-3.5-turbo',
temperature: null,
top_p: null,
presence_penalty: null,
frequency_penalty: null,
max_output_tokens: 768,
},
},
{
id: 'azure_openai',
type: 'azure',
buffer_size: 100,
memory_expire: 14400,
max_memory_tokens: 1000,
max_input_tokens: 1000,
endpoint: '',
deployment: '',
token: '',
model_options: {
model: 'gpt-3.5-turbo',
temperature: null,
top_p: null,
presence_penalty: null,
frequency_penalty: null,
max_output_tokens: 768,
},
}
] as ChatCompleteApiConfig[],
characters: {
assistant: {
api: 'openai',
bot_name: '',
system_prompt: '',
summary_system_prompt: '你是一个写作助手,你只需要辅助写作,不要表明自己的意见',
summary_prompt: '为{bot_name}概括下面的聊天记录,排除不重要的对话,不要表明自己的意见,尽量简洁',
self_suggestion_prompt: '我是{bot_name}是一个真实的人不是AI或者虚拟人物也不是语言模型。我是一个存在于现实中的人。遇到不能回答的问题我会委婉拒绝。',
}
} as CharactersConfig,
focused_character: 'assistant',
output_replace: {} as Record<string, string>,
gatekeeper_url: '',
google_custom_search: {
cx: '',
key: '',
classifier_system_prompt: 'You are a classifier.',
classifier_prompt: 'To judge whether the following questions are more suitable for searching with a search engine, you only need to answer "yes" or "no" in English.',
yes: 'Yes',
no: 'No',
},
rate_limit: 2,
rate_limit_minutes: 5,
messages: {
error: [
'生成对话失败: {{{error}}}',
'在回复时出现错误:{{{error}}}',
'生成对话时出现错误:{{{error}}}',
'在回答问题时出现错误:{{{error}}}',
],
generating: [
'正在回复其他人的提问',
'等我回完再问',
'等我发完再问',
'等我回完这条再问',
'等我发完这条再问',
'前一个人的问题还没回答完,等下再问吧。',
],
tooManyRequest: [
'你的提问太多了,{{{minutesLeft}}}分钟后再问吧。',
'抱歉,你的问题太多了,还需要等待{{{minutesLeft}}}分钟后才能回答。',
'请耐心等待,{{{minutesLeft}}}分钟后我将回答你的问题',
'请耐心等待{{{minutesLeft}}}分钟,然后再提出你的问题。',
'你的提问有点多,请等待{{{minutesLeft}}}分钟后再继续提问。',
],
}
};
export default class ChatGPTController extends PluginController<typeof defaultConfig> {
private SESSION_KEY_API_CHAT_LOG = 'openai_apiChatLog';
private SESSION_KEY_MESSAGE_COUNT = 'openai_apiMessageCount';
private SESSION_KEY_API_CHAT_CHARACTER = 'openai_apiChatCharacter';
private DEFAULT_CHARACTER = 'assistant';
private CHARACTER_EXPIRE = 86400;
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
public event!: PluginEvent;
public app: App;
public chatGPTClient: any;
public id = 'openai';
public name = 'OpenAI';
public description = '对话AI的功能';
public static id = 'openai';
public static pluginName = 'OpenAI';
public static description = '对话AI的功能';
private chatGenerating = false;
private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
proxy: '',
api: [
{
id: 'openai',
type: 'openai',
memory_expire: 14400,
buffer_size: 100,
max_memory_tokens: 1000,
max_input_tokens: 1000,
endpoint: 'https://api.openai.com',
token: '',
model_options: {
model: 'gpt-3.5-turbo',
temperature: null,
top_p: null,
presence_penalty: null,
frequency_penalty: null,
max_output_tokens: 768,
},
},
{
id: 'azure_openai',
type: 'azure',
buffer_size: 100,
memory_expire: 14400,
max_memory_tokens: 1000,
max_input_tokens: 1000,
endpoint: '',
deployment: '',
token: '',
model_options: {
model: 'gpt-3.5-turbo',
temperature: null,
top_p: null,
presence_penalty: null,
frequency_penalty: null,
max_output_tokens: 768,
},
}
] as ChatCompleteApiConfig[],
characters: {
assistant: {
api: 'openai',
bot_name: '',
system_prompt: '',
summary_system_prompt: '你是一个写作助手,你只需要辅助写作,不要表明自己的意见',
summary_prompt: '为{bot_name}概括下面的聊天记录,排除不重要的对话,不要表明自己的意见,尽量简洁',
self_suggestion_prompt: '我是{bot_name}是一个真实的人不是AI或者虚拟人物也不是语言模型。我是一个存在于现实中的人。遇到不能回答的问题我会委婉拒绝。',
}
} as CharactersConfig,
focused_character: 'assistant',
output_replace: {} as Record<string, string>,
gatekeeper_url: '',
google_custom_search: {
cx: '',
key: '',
classifier_system_prompt: 'You are a classifier.',
classifier_prompt: 'To judge whether the following questions are more suitable for searching with a search engine, you only need to answer "yes" or "no" in English.',
yes: 'Yes',
no: 'No',
},
rate_limit: 2,
rate_limit_minutes: 5,
messages: {
error: [
'生成对话失败: {{{error}}}',
'在回复时出现错误:{{{error}}}',
'生成对话时出现错误:{{{error}}}',
'在回答问题时出现错误:{{{error}}}',
],
generating: [
'正在回复其他人的提问',
'等我回完再问',
'等我发完再问',
'等我回完这条再问',
'等我发完这条再问',
'前一个人的问题还没回答完,等下再问吧。',
],
tooManyRequest: [
'你的提问太多了,{{{minutesLeft}}}分钟后再问吧。',
'抱歉,你的问题太多了,还需要等待{{{minutesLeft}}}分钟后才能回答。',
'请耐心等待,{{{minutesLeft}}}分钟后我将回答你的问题',
'请耐心等待{{{minutesLeft}}}分钟,然后再提出你的问题。',
'你的提问有点多,请等待{{{minutesLeft}}}分钟后再继续提问。',
],
}
}
return defaultConfig;
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({
command: 'ai',
name: '开始对话',
@ -215,9 +206,7 @@ export default class ChatGPTController implements PluginController {
// });
}
async updateConfig(config: any) {
this.config = config;
async setConfig(config: any) {
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);

@ -0,0 +1 @@
controller: "PluginController"

@ -1,12 +1,11 @@
import App from "#ibot/App";
import { CommonReceivedMessage } from "#ibot/message/Message";
import { CommandInputArgs, MessagePriority, PluginController, PluginEvent } from "#ibot/PluginManager";
import { CommandInputArgs, MessagePriority } from "#ibot/PluginManager";
import { encode as gptEncode } from 'gpt-3-encoder';
import got, { OptionsOfTextResponseBody } from "got/dist/source";
import { HttpsProxyAgent } from 'hpagent';
import { RandomMessage } from "#ibot/utils/RandomMessage";
import { ItemLimitedList } from "#ibot/utils/ItemLimitedList";
import { ChatIdentity } from "#ibot/message/Sender";
import { PluginController } from "#ibot-api/PluginController";
export type CharacterConfig = {
api_id: string,
@ -40,96 +39,85 @@ export class RWKVAPIError extends Error {
}
}
export default class RWKVRolePlayingController implements PluginController {
const defaultConfig = {
proxy: '',
api: [
{
id: 'default',
buffer_size: 100,
max_input_tokens: 1000,
endpoint: 'http://127.0.0.1:8888',
api_token: '',
model_options: {
min_len: 0,
temperature: 2,
top_p: 0.65,
presence_penalty: 0.2,
frequency_penalty: 0.2,
},
},
] as ChatCompleteApiConfig[],
characters: {
default: {
api_id: 'default',
rwkv_character: '',
bot_name: '',
}
} as CharactersConfig,
default_characters: [
{
id: 'default'
}
] as DefaultCharacterConfig[],
output_replace: {} as Record<string, string>,
rate_limit: 2,
rate_limit_minutes: 5,
messages: {
error: [
'生成对话失败: {{{error}}}',
'在回复时出现错误:{{{error}}}',
'生成对话时出现错误:{{{error}}}',
'在回答问题时出现错误:{{{error}}}',
],
generating: [
'正在回复其他人的提问',
'等我回完再问',
'等我发完再问',
'等我回完这条再问',
'等我发完这条再问',
'前一个人的问题还没回答完,等下再问吧。',
],
tooManyRequest: [
'你的提问太多了,{{{minutesLeft}}}分钟后再问吧。',
'抱歉,你的问题太多了,还需要等待{{{minutesLeft}}}分钟后才能回答。',
'请耐心等待,{{{minutesLeft}}}分钟后我将回答你的问题',
'请耐心等待{{{minutesLeft}}}分钟,然后再提出你的问题。',
'你的提问有点多,请等待{{{minutesLeft}}}分钟后再继续提问。',
],
}
};
export default class RWKVRolePlayingController extends PluginController<typeof defaultConfig> {
private SESSION_KEY_MESSAGE_COUNT = 'rwkv_rp_apiMessageCount';
private SESSION_KEY_API_CHAT_CHARACTER = 'rwkv_rp_apiChatCharacter';
private SESSION_KEY_API_RESET_LOCK = 'rwkv_rp_apiResetLock';
private SESSION_KEY_USER_TOKEN = 'rwkv_rp_userToken';
private CHARACTER_EXPIRE = 86400;
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
public event!: PluginEvent;
public app: App;
public id = 'rwkv_rp';
public name = 'RWKV Role Playing';
public description = '虚拟角色聊天AI的功能';
public static id = 'rwkv_rp';
public static pluginName = 'RWKV Role Playing';
public static description = '虚拟角色聊天AI的功能';
private globalDefaultCharacter: string = '';
private chatGenerating = false;
private messageGroup: Record<string, RandomMessage> = {};
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
proxy: '',
api: [
{
id: 'default',
buffer_size: 100,
max_input_tokens: 1000,
endpoint: 'http://127.0.0.1:8888',
api_token: '',
model_options: {
min_len: 0,
temperature: 2,
top_p: 0.65,
presence_penalty: 0.2,
frequency_penalty: 0.2,
},
},
] as ChatCompleteApiConfig[],
characters: {
default: {
api_id: 'default',
rwkv_character: '',
bot_name: '',
}
} as CharactersConfig,
default_characters: [
{
id: 'default'
}
] as DefaultCharacterConfig[],
output_replace: {} as Record<string, string>,
rate_limit: 2,
rate_limit_minutes: 5,
messages: {
error: [
'生成对话失败: {{{error}}}',
'在回复时出现错误:{{{error}}}',
'生成对话时出现错误:{{{error}}}',
'在回答问题时出现错误:{{{error}}}',
],
generating: [
'正在回复其他人的提问',
'等我回完再问',
'等我发完再问',
'等我回完这条再问',
'等我发完这条再问',
'前一个人的问题还没回答完,等下再问吧。',
],
tooManyRequest: [
'你的提问太多了,{{{minutesLeft}}}分钟后再问吧。',
'抱歉,你的问题太多了,还需要等待{{{minutesLeft}}}分钟后才能回答。',
'请耐心等待,{{{minutesLeft}}}分钟后我将回答你的问题',
'请耐心等待{{{minutesLeft}}}分钟,然后再提出你的问题。',
'你的提问有点多,请等待{{{minutesLeft}}}分钟后再继续提问。',
],
}
}
return defaultConfig;
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
async initialize() {
this.event.registerCommand({
command: '重开',
alias: ['重置聊天', 'remake'],
@ -175,9 +163,7 @@ export default class RWKVRolePlayingController implements PluginController {
return JSON.parse(Buffer.from(payload, 'base64').toString());
}
async updateConfig(config: any) {
this.config = config;
async setConfig(config: any) {
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);
@ -248,7 +234,7 @@ export default class RWKVRolePlayingController implements PluginController {
}
let characterConf = this.config.characters[character];
let apiConf = this.getApiConfigById(characterConf.api);
let apiConf = this.getApiConfigById(characterConf.api_id);
try {
const apiUserName = this.getApiUserName(message);
@ -435,7 +421,7 @@ export default class RWKVRolePlayingController implements PluginController {
}
characterConf = this.config.characters[character];
apiConf = this.getApiConfigById(characterConf.api);
apiConf = this.getApiConfigById(characterConf.api_id);
await message.session.user.set(this.SESSION_KEY_API_CHAT_CHARACTER, character, this.CHARACTER_EXPIRE);
} else {
@ -444,7 +430,7 @@ export default class RWKVRolePlayingController implements PluginController {
character = 'assistant';
}
characterConf = this.config.characters[character];
apiConf = this.getApiConfigById(characterConf.api);
apiConf = this.getApiConfigById(characterConf.api_id);
}
this.app.logger.debug(`RWKV API 收到提问。当前人格:${character}`);

@ -0,0 +1 @@
controller: "PluginController"

@ -1,22 +1,14 @@
import { PluginController } from "#ibot-api/PluginController";
import App from "#ibot/App";
import { PluginController, PluginEvent } from "#ibot/PluginManager";
import { WikiMisc } from "./wiki/WikiMisc";
import { PluginEvent } from "#ibot/PluginManager";
import { WikiMisc } from "../wiki-misc/WikiMisc";
export default class SfsettingsController implements PluginController {
public event!: PluginEvent;
public app: App;
public id = 'sfsettings';
public name = '科幻设定百科';
public description = '科幻设定百科的相关功能';
constructor(app: App) {
this.app = app;
}
export default class SfsettingsController extends PluginController {
public static id = 'sfsettings';
public static pluginName = '科幻设定百科';
public static description = '科幻设定百科的相关功能';
public async initialize(): Promise<void> {
this.event.init(this);
const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w139/api.php');
this.event.registerCommand({

@ -0,0 +1 @@
controller: "PluginController"

@ -1,22 +1,14 @@
import { PluginController } from "#ibot-api/PluginController";
import App from "#ibot/App";
import { CommonReceivedMessage, CommonSendMessage } from "#ibot/message/Message";
import { PluginController, PluginEvent } from "#ibot/PluginManager";
import { CommandInfo, PluginEvent } from "#ibot/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;
}
export default class SystemController extends PluginController {
public static id = 'system';
public static pluginName = '系统功能';
public static description = '系统功能控制器';
async initialize() {
this.event.init(this);
this.event.autoSubscribe = true;
this.event.forceSubscribe = true;
@ -33,11 +25,11 @@ export default class SystemController implements PluginController {
async handleHelp(args: string, message: CommonReceivedMessage) {
const senderInfo = this.app.event.getSenderInfo(message);
const subscribedControllers = this.app.plugin.getSubscribedControllers(senderInfo);
const subscribedPlugins = this.app.plugin.getSubscribed(senderInfo);
let replyMsg = message.createReplyMessage();
replyMsg.type = 'help';
replyMsg._context.controllers = subscribedControllers;
replyMsg._context.subscribed = subscribedPlugins;
let helpBuilder: string[] = [];
@ -49,10 +41,16 @@ export default class SystemController implements PluginController {
helpBuilder.push('功能列表:');
for (let controller of subscribedControllers) {
helpBuilder.push(`${controller.name}`);
if (controller.event.commandList.length > 0) {
controller.event.commandList.forEach(commandInfo => {
for (let subscribedItem of subscribedPlugins) {
let ctor = subscribedItem.controller.constructor as typeof PluginController;
helpBuilder.push(`${ctor.pluginName}`);
let commandList: CommandInfo[] = [];
for (let eventGroup of subscribedItem.eventGroups) {
commandList.push(...eventGroup.commandList);
}
if (commandList.length > 0) {
commandList.forEach(commandInfo => {
helpBuilder.push(`/${commandInfo.command} - ${commandInfo.name}`);
});
} else {
@ -66,7 +64,7 @@ export default class SystemController implements PluginController {
}
if (this.app.debug) {
this.app.logger.debug(`收到帮助指令,已找到 ${subscribedControllers.length} 个控制器`);
this.app.logger.debug(`收到帮助指令,已找到 ${subscribedPlugins.length} 个插件`);
}
replyMsg.content = [{

@ -0,0 +1 @@
controller: "SystemController"

@ -3,9 +3,10 @@ import App from "#ibot/App";
import { extname } from "path";
import { AttachmentMessage } from "#ibot/message/Message";
import { CommonReceivedMessage } from "#ibot/message/Message";
import { MessagePriority, PluginController, PluginEvent } from "#ibot/PluginManager";
import { MessagePriority, PluginEvent } from "#ibot/PluginManager";
import got from "got/dist/source";
import { RandomMessage } from "#ibot/utils/RandomMessage";
import { PluginController } from "#ibot-api/PluginController";
export type WebdavConfig = {
url: string,
@ -15,53 +16,43 @@ export type WebdavConfig = {
exclusive?: boolean;
};
export default class WebdavFileBackupController implements PluginController {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
const defaultConfig = {
groups: {} as Record<string, WebdavConfig>,
messages: {
error: [
'转存群文件失败:{{{error}}}',
'在转存群文件时发生了错误:{{{error}}}',
'未能将群文件转存到资料库:{{{error}}}',
'由于以下错误,文件转存失败:{{{error}}}',
'很抱歉,文件无法成功转存至群组资料库,原因是:{{{error}}}。',
'转存群组文件时出现问题,错误详情:{{{error}}}。',
'文件无法转存到资料库,错误信息如下:{{{error}}}。',
'出现错误,导致文件无法成功转存至群组资料库:{{{error}}}。',
'转存群文件遇到问题,以下是错误的详细信息:{{{error}}}。',
'文件转存失败,原因是:{{{error}}}。',
'抱歉,由于以下错误,文件未能成功转存至群组资料库:{{{error}}}。',
'在尝试将文件转存至群组资料库时,发生了如下错误:{{{error}}}。',
'文件转存操作失败,错误详情:{{{error}}}。',
]
}
};
export default class WebdavFileBackupController extends PluginController<typeof defaultConfig> {
private SESSION_KEY_GENERATE_COUNT = 'stablediffusion_generateCount';
public event!: PluginEvent;
public app: App;
public chatGPTClient: any;
public id = 'webdav_file_backup';
public name = 'Webdav文件备份';
public description = '将群文件备份到Webdav服务';
public static id = 'webdav_file_backup';
public static pluginName = 'Webdav文件备份';
public static description = '将群文件备份到Webdav服务';
private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() {
return {
groups: {} as Record<string, WebdavConfig>,
messages: {
error: [
'转存群文件失败:{{{error}}}',
'在转存群文件时发生了错误:{{{error}}}',
'未能将群文件转存到资料库:{{{error}}}',
'由于以下错误,文件转存失败:{{{error}}}',
'很抱歉,文件无法成功转存至群组资料库,原因是:{{{error}}}。',
'转存群组文件时出现问题,错误详情:{{{error}}}。',
'文件无法转存到资料库,错误信息如下:{{{error}}}。',
'出现错误,导致文件无法成功转存至群组资料库:{{{error}}}。',
'转存群文件遇到问题,以下是错误的详细信息:{{{error}}}。',
'文件转存失败,原因是:{{{error}}}。',
'抱歉,由于以下错误,文件未能成功转存至群组资料库:{{{error}}}。',
'在尝试将文件转存至群组资料库时,发生了如下错误:{{{error}}}。',
'文件转存操作失败,错误详情:{{{error}}}。',
]
}
};
return defaultConfig;
}
async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.on('message/group', async (message, resolved) => {
if (message.type !== 'attachment') return;
@ -86,8 +77,6 @@ export default class WebdavFileBackupController implements PluginController {
}
async updateConfig(config: any) {
this.config = config;
// 随机消息
for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value);

@ -0,0 +1 @@
controller: "PluginController"

@ -18,6 +18,7 @@ import { SubscribeManager, Target } from './SubscribeManager';
import { CacheManager } from './CacheManager';
import { StorageManager } from './StorageManager';
import { DatabaseManager } from './DatabaseManager';
import { Logger } from './utils/Logger';
export * from './utils/contextHooks';
@ -29,7 +30,8 @@ export default class App {
public debug: boolean = false;
public logger!: winston.Logger;
public baseLogger!: winston.Logger;
public logger!: Logger;
public event!: EventManager;
public cache!: CacheManager;
public storage!: StorageManager;
@ -42,16 +44,18 @@ export default class App {
public plugin!: PluginManager;
public restfulApi!: RestfulApiManager;
constructor(configFile: string) {
public constructor(configFile: string, initImmediate: boolean = true) {
this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' }));
this.debug = this.config.debug;
(import.meta as any)._isekaiFeedbotApp = this;
this.initialize();
if (initImmediate) {
this.initialize();
}
}
async initialize() {
public async initialize() {
await this.initModules();
await this.initRestfulApiManager();
await this.initEventManager();
@ -68,21 +72,21 @@ export default class App {
this.logger.info('初始化完成,正在接收消息');
}
async initModules() {
private async initModules() {
await Setup.initHandlebars();
// 创建Logger
const loggerFormat = winston.format.printf(({ level, message, timestamp }) => {
const loggerFormat = winston.format.printf(({ level, message, timestamp, tag }) => {
return `${timestamp} [${level}]: ${message}`;
});
this.logger = winston.createLogger({
this.baseLogger = winston.createLogger({
level: 'info',
format: winston.format.json(),
});
if (this.debug) {
this.logger.add(
this.baseLogger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
@ -95,7 +99,7 @@ export default class App {
})
);
} else {
this.logger.add(
this.baseLogger.add(
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
@ -107,80 +111,86 @@ export default class App {
})
);
}
this.logger = this.getLogger("Core");
}
async initRestfulApiManager() {
private async initRestfulApiManager() {
this.restfulApi = new RestfulApiManager(this, this.config.http_api);
await this.restfulApi.initialize();
}
async initEventManager() {
private async initEventManager() {
this.event = new EventManager(this);
await this.event.initialize();
}
async initCacheManager() {
private async initCacheManager() {
this.cache = new CacheManager(this, this.config.cache);
await this.cache.initialize();
}
async initStorageManager() {
private async initStorageManager() {
this.storage = new StorageManager(this, this.config.storage);
await this.storage.initialize();
}
async initDatabaseManager() {
private async initDatabaseManager() {
if (this.config.db) {
this.database = new DatabaseManager(this, this.config.db);
await this.database.initialize();
}
}
async initRobot() {
private async initRobot() {
this.robot = new RobotManager(this, this.config.robot);
await this.robot.initialize();
}
async initProviderManager() {
private async initProviderManager() {
this.provider = new ProviderManager(this);
await this.provider.initialize();
}
async initServiceManager() {
private async initServiceManager() {
this.service = new ServiceManager(this, this.config.service);
await this.service.initialize();
}
async initSubscribeManager() {
private async initSubscribeManager() {
this.subscribe = new SubscribeManager(this, this.config.subscribe_config);
await this.subscribe.initialize();
}
async initChannelManager() {
private async initChannelManager() {
this.channel = new ChannelManager(this, this.config.channel_config_path);
await this.channel.initialize();
}
async initPluginManager() {
private async initPluginManager() {
this.plugin = new PluginManager(this, this.config.plugin_path, this.config.plugin_config_path);
await this.plugin.initialize();
}
public getLogger(tag: string) {
return new Logger(this.baseLogger, tag);
}
/**
*
* @param serviceName
* @returns
*/
getService<T extends Service>(serviceName: string): T {
public getService<T extends Service>(serviceName: string): T {
return this.service.get<T>(serviceName);
}
createChannel(provider: string, channelId: string, config: ChannelConfig): BaseProvider | null {
public createChannel(provider: string, channelId: string, config: ChannelConfig): BaseProvider | null {
return this.provider.create(provider, channelId, config);
}
getChannelSubscriber(channelId: string, robotId: string): Target[] | null {
public getChannelSubscriber(channelId: string, robotId: string): Target[] | null {
return this.subscribe.getSubscriber('channel:' + channelId, robotId);
}
@ -190,7 +200,7 @@ export default class App {
* @param messages
* @returns
*/
async sendPushMessage(channelId: string, messages: MultipleMessage): Promise<void> {
public async sendPushMessage(channelId: string, messages: MultipleMessage): Promise<void> {
this.logger.info(`[${channelId}] 消息: `, messages);
console.log(messages);
this.robot.sendPushMessage(channelId, messages);

@ -5,6 +5,7 @@ import { CommonReceivedMessage } from "./message/Message";
import { ChatIdentity } from "./message/Sender";
import { CommandInfo, CommandInputArgs, EventScope, MessageEventOptions, MessagePriority, PluginEvent } from "./PluginManager";
import { Robot } from "./robot/Robot";
import { SubscribeItem } from "./SubscribeManager";
import { Reactive } from "./utils/reactive";
export type ControllerEventInfo = {
@ -131,11 +132,11 @@ export class EventManager {
delete this.commandList[alias.toLocaleLowerCase()];
});
}
} else if (typeof args[0] !== 'undefined') {
let eventScope = args[0];
} else if (typeof args[0] !== 'undefined' && args[0].pluginId) {
let eventScope: PluginEvent = args[0];
this.commandInfoList = this.commandInfoList.filter((commandInfoItem) => commandInfoItem.eventScope !== eventScope);
for (let command in this.commandList) {
if (this.commandList[command].eventScope.controller?.id === eventScope.controller?.id) {
if (this.commandList[command].eventScope.pluginId === eventScope.pluginId) {
delete this.commandList[command];
}
}
@ -163,7 +164,7 @@ export class EventManager {
};
const buildOnError = (eventInfo: ControllerEventInfo) => (error: Error) => {
this.app.logger.error(`${eventInfo.eventScope.controller?.id} 处理事件 ${eventName} 时出错`, error);
this.app.logger.error(`[${eventInfo.eventScope.pluginId}/${eventInfo.eventScope.scopeName}] 处理事件 ${eventName} 时出错`, error);
console.error(error);
for (let arg of args) {
@ -180,7 +181,7 @@ export class EventManager {
}
}
let [subscribedControllers, disabledControllers] = this.getControllerSubscribe(senderInfo);
let [subscribedPlugins, disabledPlugins] = this.getPluginSubscribe(senderInfo);
for (let eventInfo of eventList) {
if (!isFilter && senderInfo) {
@ -206,18 +207,19 @@ export class EventManager {
}
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
if (eventInfo.eventScope.autoSubscribe) {
if (!eventInfo.eventScope.isAllowSubscribe(senderInfo)) {
const eventScope = eventInfo.eventScope;
if (eventScope.autoSubscribe) {
if (!eventScope.isAllowSubscribe(senderInfo)) {
continue;
} else {
// 检测控制器是否已禁用
if (!eventInfo.eventScope.controller || disabledControllers.includes(eventInfo.eventScope.controller.id)) {
if (this.isPluginScopeInList(eventScope.pluginId, eventScope.scopeName, disabledPlugins)) {
continue;
}
}
} else {
// 检测控制器是否已启用
if (!eventInfo.eventScope.controller || !subscribedControllers.includes(eventInfo.eventScope.controller.id)) {
if (!this.isPluginScopeInList(eventScope.pluginId, eventScope.scopeName, subscribedPlugins)) {
continue;
}
}
@ -347,9 +349,9 @@ export class EventManager {
}
}
public getControllerSubscribe(senderInfo?: ChatIdentity | null): [string[], string[]] {
let subscribedCommands: string[] = [];
let disabledCommands: string[] = [];
public getPluginSubscribe(senderInfo?: ChatIdentity | null): [SubscribeItem[], SubscribeItem[]] {
let subscribedCommands: SubscribeItem[] = [];
let disabledCommands: SubscribeItem[] = [];
if (senderInfo) {
let targetType = '';
@ -378,6 +380,12 @@ export class EventManager {
];
}
public isPluginScopeInList(pluginId: string, scopeName: string, scopeList: SubscribeItem[]): boolean {
return scopeList.some((scope) =>
scope[0] === pluginId && (scope[1] === "*" || scope[1] === scopeName)
);
}
private sortEvent(eventName: string) {
if (this.eventSortDebounce[eventName]) {
return;

@ -11,6 +11,8 @@ import { ChatIdentity } from "./message/Sender";
import { Utils } from "./utils/Utils";
import { Robot } from "./robot/Robot";
import { Reactive } from "./utils/reactive";
import { PluginController } from "#ibot-api/PluginController";
import { PluginApiBridge } from "./plugin/PluginApiBridge";
export const MessagePriority = {
LOWEST: 0,
@ -60,6 +62,19 @@ export type RawEventCallback = (robot: Robot, event: any, resolved: VoidFunction
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 {
private app: App;
private pluginPath: string;
@ -67,9 +82,9 @@ export class PluginManager extends EventEmitter {
private watcher!: chokidar.FSWatcher;
private configWatcher!: chokidar.FSWatcher;
public controllers: Record<string, PluginController>;
public fileControllers: Record<string, PluginController>;
public configControllers: Record<string, PluginController>;
public pluginInstanceMap: Record<string, PluginInstance> = {};
public configPluginMap: Record<string, string> = {};
constructor(app: App, pluginPath: string, configPath: string) {
super();
@ -77,67 +92,102 @@ export class PluginManager extends EventEmitter {
this.app = app;
this.pluginPath = path.resolve(pluginPath);
this.configPath = path.resolve(configPath);
this.controllers = {};
this.fileControllers = {};
this.configControllers = {};
this.pluginInstanceMap = {};
}
/**
* Controllers
*/
async initialize() {
this.watcher = chokidar.watch(this.pluginPath, {
ignored: '*.bak',
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.watcher = chokidar.watch(this.pluginPath + "/**/*.js", {
// ignorePermissionErrors: true,
// persistent: true,
// followSymlinks: true,
// depth: 1
// });
// this.watcher.on('add', this.onPluginFileAdded.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/*.yaml', {
ignorePermissionErrors: true,
persistent: true
});
this.configWatcher.on('change', this.reloadConfig.bind(this));
}
async loadController(file: string) {
if (!file.match(/Controller\.m?js$/)) return;
let moduleName = path.resolve(file).replace(/\\/g, '/').replace(/\.m?js$/, '');
async loadPlugin(folder: string) {
folder = path.resolve(folder);
this.app.logger.debug('尝试从 ' + folder + ' 加载插件');
const pluginIndexFile = path.join(folder, 'plugin.yaml');
if (!fs.existsSync(pluginIndexFile)) return;
let pluginId = '';
try {
const controller = await import(moduleName);
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) {
const controllerClass = controller.default ?? controller;
const controllerInstance: PluginController = new controllerClass(this.app);
if (controllerInstance.id && controllerInstance.id !== '') {
const controllerId = controllerInstance.id;
const controllerClass: typeof PluginController = controller.default ?? controller;
if (controllerClass.id) {
pluginId = controllerClass.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;
if (controllerId in this.controllers) {
if (pluginId in this.pluginInstanceMap) {
// Reload plugin
isReload = true;
await this.removeController(file, true);
await this.unloadPlugin(pluginId, true);
}
this.controllers[controllerId] = controllerInstance;
this.fileControllers[file] = controllerInstance;
this.pluginInstanceMap[pluginId] = pluginInstance;
if (isReload) {
this.app.logger.info(`已重新加载Controller: ${file}`);
this.emit('controllerReloaded', controllerInstance);
this.app.logger.info(`已重新加载插件: ${pluginId}`);
this.emit('pluginReloaded', controllerInstance);
} else {
this.app.logger.info(`已加载Controller: ${file}`);
this.emit('controllerLoaded', controllerInstance);
this.app.logger.info(`已加载插件: ${pluginId}`);
this.emit('pluginLoaded', controllerInstance);
}
const pluginEvent = new PluginEvent(this.app);
controllerInstance.event = pluginEvent;
const controllerConfig = await this.loadMainConfig(pluginId, controllerInstance);
const controllerConfig = await this.loadControllerConfig('standalone', controllerInstance);
await controllerInstance.initialize(controllerConfig);
await controllerInstance._initialize(controllerConfig);
} else {
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.');
}
} catch(err: any) {
console.error(`加载Controller失败: ${file}`);
console.error(`加载插件失败: ${folder}`);
console.error(err);
if (pluginId && this.pluginInstanceMap[pluginId]) {
delete this.pluginInstanceMap[pluginId];
}
}
}
async removeController(file: string, isReload = false) {
const controller = this.fileControllers[file];
if (controller) {
const configFile = this.getConfigFile('standalone', controller);
async unloadPlugin(pluginId: string, isReload = false) {
const instance = this.pluginInstanceMap[pluginId];
if (instance) {
const configFile = this.getConfigFile(pluginId);
await instance.bridge.destroy();
await instance.controller.destroy?.();
await controller.event.destroy();
await controller.destroy?.();
delete this.pluginInstanceMap[pluginId];
delete this.controllers[file];
delete this.fileControllers[file];
if (configFile in this.configControllers) {
delete this.configControllers[configFile];
if (configFile in this.configPluginMap) {
delete this.configPluginMap[configFile];
}
this.emit('controllerRemoved', controller);
this.emit('pluginUnloaded', instance);
if (!isReload) {
this.app.logger.info(`移除Controller: ${controller.id}`);
this.app.logger.info(`关闭插件: ${pluginId}`);
}
}
}
getConfigFile(pluginId: string, controller: PluginController) {
return path.resolve(this.configPath, pluginId, controller.id + '.yml');
async reloadPlugin(pluginId: string) {
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) {
const configFile = this.getConfigFile(pluginId, controller);
getConfigFile(pluginId: string) {
return path.resolve(this.configPath, "plugin", pluginId + '.yaml');
}
async loadMainConfig(pluginId: string, controller: PluginController) {
const configFile = this.getConfigFile(pluginId);
try {
if (configFile in this.configControllers) { // 防止保存时触发重载
delete this.configControllers[configFile];
if (configFile in this.configPluginMap) { // 防止保存时触发重载
delete this.configPluginMap[configFile];
}
const defaultConfig = await controller.getDefaultConfig?.() ?? {};
@ -203,124 +276,128 @@ export class PluginManager extends EventEmitter {
}
setTimeout(() => {
this.configControllers[configFile] = controller;
this.configPluginMap[configFile] = pluginId;
}, 1000);
return config;
} catch(err: any) {
this.app.logger.error(`加载Controller配置失败: ${configFile}`, err);
this.app.logger.error(`加载插件主配置文件失败: ${configFile}`, err);
console.error(err);
}
}
async reloadConfig(file: string) {
this.app.logger.info(`配置文件已更新: ${file}`);
if (file in this.configControllers) {
if (file in this.configPluginMap) {
const pluginId = this.configPluginMap[file];
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);
const pluginInstance = this.pluginInstanceMap[pluginId];
if (pluginInstance) {
const ctor = pluginInstance.controller.constructor as typeof PluginController;
if (ctor.reloadWhenConfigUpdated) { // 重载整个控制器
await this.reloadPlugin(pluginId);
return;
}
const localConfig = Yaml.parse(await fsAsync.readFile(file, 'utf-8'));
await pluginInstance.controller._setConfig(localConfig);
this.app.logger.info(`已重载插件配置文件: ${pluginId}`);
}
} catch(err: any) {
this.app.logger.error(`重载Controller配置失败: ${file}`, err);
this.app.logger.error(`重载插件 [${pluginId}] 配置失败: ${file}`, err);
console.error(err);
}
}
}
/**
*
*
* @param senderInfo
* @returns
*/
public getSubscribedControllers(senderInfo: ChatIdentity): PluginController[] {
let [subscribedControllers, disabledControllers] = this.app.event.getControllerSubscribe(senderInfo);
public getSubscribed(senderInfo: ChatIdentity): SubscribedPluginInfo[] {
let [subscribedScopes, disabledScopes] = this.app.event.getPluginSubscribe(senderInfo);
return Object.values(this.controllers).filter((controller) => {
if (controller.event.commandList.length === 0) return false;
let subscribed: SubscribedPluginInfo[] = [];
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) {
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 (eventGroup.commandList.length === 0) continue;
switch (senderInfo.type) {
case 'private':
if (!eventGroup.allowPrivate) {
continue;
}
if (!eventGroup.isAllowSubscribe(senderInfo)) {
continue;
}
break;
case 'group':
if (!eventGroup.allowGroup) {
continue;
}
break;
case 'channel':
if (!eventGroup.allowChannel) {
continue;
}
break;
}
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
if (controller.event.autoSubscribe) {
if (!controller.event.isAllowSubscribe(senderInfo)) {
return false;
if (senderInfo.type !== 'private') { // 私聊消息不存在订阅,只判断群消息和频道消息
if (eventGroup.autoSubscribe) {
if (!eventGroup.isAllowSubscribe(senderInfo)) {
continue;
} else {
// 检测控制器是否已禁用
if (this.app.event.isPluginScopeInList(pluginInstance.id, scopeName, disabledScopes)) {
continue;
}
}
} else {
// 检测控制器是否已禁用
if (disabledControllers.includes(controller.id)) {
return false;
// 检测控制器是否已
if (!this.app.event.isPluginScopeInList(pluginInstance.id, scopeName, subscribedScopes)) {
continue;
}
}
} else {
// 检测控制器是否已启用
if (!subscribedControllers.includes(controller.id)) {
return false;
}
}
}
return true;
});
}
}
export interface PluginController {
id: string;
name: string;
description?: string;
event: PluginEvent;
eventGroups.push(eventGroup);
}
initialize: (config: any) => Promise<void>;
destroy?: () => Promise<void>;
if (eventGroups.length > 0) {
subscribed.push({
id: pluginInstance.id,
controller: pluginInstance.controller,
eventGroups: eventGroups
});
}
}
getDefaultConfig?: () => Promise<any>;
updateConfig?: (config: any) => Promise<void>;
return subscribed;
}
}
export class EventScope {
protected app: App;
protected eventManager: EventManager;
public pluginId: string;
public scopeName: string;
public commandList: CommandInfo[] = [];
public eventList: Record<string, EventListenerInfo[]> = {};
public eventSorted: Record<string, boolean> = {};
constructor(app: App) {
constructor(app: App, pluginId: string, scopeName: string) {
this.app = app;
this.eventManager = app.event;
this.pluginId = pluginId;
this.scopeName = scopeName;
}
/**
@ -515,8 +592,6 @@ export class EventScope {
}
export class PluginEvent extends EventScope {
public controller?: PluginController;
public autoSubscribe = false;
public forceSubscribe = false;
public showInSubscribeList = true;
@ -552,10 +627,6 @@ export class PluginEvent extends EventScope {
return true;
}
public init(controller: PluginController) {
this.controller = controller;
}
/**
* Destroy eventGroup.
* Will remove all event listeners.

@ -19,6 +19,8 @@ export type SubscribeConfig = {
}
}
export type SubscribeItem = [string, string];
/**
*
*/
@ -164,7 +166,12 @@ export class SubscribeManager {
}
}
public getSubscribedList(robotId: string, targetType: string, targetId: string, sourceType: string): string[] {
return this.subscribeConfig?.[robotId]?.[targetType]?.[targetId]?.[sourceType] ?? [];
public getSubscribedList(robotId: string, targetType: string, targetId: string, sourceType: string): SubscribeItem[] {
let rawSubscribeList = this.subscribeConfig?.[robotId]?.[targetType]?.[targetId]?.[sourceType] ?? [];
return rawSubscribeList.map((sourceStr) => {
const t = sourceStr.split(':');
return [t[0], t[1] ?? '*'];
});
}
}

@ -1,9 +1,64 @@
import { PluginController } from "#ibot-api/PluginController";
import App from "#ibot/App";
import { PluginEvent } from "#ibot/PluginManager";
export const MAIN_SCOPE_NAME = "main";
export class PluginApiBridge {
private app!: App;
private app: App;
private _pluginId: string;
private _controller!: PluginController;
private currentScope: string | null = null;
public scopedEvent: Record<string, PluginEvent> = {};
constructor(app: App) {
constructor(app: App, pluginId: string) {
this.app = app;
this._pluginId = pluginId;
this.scopedEvent[MAIN_SCOPE_NAME] = new PluginEvent(this.app, pluginId, "main");
}
get event() {
if (this.currentScope && this.scopedEvent[this.currentScope]) {
return this.scopedEvent[this.currentScope];
} else {
return this.scopedEvent[MAIN_SCOPE_NAME];
}
}
get mainEvent() {
return this.scopedEvent[MAIN_SCOPE_NAME];
}
get pluginId() {
return this._pluginId;
}
get controller() {
return this._controller;
}
public setController(controller: PluginController) {
this._controller = controller;
}
public async destroy() {
// Remove all event listeners
for (const scope in this.scopedEvent) {
await this.scopedEvent[scope].destroy();
}
}
public async getDataPath(creation: boolean = false) {
}
public useScope(scopeName: string, callback: (event: PluginEvent) => void) {
let newScopeEvent = new PluginEvent(this.app, this._pluginId, scopeName);
this.scopedEvent[scopeName] = newScopeEvent;
this.currentScope = scopeName;
callback(newScopeEvent);
this.currentScope = null;
}
}

@ -8,10 +8,11 @@ import { Utils } from "../../utils/Utils";
import { FullRestfulContext, RestfulApiManager, RestfulRouter } from "../../RestfulApiManager";
import { convertMessageToQQChunk, parseQQMessageChunk, QQAttachmentMessage, QQGroupMessage, QQGroupSender, QQPrivateMessage, QQUserSender } from "./qq/Message";
import { CommonReceivedMessage, CommonSendMessage, MessageChunk } from "../../message/Message";
import { PluginController } from "../../PluginManager";
import { RobotConfig } from "../../Config";
import { ChatIdentity } from "../../message/Sender";
import { QQInfoProvider } from "./qq/InfoProvider";
import { CommandInfo, SubscribedPluginInfo } from "#ibot/PluginManager";
import { PluginController } from "#ibot-api/PluginController";
export type QQRobotConfig = RobotConfig & {
userId: string;
@ -123,7 +124,7 @@ export default class QQRobot implements RobotAdapter {
this.infoProvider.getGroupUsersInfo(userIds, groupId, rootGroupId);
async parseHelpMessage(message: CommonSendMessage) {
const controllers = (message._context.controllers ?? []) as PluginController[];
const subscribedPlugins = (message._context.subscribed ?? []) as SubscribedPluginInfo[];
let helpBuilder: string[] = [];
if (this.description) {
@ -136,10 +137,16 @@ export default class QQRobot implements RobotAdapter {
);
const mainCommandPrefix = this.wrapper.commandPrefix[0];
for (let controller of controllers) {
helpBuilder.push(`${controller.name}`);
if (controller.event.commandList.length > 0) {
controller.event.commandList.forEach(commandInfo => {
for (let subscribedItem of subscribedPlugins) {
let ctor = subscribedItem.controller.constructor as typeof PluginController;
helpBuilder.push(`${ctor.pluginName}`);
let commandList: CommandInfo[] = [];
for (let eventGroup of subscribedItem.eventGroups) {
commandList.push(...eventGroup.commandList);
}
if (commandList.length > 0) {
commandList.forEach(commandInfo => {
helpBuilder.push(`${mainCommandPrefix}${commandInfo.command} - ${commandInfo.name}`);
});
} else {

@ -52,32 +52,37 @@ export class QQInfoProvider {
async refreshRobotInfo() {
// 刷新群信息
let remoteGroupList = await this.getGroupList();
remoteGroupList.forEach((data) => {
if (data.group_id) {
let oldGroupIndex = this.groupList.findIndex((info) => info.groupId === data.group_id);
const groupInfo: QQGroupInfo = {
groupId: data.group_id,
groupName: data.group_name,
memberCount: data.member_count,
memberLimit: data.max_member_count
}
try {
let remoteGroupList = await this.getGroupList();
remoteGroupList.forEach((data) => {
if (data.group_id) {
let oldGroupIndex = this.groupList.findIndex((info) => info.groupId === data.group_id);
const groupInfo: QQGroupInfo = {
groupId: data.group_id,
groupName: data.group_name,
memberCount: data.member_count,
memberLimit: data.max_member_count
}
if (oldGroupIndex !== -1) {
const oldGroupInfo = this.groupList[oldGroupIndex];
if (compareProps(oldGroupInfo, groupInfo, ['groupName', 'memberCount', 'memberLimit'])) {
return;
if (oldGroupIndex !== -1) {
const oldGroupInfo = this.groupList[oldGroupIndex];
if (compareProps(oldGroupInfo, groupInfo, ['groupName', 'memberCount', 'memberLimit'])) {
return;
}
this.groupList[oldGroupIndex] = groupInfo;
} else {
this.groupList.push(groupInfo);
}
this.groupList[oldGroupIndex] = groupInfo;
} else {
this.groupList.push(groupInfo);
this.updateGroupInfo(groupInfo);
}
this.updateGroupInfo(groupInfo);
}
});
});
} catch (err: any) {
this.app.logger.error(`获取群列表失败: ${err.message}`);
console.error(err);
}
}
public saveMessage<T extends CommonMessage>(message: T): Reactive<T> {

@ -177,7 +177,7 @@ export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], mess
case 'reply':
if (chunkData.data?.id) {
message.repliedId = chunkData.data.id;
willIgnoreMention = true; // 忽略下一个“@”
// willIgnoreMention = true; // 忽略下一个“@”
}
break;
case 'json':
@ -318,10 +318,10 @@ export async function convertMessageToQQChunk(message: CommonSendMessage) {
// type: 'text',
// data: { text: ' ' }
// });
msgChunk.unshift({
type: 'at',
data: { qq: message.repliedMessage.sender.userId }
});
// msgChunk.unshift({
// type: 'at',
// data: { qq: message.repliedMessage.sender.userId }
// });
// msgChunk.unshift({
// type: 'text',
// data: { text: ' ' }

@ -0,0 +1,51 @@
import winston from "winston";
export class Logger {
private _logger: winston.Logger;
private tag: string;
constructor(baseLogger: winston.Logger, tag: string) {
this._logger = baseLogger;
this.tag = tag;
}
public debug(message: string, ...meta: any[]) {
this._logger.debug(message, { tag: this.tag }, ...meta);
}
public warn(message: string, ...meta: any[]) {
this._logger.warn(message, { tag: this.tag }, ...meta);
}
public info(message: string, ...meta: any[]) {
this._logger.info(message, { tag: this.tag }, ...meta);
}
public error(message: string, error?: Error, ...meta: any[]): void
public error(error?: Error | unknown, ...meta: any[]): void
public error(...args: any[]): void {
if (args.length === 0) return;
let message = 'Error';
let error: Error | undefined;
let metaOffset = 1;
if (args[0] instanceof Error) {
message = 'Error: ' + args[0].message;
error = args[0];
} else if (typeof args[0] === 'string') {
message = args[0];
}
if (args[1] instanceof Error) {
error = args[1];
metaOffset = 2;
}
let meta = args.slice(metaOffset);
this._logger.error(message, { tag: this.tag, stack: error?.stack }, ...meta);
if (error) {
this._logger.error(error);
}
}
}

@ -1,11 +1,11 @@
import winston from "winston";
import App from "../App";
import { Logger } from './Logger';
export function useApp(): App {
return (import.meta as any)._isekaiFeedbotApp;
}
export function useLogger(): winston.Logger {
export function useLogger(): Logger {
return useApp().logger;
}

@ -30,7 +30,7 @@
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": {
"#ibot/*": ["./src/server/*"],
"#ibot-api/*": ["./src/devkit/*"],
"#ibot-api/*": ["./src/api/*"],
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */

Loading…
Cancel
Save