From b1f2a27eacdad81c7630edb65c1b2799f7ac4cf1 Mon Sep 17 00:00:00 2001 From: lyswhut Date: Tue, 7 Nov 2023 22:18:56 +0800 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E6=BA=90=E5=8F=98?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- publish/changeLog.md | 14 + src/common/types/user_api.d.ts | 3 + src/lang/en-us.json | 1 + src/lang/zh-cn.json | 1 + src/lang/zh-tw.json | 1 + src/main/modules/userApi/main.ts | 13 +- src/main/modules/userApi/renderer/preload.js | 304 ++++++++++-------- .../modules/userApi/rendererEvent/name.js | 1 + src/main/modules/userApi/utils.ts | 44 ++- src/renderer/core/useApp/useInitUserApi.ts | 15 +- .../views/Setting/components/SettingBasic.vue | 2 +- 11 files changed, 248 insertions(+), 151 deletions(-) diff --git a/publish/changeLog.md b/publish/changeLog.md index 6a71fe16..e3c26adf 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -1,3 +1,17 @@ +### 自定义源的不兼容变更与新增内容(源开发者需要看) + +自定义源的调用方式已改变: + +- 为了与移动端的调用方式统一,不再推荐使用 `window.lx` 对象(移动端无`window`对象),改用 `globalThis.lx` +- `inited` 事件不再需要传递 `status` 属性,脚本运行过程中,在成功调用 `inited` 事件之前的任何首次未捕获的错误都将视为初始化失败,所以现在若想人为让脚本初始化失败,直接抛出一个错误即可 +- 新增 `globalThis.lx.env` 属性,桌面端环境固定为 `desktop`,移动端环境固定为 `mobile` +- 新增 `globalThis.lx.currentScriptInfo` 对象,可以从这里获取解析后的脚本头部注释信息及脚本原始内容,具体可用属性看文档说明 +- `globalThis.lx.version` 属性更新到 `2.0.0` + +### 新增 + +- 若自定义源初始化失败,将会出现弹窗提示初始化失败的详情 + ### 修复 - 修复备份文件无法导入json格式的问题 diff --git a/src/common/types/user_api.d.ts b/src/common/types/user_api.d.ts index ff9d0f27..264e55ab 100644 --- a/src/common/types/user_api.d.ts +++ b/src/common/types/user_api.d.ts @@ -19,6 +19,9 @@ declare namespace LX { description: string script: string allowShowUpdateAlert: boolean + author?: string + homepage?: string + version?: string sources?: UserApiSources } diff --git a/src/lang/en-us.json b/src/lang/en-us.json index d2e026de..27468f3f 100644 --- a/src/lang/en-us.json +++ b/src/lang/en-us.json @@ -684,6 +684,7 @@ "user_api__btn_import": "Import", "user_api__btn_remove": "Remove", "user_api__import_file": "Select music API script file", + "user_api__init_failed_alert": "Custom source [{name}] failed to initialize:", "user_api__max_tip": "There can only be a maximum of 20 sources at the same time🤪\nIf you want to continue importing, please remove some old sources to make room", "user_api__noitem": "There is nothing here...😲", "user_api__note": "Tip: Although we have isolated the script's running environment as much as possible, importing scripts containing malicious behaviors may still affect your system. Please import them carefully.", diff --git a/src/lang/zh-cn.json b/src/lang/zh-cn.json index 2ef533d1..2ff664d4 100644 --- a/src/lang/zh-cn.json +++ b/src/lang/zh-cn.json @@ -683,6 +683,7 @@ "user_api__btn_import": "导入", "user_api__btn_remove": "移除", "user_api__import_file": "选择音乐API脚本文件", + "user_api__init_failed_alert": "自定义源 [{name}] 初始化失败:", "user_api__max_tip": "最多只能同时存在20个源哦🤪\n想要继续导入的话,请先移除一些旧的源腾出位置吧", "user_api__noitem": "这里竟然是空的 😲", "user_api__note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。", diff --git a/src/lang/zh-tw.json b/src/lang/zh-tw.json index c5eefae2..57dc4b9f 100644 --- a/src/lang/zh-tw.json +++ b/src/lang/zh-tw.json @@ -683,6 +683,7 @@ "user_api__btn_import": "導入", "user_api__btn_remove": "移除", "user_api__import_file": "選擇音樂API腳本文件", + "user_api__init_failed_alert": "自訂來源 [{name}] 初始化失敗:", "user_api__max_tip": "最多只能同時存在20個源哦🤪\n想要繼續導入的話,請先移除一些舊的源騰出位置吧", "user_api__noitem": "這裡竟然是空的 😲", "user_api__note": "提示:雖然我們已經盡可能地隔離了腳本的運行環境,但導入包含惡意行為的腳本仍可能會影響你的系統,請謹慎導入。", diff --git a/src/main/modules/userApi/main.ts b/src/main/modules/userApi/main.ts index fd6a40f4..d1561cc4 100644 --- a/src/main/modules/userApi/main.ts +++ b/src/main/modules/userApi/main.ts @@ -4,6 +4,7 @@ import fs from 'fs' import path from 'node:path' import { openDevTools as handleOpenDevTools } from '@main/utils' import { encodePath } from '@common/utils/electron' +import USER_API_RENDERER_EVENT_NAME from './rendererEvent/name' let browserWindow: Electron.BrowserWindow | null = null @@ -87,12 +88,12 @@ export const createWindow = async(userApi: LX.UserApi.UserApiInfo) => { winEvent() // console.log(html.replace('', ``)) - const randomNum = Math.random().toString().substring(2, 10) - await browserWindow.loadURL( - 'data:text/html;charset=UTF-8,' + encodeURIComponent(html - .replace('', - ``) - .replace('', ``))) + // const randomNum = Math.random().toString().substring(2, 10) + await browserWindow.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html)) + + browserWindow.on('ready-to-show', () => { + sendEvent(USER_API_RENDERER_EVENT_NAME.initEnv, userApi) + }) // global.modules.userApiWindow.loadFile(join(dir, 'renderer/user-api.html')) // global.modules.userApiWindow.webContents.openDevTools() diff --git a/src/main/modules/userApi/renderer/preload.js b/src/main/modules/userApi/renderer/preload.js index 1e2c121c..558096d4 100644 --- a/src/main/modules/userApi/renderer/preload.js +++ b/src/main/modules/userApi/renderer/preload.js @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from 'electron' +import { contextBridge, ipcRenderer, webFrame } from 'electron' import needle from 'needle' import zlib from 'zlib' import { createCipheriv, publicEncrypt, constants, randomBytes, createHash } from 'crypto' @@ -74,7 +74,6 @@ const handleRequest = (context, { requestKey, data }) => { * @param {*} context * @param {*} info { * openDevTools: false, - * status: true, * message: 'xxx', * sources: { * kw: ['128k', '320k', 'flac', 'flac24bit'], @@ -87,18 +86,18 @@ const handleRequest = (context, { requestKey, data }) => { */ const handleInit = (context, info) => { if (!info) { - sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Init failed') + sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Missing required parameter init info') // sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '') return } if (info.openDevTools === true) { sendMessage(USER_API_RENDERER_EVENT_NAME.openDevTools) } - if (!info.status) { - sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Init failed') - // sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '') - return - } + // if (!info.status) { + // sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Missing required parameter init info') + // // sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '') + // return + // } const sourceInfo = { sources: {}, } @@ -138,140 +137,181 @@ const handleShowUpdateAlert = (data, resolve, reject) => { resolve() } -contextBridge.exposeInMainWorld('lx', { - EVENT_NAMES, - request(url, { method = 'get', timeout, headers, body, form, formData }, callback) { - let options = { headers } - let data - if (body) { - data = body - } else if (form) { - data = form - // data.content_type = 'application/x-www-form-urlencoded' - options.json = false - } else if (formData) { - data = formData - // data.content_type = 'multipart/form-data' - options.json = false - } - options.response_timeout = timeout - - let request = needle.request(method, url, data, options, (err, resp, body) => { - if (!err) { - body = resp.body = resp.raw.toString() - try { - resp.body = JSON.parse(resp.body) - } catch (_) {} - body = resp.body +const initEnv = (userApi) => { + contextBridge.exposeInMainWorld('lx', { + EVENT_NAMES, + request(url, { method = 'get', timeout, headers, body, form, formData }, callback) { + let options = { headers } + let data + if (body) { + data = body + } else if (form) { + data = form + // data.content_type = 'application/x-www-form-urlencoded' + options.json = false + } else if (formData) { + data = formData + // data.content_type = 'multipart/form-data' + options.json = false } - callback(err, { - statusCode: resp.statusCode, - statusMessage: resp.statusMessage, - headers: resp.headers, - bytes: resp.bytes, - raw: resp.raw, - body, - }, body) - }).request + options.response_timeout = timeout - return () => { - if (!request.aborted) request.abort() - request = null - } - }, - send(eventName, data) { - return new Promise((resolve, reject) => { - if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName)) + let request = needle.request(method, url, data, options, (err, resp, body) => { + // console.log(err, resp, body) + if (err) { + callback(err, null, null) + } else { + body = resp.body = resp.raw.toString() + try { + resp.body = JSON.parse(resp.body) + } catch (_) {} + body = resp.body + callback(err, { + statusCode: resp.statusCode, + statusMessage: resp.statusMessage, + headers: resp.headers, + bytes: resp.bytes, + raw: resp.raw, + body, + }, body) + } + }).request + + return () => { + if (!request.aborted) request.abort() + request = null + } + }, + send(eventName, data) { + return new Promise((resolve, reject) => { + if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName)) + switch (eventName) { + case EVENT_NAMES.inited: + if (isInitedApi) return reject(new Error('Script is inited')) + isInitedApi = true + handleInit(this, data) + resolve() + break + case EVENT_NAMES.updateAlert: + if (isShowedUpdateAlert) return reject(new Error('The update alert can only be called once.')) + isShowedUpdateAlert = true + handleShowUpdateAlert(data, resolve, reject) + break + default: + reject(new Error('Unknown event name: ' + eventName)) + } + }) + }, + on(eventName, handler) { + if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName)) switch (eventName) { - case EVENT_NAMES.inited: - if (isInitedApi) return reject(new Error('Script is inited')) - isInitedApi = true - handleInit(this, data) - resolve() + case EVENT_NAMES.request: + events.request = handler break - case EVENT_NAMES.updateAlert: - if (isShowedUpdateAlert) return reject(new Error('The update alert can only be called once.')) - isShowedUpdateAlert = true - handleShowUpdateAlert(data, resolve, reject) - break - default: - reject(new Error('Unknown event name: ' + eventName)) + default: return Promise.reject(new Error('The event is not supported: ' + eventName)) } - }) - }, - on(eventName, handler) { - if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName)) - switch (eventName) { - case EVENT_NAMES.request: - events.request = handler - break - default: return Promise.reject(new Error('The event is not supported: ' + eventName)) - } - return Promise.resolve() - }, - utils: { - crypto: { - aesEncrypt(buffer, mode, key, iv) { - const cipher = createCipheriv(mode, key, iv) - return Buffer.concat([cipher.update(buffer), cipher.final()]) - }, - rsaEncrypt(buffer, key) { - buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]) - return publicEncrypt({ key, padding: constants.RSA_NO_PADDING }, buffer) - }, - randomBytes(size) { - return randomBytes(size) - }, - md5(str) { - return createHash('md5').update(str).digest('hex') - }, + return Promise.resolve() }, - buffer: { - from(...args) { - return Buffer.from(...args) + utils: { + crypto: { + aesEncrypt(buffer, mode, key, iv) { + const cipher = createCipheriv(mode, key, iv) + return Buffer.concat([cipher.update(buffer), cipher.final()]) + }, + rsaEncrypt(buffer, key) { + buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]) + return publicEncrypt({ key, padding: constants.RSA_NO_PADDING }, buffer) + }, + randomBytes(size) { + return randomBytes(size) + }, + md5(str) { + return createHash('md5').update(str).digest('hex') + }, }, - bufToString(buf, format) { - return Buffer.from(buf, 'binary').toString(format) + buffer: { + from(...args) { + return Buffer.from(...args) + }, + bufToString(buf, format) { + return Buffer.from(buf, 'binary').toString(format) + }, }, - }, - zlib: { - inflate(buf) { - return new Promise((resolve, reject) => { - zlib.inflate(buf, (err, data) => { - if (err) reject(new Error(err.message)) - else resolve(data) + zlib: { + inflate(buf) { + return new Promise((resolve, reject) => { + zlib.inflate(buf, (err, data) => { + if (err) reject(new Error(err.message)) + else resolve(data) + }) }) - }) - }, - deflate(data) { - return new Promise((resolve, reject) => { - zlib.deflate(data, (err, buf) => { - if (err) reject(new Error(err.message)) - else resolve(buf) + }, + deflate(data) { + return new Promise((resolve, reject) => { + zlib.deflate(data, (err, buf) => { + if (err) reject(new Error(err.message)) + else resolve(buf) + }) }) - }) + }, }, }, - }, - version: '1.3.0', - // removeEvent(eventName, handler) { - // if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName)) - // let handlers - // switch (eventName) { - // case EVENT_NAMES.request: - // handlers = events.request - // break - // } - // for (let index = 0; index < handlers.length; index++) { - // if (handlers[index] === handler) { - // handlers.splice(index, 1) - // break - // } - // } - // }, - // removeAllEvents() { - // for (const handlers of Object.values(events)) { - // handlers.splice(0, handlers.length) - // } - // }, + currentScriptInfo: { + name: userApi.name, + description: userApi.description, + version: userApi.version, + author: userApi.author, + homepage: userApi.homepage, + rawScript: userApi.script, + }, + version: '2.0.0', + env: 'desktop', + // removeEvent(eventName, handler) { + // if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName)) + // let handlers + // switch (eventName) { + // case EVENT_NAMES.request: + // handlers = events.request + // break + // } + // for (let index = 0; index < handlers.length; index++) { + // if (handlers[index] === handler) { + // handlers.splice(index, 1) + // break + // } + // } + // }, + // removeAllEvents() { + // for (const handlers of Object.values(events)) { + // handlers.splice(0, handlers.length) + // } + // }, + }) + + contextBridge.exposeInMainWorld('__lx_init_error_handler__', { + sendError(errorMessage) { + if (isInitedApi) return + isInitedApi = true + if (errorMessage.length > 1024) errorMessage = errorMessage.substring(0, 1024) + '...' + sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, errorMessage) + }, + }) + + webFrame.executeJavaScript(`(() => { +window.addEventListener('error', (event) => { + if (event.isTrusted) globalThis.__lx_init_error_handler__.sendError(event.message.replace(/^Uncaught\\sError:\\s/, '')) +}) +window.addEventListener('unhandledrejection', (event) => { + if (!event.isTrusted) return + const message = typeof event.reason === 'string' ? event.reason : event.reason?.message ?? String(event.reason) + globalThis.__lx_init_error_handler__.sendError(message.replace(/^Error:\\s/, '')) +}) +})()`) + + webFrame.executeJavaScript(userApi.script).catch(_ => _) +} + + +ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.initEnv, (event, data) => { + initEnv(data) }) diff --git a/src/main/modules/userApi/rendererEvent/name.js b/src/main/modules/userApi/rendererEvent/name.js index 07b89d00..542cc2e2 100644 --- a/src/main/modules/userApi/rendererEvent/name.js +++ b/src/main/modules/userApi/rendererEvent/name.js @@ -1,4 +1,5 @@ const names = { + initEnv: '', init: '', request: '', response: '', diff --git a/src/main/modules/userApi/utils.ts b/src/main/modules/userApi/utils.ts index eea3a858..0d695f6d 100644 --- a/src/main/modules/userApi/utils.ts +++ b/src/main/modules/userApi/utils.ts @@ -18,18 +18,44 @@ export const getUserApis = (): LX.UserApi.UserApiInfo[] => { return userApis } +const INFO_NAMES = { + name: 24, + description: 36, + author: 56, + homepage: 1024, + version: 36, +} as const +type INFO_NAMES_Type = typeof INFO_NAMES +const matchInfo = (scriptInfo: string) => { + const infoArr = scriptInfo.split(/\r?\n/) + const rxp = /^\s?\*\s?@(\w+)\s(.+)$/ + const infos: Partial> = {} + for (const info of infoArr) { + const result = rxp.exec(info) + if (!result) continue + const key = result[1] as keyof typeof INFO_NAMES + if (INFO_NAMES[key] == null) continue + infos[key] = result[2].trim() + } + + for (const [key, len] of Object.entries(INFO_NAMES) as Array<{ [K in keyof INFO_NAMES_Type]: [K, INFO_NAMES_Type[K]] }[keyof INFO_NAMES_Type]>) { + infos[key] ||= '' + if (infos[key] == null) infos[key] = '' + else if (infos[key]!.length > len) infos[key] = infos[key]!.substring(0, len) + '...' + } + + return infos as Record +} export const importApi = (script: string): LX.UserApi.UserApiInfo => { - let scriptInfo = script.split(/\r?\n/) - let name = scriptInfo[1] || '' - let description = scriptInfo[2] || '' - name = name.startsWith(' * @name ') ? name.replace(' * @name ', '').trim() : `user_api_${new Date().toLocaleString()}` - if (name.length > 24) name = name.substring(0, 24) + '...' - description = description.startsWith(' * @description ') ? description.replace(' * @description ', '').trim() : '' - if (description.length > 36) description = description.substring(0, 36) + '...' + const result = /^\/\*[\S|\s]+?\*\//.exec(script) + if (!result) throw new Error('无效的自定义源文件') + + let scriptInfo = matchInfo(result[0]) + + scriptInfo.name ||= `user_api_${new Date().toLocaleString()}` const apiInfo = { id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`, - name, - description, + ...scriptInfo, script, allowShowUpdateAlert: true, } diff --git a/src/renderer/core/useApp/useInitUserApi.ts b/src/renderer/core/useApp/useInitUserApi.ts index 631c6f6a..b2e02032 100644 --- a/src/renderer/core/useApp/useInitUserApi.ts +++ b/src/renderer/core/useApp/useInitUserApi.ts @@ -16,8 +16,9 @@ export default () => { userApi.status = status userApi.message = message - if (status && apiInfo?.sources) { - if (apiInfo.id === appSetting['common.apiSource']) { + if (!apiInfo || apiInfo.id !== appSetting['common.apiSource']) return + if (status) { + if (apiInfo.sources) { let apis: any = {} let qualitys: LX.QualityList = {} for (const [source, { actions, type, qualitys: sourceQualitys }] of Object.entries(apiInfo.sources)) { @@ -42,7 +43,7 @@ export default () => { musicInfo: songInfo, }, }, - // eslint-disable-next-line @typescript-eslint/promise-function-async + // eslint-disable-next-line @typescript-eslint/promise-function-async }).then(res => { // console.log(res) if (!/^https?:/.test(res.data.url)) return Promise.reject(new Error('Get url failed')) @@ -64,6 +65,14 @@ export default () => { qualityList.value = qualitys userApi.apis = apis } + } else { + if (message) { + void dialog({ + message: `${t('user_api__init_failed_alert', { name: apiInfo.name })}\n${message}`, + selection: true, + confirmButtonText: t('ok'), + }) + } } }) diff --git a/src/renderer/views/Setting/components/SettingBasic.vue b/src/renderer/views/Setting/components/SettingBasic.vue index fa154288..fa86d9e2 100644 --- a/src/renderer/views/Setting/components/SettingBasic.vue +++ b/src/renderer/views/Setting/components/SettingBasic.vue @@ -231,7 +231,7 @@ export default { let status if (userApi.status) status = t('setting__basic_source_status_success') else if (userApi.message == 'initing') status = t('setting__basic_source_status_initing') - else status = `${t('setting__basic_source_status_failed')} - ${userApi.message}` + else status = `${t('setting__basic_source_status_failed')}` return status }