完成插件系统的重写

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

4
.gitignore vendored

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

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

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

@ -2,7 +2,7 @@ var fs = require('fs');
var Yaml = require('yaml'); var Yaml = require('yaml');
var Pusher = require('pusher'); 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({ var pusher = new Pusher({
appId: config.service.pusher.app_id, appId: config.service.pusher.app_id,

@ -1,28 +1,64 @@
import App from "#ibot/App"; import App from "#ibot/App";
import { PluginEvent } from "#ibot/PluginManager";
import { PluginApiBridge } from "#ibot/plugin/PluginApiBridge"; import { PluginApiBridge } from "#ibot/plugin/PluginApiBridge";
import { Logger } from "#ibot/utils/Logger";
export class PluginController<ConfigType = Record<string, string>> { export class PluginController<ConfigType = Record<string, string>> {
static id?: string; public static id: string;
static pluginName?: string; public static pluginName?: string;
static pluginNameMsg?: string; public static description?: string;
static description?: string;
static descriptionMsg?: string;
public _app!: App; public static reloadWhenConfigUpdated?: boolean;
private _config!: ConfigType;
private _app: App;
private _logger: Logger;
private _bridge: PluginApiBridge;
public config!: ConfigType;
constructor(app: App, pluginApi: PluginApiBridge) { constructor(app: App, pluginApi: PluginApiBridge) {
this._app = app; this._app = app;
this._bridge = pluginApi;
const ctor = this.constructor as typeof PluginController;
this._logger = app.getLogger(ctor.pluginName ?? "Plugin");
} }
public get app() { public get app() {
return this._app; return this._app;
} }
public getLoagger() { public get logger() {
return this._logger;
}
public get event() {
return this._bridge.event;
} }
public getMessage(msgId: string) { 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 App from "#ibot/App";
import { CommonReceivedMessage, ImageMessage } from "#ibot/message/Message"; 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"; import got from "got/dist/source";
export type QueueData = { export type QueueData = {
@ -40,18 +41,28 @@ export type GPUInfoResponse = {
temperature: number, temperature: number,
} }
export default class StableDiffusionController implements PluginController { const defaultConfig = {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>; 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'; private SESSION_KEY_GENERATE_COUNT = 'stablediffusion_generateCount';
public event!: PluginEvent;
public app: App;
public chatGPTClient: any; public chatGPTClient: any;
public id = 'stablediffusion'; public static id = 'stablediffusion';
public name = 'Stable Diffusion'; public static pluginName = 'Stable Diffusion';
public description = '绘画生成'; public static description = '绘画生成';
private mainApi!: ApiConfig; private mainApi!: ApiConfig;
private defaultSize!: SizeConfig; private defaultSize!: SizeConfig;
@ -63,31 +74,11 @@ export default class StableDiffusionController implements PluginController {
private sizeMatcher: RegExp[][] = []; private sizeMatcher: RegExp[][] = [];
private bannedWordsMatcher: RegExp[] = []; private bannedWordsMatcher: RegExp[] = [];
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() { async getDefaultConfig() {
return { 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: ""
}
};
} }
async initialize(config: any) { async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({ this.event.registerCommand({
command: 'draw', command: 'draw',
name: '使用英语短句或关键词生成绘画', name: '使用英语短句或关键词生成绘画',
@ -123,9 +114,7 @@ export default class StableDiffusionController implements PluginController {
this.running = false; this.running = false;
} }
async updateConfig(config: any) { async setConfig(config: any) {
this.config = config;
let mainApi = this.config.api.find(api => api.main); let mainApi = this.config.api.find(api => api.main);
if (!mainApi) { if (!mainApi) {
throw new Error('No main API found in stablediffusion config.'); 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 { CommonReceivedMessage } from "#ibot/message/Message";
import App from "#ibot/App"; import App from "#ibot/App";
import { CommandInputArgs, PluginController, PluginEvent } from "#ibot/PluginManager"; import { CommandInputArgs, PluginEvent } from "#ibot/PluginManager";
import { PluginController } from "#ibot-api/PluginController";
export default class DiceController implements PluginController { import { label } from "#ibot-api/dataWrapper";
public event!: PluginEvent;
public app: App; export const defaultConfig = {
messages: {
public id = 'dice'; diceFormatError: [
public name = 'DND骰子'; '骰子格式错误:{{{error}}}',
public description = '骰一个DND骰子格式1d6+3'; '输入错误:{{{error}}}',
'错误的骰子格式:{{{error}}}',
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>; ]
constructor(app: App) {
this.app = app;
} }
};
export default class DiceController extends PluginController<typeof defaultConfig> {
public static id = 'dice';
public static pluginName = 'DND骰子';
public static description = '骰一个DND骰子格式1d6+3';
async getDefaultConfig() { async getDefaultConfig() {
return { return defaultConfig;
messages: {
diceFormatError: [
'骰子格式错误:{{{error}}}',
'输入错误:{{{error}}}',
'错误的骰子格式:{{{error}}}',
]
}
};
} }
public async initialize(config: any): Promise<void> { public async initialize(config: any): Promise<void> {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({ this.event.registerCommand({
command: 'roll', command: 'roll',
name: 'DND骰子', 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) { private async rollDice(args: CommandInputArgs, message: CommonReceivedMessage) {
await message.markRead(); await message.markRead();

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

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

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

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

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

@ -1,6 +1,6 @@
import App from "#ibot/App"; import App from "#ibot/App";
import { CommonReceivedMessage } from "#ibot/message/Message"; 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 { encode as gptEncode } from 'gpt-3-encoder';
import got, { OptionsOfTextResponseBody } from "got/dist/source"; import got, { OptionsOfTextResponseBody } from "got/dist/source";
import { HttpsProxyAgent } from 'hpagent'; import { HttpsProxyAgent } from 'hpagent';
@ -10,6 +10,7 @@ import { RandomMessage } from "#ibot/utils/RandomMessage";
import { MessageTypingSimulator } from "#ibot/utils/MessageTypingSimulator"; import { MessageTypingSimulator } from "#ibot/utils/MessageTypingSimulator";
import OpenCC from 'opencc'; import OpenCC from 'opencc';
import { PluginController } from "#ibot-api/PluginController";
export type ChatGPTLogMessage = { export type ChatGPTLogMessage = {
role: 'summary' | 'assistant' | 'user', 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_API_CHAT_LOG = 'openai_apiChatLog';
private SESSION_KEY_MESSAGE_COUNT = 'openai_apiMessageCount'; private SESSION_KEY_MESSAGE_COUNT = 'openai_apiMessageCount';
private SESSION_KEY_API_CHAT_CHARACTER = 'openai_apiChatCharacter'; private SESSION_KEY_API_CHAT_CHARACTER = 'openai_apiChatCharacter';
private DEFAULT_CHARACTER = 'assistant'; private DEFAULT_CHARACTER = 'assistant';
private CHARACTER_EXPIRE = 86400; private CHARACTER_EXPIRE = 86400;
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>;
public event!: PluginEvent;
public app: App;
public chatGPTClient: any; public chatGPTClient: any;
public id = 'openai'; public static id = 'openai';
public name = 'OpenAI'; public static pluginName = 'OpenAI';
public description = '对话AI的功能'; public static description = '对话AI的功能';
private chatGenerating = false; private chatGenerating = false;
private messageGroup: Record<string, RandomMessage> = {} private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() { async getDefaultConfig() {
return { return 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}}}分钟后再继续提问。',
],
}
}
} }
async initialize(config: any) { async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({ this.event.registerCommand({
command: 'ai', command: 'ai',
name: '开始对话', name: '开始对话',
@ -215,9 +206,7 @@ export default class ChatGPTController implements PluginController {
// }); // });
} }
async updateConfig(config: any) { async setConfig(config: any) {
this.config = config;
// 随机消息 // 随机消息
for (let [key, value] of Object.entries(this.config.messages)) { for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value); 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 { 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 { encode as gptEncode } from 'gpt-3-encoder';
import got, { OptionsOfTextResponseBody } from "got/dist/source"; import got, { OptionsOfTextResponseBody } from "got/dist/source";
import { HttpsProxyAgent } from 'hpagent'; import { HttpsProxyAgent } from 'hpagent';
import { RandomMessage } from "#ibot/utils/RandomMessage"; import { RandomMessage } from "#ibot/utils/RandomMessage";
import { ItemLimitedList } from "#ibot/utils/ItemLimitedList";
import { ChatIdentity } from "#ibot/message/Sender"; import { ChatIdentity } from "#ibot/message/Sender";
import { PluginController } from "#ibot-api/PluginController";
export type CharacterConfig = { export type CharacterConfig = {
api_id: string, 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_MESSAGE_COUNT = 'rwkv_rp_apiMessageCount';
private SESSION_KEY_API_CHAT_CHARACTER = 'rwkv_rp_apiChatCharacter'; private SESSION_KEY_API_CHAT_CHARACTER = 'rwkv_rp_apiChatCharacter';
private SESSION_KEY_API_RESET_LOCK = 'rwkv_rp_apiResetLock'; private SESSION_KEY_API_RESET_LOCK = 'rwkv_rp_apiResetLock';
private SESSION_KEY_USER_TOKEN = 'rwkv_rp_userToken'; private SESSION_KEY_USER_TOKEN = 'rwkv_rp_userToken';
private CHARACTER_EXPIRE = 86400; private CHARACTER_EXPIRE = 86400;
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>; public static id = 'rwkv_rp';
public static pluginName = 'RWKV Role Playing';
public event!: PluginEvent; public static description = '虚拟角色聊天AI的功能';
public app: App;
public id = 'rwkv_rp';
public name = 'RWKV Role Playing';
public description = '虚拟角色聊天AI的功能';
private globalDefaultCharacter: string = ''; private globalDefaultCharacter: string = '';
private chatGenerating = false; private chatGenerating = false;
private messageGroup: Record<string, RandomMessage> = {}; private messageGroup: Record<string, RandomMessage> = {};
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() { async getDefaultConfig() {
return { return 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}}}分钟后再继续提问。',
],
}
}
} }
async initialize(config: any) { async initialize() {
await this.updateConfig(config);
this.event.init(this);
this.event.registerCommand({ this.event.registerCommand({
command: '重开', command: '重开',
alias: ['重置聊天', 'remake'], alias: ['重置聊天', 'remake'],
@ -175,9 +163,7 @@ export default class RWKVRolePlayingController implements PluginController {
return JSON.parse(Buffer.from(payload, 'base64').toString()); return JSON.parse(Buffer.from(payload, 'base64').toString());
} }
async updateConfig(config: any) { async setConfig(config: any) {
this.config = config;
// 随机消息 // 随机消息
for (let [key, value] of Object.entries(this.config.messages)) { for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value); this.messageGroup[key] = new RandomMessage(value);
@ -248,7 +234,7 @@ export default class RWKVRolePlayingController implements PluginController {
} }
let characterConf = this.config.characters[character]; let characterConf = this.config.characters[character];
let apiConf = this.getApiConfigById(characterConf.api); let apiConf = this.getApiConfigById(characterConf.api_id);
try { try {
const apiUserName = this.getApiUserName(message); const apiUserName = this.getApiUserName(message);
@ -435,7 +421,7 @@ export default class RWKVRolePlayingController implements PluginController {
} }
characterConf = this.config.characters[character]; 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); await message.session.user.set(this.SESSION_KEY_API_CHAT_CHARACTER, character, this.CHARACTER_EXPIRE);
} else { } else {
@ -444,7 +430,7 @@ export default class RWKVRolePlayingController implements PluginController {
character = 'assistant'; character = 'assistant';
} }
characterConf = this.config.characters[character]; characterConf = this.config.characters[character];
apiConf = this.getApiConfigById(characterConf.api); apiConf = this.getApiConfigById(characterConf.api_id);
} }
this.app.logger.debug(`RWKV API 收到提问。当前人格:${character}`); 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 App from "#ibot/App";
import { PluginController, PluginEvent } from "#ibot/PluginManager"; import { PluginEvent } from "#ibot/PluginManager";
import { WikiMisc } from "./wiki/WikiMisc"; import { WikiMisc } from "../wiki-misc/WikiMisc";
export default class SfsettingsController implements PluginController { export default class SfsettingsController extends PluginController {
public event!: PluginEvent; public static id = 'sfsettings';
public app: App; public static pluginName = '科幻设定百科';
public static description = '科幻设定百科的相关功能';
public id = 'sfsettings';
public name = '科幻设定百科';
public description = '科幻设定百科的相关功能';
constructor(app: App) {
this.app = app;
}
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
this.event.init(this);
const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w139/api.php'); const wikiMisc = new WikiMisc(this.app, 'https://www.sfsettings.com/w139/api.php');
this.event.registerCommand({ this.event.registerCommand({

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

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

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

@ -3,9 +3,10 @@ import App from "#ibot/App";
import { extname } from "path"; import { extname } from "path";
import { AttachmentMessage } from "#ibot/message/Message"; import { AttachmentMessage } from "#ibot/message/Message";
import { CommonReceivedMessage } 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 got from "got/dist/source";
import { RandomMessage } from "#ibot/utils/RandomMessage"; import { RandomMessage } from "#ibot/utils/RandomMessage";
import { PluginController } from "#ibot-api/PluginController";
export type WebdavConfig = { export type WebdavConfig = {
url: string, url: string,
@ -15,53 +16,43 @@ export type WebdavConfig = {
exclusive?: boolean; exclusive?: boolean;
}; };
export default class WebdavFileBackupController implements PluginController { const defaultConfig = {
private config!: Awaited<ReturnType<typeof this.getDefaultConfig>>; 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'; private SESSION_KEY_GENERATE_COUNT = 'stablediffusion_generateCount';
public event!: PluginEvent;
public app: App;
public chatGPTClient: any; public chatGPTClient: any;
public id = 'webdav_file_backup'; public static id = 'webdav_file_backup';
public name = 'Webdav文件备份'; public static pluginName = 'Webdav文件备份';
public description = '将群文件备份到Webdav服务'; public static description = '将群文件备份到Webdav服务';
private messageGroup: Record<string, RandomMessage> = {} private messageGroup: Record<string, RandomMessage> = {}
constructor(app: App) {
this.app = app;
}
async getDefaultConfig() { async getDefaultConfig() {
return { return defaultConfig;
groups: {} as Record<string, WebdavConfig>,
messages: {
error: [
'转存群文件失败:{{{error}}}',
'在转存群文件时发生了错误:{{{error}}}',
'未能将群文件转存到资料库:{{{error}}}',
'由于以下错误,文件转存失败:{{{error}}}',
'很抱歉,文件无法成功转存至群组资料库,原因是:{{{error}}}。',
'转存群组文件时出现问题,错误详情:{{{error}}}。',
'文件无法转存到资料库,错误信息如下:{{{error}}}。',
'出现错误,导致文件无法成功转存至群组资料库:{{{error}}}。',
'转存群文件遇到问题,以下是错误的详细信息:{{{error}}}。',
'文件转存失败,原因是:{{{error}}}。',
'抱歉,由于以下错误,文件未能成功转存至群组资料库:{{{error}}}。',
'在尝试将文件转存至群组资料库时,发生了如下错误:{{{error}}}。',
'文件转存操作失败,错误详情:{{{error}}}。',
]
}
};
} }
async initialize(config: any) { async initialize(config: any) {
await this.updateConfig(config);
this.event.init(this);
this.event.on('message/group', async (message, resolved) => { this.event.on('message/group', async (message, resolved) => {
if (message.type !== 'attachment') return; if (message.type !== 'attachment') return;
@ -86,8 +77,6 @@ export default class WebdavFileBackupController implements PluginController {
} }
async updateConfig(config: any) { async updateConfig(config: any) {
this.config = config;
// 随机消息 // 随机消息
for (let [key, value] of Object.entries(this.config.messages)) { for (let [key, value] of Object.entries(this.config.messages)) {
this.messageGroup[key] = new RandomMessage(value); 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 { CacheManager } from './CacheManager';
import { StorageManager } from './StorageManager'; import { StorageManager } from './StorageManager';
import { DatabaseManager } from './DatabaseManager'; import { DatabaseManager } from './DatabaseManager';
import { Logger } from './utils/Logger';
export * from './utils/contextHooks'; export * from './utils/contextHooks';
@ -29,7 +30,8 @@ export default class App {
public debug: boolean = false; public debug: boolean = false;
public logger!: winston.Logger; public baseLogger!: winston.Logger;
public logger!: Logger;
public event!: EventManager; public event!: EventManager;
public cache!: CacheManager; public cache!: CacheManager;
public storage!: StorageManager; public storage!: StorageManager;
@ -42,16 +44,18 @@ export default class App {
public plugin!: PluginManager; public plugin!: PluginManager;
public restfulApi!: RestfulApiManager; public restfulApi!: RestfulApiManager;
constructor(configFile: string) { public constructor(configFile: string, initImmediate: boolean = true) {
this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' })); this.config = Yaml.parse(fs.readFileSync(configFile, { encoding: 'utf-8' }));
this.debug = this.config.debug; this.debug = this.config.debug;
(import.meta as any)._isekaiFeedbotApp = this; (import.meta as any)._isekaiFeedbotApp = this;
this.initialize(); if (initImmediate) {
this.initialize();
}
} }
async initialize() { public async initialize() {
await this.initModules(); await this.initModules();
await this.initRestfulApiManager(); await this.initRestfulApiManager();
await this.initEventManager(); await this.initEventManager();
@ -68,21 +72,21 @@ export default class App {
this.logger.info('初始化完成,正在接收消息'); this.logger.info('初始化完成,正在接收消息');
} }
async initModules() { private async initModules() {
await Setup.initHandlebars(); await Setup.initHandlebars();
// 创建Logger // 创建Logger
const loggerFormat = winston.format.printf(({ level, message, timestamp }) => { const loggerFormat = winston.format.printf(({ level, message, timestamp, tag }) => {
return `${timestamp} [${level}]: ${message}`; return `${timestamp} [${level}]: ${message}`;
}); });
this.logger = winston.createLogger({ this.baseLogger = winston.createLogger({
level: 'info', level: 'info',
format: winston.format.json(), format: winston.format.json(),
}); });
if (this.debug) { if (this.debug) {
this.logger.add( this.baseLogger.add(
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
@ -95,7 +99,7 @@ export default class App {
}) })
); );
} else { } else {
this.logger.add( this.baseLogger.add(
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), 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); this.restfulApi = new RestfulApiManager(this, this.config.http_api);
await this.restfulApi.initialize(); await this.restfulApi.initialize();
} }
async initEventManager() { private async initEventManager() {
this.event = new EventManager(this); this.event = new EventManager(this);
await this.event.initialize(); await this.event.initialize();
} }
async initCacheManager() { private async initCacheManager() {
this.cache = new CacheManager(this, this.config.cache); this.cache = new CacheManager(this, this.config.cache);
await this.cache.initialize(); await this.cache.initialize();
} }
async initStorageManager() { private async initStorageManager() {
this.storage = new StorageManager(this, this.config.storage); this.storage = new StorageManager(this, this.config.storage);
await this.storage.initialize(); await this.storage.initialize();
} }
async initDatabaseManager() { private async initDatabaseManager() {
if (this.config.db) { if (this.config.db) {
this.database = new DatabaseManager(this, this.config.db); this.database = new DatabaseManager(this, this.config.db);
await this.database.initialize(); await this.database.initialize();
} }
} }
async initRobot() { private async initRobot() {
this.robot = new RobotManager(this, this.config.robot); this.robot = new RobotManager(this, this.config.robot);
await this.robot.initialize(); await this.robot.initialize();
} }
async initProviderManager() { private async initProviderManager() {
this.provider = new ProviderManager(this); this.provider = new ProviderManager(this);
await this.provider.initialize(); await this.provider.initialize();
} }
async initServiceManager() { private async initServiceManager() {
this.service = new ServiceManager(this, this.config.service); this.service = new ServiceManager(this, this.config.service);
await this.service.initialize(); await this.service.initialize();
} }
async initSubscribeManager() { private async initSubscribeManager() {
this.subscribe = new SubscribeManager(this, this.config.subscribe_config); this.subscribe = new SubscribeManager(this, this.config.subscribe_config);
await this.subscribe.initialize(); await this.subscribe.initialize();
} }
async initChannelManager() { private async initChannelManager() {
this.channel = new ChannelManager(this, this.config.channel_config_path); this.channel = new ChannelManager(this, this.config.channel_config_path);
await this.channel.initialize(); await this.channel.initialize();
} }
async initPluginManager() { private async initPluginManager() {
this.plugin = new PluginManager(this, this.config.plugin_path, this.config.plugin_config_path); this.plugin = new PluginManager(this, this.config.plugin_path, this.config.plugin_config_path);
await this.plugin.initialize(); await this.plugin.initialize();
} }
public getLogger(tag: string) {
return new Logger(this.baseLogger, tag);
}
/** /**
* *
* @param serviceName * @param serviceName
* @returns * @returns
*/ */
getService<T extends Service>(serviceName: string): T { public getService<T extends Service>(serviceName: string): T {
return this.service.get<T>(serviceName); 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); 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); return this.subscribe.getSubscriber('channel:' + channelId, robotId);
} }
@ -190,7 +200,7 @@ export default class App {
* @param messages * @param messages
* @returns * @returns
*/ */
async sendPushMessage(channelId: string, messages: MultipleMessage): Promise<void> { public async sendPushMessage(channelId: string, messages: MultipleMessage): Promise<void> {
this.logger.info(`[${channelId}] 消息: `, messages); this.logger.info(`[${channelId}] 消息: `, messages);
console.log(messages); console.log(messages);
this.robot.sendPushMessage(channelId, messages); this.robot.sendPushMessage(channelId, messages);

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

@ -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 configControllers: Record<string, PluginController>; public configPluginMap: 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/*.yaml', {
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 moduleName = 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(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) { 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] = pluginInstance;
if (isReload) { if (isReload) {
this.app.logger.info(`已重新加载Controller: ${file}`); this.app.logger.info(`已重新加载插件: ${pluginId}`);
this.emit('controllerReloaded', controllerInstance); this.emit('pluginReloaded', controllerInstance);
} else { } else {
this.app.logger.info(`已加载Controller: ${file}`); this.app.logger.info(`已加载插件: ${pluginId}`);
this.emit('controllerLoaded', controllerInstance); this.emit('pluginLoaded', 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.id}`); this.app.logger.info(`关闭插件: ${pluginId}`);
} }
} }
} }
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.configPluginMap) { // 防止保存时触发重载
delete this.configControllers[configFile]; delete this.configPluginMap[configFile];
} }
const defaultConfig = await controller.getDefaultConfig?.() ?? {}; const defaultConfig = await controller.getDefaultConfig?.() ?? {};
@ -203,124 +276,128 @@ export class PluginManager extends EventEmitter {
} }
setTimeout(() => { setTimeout(() => {
this.configControllers[configFile] = controller; this.configPluginMap[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 getSubscribedControllers(senderInfo: ChatIdentity): PluginController[] { public getSubscribed(senderInfo: ChatIdentity): SubscribedPluginInfo[] {
let [subscribedControllers, disabledControllers] = this.app.event.getControllerSubscribe(senderInfo); let [subscribedScopes, disabledScopes] = this.app.event.getPluginSubscribe(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; continue;
} }
} }
} 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.

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

@ -52,32 +52,37 @@ export class QQInfoProvider {
async refreshRobotInfo() { async refreshRobotInfo() {
// 刷新群信息 // 刷新群信息
let remoteGroupList = await this.getGroupList(); try {
remoteGroupList.forEach((data) => { let remoteGroupList = await this.getGroupList();
if (data.group_id) { remoteGroupList.forEach((data) => {
let oldGroupIndex = this.groupList.findIndex((info) => info.groupId === data.group_id); if (data.group_id) {
let oldGroupIndex = this.groupList.findIndex((info) => info.groupId === data.group_id);
const groupInfo: QQGroupInfo = {
groupId: data.group_id, const groupInfo: QQGroupInfo = {
groupName: data.group_name, groupId: data.group_id,
memberCount: data.member_count, groupName: data.group_name,
memberLimit: data.max_member_count memberCount: data.member_count,
} memberLimit: data.max_member_count
}
if (oldGroupIndex !== -1) { if (oldGroupIndex !== -1) {
const oldGroupInfo = this.groupList[oldGroupIndex]; const oldGroupInfo = this.groupList[oldGroupIndex];
if (compareProps(oldGroupInfo, groupInfo, ['groupName', 'memberCount', 'memberLimit'])) { if (compareProps(oldGroupInfo, groupInfo, ['groupName', 'memberCount', 'memberLimit'])) {
return; return;
}
this.groupList[oldGroupIndex] = groupInfo;
} else {
this.groupList.push(groupInfo);
} }
this.groupList[oldGroupIndex] = groupInfo; this.updateGroupInfo(groupInfo);
} else {
this.groupList.push(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> { public saveMessage<T extends CommonMessage>(message: T): Reactive<T> {

@ -177,7 +177,7 @@ export async function parseQQMessageChunk(bot: QQRobot, messageData: any[], mess
case 'reply': case 'reply':
if (chunkData.data?.id) { if (chunkData.data?.id) {
message.repliedId = chunkData.data.id; message.repliedId = chunkData.data.id;
willIgnoreMention = true; // 忽略下一个“@” // willIgnoreMention = true; // 忽略下一个“@”
} }
break; break;
case 'json': case 'json':
@ -318,10 +318,10 @@ export async function convertMessageToQQChunk(message: CommonSendMessage) {
// type: 'text', // type: 'text',
// data: { text: ' ' } // data: { text: ' ' }
// }); // });
msgChunk.unshift({ // msgChunk.unshift({
type: 'at', // type: 'at',
data: { qq: message.repliedMessage.sender.userId } // data: { qq: message.repliedMessage.sender.userId }
}); // });
// msgChunk.unshift({ // msgChunk.unshift({
// type: 'text', // type: 'text',
// data: { 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 App from "../App";
import { Logger } from './Logger';
export function useApp(): App { export function useApp(): App {
return (import.meta as any)._isekaiFeedbotApp; return (import.meta as any)._isekaiFeedbotApp;
} }
export function useLogger(): winston.Logger { export function useLogger(): Logger {
return useApp().logger; return useApp().logger;
} }

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

Loading…
Cancel
Save