From edad447c6ecc6b73ba6e0ea000bbcc8548807fd7 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Fri, 26 Sep 2025 17:20:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=83=A8=E5=88=86onebot11=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 11 +++++ config/config.json | 7 +++ config/napcat.json | 26 +++++++++++ lib/login/lgr.js | 101 +++++++++++++++++++++++++++++++++++++++++ lib/login/napcat.js | 107 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+) create mode 100644 apps/login.js create mode 100644 config/napcat.json create mode 100644 lib/login/lgr.js create mode 100644 lib/login/napcat.js diff --git a/apps/login.js b/apps/login.js new file mode 100644 index 0000000..e4a428f --- /dev/null +++ b/apps/login.js @@ -0,0 +1,11 @@ +export default class LoginService extends plugin { + constructor() { + super({ + name: 'Onebot登录相关服务', + dsc: '方便操作', + rule: [], + }); + } + + async test(e) {} +} diff --git a/config/config.json b/config/config.json index b8a2b94..ee1b84e 100644 --- a/config/config.json +++ b/config/config.json @@ -7,5 +7,12 @@ "wsSecret": "", "wsReConnectInterval": "5000", "token": "" + }, + "napcat": { + "basePath": "", + "userPath": "" + }, + "lgr": { + "basePath": "" } } diff --git a/config/napcat.json b/config/napcat.json new file mode 100644 index 0000000..7b90f48 --- /dev/null +++ b/config/napcat.json @@ -0,0 +1,26 @@ +{ + "network": { + "httpServers": [], + "httpSseServers": [], + "httpClients": [], + "websocketServers": [ + ], + "websocketClients": [ + { + "enable": true, + "name": "trss", + "url": "ws://localhost:2536/OneBotv11", + "reportSelfMessage": false, + "messagePostFormat": "array", + "token": "", + "debug": false, + "heartInterval": 30000, + "reconnectInterval": 10000 + } + ], + "plugins": [] + }, + "musicSignUrl": "", + "enableLocalFile2Url": false, + "parseMultMsg": false +} diff --git a/lib/login/lgr.js b/lib/login/lgr.js new file mode 100644 index 0000000..df53ed7 --- /dev/null +++ b/lib/login/lgr.js @@ -0,0 +1,101 @@ +import fs from 'fs'; +import path from 'path'; +import { exec } from 'child_process'; +import util from 'util'; +import configControl from '../config/configControl.js'; + +const execAsync = util.promisify(exec); + +export default class LgrService { + constructor() { + const config = configControl.get('config')?.lgr || {}; + this.basePath = config.basePath; + if (!this.basePath) { + logger.error('[crystelf-admin] 未检测到lgr配置..'); + } + } + + /** + * lgr登录方法 + * @param qq qq号 + * @param nickname 昵称 + * @returns {Promise} + */ + async login(qq, nickname) { + if (!this.basePath) { + logger.error('[crystelf-admin] 未配置 lgr.basePath'); + } + const parentDir = path.dirname(this.basePath); + const targetDir = path.join(parentDir, String(qq)); + if (!fs.existsSync(targetDir)) { + try { + await execAsync(`cp -r "${this.basePath}" "${targetDir}"`); + logger.info(`[crystelf-admin] 已复制 ${this.basePath} 到 ${targetDir}..`); + } catch (err) { + logger.error(`[crystelf-admin] 复制文件夹失败: ${err.message}..`); + } + } + const exeFile = path.join(targetDir, 'lgr'); + try { + await execAsync(`chmod 777 "${exeFile}"`); + } catch (err) { + logger.error(`[crystelf-admin] chmod 失败: ${err.message}..`); + } + try { + await execAsync(`tmux has-session -t ${nickname}`); + await execAsync(`tmux kill-session -t ${nickname}`); + await execAsync(`tmux new -s ${nickname} -d "cd '${targetDir}' && ./lgr"`); + } catch { + await execAsync(`tmux new -s ${nickname} -d "cd '${targetDir}' && ./lgr"`); + } + + return await this.waitForQrUpdate(targetDir); + } + + /** + * 等待qr图片更新 + * @param targetDir 目标文件夹 + * @param timeout + * @returns {Promise} + */ + async waitForQrUpdate(targetDir, timeout = 10000) { + const qrPath = path.join(targetDir, 'qr-0.png'); + if (!fs.existsSync(qrPath)) { + return 'none'; + } + + let lastMtime = fs.statSync(qrPath).mtimeMs; + + return new Promise((resolve) => { + const timer = setTimeout(() => { + watcher.close(); + resolve('none'); + }, timeout); + + const watcher = fs.watch(qrPath, (eventType) => { + if (eventType === 'change') { + const stat = fs.statSync(qrPath); + if (stat.mtimeMs !== lastMtime) { + lastMtime = stat.mtimeMs; + clearTimeout(timer); + watcher.close(); + resolve(qrPath); + } + } + }); + }); + } + + /** + * 断开lgr连接 + * @param {string} nickname + */ + async disconnect(nickname) { + try { + await execAsync(`tmux kill-session -t ${nickname}`); + return `已关闭会话: ${nickname}`; + } catch (err) { + return `关闭会话失败: ${err.message}`; + } + } +} diff --git a/lib/login/napcat.js b/lib/login/napcat.js new file mode 100644 index 0000000..20372c5 --- /dev/null +++ b/lib/login/napcat.js @@ -0,0 +1,107 @@ +import fs from 'fs'; +import path from 'path'; +import { exec } from 'child_process'; +import util from 'util'; +import configControl from '../config/configControl.js'; + +const execAsync = util.promisify(exec); + +export default class NapcatService { + constructor() { + const config = configControl.get('config')?.napcat || {}; + this.basePath = config.basePath; + this.userPath = config.userPath; + if (!this.basePath || !this.userPath) { + logger.error('[crystelf-admin] 未检测到napcat配置..'); + } + this.qrPath = path.join(this.basePath, 'cache', 'qrcode.png'); + this.configPath = path.join(this.basePath, 'config'); + } + + /** + * nc登录方法 + * @param qq qq号 + * @param nickname 昵称 + * @returns {Promise} + */ + async login(qq, nickname) { + const shFile = path.join(this.userPath, `${qq}.sh`); + if (!fs.existsSync(this.userPath)) { + fs.mkdirSync(this.userPath, { recursive: true }); + } + const userConfigFile = path.join(this.configPath, `onebot11_${qq}.json`); + if (!fs.existsSync(userConfigFile)) { + try { + const defaultConfigFile = path.join(configControl.get('config')?.path || '', 'napcat.json'); + if (!fs.existsSync(defaultConfigFile)) { + logger.error(`[crystelf-admin] 默认配置文件不存在: ${defaultConfigFile}`); + } + fs.copyFileSync(defaultConfigFile, userConfigFile); + logger.info(`[crystelf-admin] 已复制默认配置到 ${userConfigFile}`); + } catch (err) { + logger.error(`[crystelf-admin] 复制默认配置失败: ${err.message}`); + } + } + if (!fs.existsSync(shFile)) { + const scriptContent = `#!/bin/bash\ncd "${this.basePath}"\n./napcat --qq ${qq}\n`; + fs.writeFileSync(shFile, scriptContent, { mode: 0o755 }); + } + try { + await execAsync(`tmux has-session -t ${nickname}`); + // 存在就先干掉 + await execAsync(`tmux kill-session -t ${nickname}`); + await execAsync(`tmux new -s ${nickname} -d "bash '${shFile}'"`); + } catch { + // 不存在再新建 + await execAsync(`tmux new -s ${nickname} -d "bash '${shFile}'"`); + } + + return await this.waitForQrUpdate(); + } + + /** + * 等待qrcode图像更新 + * @param timeout + * @returns {Promise} + */ + async waitForQrUpdate(timeout = 10000) { + if (!fs.existsSync(this.qrPath)) { + return 'none'; + } + + let lastMtime = fs.statSync(this.qrPath).mtimeMs; + + return new Promise((resolve) => { + const timer = setTimeout(() => { + watcher.close(); + resolve('none'); + }, timeout); + + const watcher = fs.watch(this.qrPath, (eventType) => { + if (eventType === 'change') { + const stat = fs.statSync(this.qrPath); + if (stat.mtimeMs !== lastMtime) { + lastMtime = stat.mtimeMs; + clearTimeout(timer); + watcher.close(); + resolve(this.qrPath); + } + } + }); + }); + } + + /** + * 断开nc连接 + * @param nickname 昵称 + * @returns {Promise} + */ + async disconnect(nickname) { + try { + await execAsync(`tmux kill-session -t ${nickname}`); + return `已关闭会话: ${nickname}`; + } catch (err) { + return `关闭会话失败: ${err.message}`; + } + } +}