miraipie
Version:
the most powerful nodejs development kit for mirai-api-http
368 lines (351 loc) • 14.4 kB
text/typescript
import {exec, execSync} from 'child_process';
import {program} from 'commander';
import {prompt} from 'enquirer';
import EventEmitter from 'events';
import fs from 'fs-extra';
import log4js from 'log4js';
import path from 'path';
import MixedApiAdapter from './builtin/MixedApiAdapter';
import {ApplicationConfig, ConfigMeta} from './config';
import createApp, {Mirai, MiraiApiHttpAdapter, Pie} from './miraipie';
import {makeTemplate} from './utils';
log4js.configure({
appenders: {
console: {type: 'console'}
},
categories: {
default: {
appenders: ['console'],
level: 'info'
}
}
});
const logger = log4js.getLogger('cli');
const banner = `
__ __ __ ______ ______ __ ______ __ ______
/\\ "-./ \\ /\\ \\ /\\ == \\ /\\ __ \\ /\\ \\ /\\ == \\ /\\ \\ /\\ ___\\
\\ \\ \\-./\\ \\ \\ \\ \\ \\ \\ __< \\ \\ __ \\ \\ \\ \\ \\ \\ _-/ \\ \\ \\ \\ \\ __\\
\\ \\_\\ \\ \\_\\ \\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\_\\ \\ \\_\\ \\ \\_\\ \\ \\_\\ \\ \\_____\\
\\/_/ \\/_/ \\/_/ \\/_/ /_/ \\/_/\\/_/ \\/_/ \\/_/ \\/_/ \\/_____/
`;
// 提供虚拟环境
function MiraiPieApplicationMock() {
return Object.assign(new EventEmitter(), {
config: {},
extensions: [],
install(extension: MiraiApiHttpAdapter | Pie) {
this.extensions.push(extension);
},
adapter(adapter: MiraiApiHttpAdapter) {
this.extensions.push(adapter);
},
pie(pie: Pie) {
this.extensions.push(pie);
},
useAdapter: () => undefined,
uninstallAdapter: () => undefined,
enable: () => undefined,
disable: () => undefined,
uninstallPie: () => undefined,
listen: () => undefined,
stop: () => undefined
});
}
// 检查 miraipie 更新
function checkForUpdate() {
const version = require('../package.json').version;
exec('npm search miraipie -p --no-description', (error, stdout) => {
if (!error && stdout) {
const lines = stdout.split('\n');
for (const line of lines) {
const desc = line.split('\t');
if (desc[0] === 'miraipie' && version < desc[3]) {
logger.warn(`新的 miraipie 版本可用: ${desc[3]}, 当前版本: ${version}, 使用命令 npm install miraipie -g 更新 miraipie`);
}
}
}
});
}
// 填写用户配置
async function writeUserConfig(configMeta: ConfigMeta) {
const unsupportedConfig = {};
const questions = Object.keys(configMeta).map((name) => {
const meta = configMeta[name];
const question = {
type: null,
name,
message: `请输入配置项 - ${name}` + (meta.description ? `(${meta.description})` : ''),
initial: typeof meta.default === 'function' ? meta.default() : null
};
const type = typeof meta.type();
if (type === 'number') question.type = 'numeral';
else if (type === 'boolean') question.type = 'confirm';
else if (type === 'string') question.type = 'input';
else {
logger.warn(`暂不支持CLI填写的配置项: ${name} 类型: ${type} , 请手动填写配置文件`);
unsupportedConfig[name] = typeof meta.default === 'function' ? meta.default() : null
}
return question.type ? question : null;
}).filter((question) => !!question);
try {
return Object.assign(await prompt(questions), unsupportedConfig);
} catch (e) {
return {};
}
}
// 加载配置文件
async function useConfigFile(path: string): Promise<ApplicationConfig> {
return new Promise(async (resolve, reject) => {
if (!fs.existsSync(path)) {
prompt({
type: 'confirm',
name: 'new',
message: '没有检测到配置文件, 是否新建配置文件?',
initial: true
}).then((confirm: any) => {
if (confirm.new) {
prompt([
{
type: 'numeral',
name: 'qq',
message: '请输入机器人服务的QQ号'
},
{
type: 'select',
name: 'adapter',
message: '请选择默认服务客户端adapter',
choices: ['mixed', 'http', 'ws'],
initial: 0
}
]).then(async (pro: any) => {
const configMeta = Object.assign({}, MixedApiAdapter.configMeta);
delete configMeta['qq'];
const adapterConfigs = await writeUserConfig(configMeta);
const config = ApplicationConfig.make({qq: pro.qq, adapterInUse: pro.adapter});
config.adapters[pro.adapter] = {configs: adapterConfigs};
saveConfigFile(config, path);
resolve(config);
}).catch(reject);
} else reject();
}).catch(reject);
} else {
fs.readJson(path).then((content) => {
resolve(ApplicationConfig.make(content));
}).catch(reject);
}
});
}
// 保存配置文件
function saveConfigFile(config: ApplicationConfig, path: string) {
fs.writeJsonSync(path, config, {
spaces: 4
});
}
// miraipie
program
.version(`miraipie ${require('../package.json').version} for mirai-api-http ${Mirai.MIRAI_API_HTTP_VERSION}`, '-V, --version', '显示版本信息')
.option('-c, --config <path>', 'miraipie的配置文件路径', 'miraipie.json')
.helpOption('-h, --help', '显示帮助信息')
.addHelpCommand('help [command]', '显示命令帮助');
// miraipie start
program
.command('start')
.description('启动miraipie应用程序')
.option('-m, --modules <paths...>', 'miraipie需要额外加载的模块路径')
.option('-l, --log-level <level>', 'miraipie日志输出级别')
.option('-v, --verbose', '控制台打印miraipie接收到的消息和事件')
.action((opts) => {
checkForUpdate();
useConfigFile(program.opts().config).then(async (config) => {
if (opts.logLevel) config.logLevel = opts.logLevel;
if (opts.modules) config.extensions = [...config.extensions, ...opts.modules];
if (opts.verbose) config.verbose = true;
const app = createApp(config);
app.once('listen', () => {
logger.info('miraipie应用程序已成功启动!\n' + banner);
});
await app.listen();
}).catch((err) => {
logger.error('启动应用程序失败:', err);
});
});
// miraipie install
program
.command('install')
.description('添加本地模块(客户端adapter或pie)')
.argument('module', '待安装的本地模块路径')
.aliases(['i', 'add'])
.action((module) => {
useConfigFile(program.opts().config).then((config) => {
const mockApp = MiraiPieApplicationMock();
try {
require(path.join(process.cwd(), module))(mockApp);
for (const extension of mockApp.extensions) {
if ((extension.__isPie || extension.__isApiAdapter) && extension.id) {
writeUserConfig(extension.configMeta || {}).then((userConfigs) => {
if (extension.__isPie) {
config.pies[extension.id] = {
module,
enabled: true,
configs: userConfigs
};
logger.info(`已添加pie '${extension.id}'`);
} else if (extension.__isApiAdapter) {
config.adapters[extension.id] = {
module,
configs: userConfigs
};
logger.info(`已添加adapter '${extension.id}'`);
}
saveConfigFile(config, program.opts().config);
});
} else {
config.extensions.push(module);
logger.info(`已添加模块 ${module}`);
saveConfigFile(config, program.opts().config);
}
}
} catch (err) {
logger.error('添加模块失败:', err);
}
}).catch(() => {
logger.error('没有找到miraipie配置文件');
});
});
// miraipie pie
const pie = program
.command('pie [command]')
.description('miraipie关于pie相关选项')
// miraipie pie create
pie
.command('create')
.description('使用模板创建pie项目')
.aliases(['new', 'init'])
.argument('[id]', 'pie id')
.option('-j, --javascript', '使用javascript模板')
.action((id, opts) => {
prompt([
{
type: 'input',
name: 'id',
message: '请输入pie id',
initial: id || 'my-pie'
},
{
type: 'input',
name: 'name',
message: '请输入pie名称',
initial: id || 'my-pie'
},
{
type: 'input',
name: 'path',
message: '请输入pie存放位置',
initial: `pies/${id || 'my-pie'}/`
}
]).then((pro: any) => {
const src = path.join(__dirname, opts.javascript ? '../template/pie-js' : '../template/pie-ts');
fs.ensureDirSync(pro.path);
fs.readdir(src, (err, files) => {
if (err) {
logger.error('创建pie项目失败:', err);
return;
}
for (const file of files) makeTemplate(path.resolve(src, file), path.resolve(pro.path, file), pro);
});
logger.info('已使用模板创建pie项目');
prompt({
type: 'confirm',
name: 'init',
message: '是否使用npm初始化pie项目',
initial: true
}).then((proConfirm: any) => {
if (proConfirm.init) execSync(`cd "${pro.path}" && npm install`);
}).catch(() => undefined);
}).catch(() => {
logger.info('已取消创建项目');
});
});
// miraipie pie list
pie
.command('list')
.description('显示已安装pie的列表')
.option('-e, --enabled', '只显示已启用的pie')
.action((opts) => {
useConfigFile(program.opts().config).then((config) => {
const pies = config.pies;
if (opts.enabled) {
for (const id in pies) {
if (!pies[id].enabled) delete pies[id];
}
}
if (Object.keys(pies).length === 0) {
logger.info('当前没有已添加的pie');
return;
}
const header = `${'id'.padEnd(32)}${'enabled'.padEnd(12)}path\n` +
`${'-'.repeat(2).padEnd(32)}${'-'.repeat(7).padEnd(12)}${'-'.repeat(4)}`;
const content = Object.keys(pies).map((id) => {
return `${id.padEnd(32)}${pies[id].enabled.toString().padEnd(12)}${pies[id].module.toString()}`
}).join('\n');
logger.info(`当前加载的pie列表:\n${header}\n${content}`);
}).catch(() => {
logger.error('没有找到miraipie配置文件');
});
});
// miraipie pie enable
pie
.command('enable')
.description('启用已添加的pie')
.argument('id', 'pie id')
.action((id) => {
useConfigFile(program.opts().config).then((config) => {
if (id in config.pies) {
config.pies[id].enabled = true;
logger.info(`已启用pie '${id}'`);
saveConfigFile(config, program.opts().config);
} else {
logger.error(`配置文件中不存在pie '${id}'`)
}
}).catch(() => {
logger.error('没有找到miraipie配置文件');
});
});
// miraipie pie disable
pie
.command('disable')
.description('禁用已添加的pie')
.argument('id', 'pie id')
.action((id) => {
useConfigFile(program.opts().config).then((config) => {
if (id in config.pies) {
config.pies[id].enabled = false;
logger.info(`已禁用pie '${id}'`);
saveConfigFile(config, program.opts().config);
} else {
logger.error(`配置文件中不存在pie '${id}'`)
}
}).catch(() => {
logger.error('没有找到miraipie配置文件');
});
});
// miraipie pie delete
pie
.command('delete')
.description('移除已添加的pie')
.alias('remove')
.argument('id', 'pie id')
.action((id) => {
useConfigFile(program.opts().config).then((config) => {
if (id in config.pies) {
delete config.pies[id];
logger.info(`已移除pie '${id}'`);
saveConfigFile(config, program.opts().config);
} else {
logger.error(`配置文件中不存在pie '${id}'`)
}
}).catch(() => {
logger.error('没有找到miraipie配置文件');
});
});
program.parse(process.argv);