From 5add485556839639f6c23b7f813a997712f864a0 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Thu, 25 Sep 2025 13:55:04 +0800 Subject: [PATCH 01/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/coreRestart.js | 2 +- lib/system/init.js | 2 ++ modules/ws/wsClient.js | 10 +++++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/coreRestart.js b/apps/coreRestart.js index 26679ca..b5e8a1e 100644 --- a/apps/coreRestart.js +++ b/apps/coreRestart.js @@ -18,7 +18,7 @@ export default class CoreRestart extends plugin { } async restart(e) { - if (!configControl.get('core')) { + if (!configControl.get('config')?.core) { return e.reply(`晶灵核心未启用..`, true); } const returnData = await systemControl.systemRestart(); diff --git a/lib/system/init.js b/lib/system/init.js index 1c20632..8b49eca 100644 --- a/lib/system/init.js +++ b/lib/system/init.js @@ -1,7 +1,9 @@ import configControl from '../config/configControl.js'; +import wsClient from '../../modules/ws/wsClient.js'; export const crystelfInit = { async CSH() { await configControl.init(); + await wsClient.initialize(); }, }; diff --git a/modules/ws/wsClient.js b/modules/ws/wsClient.js index 7fd77c5..8c3b86f 100644 --- a/modules/ws/wsClient.js +++ b/modules/ws/wsClient.js @@ -19,12 +19,12 @@ class WsClient { return; } - this.wsURL = configControl.get('coreConfig')?.wsUrl; - this.secret = configControl.get('coreConfig')?.wsSecret; - this.clientId = configControl.get('coreConfig')?.wsClientId; - this.reconnectInterval = configControl.get('coreConfig')?.wsReConnectInterval; + this.wsURL = configControl.get('config')?.coreConfig?.coreUrl; + this.secret = configControl.get('config')?.coreConfig?.wsSecret; + this.clientId = configControl.get('config')?.coreConfig?.wsClientId; + this.reconnectInterval = configControl.get('config')?.coreConfig?.wsReConnectInterval; - //logger.info(this.wsURL); + //logger.info(configControl.get('config')); this.ws = new WebSocket(this.wsURL); this.ws.on('open', () => { From edad447c6ecc6b73ba6e0ea000bbcc8548807fd7 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Fri, 26 Sep 2025 17:20:03 +0800 Subject: [PATCH 02/20] =?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}`; + } + } +} From 6c14830af3c48dd0aadf23ab1233b3cae17ff882 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Fri, 26 Sep 2025 17:20:17 +0800 Subject: [PATCH 03/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reportBots.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/reportBots.js b/apps/reportBots.js index 43a65b6..1409486 100644 --- a/apps/reportBots.js +++ b/apps/reportBots.js @@ -26,13 +26,13 @@ export default class ReportBots extends plugin { async autoReport() { logger.mark(`[crystelf-admin] 正在自动同步bot数据到晶灵核心..`); - if (configControl.get('core')) { + if (configControl.get('config')?.core) { await botControl.reportBots(); } } async manualReport(e) { - if (!configControl.get('core')) { + if (!configControl.get('config')?.core) { return e.reply(`[crystelf-admin] 晶灵核心未启用..`, true); } let success = await botControl.reportBots(); @@ -51,10 +51,10 @@ export default class ReportBots extends plugin { await e.reply(`开始广播消息到所有群..`); try { const sendData = { - token: configControl.get('coreConfig')?.token, + token: configControl.get('config')?.coreConfig?.token, message: msg.toString(), }; - const url = configControl.get('coreConfig')?.coreUrl; + const url = configControl.get('config')?.coreConfig?.coreUrl; const returnData = await axios.post(`${url}/api/bot/broadcast`, sendData); if (returnData?.data?.success) { return await e.reply(`操作成功:${returnData?.data.data?.toString()}`); From 4d40e13432cd30fde285fc7c08f9e946c60c7b22 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Fri, 26 Sep 2025 18:28:30 +0800 Subject: [PATCH 04/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/reportBots.js | 8 ++++---- lib/login/lgr.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/reportBots.js b/apps/reportBots.js index 1409486..cd073e8 100644 --- a/apps/reportBots.js +++ b/apps/reportBots.js @@ -26,13 +26,13 @@ export default class ReportBots extends plugin { async autoReport() { logger.mark(`[crystelf-admin] 正在自动同步bot数据到晶灵核心..`); - if (configControl.get('config')?.core) { + if (configControl.get()?.core) { await botControl.reportBots(); } } async manualReport(e) { - if (!configControl.get('config')?.core) { + if (!configControl.get()?.core) { return e.reply(`[crystelf-admin] 晶灵核心未启用..`, true); } let success = await botControl.reportBots(); @@ -51,10 +51,10 @@ export default class ReportBots extends plugin { await e.reply(`开始广播消息到所有群..`); try { const sendData = { - token: configControl.get('config')?.coreConfig?.token, + token: configControl.get()?.coreConfig?.token, message: msg.toString(), }; - const url = configControl.get('config')?.coreConfig?.coreUrl; + const url = configControl.get()?.coreConfig?.coreUrl; const returnData = await axios.post(`${url}/api/bot/broadcast`, sendData); if (returnData?.data?.success) { return await e.reply(`操作成功:${returnData?.data.data?.toString()}`); diff --git a/lib/login/lgr.js b/lib/login/lgr.js index df53ed7..422c60c 100644 --- a/lib/login/lgr.js +++ b/lib/login/lgr.js @@ -37,7 +37,7 @@ export default class LgrService { } const exeFile = path.join(targetDir, 'lgr'); try { - await execAsync(`chmod 777 "${exeFile}"`); + await execAsync(`chmod +777 "${exeFile}"`); } catch (err) { logger.error(`[crystelf-admin] chmod 失败: ${err.message}..`); } From 9cda3fe7f0986e7360b0601dd500d30236131509 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Sat, 27 Sep 2025 13:08:27 +0800 Subject: [PATCH 05/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/coreRestart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/coreRestart.js b/apps/coreRestart.js index b5e8a1e..ed16173 100644 --- a/apps/coreRestart.js +++ b/apps/coreRestart.js @@ -18,7 +18,7 @@ export default class CoreRestart extends plugin { } async restart(e) { - if (!configControl.get('config')?.core) { + if (!configControl.get()?.core) { return e.reply(`晶灵核心未启用..`, true); } const returnData = await systemControl.systemRestart(); From fac2d00027abe173674ead8f563bdc89d54c5c93 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Sun, 28 Sep 2025 15:29:25 +0800 Subject: [PATCH 06/20] feat:111 --- apps/coreRestart.js | 15 ++++ apps/login.js | 202 +++++++++++++++++++++++++++++++++++++++++++- config/login.json | 4 + lib/core/meme.js | 25 ++++++ 4 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 config/login.json create mode 100644 lib/core/meme.js diff --git a/apps/coreRestart.js b/apps/coreRestart.js index ed16173..21128ab 100644 --- a/apps/coreRestart.js +++ b/apps/coreRestart.js @@ -13,6 +13,11 @@ export default class CoreRestart extends plugin { fnc: 'restart', permission: 'master', }, + { + reg: '^test$', + fnc: 'test', + permission: 'master', + }, ], }); } @@ -35,4 +40,14 @@ export default class CoreRestart extends plugin { await e.reply(`核心重启花的时间有点久了呢..${restartTime?.data?.data}`, true); } } + + async test(e) { + logger.info(configControl.get()); + + await configControl.set('napcat', { + basePath: '114514', + userPath: '44444', + }); + logger.info(configControl.get()); + } } diff --git a/apps/login.js b/apps/login.js index e4a428f..2fdae68 100644 --- a/apps/login.js +++ b/apps/login.js @@ -1,11 +1,209 @@ +import plugin from '../../../lib/plugins/plugin.js'; +import path from 'path'; +import ConfigControl from '../lib/config/configControl.js'; +import config from '../../../lib/config/config.js'; +import configControl from '../lib/config/configControl.js'; +import axios from 'axios'; +import Meme from '../lib/core/meme.js'; + +const configPath = path.join(process.cwd(), 'data/crystelf/config'); +const loginSessions = new Map(); //正在进行的登录会话 + export default class LoginService extends plugin { constructor() { super({ name: 'Onebot登录相关服务', dsc: '方便操作', - rule: [], + event: 'message', + priority: 50, + rule: [ + { + reg: '^#登录(\\d+)?$', + fnc: 'loginHandler', + }, + { + reg: '^#绑定账号\\s+\\d+$', + fnc: 'bindAccount', + }, + { + reg: '^#解绑账号\\s+\\d+$', + fnc: 'unbindAccount', + }, + ], }); } - async test(e) {} + /** + * 登录命令入口 + * @param e + * @returns {Promise} + */ + async loginHandler(e) { + let config = await configControl.get(); + if (!config?.login?.allowGroups.includes(e.group_id)) { + const img = await Meme.getMeme('zhenxun', 'default'); + return e.reply(segment.img(img)); //都不在群里玩什么;[ + } + const isAdmin = e.isAdmin; + const userId = e.user_id; + const match = e.msg.match(/^#登录(\d+)?$/); + let targetQq = match[1]; + + if (!targetQq) { + const binds = config?.login?.userBinds[userId] || []; + if (binds.length === 0) { + if (isAdmin) { + e.reply('请告诉我要登录的 QQ 号', true); + loginSessions.set(userId, { step: 'askQq', admin: true }); + } else { + return e.reply('你没有绑定可登录的账号,请联系管理员分配..', true); + } + } else if (binds.length === 1) { + targetQq = binds[0]; + } else { + e.reply(`你绑定了多个账号,请选择要登录的 QQ:\n${binds.join('\n')}`, true); + loginSessions.set(userId, { step: 'chooseQq', options: binds }); + } + } + + if (isAdmin) { + await this.startAdminLogin(e, targetQq); + } else { + const binds = config?.login?.userBinds[userId] || []; + if (!binds.includes(targetQq)) { + return e.reply('你没有权限登录该账号,请联系管理员分配', true); + } + await this.startUserLogin(e, targetQq); + } + } + + /** 管理员登录交互 */ + async startAdminLogin(e, qq) { + loginSessions.set(e.user_id, { + step: 'askNickname', + qq, + admin: true, + }); + e.reply(`请告诉我 QQ[${qq}] 的英文昵称`, true); + } + + /** 普通用户登录 */ + async startUserLogin(e, qq) { + loginSessions.set(e.user_id, { + step: 'askMethod', + qq, + admin: false, + }); + e.reply(`请选择登录方式\nnc或lgr\n来登录 QQ[${qq}]`, true); + } + + /** 绑定账号 */ + async bindAccount(e) { + let config = await configControl.get()?.login; + if (!e.isAdmin) return; + const match = e.msg.match(/^#绑定账号\s+(\d+)$/); + if (!match) return; + const qq = match[1]; + const at = e.at || e.user_id; + if (!config?.userBinds[at]) config.userBinds[at] = []; + if (!config?.userBinds[at].includes(qq)) { + config.userBinds[at].push(qq); + await ConfigControl.set('login', config); + e.reply(`已为 ${at} 绑定账号 ${qq}`, true); + } else { + e.reply(`该用户已绑定此账号`, true); + } + } + + /** 解绑账号 */ + async unbindAccount(e) { + if (!e.isAdmin) return false; + let config = await configControl.get()?.login; + const match = e.msg.match(/^#解绑账号\s+(\d+)$/); + if (!match) return; + const qq = match[1]; + const at = e.at || e.user_id; + if (!config?.userBinds[at]) { + e.reply('该用户没有绑定账号', true); + return; + } + config.userBinds[at] = config.userBinds[at].filter((q) => q !== qq); + await ConfigControl.set('login', config); + e.reply(`已为 ${at} 解绑账号 ${qq}`, true); + } + + /** 捕获消息继续交互 */ + async accept(e) { + const session = loginSessions.get(e.user_id); + if (!session) return; + + if (session.step === 'askQq') { + session.qq = e.msg.trim(); + session.step = 'askNickname'; + e.reply(`请告诉我 QQ[${session.qq}] 的英文昵称`, true); + return; + } + + if (session.step === 'chooseQq') { + if (!session.options.includes(e.msg.trim())) { + e.reply('请选择列表中的 QQ', true); + return; + } + session.qq = e.msg.trim(); + session.step = 'askMethod'; + e.reply(`请选择登录方式(回复 "nc" 或 "lgr")来登录 QQ[${session.qq}]`, true); + return; + } + + if (session.step === 'askNickname') { + session.nickname = e.msg.trim(); + session.step = 'askMethod'; + e.reply('请选择登录方式(回复 "nc" 或 "lgr")', true); + return; + } + + if (session.step === 'askMethod') { + const method = e.msg.trim().toLowerCase(); + if (!['nc', 'lgr'].includes(method)) { + e.reply('登录方式无效,请回复 "nc" 或 "lgr"', true); + return; + } + session.method = method; + loginSessions.delete(e.user_id); + await this.doLogin(e, session); + } + } + + /** 执行登录 */ + async doLogin(e, session) { + const { qq, method, nickname } = session; + e.reply(`开始使用 ${method} 登录 QQ[${qq}]`, true); + + let loginInstance; + if (method === 'nc') { + loginInstance = NapcatService(); + } else { + loginInstance = LgrService(); + } + + const qrPath = await loginInstance.login(); + e.reply(segment.image(qrPath), true); + const timerKey = `login:timer:${qq}`; + redis.setex(timerKey, 120, 'pending'); + + const check = setInterval(async () => { + const status = await loginInstance.checkStatus(); + if (status === 'success') { + clearInterval(check); + redis.del(timerKey); + e.reply(`QQ[${qq}] 登录成功!`, true); + } + const ttl = await redis.ttl(timerKey); + if (ttl <= 0) { + clearInterval(check); + await loginInstance.disconnect(); + e.reply(`QQ[${qq}] 登录超时,已断开`, true); + } + }, 5000); + } } diff --git a/config/login.json b/config/login.json new file mode 100644 index 0000000..8b499c7 --- /dev/null +++ b/config/login.json @@ -0,0 +1,4 @@ +{ + "allowGroups": [], + "userBinds": {} +} diff --git a/lib/core/meme.js b/lib/core/meme.js new file mode 100644 index 0000000..8cb00b4 --- /dev/null +++ b/lib/core/meme.js @@ -0,0 +1,25 @@ +import ConfigControl from '../config/configControl.js'; +import axios from 'axios'; + +const Meme = { + /** + * 获取随机表情 + * @param character 角色名称 + * @param status 角色状态 + * @returns {Promise>} + */ + async getMeme(character, status) { + const coreConfig = await ConfigControl.get()?.coreConfig; + const coreUrl = coreConfig?.coreUrl; + const token = coreConfig?.token; + return await axios.get(`${coreUrl}/api/meme`, { + params: { + character: character, + status: status, + token: token, + }, + }); + }, +}; + +export default Meme; From 7c77f26975a23a310b6dc06c75ca934b1175938a Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Sun, 28 Sep 2025 18:43:21 +0800 Subject: [PATCH 07/20] feat:111 --- apps/login.js | 39 ++++++++++++++++++++++----------------- lib/login/lgr.js | 9 +++++++++ lib/login/napcat.js | 9 +++++++++ 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/login.js b/apps/login.js index 2fdae68..bc5256f 100644 --- a/apps/login.js +++ b/apps/login.js @@ -5,6 +5,8 @@ import config from '../../../lib/config/config.js'; import configControl from '../lib/config/configControl.js'; import axios from 'axios'; import Meme from '../lib/core/meme.js'; +import NapcatService from '../lib/login/napcat.js'; +import LgrService from '../lib/login/lgr.js'; const configPath = path.join(process.cwd(), 'data/crystelf/config'); const loginSessions = new Map(); //正在进行的登录会话 @@ -53,15 +55,15 @@ export default class LoginService extends plugin { const binds = config?.login?.userBinds[userId] || []; if (binds.length === 0) { if (isAdmin) { - e.reply('请告诉我要登录的 QQ 号', true); + e.reply('你想登哪个qq?', true); loginSessions.set(userId, { step: 'askQq', admin: true }); } else { - return e.reply('你没有绑定可登录的账号,请联系管理员分配..', true); + return e.reply('管理员似乎没有给你分配可用账户,请联系管理员添加..', true); } } else if (binds.length === 1) { targetQq = binds[0]; } else { - e.reply(`你绑定了多个账号,请选择要登录的 QQ:\n${binds.join('\n')}`, true); + e.reply(`你小子账号还挺多,选一个qq登录吧:\n${binds.join('\n')}`, true); loginSessions.set(userId, { step: 'chooseQq', options: binds }); } } @@ -71,7 +73,7 @@ export default class LoginService extends plugin { } else { const binds = config?.login?.userBinds[userId] || []; if (!binds.includes(targetQq)) { - return e.reply('你没有权限登录该账号,请联系管理员分配', true); + return e.reply('你没有权限登录该账号,请联系管理员分配..', true); } await this.startUserLogin(e, targetQq); } @@ -84,7 +86,7 @@ export default class LoginService extends plugin { qq, admin: true, }); - e.reply(`请告诉我 QQ[${qq}] 的英文昵称`, true); + e.reply(`QQ[${qq}]的英文名叫什么?`, true); } /** 普通用户登录 */ @@ -124,6 +126,7 @@ export default class LoginService extends plugin { const qq = match[1]; const at = e.at || e.user_id; if (!config?.userBinds[at]) { + ``; e.reply('该用户没有绑定账号', true); return; } @@ -140,7 +143,7 @@ export default class LoginService extends plugin { if (session.step === 'askQq') { session.qq = e.msg.trim(); session.step = 'askNickname'; - e.reply(`请告诉我 QQ[${session.qq}] 的英文昵称`, true); + e.reply(`QQ[${session.qq}]的英文名叫什么?`, true); return; } @@ -151,21 +154,21 @@ export default class LoginService extends plugin { } session.qq = e.msg.trim(); session.step = 'askMethod'; - e.reply(`请选择登录方式(回复 "nc" 或 "lgr")来登录 QQ[${session.qq}]`, true); + e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); return; } if (session.step === 'askNickname') { session.nickname = e.msg.trim(); session.step = 'askMethod'; - e.reply('请选择登录方式(回复 "nc" 或 "lgr")', true); + e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); return; } if (session.step === 'askMethod') { const method = e.msg.trim().toLowerCase(); if (!['nc', 'lgr'].includes(method)) { - e.reply('登录方式无效,请回复 "nc" 或 "lgr"', true); + e.reply('登录方式无效', true); return; } session.method = method; @@ -176,33 +179,35 @@ export default class LoginService extends plugin { /** 执行登录 */ async doLogin(e, session) { + const redis = global.redis | undefined; + if (!redis) return e.reply('未找到全局redis服务..', true); const { qq, method, nickname } = session; e.reply(`开始使用 ${method} 登录 QQ[${qq}]`, true); let loginInstance; if (method === 'nc') { - loginInstance = NapcatService(); + loginInstance = new NapcatService(); } else { - loginInstance = LgrService(); + loginInstance = new LgrService(); } - const qrPath = await loginInstance.login(); + const qrPath = await loginInstance.login(qq, nickname); e.reply(segment.image(qrPath), true); const timerKey = `login:timer:${qq}`; - redis.setex(timerKey, 120, 'pending'); + redis.set(timerKey, 120, 'pending'); const check = setInterval(async () => { const status = await loginInstance.checkStatus(); - if (status === 'success') { + if (status) { clearInterval(check); redis.del(timerKey); - e.reply(`QQ[${qq}] 登录成功!`, true); + e.reply(`QQ[${qq}] 登录成功!`, true); } const ttl = await redis.ttl(timerKey); if (ttl <= 0) { clearInterval(check); - await loginInstance.disconnect(); - e.reply(`QQ[${qq}] 登录超时,已断开`, true); + await loginInstance.disconnect(nickname); + e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); } }, 5000); } diff --git a/lib/login/lgr.js b/lib/login/lgr.js index 422c60c..d8e1802 100644 --- a/lib/login/lgr.js +++ b/lib/login/lgr.js @@ -98,4 +98,13 @@ export default class LgrService { return `关闭会话失败: ${err.message}`; } } + + /** + * qq是否登录成功 + * @param qq + * @returns {Promise} + */ + async checkStatus(qq) { + return Bot.uin.includes(qq); + } } diff --git a/lib/login/napcat.js b/lib/login/napcat.js index 20372c5..22f72a0 100644 --- a/lib/login/napcat.js +++ b/lib/login/napcat.js @@ -104,4 +104,13 @@ export default class NapcatService { return `关闭会话失败: ${err.message}`; } } + + /** + * qq是否登录成功 + * @param qq + * @returns {Promise} + */ + async checkStatus(qq) { + return Bot.uin.includes(qq); + } } From 1481eb024a81e6fec1a8d8515ce38f5ab8c305be Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Mon, 29 Sep 2025 13:31:08 +0800 Subject: [PATCH 08/20] feat:111 --- apps/login.js | 153 +++++++++++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 58 deletions(-) diff --git a/apps/login.js b/apps/login.js index bc5256f..a97c703 100644 --- a/apps/login.js +++ b/apps/login.js @@ -46,7 +46,7 @@ export default class LoginService extends plugin { const img = await Meme.getMeme('zhenxun', 'default'); return e.reply(segment.img(img)); //都不在群里玩什么;[ } - const isAdmin = e.isAdmin; + const isAdmin = e.isMaster; const userId = e.user_id; const match = e.msg.match(/^#登录(\d+)?$/); let targetQq = match[1]; @@ -55,16 +55,16 @@ export default class LoginService extends plugin { const binds = config?.login?.userBinds[userId] || []; if (binds.length === 0) { if (isAdmin) { - e.reply('你想登哪个qq?', true); loginSessions.set(userId, { step: 'askQq', admin: true }); + return e.reply('你想登哪个qq?', true); } else { return e.reply('管理员似乎没有给你分配可用账户,请联系管理员添加..', true); } } else if (binds.length === 1) { targetQq = binds[0]; } else { - e.reply(`你小子账号还挺多,选一个qq登录吧:\n${binds.join('\n')}`, true); loginSessions.set(userId, { step: 'chooseQq', options: binds }); + return e.reply(`你小子账号还挺多,选一个qq登录吧:\n${binds.join('\n')}`, true); } } @@ -79,30 +79,47 @@ export default class LoginService extends plugin { } } - /** 管理员登录交互 */ + /** + * 管理员登录 + * @param e + * @param qq + * @returns {Promise<*>} + */ async startAdminLogin(e, qq) { loginSessions.set(e.user_id, { step: 'askNickname', qq, admin: true, }); - e.reply(`QQ[${qq}]的英文名叫什么?`, true); + return e.reply(`QQ[${qq}]的英文名叫什么?`, true); } - /** 普通用户登录 */ + /** + * 普通用户 + * @param e + * @param qq + * @returns {Promise<*>} + */ async startUserLogin(e, qq) { loginSessions.set(e.user_id, { step: 'askMethod', qq, admin: false, }); - e.reply(`请选择登录方式\nnc或lgr\n来登录 QQ[${qq}]`, true); + return e.reply(`请选择登录方式\nnc或lgr\n来登录 QQ[${qq}]`, true); } - /** 绑定账号 */ + /** + * 绑定账号 + * @param e + * @returns {Promise<*>} + */ async bindAccount(e) { let config = await configControl.get()?.login; - if (!e.isAdmin) return; + if (!e.isMaster) { + const img = await Meme.getMeme('zhenxun', 'default'); + return e.reply(segment.img(img)); + } const match = e.msg.match(/^#绑定账号\s+(\d+)$/); if (!match) return; const qq = match[1]; @@ -111,15 +128,22 @@ export default class LoginService extends plugin { if (!config?.userBinds[at].includes(qq)) { config.userBinds[at].push(qq); await ConfigControl.set('login', config); - e.reply(`已为 ${at} 绑定账号 ${qq}`, true); + return e.reply(`已为 ${at} 绑定账号 ${qq}`, true); } else { - e.reply(`该用户已绑定此账号`, true); + return e.reply(`该用户已绑定此账号`, true); } } - /** 解绑账号 */ + /** + * 解绑账号 + * @param e + * @returns {Promise<*|boolean>} + */ async unbindAccount(e) { - if (!e.isAdmin) return false; + if (!e.isMaster) { + const img = await Meme.getMeme('zhenxun', 'default'); + return e.reply(segment.img(img)); + } let config = await configControl.get()?.login; const match = e.msg.match(/^#解绑账号\s+(\d+)$/); if (!match) return; @@ -127,49 +151,52 @@ export default class LoginService extends plugin { const at = e.at || e.user_id; if (!config?.userBinds[at]) { ``; - e.reply('该用户没有绑定账号', true); - return; + return e.reply('该用户没有绑定账号', true); } config.userBinds[at] = config.userBinds[at].filter((q) => q !== qq); await ConfigControl.set('login', config); - e.reply(`已为 ${at} 解绑账号 ${qq}`, true); + return e.reply(`已为 ${at} 解绑账号 ${qq}`, true); } - /** 捕获消息继续交互 */ + /** + * 登录流程 + * @param e + * @returns {Promise<*>} + */ async accept(e) { const session = loginSessions.get(e.user_id); - if (!session) return; + if ( + !session || + !e.group_id || + !(await ConfigControl.get()?.login?.allowGroups.includes(e.group_id)) + ) + return; if (session.step === 'askQq') { session.qq = e.msg.trim(); session.step = 'askNickname'; - e.reply(`QQ[${session.qq}]的英文名叫什么?`, true); - return; + return e.reply(`QQ[${session.qq}]的英文名叫什么?`, true); } if (session.step === 'chooseQq') { if (!session.options.includes(e.msg.trim())) { - e.reply('请选择列表中的 QQ', true); - return; + return e.reply('请选择列表中的 QQ', true); } session.qq = e.msg.trim(); session.step = 'askMethod'; - e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); - return; + return e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); } if (session.step === 'askNickname') { session.nickname = e.msg.trim(); session.step = 'askMethod'; - e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); - return; + return e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); } if (session.step === 'askMethod') { const method = e.msg.trim().toLowerCase(); if (!['nc', 'lgr'].includes(method)) { - e.reply('登录方式无效', true); - return; + return e.reply('登录方式无效', true); } session.method = method; loginSessions.delete(e.user_id); @@ -177,38 +204,48 @@ export default class LoginService extends plugin { } } - /** 执行登录 */ + /** + * 执行登录 + * @param e + * @param session + * @returns {Promise<*>} + */ async doLogin(e, session) { - const redis = global.redis | undefined; - if (!redis) return e.reply('未找到全局redis服务..', true); - const { qq, method, nickname } = session; - e.reply(`开始使用 ${method} 登录 QQ[${qq}]`, true); + try { + const redis = global.redis; + //if (!redis) return e.reply('未找到全局redis服务..', true); + const { qq, method, nickname } = session; + e.reply(`开始使用 ${method} 登录 QQ[${qq}]`, true); - let loginInstance; - if (method === 'nc') { - loginInstance = new NapcatService(); - } else { - loginInstance = new LgrService(); + let loginInstance; + if (method === 'nc') { + loginInstance = new NapcatService(); + } else { + loginInstance = new LgrService(); + } + + const qrPath = await loginInstance.login(qq, nickname); + e.reply(segment.image(qrPath), true); + const timerKey = `login:timer:${qq}`; + await redis.set(timerKey, 120, 'pending'); + + const check = setInterval(async () => { + const status = await loginInstance.checkStatus(); + if (status) { + clearInterval(check); + await redis.del(timerKey); + return e.reply(`QQ[${qq}] 登录成功!`, true); + } + const ttl = await redis.ttl(timerKey); + if (ttl <= 0) { + clearInterval(check); + await loginInstance.disconnect(nickname); + return e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); + } + }, 5000); + } catch (err) { + logger.error('[crystelf-admin]登录流程出现错误', err); + return e.reply(`出了点小问题,过会儿再来试试吧..`); } - - const qrPath = await loginInstance.login(qq, nickname); - e.reply(segment.image(qrPath), true); - const timerKey = `login:timer:${qq}`; - redis.set(timerKey, 120, 'pending'); - - const check = setInterval(async () => { - const status = await loginInstance.checkStatus(); - if (status) { - clearInterval(check); - redis.del(timerKey); - e.reply(`QQ[${qq}] 登录成功!`, true); - } - const ttl = await redis.ttl(timerKey); - if (ttl <= 0) { - clearInterval(check); - await loginInstance.disconnect(nickname); - e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); - } - }, 5000); } } From 1c0c71a405acb8ed005638e364ec79a1fbe4bdaf Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Mon, 29 Sep 2025 16:39:33 +0800 Subject: [PATCH 09/20] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 6 +++--- lib/config/configControl.js | 2 +- lib/core/meme.js | 15 ++------------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/apps/login.js b/apps/login.js index a97c703..1d95e26 100644 --- a/apps/login.js +++ b/apps/login.js @@ -44,7 +44,7 @@ export default class LoginService extends plugin { let config = await configControl.get(); if (!config?.login?.allowGroups.includes(e.group_id)) { const img = await Meme.getMeme('zhenxun', 'default'); - return e.reply(segment.img(img)); //都不在群里玩什么;[ + return e.reply(segment.image(img)); //都不在群里玩什么;[ } const isAdmin = e.isMaster; const userId = e.user_id; @@ -118,7 +118,7 @@ export default class LoginService extends plugin { let config = await configControl.get()?.login; if (!e.isMaster) { const img = await Meme.getMeme('zhenxun', 'default'); - return e.reply(segment.img(img)); + return e.reply(segment.image(img)); } const match = e.msg.match(/^#绑定账号\s+(\d+)$/); if (!match) return; @@ -142,7 +142,7 @@ export default class LoginService extends plugin { async unbindAccount(e) { if (!e.isMaster) { const img = await Meme.getMeme('zhenxun', 'default'); - return e.reply(segment.img(img)); + return e.reply(segment.image(img)); } let config = await configControl.get()?.login; const match = e.msg.match(/^#解绑账号\s+(\d+)$/); diff --git a/lib/config/configControl.js b/lib/config/configControl.js index 85e0847..546f263 100644 --- a/lib/config/configControl.js +++ b/lib/config/configControl.js @@ -70,7 +70,7 @@ async function init() { } } if (!Array.isArray(configCache)) { - configCache = fc.mergeConfig(configCache, defaultConfig); + configCache = fc.mergeConfig(configCache, configCache.config || {}); } if (configCache.debug) { logger.info('[crystelf-admin] 配置模块初始化成功..'); diff --git a/lib/core/meme.js b/lib/core/meme.js index 8cb00b4..375c27c 100644 --- a/lib/core/meme.js +++ b/lib/core/meme.js @@ -2,23 +2,12 @@ import ConfigControl from '../config/configControl.js'; import axios from 'axios'; const Meme = { - /** - * 获取随机表情 - * @param character 角色名称 - * @param status 角色状态 - * @returns {Promise>} - */ async getMeme(character, status) { const coreConfig = await ConfigControl.get()?.coreConfig; const coreUrl = coreConfig?.coreUrl; const token = coreConfig?.token; - return await axios.get(`${coreUrl}/api/meme`, { - params: { - character: character, - status: status, - token: token, - }, - }); + //logger.info(`${coreUrl}/api/meme`); + return `${coreUrl}/api/meme?token=${token}?character=${character}&status=${status}`; }, }; From 304f2c9e6cec1a1afbfbb9f8c33a5b34cd46c1fc Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Mon, 29 Sep 2025 16:44:39 +0800 Subject: [PATCH 10/20] fix:111 --- apps/coreRestart.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/apps/coreRestart.js b/apps/coreRestart.js index 21128ab..ed16173 100644 --- a/apps/coreRestart.js +++ b/apps/coreRestart.js @@ -13,11 +13,6 @@ export default class CoreRestart extends plugin { fnc: 'restart', permission: 'master', }, - { - reg: '^test$', - fnc: 'test', - permission: 'master', - }, ], }); } @@ -40,14 +35,4 @@ export default class CoreRestart extends plugin { await e.reply(`核心重启花的时间有点久了呢..${restartTime?.data?.data}`, true); } } - - async test(e) { - logger.info(configControl.get()); - - await configControl.set('napcat', { - basePath: '114514', - userPath: '44444', - }); - logger.info(configControl.get()); - } } From 61cec10da14e17a3612ebc3bbdb40331383592f3 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Tue, 30 Sep 2025 18:09:53 +0800 Subject: [PATCH 11/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=9F=90=E4=BA=9B?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 39 ++++++++++++++++++++++++--------------- lib/login/lgr.js | 29 +++++++++++++++++------------ lib/login/napcat.js | 22 +++++++++++++--------- 3 files changed, 54 insertions(+), 36 deletions(-) diff --git a/apps/login.js b/apps/login.js index 1d95e26..6afba99 100644 --- a/apps/login.js +++ b/apps/login.js @@ -225,24 +225,33 @@ export default class LoginService extends plugin { } const qrPath = await loginInstance.login(qq, nickname); - e.reply(segment.image(qrPath), true); - const timerKey = `login:timer:${qq}`; - await redis.set(timerKey, 120, 'pending'); + if (qrPath) { + e.reply(segment.image(qrPath), true); + const timerKey = `login:timer:${qq}`; + await redis.set(timerKey, 120, 'pending'); - const check = setInterval(async () => { - const status = await loginInstance.checkStatus(); + const check = setInterval(async () => { + const status = await loginInstance.checkStatus(qq); + if (status) { + clearInterval(check); + await redis.del(timerKey); + return e.reply(`QQ[${qq}] 登录成功!`, true); + } + const ttl = await redis.ttl(timerKey); + if (ttl <= 0) { + clearInterval(check); + await loginInstance.disconnect(nickname); + return e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); + } + }, 5000); + } else { + const status = await loginInstance.checkStatus(qq); if (status) { - clearInterval(check); - await redis.del(timerKey); - return e.reply(`QQ[${qq}] 登录成功!`, true); + return e.reply(`QQ[${qq}] 使用上次登录缓存登录成功!`, true); + } else { + return e.reply(`QQ[${qq}] 登录出现未知错误,请联系管理员操作..`, true); } - const ttl = await redis.ttl(timerKey); - if (ttl <= 0) { - clearInterval(check); - await loginInstance.disconnect(nickname); - return e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); - } - }, 5000); + } } catch (err) { logger.error('[crystelf-admin]登录流程出现错误', err); return e.reply(`出了点小问题,过会儿再来试试吧..`); diff --git a/lib/login/lgr.js b/lib/login/lgr.js index d8e1802..fccf383 100644 --- a/lib/login/lgr.js +++ b/lib/login/lgr.js @@ -55,31 +55,36 @@ export default class LgrService { /** * 等待qr图片更新 * @param targetDir 目标文件夹 - * @param timeout - * @returns {Promise} + * @param timeout 最大等待时间 (默认 30s) + * @returns {Promise} */ - async waitForQrUpdate(targetDir, timeout = 10000) { + async waitForQrUpdate(targetDir, timeout = 30000) { 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); + let resolved = false; + const timer = setTimeout(() => { + if (!resolved) { + resolved = true; + watcher.close(); + resolve(undefined); + } + }, 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); + if (!resolved) { + resolved = true; + clearTimeout(timer); + watcher.close(); + resolve(qrPath); + } } } }); diff --git a/lib/login/napcat.js b/lib/login/napcat.js index 22f72a0..fc5dc79 100644 --- a/lib/login/napcat.js +++ b/lib/login/napcat.js @@ -64,27 +64,31 @@ export default class NapcatService { * @param timeout * @returns {Promise} */ - async waitForQrUpdate(timeout = 10000) { + async waitForQrUpdate(timeout = 30000) { if (!fs.existsSync(this.qrPath)) { return 'none'; } - let lastMtime = fs.statSync(this.qrPath).mtimeMs; - return new Promise((resolve) => { + let resolved = false; const timer = setTimeout(() => { - watcher.close(); - resolve('none'); + if (!resolved) { + resolved = true; + watcher.close(); + resolve(undefined); + } }, 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); + if (!resolved) { + resolved = true; + clearTimeout(timer); + watcher.close(); + resolve(this.qrPath); + } } } }); From 1b447b29fa3196bc2e754e713ef6253f80874e53 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Tue, 30 Sep 2025 23:42:11 +0800 Subject: [PATCH 12/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=9F=90=E4=BA=9B?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 2 +- lib/login/napcat.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/login.js b/apps/login.js index 6afba99..d8d61b2 100644 --- a/apps/login.js +++ b/apps/login.js @@ -225,7 +225,7 @@ export default class LoginService extends plugin { } const qrPath = await loginInstance.login(qq, nickname); - if (qrPath) { + if (qrPath || qrPath !== 'none') { e.reply(segment.image(qrPath), true); const timerKey = `login:timer:${qq}`; await redis.set(timerKey, 120, 'pending'); diff --git a/lib/login/napcat.js b/lib/login/napcat.js index fc5dc79..f175717 100644 --- a/lib/login/napcat.js +++ b/lib/login/napcat.js @@ -43,7 +43,7 @@ export default class NapcatService { } } if (!fs.existsSync(shFile)) { - const scriptContent = `#!/bin/bash\ncd "${this.basePath}"\n./napcat --qq ${qq}\n`; + const scriptContent = `#!/bin/bash\nxvfb-run -a qq --no-sandbox -q ${qq}\n`; fs.writeFileSync(shFile, scriptContent, { mode: 0o755 }); } try { From 9da541e99cc02d62e38085f212c1b8166f1ef94c Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Tue, 30 Sep 2025 23:57:16 +0800 Subject: [PATCH 13/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=9F=90=E4=BA=9B?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/login/lgr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/login/lgr.js b/lib/login/lgr.js index fccf383..e69f667 100644 --- a/lib/login/lgr.js +++ b/lib/login/lgr.js @@ -26,7 +26,7 @@ export default class LgrService { logger.error('[crystelf-admin] 未配置 lgr.basePath'); } const parentDir = path.dirname(this.basePath); - const targetDir = path.join(parentDir, String(qq)); + const targetDir = path.join(path.join(parentDir, '..'), String(qq)); if (!fs.existsSync(targetDir)) { try { await execAsync(`cp -r "${this.basePath}" "${targetDir}"`); From 7b0bc7475519f6ce209e4d114fe6dc4e25374ae8 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 1 Oct 2025 00:34:20 +0800 Subject: [PATCH 14/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=9F=90=E4=BA=9B?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login.js b/apps/login.js index d8d61b2..089d7fe 100644 --- a/apps/login.js +++ b/apps/login.js @@ -226,7 +226,7 @@ export default class LoginService extends plugin { const qrPath = await loginInstance.login(qq, nickname); if (qrPath || qrPath !== 'none') { - e.reply(segment.image(qrPath), true); + e.reply(segment.image(`file:///${qrPath}`), true); const timerKey = `login:timer:${qq}`; await redis.set(timerKey, 120, 'pending'); From 4a6bde2d0ca93d2adf170767179cce03e56ffcb5 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 1 Oct 2025 00:44:49 +0800 Subject: [PATCH 15/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=9F=90=E4=BA=9B?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/login.js b/apps/login.js index 089d7fe..c19f598 100644 --- a/apps/login.js +++ b/apps/login.js @@ -225,10 +225,10 @@ export default class LoginService extends plugin { } const qrPath = await loginInstance.login(qq, nickname); - if (qrPath || qrPath !== 'none') { - e.reply(segment.image(`file:///${qrPath}`), true); + if (qrPath && qrPath !== 'none') { + await e.reply(segment.image(`file:///${qrPath}`), true); const timerKey = `login:timer:${qq}`; - await redis.set(timerKey, 120, 'pending'); + await redis.set(timerKey, 'pending', 120); const check = setInterval(async () => { const status = await loginInstance.checkStatus(qq); From dcbab7d7ea9f759037d15ea2e5f944b2684d440c Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 1 Oct 2025 00:51:12 +0800 Subject: [PATCH 16/20] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=9F=90=E4=BA=9B?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/login.js b/apps/login.js index c19f598..c8bd9b2 100644 --- a/apps/login.js +++ b/apps/login.js @@ -212,38 +212,41 @@ export default class LoginService extends plugin { */ async doLogin(e, session) { try { - const redis = global.redis; - //if (!redis) return e.reply('未找到全局redis服务..', true); const { qq, method, nickname } = session; e.reply(`开始使用 ${method} 登录 QQ[${qq}]`, true); - let loginInstance; if (method === 'nc') { loginInstance = new NapcatService(); } else { loginInstance = new LgrService(); } - const qrPath = await loginInstance.login(qq, nickname); + const loginTimers = new Map(); if (qrPath && qrPath !== 'none') { await e.reply(segment.image(`file:///${qrPath}`), true); const timerKey = `login:timer:${qq}`; - await redis.set(timerKey, 'pending', 120); - + if (loginTimers.has(timerKey)) { + clearTimeout(loginTimers.get(timerKey).timeout); + clearInterval(loginTimers.get(timerKey).check); + loginTimers.delete(timerKey); + } const check = setInterval(async () => { const status = await loginInstance.checkStatus(qq); if (status) { clearInterval(check); - await redis.del(timerKey); + clearTimeout(timerObj.timeout); + loginTimers.delete(timerKey); return e.reply(`QQ[${qq}] 登录成功!`, true); } - const ttl = await redis.ttl(timerKey); - if (ttl <= 0) { - clearInterval(check); - await loginInstance.disconnect(nickname); - return e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); - } }, 5000); + const timeout = setTimeout(async () => { + clearInterval(check); + loginTimers.delete(timerKey); + await loginInstance.disconnect(nickname); + return e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); + }, 120 * 1000); + const timerObj = { check, timeout }; + loginTimers.set(timerKey, timerObj); } else { const status = await loginInstance.checkStatus(qq); if (status) { From 822876be6bf1331c47a16bfe1656da0138b9ebc3 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 1 Oct 2025 01:09:42 +0800 Subject: [PATCH 17/20] =?UTF-8?q?fix:=E6=9B=B4=E6=94=B9=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.json b/config/config.json index ee1b84e..5894960 100644 --- a/config/config.json +++ b/config/config.json @@ -9,7 +9,7 @@ "token": "" }, "napcat": { - "basePath": "", + "basePath": "/opt/QQ/resources/app/app_launcher/napcat", "userPath": "" }, "lgr": { From c5abaf132d96899c3e087c3bddb91245db50aa4b Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 1 Oct 2025 01:21:02 +0800 Subject: [PATCH 18/20] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96login=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 191 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 125 insertions(+), 66 deletions(-) diff --git a/apps/login.js b/apps/login.js index c8bd9b2..9509b68 100644 --- a/apps/login.js +++ b/apps/login.js @@ -1,15 +1,15 @@ import plugin from '../../../lib/plugins/plugin.js'; import path from 'path'; import ConfigControl from '../lib/config/configControl.js'; -import config from '../../../lib/config/config.js'; import configControl from '../lib/config/configControl.js'; -import axios from 'axios'; import Meme from '../lib/core/meme.js'; import NapcatService from '../lib/login/napcat.js'; import LgrService from '../lib/login/lgr.js'; const configPath = path.join(process.cwd(), 'data/crystelf/config'); const loginSessions = new Map(); //正在进行的登录会话 +const bindSessions = new Map(); //正在进行的绑定会话 +const activeLogins = new Map(); //在线登录实例 export default class LoginService extends plugin { constructor() { @@ -24,13 +24,17 @@ export default class LoginService extends plugin { fnc: 'loginHandler', }, { - reg: '^#绑定账号\\s+\\d+$', + reg: '^#绑定账号(\\s+\\d+)?$', fnc: 'bindAccount', }, { reg: '^#解绑账号\\s+\\d+$', fnc: 'unbindAccount', }, + { + reg: '^#退出登录\\s+\\d+$', + fnc: 'logoutHandler', + }, ], }); } @@ -54,17 +58,15 @@ export default class LoginService extends plugin { if (!targetQq) { const binds = config?.login?.userBinds[userId] || []; if (binds.length === 0) { - if (isAdmin) { - loginSessions.set(userId, { step: 'askQq', admin: true }); - return e.reply('你想登哪个qq?', true); - } else { - return e.reply('管理员似乎没有给你分配可用账户,请联系管理员添加..', true); - } + return e.reply('管理员似乎没有给你分配可用账户,请联系管理员添加..', true); } else if (binds.length === 1) { - targetQq = binds[0]; + targetQq = binds[0].qq; } else { loginSessions.set(userId, { step: 'chooseQq', options: binds }); - return e.reply(`你小子账号还挺多,选一个qq登录吧:\n${binds.join('\n')}`, true); + return e.reply( + `你小子账号还挺多,选一个qq登录吧:\n${binds.map((b) => b.qq).join('\n')}`, + true + ); } } @@ -72,10 +74,11 @@ export default class LoginService extends plugin { await this.startAdminLogin(e, targetQq); } else { const binds = config?.login?.userBinds[userId] || []; - if (!binds.includes(targetQq)) { + const bind = binds.find((b) => b.qq === targetQq); + if (!bind) { return e.reply('你没有权限登录该账号,请联系管理员分配..', true); } - await this.startUserLogin(e, targetQq); + await this.startUserLogin(e, bind); } } @@ -97,16 +100,22 @@ export default class LoginService extends plugin { /** * 普通用户 * @param e - * @param qq + * @param bind * @returns {Promise<*>} */ - async startUserLogin(e, qq) { + async startUserLogin(e, bind) { loginSessions.set(e.user_id, { - step: 'askMethod', - qq, + step: 'autoLogin', + qq: bind.qq, + nickname: bind.nickname, + method: bind.method, admin: false, }); - return e.reply(`请选择登录方式\nnc或lgr\n来登录 QQ[${qq}]`, true); + return this.doLogin(e, { + qq: bind.qq, + nickname: bind.nickname, + method: bind.method, + }); } /** @@ -115,23 +124,13 @@ export default class LoginService extends plugin { * @returns {Promise<*>} */ async bindAccount(e) { - let config = await configControl.get()?.login; if (!e.isMaster) { const img = await Meme.getMeme('zhenxun', 'default'); return e.reply(segment.image(img)); } - const match = e.msg.match(/^#绑定账号\s+(\d+)$/); - if (!match) return; - const qq = match[1]; const at = e.at || e.user_id; - if (!config?.userBinds[at]) config.userBinds[at] = []; - if (!config?.userBinds[at].includes(qq)) { - config.userBinds[at].push(qq); - await ConfigControl.set('login', config); - return e.reply(`已为 ${at} 绑定账号 ${qq}`, true); - } else { - return e.reply(`该用户已绑定此账号`, true); - } + bindSessions.set(e.user_id, { step: 'askQq', targetUser: at }); + return e.reply('要绑定的QQ号是哪个?', true); } /** @@ -150,14 +149,38 @@ export default class LoginService extends plugin { const qq = match[1]; const at = e.at || e.user_id; if (!config?.userBinds[at]) { - ``; - return e.reply('该用户没有绑定账号', true); + return e.reply('该用户没有绑定账号..', true); } - config.userBinds[at] = config.userBinds[at].filter((q) => q !== qq); + config.userBinds[at] = config.userBinds[at].filter((q) => q.qq !== qq); await ConfigControl.set('login', config); return e.reply(`已为 ${at} 解绑账号 ${qq}`, true); } + /** + * 退出登录 + * @param e + * @returns {Promise<*>} + */ + async logoutHandler(e) { + const match = e.msg.match(/^#退出登录\s+(\d+)$/); + if (!match) return; + const qq = match[1]; + const instance = activeLogins.get(qq); + if (!instance) { + return e.reply(`QQ[${qq}] 没有活跃的登录会话..`, true); + } + let config = await configControl.get(); + const isAdmin = e.isMaster; + const userId = e.user_id; + const binds = config?.login?.userBinds[userId] || []; + if (!isAdmin && !binds.includes(qq)) { + return e.reply(`你没有权限退出 QQ[${qq}] 的会话..`, true); + } + await instance.disconnect(); + activeLogins.delete(qq); + return e.reply(`QQ[${qq}] 已退出登录..`, true); + } + /** * 登录流程 * @param e @@ -165,42 +188,76 @@ export default class LoginService extends plugin { */ async accept(e) { const session = loginSessions.get(e.user_id); - if ( - !session || - !e.group_id || - !(await ConfigControl.get()?.login?.allowGroups.includes(e.group_id)) - ) - return; + const bindSession = bindSessions.get(e.user_id); + if (!session && !bindSession) return; - if (session.step === 'askQq') { - session.qq = e.msg.trim(); - session.step = 'askNickname'; - return e.reply(`QQ[${session.qq}]的英文名叫什么?`, true); - } - - if (session.step === 'chooseQq') { - if (!session.options.includes(e.msg.trim())) { - return e.reply('请选择列表中的 QQ', true); + if (bindSession) { + if (bindSession.step === 'askQq') { + bindSession.qq = e.msg.trim(); + bindSession.step = 'askNickname'; + return e.reply(`QQ[${bindSession.qq}]的英文名叫什么?`, true); } - session.qq = e.msg.trim(); - session.step = 'askMethod'; - return e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); - } - - if (session.step === 'askNickname') { - session.nickname = e.msg.trim(); - session.step = 'askMethod'; - return e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); - } - - if (session.step === 'askMethod') { - const method = e.msg.trim().toLowerCase(); - if (!['nc', 'lgr'].includes(method)) { - return e.reply('登录方式无效', true); + if (bindSession.step === 'askNickname') { + bindSession.nickname = e.msg.trim(); + bindSession.step = 'askMethod'; + return e.reply(`请选择登录方式\n[nc]或[lgr]\n来绑定 QQ[${bindSession.qq}]`, true); + } + if (bindSession.step === 'askMethod') { + const method = e.msg.trim().toLowerCase(); + if (!['nc', 'lgr'].includes(method)) { + return e.reply('登录方式无效', true); + } + bindSession.method = method; + let config = await configControl.get()?.login; + if (!config.userBinds[bindSession.targetUser]) + config.userBinds[bindSession.targetUser] = []; + config.userBinds[bindSession.targetUser].push({ + qq: bindSession.qq, + nickname: bindSession.nickname, + method: bindSession.method, + }); + await ConfigControl.set('login', config); + bindSessions.delete(e.user_id); + return e.reply( + `已为 ${bindSession.targetUser} 绑定账号 ${bindSession.qq} (${bindSession.nickname}, ${bindSession.method})`, + true + ); + } + } + + if (session) { + let config = await configControl.get(); + if (!e.group_id || !config?.login?.allowGroups.includes(e.group_id)) return; + + if (session.step === 'chooseQq') { + const chosen = e.msg.trim(); + const bind = session.options.find((b) => b.qq === chosen); + if (!bind) { + return e.reply('请选择列表中的 QQ', true); + } + loginSessions.delete(e.user_id); + return this.doLogin(e, { + qq: bind.qq, + nickname: bind.nickname, + method: bind.method, + }); + } + + if (session.step === 'askNickname') { + session.nickname = e.msg.trim(); + session.step = 'askMethod'; + return e.reply(`请选择登录方式\n[nc]或[lgr]\n来登录 QQ[${session.qq}]`, true); + } + + if (session.step === 'askMethod') { + const method = e.msg.trim().toLowerCase(); + if (!['nc', 'lgr'].includes(method)) { + return e.reply('登录方式无效', true); + } + session.method = method; + loginSessions.delete(e.user_id); + await this.doLogin(e, session); } - session.method = method; - loginSessions.delete(e.user_id); - await this.doLogin(e, session); } } @@ -220,6 +277,7 @@ export default class LoginService extends plugin { } else { loginInstance = new LgrService(); } + activeLogins.set(qq, loginInstance); const qrPath = await loginInstance.login(qq, nickname); const loginTimers = new Map(); if (qrPath && qrPath !== 'none') { @@ -243,6 +301,7 @@ export default class LoginService extends plugin { clearInterval(check); loginTimers.delete(timerKey); await loginInstance.disconnect(nickname); + activeLogins.delete(qq); return e.reply(`QQ[${qq}] 登录超时,已断开,请重新发起登录..`, true); }, 120 * 1000); const timerObj = { check, timeout }; From e50940631f59776eb1716cfeb411195603afc29b Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 1 Oct 2025 16:15:42 +0800 Subject: [PATCH 19/20] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96login=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/login.js | 14 +++++++++----- lib/login/napcat.js | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/login.js b/apps/login.js index 9509b68..fa624ad 100644 --- a/apps/login.js +++ b/apps/login.js @@ -1,12 +1,9 @@ import plugin from '../../../lib/plugins/plugin.js'; -import path from 'path'; import ConfigControl from '../lib/config/configControl.js'; import configControl from '../lib/config/configControl.js'; import Meme from '../lib/core/meme.js'; import NapcatService from '../lib/login/napcat.js'; import LgrService from '../lib/login/lgr.js'; - -const configPath = path.join(process.cwd(), 'data/crystelf/config'); const loginSessions = new Map(); //正在进行的登录会话 const bindSessions = new Map(); //正在进行的绑定会话 const activeLogins = new Map(); //在线登录实例 @@ -270,7 +267,7 @@ export default class LoginService extends plugin { async doLogin(e, session) { try { const { qq, method, nickname } = session; - e.reply(`开始使用 ${method} 登录 QQ[${qq}]`, true); + e.reply(`开始尝试使用 ${method} 登录 QQ[${qq}]`, true); let loginInstance; if (method === 'nc') { loginInstance = new NapcatService(); @@ -281,7 +278,14 @@ export default class LoginService extends plugin { const qrPath = await loginInstance.login(qq, nickname); const loginTimers = new Map(); if (qrPath && qrPath !== 'none') { - await e.reply(segment.image(`file:///${qrPath}`), true); + await e.reply( + [ + segment.image(`file:///${qrPath}`), + '\n请使用手机qq摄像头扫码登录并勾选保存登录状态\n二维码有效期2分钟..', + ], + true, + { recallMsg: 120 } + ); const timerKey = `login:timer:${qq}`; if (loginTimers.has(timerKey)) { clearTimeout(loginTimers.get(timerKey).timeout); diff --git a/lib/login/napcat.js b/lib/login/napcat.js index f175717..793c7c5 100644 --- a/lib/login/napcat.js +++ b/lib/login/napcat.js @@ -3,6 +3,7 @@ import path from 'path'; import { exec } from 'child_process'; import util from 'util'; import configControl from '../config/configControl.js'; +import Path from '../../constants/path.js'; const execAsync = util.promisify(exec); @@ -32,7 +33,7 @@ export default class NapcatService { const userConfigFile = path.join(this.configPath, `onebot11_${qq}.json`); if (!fs.existsSync(userConfigFile)) { try { - const defaultConfigFile = path.join(configControl.get('config')?.path || '', 'napcat.json'); + const defaultConfigFile = path.join(Path.config || '', 'napcat.json'); if (!fs.existsSync(defaultConfigFile)) { logger.error(`[crystelf-admin] 默认配置文件不存在: ${defaultConfigFile}`); } From a8f23eb3d6eb50f82d80fc1481ce41c3ec734951 Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Wed, 1 Oct 2025 18:08:07 +0800 Subject: [PATCH 20/20] fix:path error --- lib/login/lgr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/login/lgr.js b/lib/login/lgr.js index e69f667..fccf383 100644 --- a/lib/login/lgr.js +++ b/lib/login/lgr.js @@ -26,7 +26,7 @@ export default class LgrService { logger.error('[crystelf-admin] 未配置 lgr.basePath'); } const parentDir = path.dirname(this.basePath); - const targetDir = path.join(path.join(parentDir, '..'), String(qq)); + const targetDir = path.join(parentDir, String(qq)); if (!fs.existsSync(targetDir)) { try { await execAsync(`cp -r "${this.basePath}" "${targetDir}"`);