diff --git a/package.json b/package.json index 7609762..964c017 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stapxs-qq-lite", - "version": "2.8.0", + "version": "2.8.2", "private": false, "author": "Stapx Steve [林槐]", "description": "一个兼容 OneBot 的非官方网页版 QQ 客户端,使用 Vue 重制的全新版本。", @@ -28,6 +28,7 @@ "js-file-downloader": "^1.1.24", "js-yaml-loader": "^1.2.2", "jsonpath": "^1.1.1", + "log4js": "^6.9.1", "pinyin": "^3.0.0-alpha.4", "prismjs": "^1.29.0", "raw-loader": "^4.0.2", diff --git a/src/assets/l10n/zh-CN.json b/src/assets/l10n/zh-CN.json index e27610c..61c36dd 100644 --- a/src/assets/l10n/zh-CN.json +++ b/src/assets/l10n/zh-CN.json @@ -340,6 +340,7 @@ "cq_code": "CQ 码", "array_code": "Array 数组", "chat_pic": "图片", + "log_con_backend": "使用后端连接模式", "menu_about": "关于", "menu_update": "检查更新…", diff --git a/src/background.ts b/src/background.ts index ef593de..9f09c94 100644 --- a/src/background.ts +++ b/src/background.ts @@ -10,9 +10,12 @@ import { noticeList, regIpcListener } from './function/electron/ipc' import { Menu, session, app, protocol, BrowserWindow } from 'electron' import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' import { touchBar } from './function/electron/touchbar' +import log4js from 'log4js' const isDevelopment = process.env.NODE_ENV !== 'production' const isPrimary = app.requestSingleInstanceLock() +const logger = log4js.getLogger('background') +export let logLevel = isDevelopment ? 'debug' : 'info' protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } } @@ -21,19 +24,25 @@ protocol.registerSchemesAsPrivileged([ export let win = undefined as BrowserWindow | undefined export let touchBarInstance = undefined as touchBar | undefined -/* eslint-disable no-console */ async function createWindow() { + if(new Store().get('opt_log_level')) { + logLevel = (String) (new Store().get('opt_log_level')) ?? 'info' + } + logger.level = logLevel + + /* eslint-disable no-console */ console.log('') console.log(' _____ _____ _____ _____ __ __ \n' + '| __|_ _| _ | _ | | | \n' + '|__ | | | | | __|- -| \n' + '|_____| |_| |__|__|__| |__|__| CopyRight © Stapx Steve') console.log('=======================================================') - console.log('Welcome to Stapxs QQ Lite, current version: ' + packageInfo.version) - console.log('The background language component will be initialized after the frontend is loaded.') + console.log('日志等级:', logLevel) + /* eslint-enable no-console */ + logger.info('欢迎使用 Stapxs QQ Lite, 当前版本: ' + packageInfo.version) - console.log('Platform:' + process.platform) - console.log('Start creating main window ……') + logger.info('启动平台架构:' + process.platform) + logger.info('正在创建窗体 ……') Menu.setApplicationMenu(null) // 创建窗口 const mainWindowState = windowStateKeeper({ @@ -89,7 +98,7 @@ async function createWindow() { win = new BrowserWindow(windowConfig) win.once('focus', () => {if(win)win.flashFrame(false)}) mainWindowState.manage(win) // 窗口状态管理器 - console.log('Create main window to complete.') + logger.info('创建窗体完成') // 注册 IPC 事务 regIpcListener() // macOS:创建 TouchBar @@ -176,7 +185,7 @@ app.on('ready', async () => { try { await installExtension('nhdogjmejiglipccpnnnanhbledajbpd') } catch (e: unknown) { - console.error('Vue Devtools failed to install:', (e as Error).toString()) + logger.error('Vue Devtools 安装失败:', (e as Error).toString()) } } createWindow() diff --git a/src/function/connect.ts b/src/function/connect.ts index df54646..a16d16c 100644 --- a/src/function/connect.ts +++ b/src/function/connect.ts @@ -32,6 +32,16 @@ export class Connector { static create(address: string, token?: string, wss: boolean | undefined = undefined) { const $t = app.config.globalProperties.$t + // Electron 默认使用后端连接模式 + if(runtimeData.tags.isElectron) { + logger.add(LogType.WS, $t('log_con_backend')) + const reader = runtimeData.reader + if(reader) { + reader.send('onebot:connect', { address: address, token: token }) + return + } + } + // PS:只有在未设定 wss 类型的情况下才认为是首次连接 if(wss == undefined) retry = 0; else retry ++ // 最多自动重试连接五次 @@ -63,34 +73,57 @@ export class Connector { } websocket.onopen = () => { - logger.add(LogType.WS, $t('log_con_success')) - // 保存登录信息 - Option.save('address', address) - // 保存密钥 - if(runtimeData.sysConfig.save_password && runtimeData.sysConfig.save_password != '') { - Option.save('save_password', token) - } - // 清空应用通知 - popInfo.clear() - // 加载初始化数据 - // PS:标记登陆成功在获取用户信息的回调位置,防止无法获取到内容 - Connector.send('get_version_info', {}, 'getVersionInfo') - // 更新菜单 - updateMenu({ - id: 'logout', - action: 'visible', - value: true - }) + this.onopen(address, token) } websocket.onmessage = (e) => { - // 心跳包输出到日志里太烦人了 - if ((e.data as string).indexOf('"meta_event_type":"heartbeat"') < 0) { - logger.add(LogType.WS, 'GET:' + e.data) - } - parse(e.data) + this.onmessage(e.data) } websocket.onclose = (e) => { - websocket = undefined + this.onclose(e.code, e.reason, address, token) + } + websocket.onerror = (e) => { + popInfo.add(PopType.ERR, $t('pop_log_con_fail') + ': ' + e.type, false) + return + } + } + + // 连接事件 ===================================================== + + static onopen(address: string, token: string | undefined) { + const $t = app.config.globalProperties.$t + + logger.add(LogType.WS, $t('log_con_success')) + // 保存登录信息 + Option.save('address', address) + // 保存密钥 + if (runtimeData.sysConfig.save_password && runtimeData.sysConfig.save_password != '') { + Option.save('save_password', token) + } + // 清空应用通知 + popInfo.clear() + // 加载初始化数据 + // PS:标记登陆成功在获取用户信息的回调位置,防止无法获取到内容 + Connector.send('get_version_info', {}, 'getVersionInfo') + // 更新菜单 + updateMenu({ + id: 'logout', + action: 'visible', + value: true + }) + } + + static onmessage(message: string) { + // 心跳包输出到日志里太烦人了 + if ((message as string).indexOf('"meta_event_type":"heartbeat"') < 0) { + logger.add(LogType.WS, 'GET:' + message) + } + parse(message) + } + + static onclose(code: number, message: string | undefined, address: string, token: string | undefined) { + const $t = app.config.globalProperties.$t + + websocket = undefined updateMenu({ id: 'logout', action: 'visible', @@ -102,7 +135,7 @@ export class Connector { value: $t('menu_login') }) - switch(e.code) { + switch(code) { case 1000: break; // 正常关闭 case 1006: { // 非正常关闭,尝试重连 if(login.status) { @@ -119,31 +152,35 @@ export class Connector { break; } default: { - popInfo.add(PopType.ERR, $t('pop_log_con_fail') + ': ' + e.code, false) + popInfo.add(PopType.ERR, $t('pop_log_con_fail') + ': ' + code, false) // eslint-disable-next-line no-console - console.log(e) + console.log(message) } } - logger.error($t('pop_log_con_fail') + ': ' + e.code) + logger.error($t('pop_log_con_fail') + ': ' + code) login.status = false // 除了 1006 意外断开(可能要保留数据重连),其他情况都会清空 - if(e.code != 1006) { + if(code != 1006) { resetRimtime() } - } - websocket.onerror = (e) => { - popInfo.add(PopType.ERR, $t('pop_log_con_fail') + ': ' + e.type, false) - return - } } + // 连接器操作 ===================================================== + /** * 正常断开 Websocket 连接 */ static close() { - popInfo.add(PopType.INFO, app.config.globalProperties.$t('pop_log_con_close')) - if(websocket) websocket.close(1000) + if(runtimeData.tags.isElectron) { + const reader = runtimeData.reader + if(reader) { + reader.send('onebot:close') + } + } else { + popInfo.add(PopType.INFO, app.config.globalProperties.$t('pop_log_con_close')) + if(websocket) websocket.close(1000) + } } /** @@ -156,11 +193,18 @@ export class Connector { // 构建 JSON const json = JSON.stringify({ action: name, params: value, echo: echo } as BotActionElem) // 发送 - if(websocket) websocket.send(json) - if (Option.get('log_level') === 'debug') { - logger.debug('PUT:' + json) + if(runtimeData.tags.isElectron) { + const reader = runtimeData.reader + if(reader) { + reader.send('onebot:send', json) + } } else { - logger.add(LogType.WS, 'PUT:' + json) + if(websocket) websocket.send(json) + if (Option.get('log_level') === 'debug') { + logger.debug('PUT:' + json) + } else { + logger.add(LogType.WS, 'PUT:' + json) + } } } } diff --git a/src/function/electron/connector.ts b/src/function/electron/connector.ts new file mode 100644 index 0000000..60b5f4f --- /dev/null +++ b/src/function/electron/connector.ts @@ -0,0 +1,81 @@ +/** + * 后端和前端的简易通信器,封装一些常用的交互 + */ + +import WebSocket from 'ws' +import log4js from 'log4js' +import { BrowserWindow, ipcMain } from 'electron' +import { logLevel } from '@/background' + +export class Connector { + + private logger = log4js.getLogger('connector') + + private win: BrowserWindow + private websocket: WebSocket | undefined + + constructor(win: BrowserWindow) { + this.logger.level = logLevel + this.win = win + // 初始化 ipc + ipcMain.on('onebot:send', (event, json) => { + this.websocket?.send(json) + }) + ipcMain.on('onebot:close', () => { + this.websocket?.close(1000) + this.websocket = undefined + }) + this.logger.info('后端连接器已初始化') + } + + connect(url: string, token: string) { + if(url.indexOf('ws://') < 0 && url.indexOf('wss://') < 0) { + url = 'wss://' + url + } + + if(!this.websocket) { + this.logger.info('正在连接到:', url) + this.websocket = new WebSocket(url + '?access_token=' + token) + } + + this.websocket.onopen = () => { + this.logger.info('已成功连接到', url) + this.win.webContents.send('onebot:onopen', { address: url, token: token }) + } + this.websocket.onmessage = (e) => { + try { + const message = JSON.parse((e.data as string)) + if(message.echo) + this.logger.debug('收到消息:', message.echo) + else if(message.post_type) { + if(message.notice_type) + this.logger.debug('收到消息:', message.post_type, message.notice_type) + else + this.logger.debug('收到消息:', message.post_type) + } + } catch(e) { + // + } + this.win.webContents.send('onebot:onmessage', e.data) + } + this.websocket.onclose = (e) => { + this.logger.info('连接已关闭,代码:', e.code) + if(e.code != 1006 && e.code != 1015) { + // 除了需要重连的情况,其他情况都直接常规处理 + this.win.webContents.send('onebot:onclose', { + code: e.code, + message: e.reason, + address: url, + token: token + }) + } else if(e.code == 1015) { + // TSL 连接失败,尝试使用非加密连接 + this.logger.warn('连接失败,尝试使用非加密连接...') + this.connect(url.replace('wss://', 'ws://'), token) + } + } + this.websocket.onerror = (e) => { + this.logger.error('连接错误:', e) + } + } +} \ No newline at end of file diff --git a/src/function/electron/ipc.ts b/src/function/electron/ipc.ts index b785080..7cf6fcf 100644 --- a/src/function/electron/ipc.ts +++ b/src/function/electron/ipc.ts @@ -6,12 +6,21 @@ import { ipcMain, shell, systemPreferences, app, Menu, MenuItemConstructorOption import { GtkTheme, GtkData } from '@jakejarrett/gtk-theme' import { runCommand } from './util' import { win, touchBarInstance } from '@/background' +import { Connector } from '@/function/electron/connector' +let connector = undefined as Connector | undefined const store = new Store() let template = [] as any[] export const noticeList = {} as {[key: string]: ELNotification[]} export function regIpcListener() { + // 后端连接模式 + ipcMain.on('onebot:connect', (event, args) => { + if(!connector && win) { + connector = new Connector(win) + } + connector?.connect(args.address, args.token) + }) // 获取系统平台 ipcMain.handle('sys:getPlatform', () => { return process.platform diff --git a/src/function/utils/appUtil.ts b/src/function/utils/appUtil.ts index 15c0d21..5fb8bf7 100644 --- a/src/function/utils/appUtil.ts +++ b/src/function/utils/appUtil.ts @@ -476,6 +476,17 @@ export function createIpc() { runtimeData.reader.on('app:jumpChat', (event, info) => { jumpToChat(info.userId, info.msgId) }) + + // 后端连接模式 + runtimeData.reader.on('onebot:onopen', (event, data) => { + Connector.onopen(data.address, data.token) + }) + runtimeData.reader.on('onebot:onmessage', (event, message) => { + Connector.onmessage(message) + }) + runtimeData.reader.on('onebot:onclose', (event, data) => { + Connector.onclose(data.code, data.reason, data.address, data.token) + }) } } diff --git a/src/pages/Scripts.vue b/src/pages/Scripts.vue index 5640e31..49cd1c0 100644 --- a/src/pages/Scripts.vue +++ b/src/pages/Scripts.vue @@ -89,7 +89,6 @@ import { highlight, languages } from 'prismjs' import { getMsgData } from '@/function/utils/msgUtil' import { Logger, PopInfo, PopType } from '@/function/base' -import { MsgElem, MsgInfoElem } from '@/function/elements/information' export default defineComponent({ name: 'ViewScripts', @@ -111,8 +110,8 @@ export default defineComponent({ }[], select: '', - message: null as MsgElem | null, - msgInfo: null as MsgInfoElem | null, + message: null as any | null, + msgInfo: null as {[key: string]: any} | null, isMe: false } },