diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 17bf571..d5b6a1b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -78,7 +78,7 @@ jobs: comments=$(gh pr view ${{ env.PR_NUMBER }} --json comments) # 过滤目标评论并按时间排序 - target_comments=$(echo "$comments" | jq -r '.comments[] | select(.body | test("🎉 构建完成!|❌ 构建失败!")) | {url: .url, created_at: .createdAt}' | jq -s '. | sort_by(.created_at) | reverse') + target_comments=$(echo "$comments" | jq -r '.comments[] | select(.body | test("🎉 🎉 🎉构建完成!|❌ 构建失败!|构建过程中出现错误")) | {url: .url, created_at: .createdAt}' | jq -s '. | sort_by(.created_at) | reverse') # 获取需要删除的评论ID (保留最新的3条) comment_ids_to_delete=$(echo "$target_comments" | jq -r 'if length > 3 then .[3:][] | .url | capture("#issuecomment-(?\\d+)$").id else empty end') diff --git a/config/PluginConfigView.yaml b/config/PluginConfigView.yaml deleted file mode 100644 index f96b51b..0000000 --- a/config/PluginConfigView.yaml +++ /dev/null @@ -1,271 +0,0 @@ -- - # 配置文件描述 - name: Cookies相关 - # 配置文件名 - file: cookies.yaml - # 视图信息 - view: - - - # 配置项 - key: 抖音ck - # 配置描述 - comment: 抖音ck,用于请求官方API数据 - # 配置路径 - path: 'douyin' - # 配置类型 - type: 'text' - - - key: B站ck - comment: B站ck,用于请求官方API数据 - path: 'bilibili' - type: 'text' - -- - # 配置文件描述 - name: 应用设置 - # 配置文件名 - file: app.yaml - # 视图信息 - view: - - - # 配置项 - key: 总开关 - # 配置描述 - comment: 视频解析工具总开关,修改后重启生效,关闭后可使用 kkk/kkk解析/解析 + 视频分享链接代替 - # 配置路径 - path: 'videotool' - # 配置类型 - type: 'boolean' - - - key: 默认解析 - comment: 识别最高优先级,修改后重启生效 - path: 'defaulttool' - type: 'boolean' - - - key: 发送合并转发消息 - comment: 可能多用于抖音解析 - path: 'sendforwardmsg' - type: 'boolean' - - - key: 视频文件上传限制 - comment: 开启后会根据解析的视频文件大小判断是否需要上传(B站番剧无影响) - path: 'usefilelimit' - type: 'boolean' - - - key: 缓存删除 - comment: 非必要不修改! - path: 'rmmp4' - type: 'boolean' - - - key: API服务 - comment: '将所有api接口放出,作为一个本地的视频解析API服务,默认端口4567' - path: 'APIServer' - type: 'boolean' - - - key: API服务端口 - comment: 'API服务端口' - path: 'APIServerPort' - type: 'number' - - - key: API服务日志 - comment: '打开或关闭运行日志' - path: 'APIServerLog' - type: 'boolean' - - - key: 自定义优先级 - comment: 「默认解析」关闭后才会生效。修改后重启生效 - path: 'priority' - type: 'number' - - - key: 视频文件大小限制 - comment: '视频文件大于该数值则不会上传 单位: MB,「视频文件上传限制」开启后才会生效(B站番剧无影响)' - path: 'filelimit' - type: 'number' - - - key: 渲染精度 - comment: 可选值50~200,建议100。设置高精度会提高图片的精细度,过高可能会影响渲染与发送速度 - path: 'renderScale' - type: 'number' - -- - # 配置文件描述 - name: 抖音相关 - # 配置文件名 - file: douyin.yaml - # 视图信息 - view: - - - # 配置项 - key: 抖音解析开关 - # 配置描述 - comment: 单独开关,受「总开关」影响 - # 配置路径 - path: 'douyintool' - # 配置类型 - type: 'boolean' - - - key: 抖音解析提示 - comment: 发送提示信息:“检测到抖音链接,开始解析” - path: 'douyintip' - type: 'boolean' - - - key: 抖音评论解析 - path: 'comments' - type: 'boolean' - - - key: 抖音评论数量 - comment: 范围1~50条 - path: 'numcomments' - type: 'number' - - - key: 发送抖音作品评论图 - path: 'commentsimg' - type: 'boolean' - - - key: 抖音推送 - comment: 开启后需重启;使用[#设置抖音推送+抖音号]配置推送列表 - path: 'douyinpush' - type: 'boolean' - - - key: 抖音推送日志 - comment: 打开或关闭定时任务日志 - path: 'douyinpushlog' - type: 'boolean' - - - key: 抖音推送设置权限 - comment: '0: (all)所有人;1: (admin)群主和管理员;2: (owner)群主;3: (master)主人' - path: 'douyinpushGroup' - type: 'select' - # 是否可以多选 - multiple: false - # 选项 - item: - - - name: 所有人都可以添加 - value: all - - - name: 只有群管理员可以添加 - value: admin - - - name: 只有群主可以添加 - value: owner - - - name: 只有主人可以添加 - value: master - - - key: 抖音推送表达式 - path: 'douyinpushcron' - type: 'boolean' - - - key: 图集BGM是否使用高清语音发送 - comment: 高清语音「ios/PC」系统均无法播放,自行衡量开关(仅icqq) - path: 'sendHDrecord' - type: 'boolean' - - - key: 抖音推送是否一同发送作品视频 - comment: 和推送图一同将新作品内容发送出去(图集暂未支持) - path: 'senddynamicwork' - type: 'boolean' - -- - # 配置文件描述 - name: bilibili相关 - # 配置文件名 - file: bilibili.yaml - # 视图信息 - view: - - - # 配置项 - key: B站解析开关 - # 配置描述 - comment: 单独开关,受「总开关」影响 - # 配置路径 - path: 'bilibilitool' - # 配置类型 - type: 'boolean' - - - key: B站解析提示 - comment: 发送提示信息:“检测到B站链接,开始解析” - path: 'bilibilitip' - type: 'boolean' - - - key: B站评论图 - comment: 发送哔哩哔哩作品评论图 - path: 'bilibilicommentsimg' - type: 'boolean' - - - key: B站评论数量 - comment: 范围1~20条 - path: 'bilibilinumcomments' - type: 'number' - - - key: B站推送 - comment: 开启后需重启;使用[#设置B站推送+UID]配置推送列表 - path: 'bilibilipush' - type: 'boolean' - - - key: B站推送日志 - comment: 打开或关闭定时任务日志 - path: 'bilibilipushlog' - type: 'boolean' - - - key: B站推送设置权限 - comment: '0: (all)所有人;1: (admin)群主和管理员;2: (owner)群主;3: (master)主人' - path: 'bilibilipushGroup' - type: 'select' - # 是否可以多选 - multiple: false - # 选项 - item: - - - name: 所有人都可以添加 - value: all - - - name: 只有群管理员可以添加 - value: admin - - - name: 只有群主可以添加 - value: owner - - - name: 只有主人可以添加 - value: master - - - key: B站推送表达式 - path: 'bilibilipushcron' - type: 'boolean' - - - key: B站动态视频发送 - comment: 该UP的最新动态可能是视频,可选是否与推送图片一同发送 - path: 'senddynamicvideo' - type: 'boolean' - - - key: B站动态视频发送 - comment: 解析视频是否优先保内容,打开为优先保证上传将使用最低分辨率,关闭为优先保清晰度将使用最高分辨率 - path: 'videopriority' - type: 'boolean' -- - # 配置文件描述 - name: 快手相关 - # 配置文件名 - file: kuaishou.yaml - # 视图信息 - view: - - - # 配置项 - key: 快手解析开关 - # 配置描述 - comment: 单独开关,受「总开关」影响 - # 配置路径 - path: 'kuaishoutool' - # 配置类型 - type: 'boolean' - - - key: 快手解析提示 - comment: 发送提示信息:“检测到快手链接,开始解析” - path: 'kuaishoutip' - type: 'boolean' - - - key: 快手评论数量 - comment: 范围1~30条 - path: 'kuaishounumcomments' - type: 'number' \ No newline at end of file diff --git a/config/default_config/app.yaml b/config/default_config/app.yaml index 09552cd..3f2c37d 100644 --- a/config/default_config/app.yaml +++ b/config/default_config/app.yaml @@ -7,9 +7,6 @@ defaulttool: true # 自定义优先级,「默认解析」关闭后才会生效。修改后重启生效 priority: 800 -# 发送合并转发消息,可能多用于抖音解析 -sendforwardmsg: true - # 缓存删除,非必要不修改! rmmp4: true diff --git a/config/default_config/kuaishou.yaml b/config/default_config/kuaishou.yaml index 0df8671..aa68df2 100644 --- a/config/default_config/kuaishou.yaml +++ b/config/default_config/kuaishou.yaml @@ -4,5 +4,8 @@ switch: true # 快手解析提示,发送提示信息:“检测到快手链接,开始解析” tip: true +# 快手评论解析,发送快手作品评论图 +comment: true + # 快手评论数量,范围1~30条 numcomment: 5 \ No newline at end of file diff --git a/package.json b/package.json index f129c95..5b2d20d 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "globals": "^15.14.0", "neostandard": "^0.11.9", - "node-karin": "^1.3.11", + "node-karin": "^1.3.14", "sort-package-json": "^2.14.0", "tsc-alias": "^1.8.10", "tsx": "^4.19.2", diff --git a/resources/template/admin/html/index.html b/resources/template/admin/html/index.html index 6275558..144a206 100644 --- a/resources/template/admin/html/index.html +++ b/resources/template/admin/html/index.html @@ -387,7 +387,7 @@ #kkk设置上传压缩后的值 + 0 ~ x {{@data.upload.compressvalue}} -
若视频文件大小大于「压缩触发阈值」的值,则会进行压缩至该值(±5%),「压缩视频」开启后才会生效
+
单位:MB,若视频文件大小大于「压缩触发阈值」的值,则会进行压缩至该值(±5%),「压缩视频」开启后才会生效
  • diff --git a/src/apps/admin.ts b/src/apps/admin.ts index 6fe5b82..4ce8a4f 100644 --- a/src/apps/admin.ts +++ b/src/apps/admin.ts @@ -240,7 +240,6 @@ const PlatformTypeConfig: Record = { 缓存删除: 'rmmp4', 视频解析: 'videotool', 默认解析: 'defaulttool', - 转发: 'sendforwardmsg', 上传限制: 'usefilelimit', API服务: 'APIServer', base64: 'sendbase64' diff --git a/src/module/utils/Config.ts b/src/module/utils/Config.ts index 1eb4b71..6e99e8d 100644 --- a/src/module/utils/Config.ts +++ b/src/module/utils/Config.ts @@ -1,7 +1,7 @@ import fs from 'node:fs' import path from 'node:path' -import { basePath, copyConfigSync, filesByExt, requireFileSync, watch } from 'node-karin' +import { basePath, copyConfigSync, filesByExt, logger, requireFileSync, watch } from 'node-karin' import YAML from 'node-karin/yaml' import { ConfigType } from '@/types' @@ -45,6 +45,7 @@ class Cfg { // logger.info('新数据:', now); })) }, 2000) + this.convertType() return this } @@ -61,7 +62,7 @@ class Cfg { /** 获取所有配置文件 */ All (): ConfigType { - const allConfig: ConfigType = {} as ConfigType // 初始化为 ConfigType 类型 + const allConfig: any = {} // 初始化为 ConfigType 类型 // 读取默认配置文件夹中的所有文件 const files = fs.readdirSync(this.defCfgPath) @@ -72,7 +73,7 @@ class Cfg { allConfig[fileName] = this.getDefOrConfig(fileName) || {} as ConfigType[keyof ConfigType] }) - return allConfig + return allConfig as ConfigType } /** @@ -81,14 +82,26 @@ class Cfg { * @param name 配置文件名 * @returns 返回 YAML 文件内容 */ - private getYaml (type: ConfigDirType, name: keyof ConfigType) { + private getYaml (type: ConfigDirType, name: T): ConfigType[T] { const file = type === 'config' ? `${this.dirCfgPath}/${name}.yaml` : `${this.defCfgPath}/${name}.yaml` // 自动管理缓存 无需手动清除 如无缓存 则会自动导入并加载 - return requireFileSync(file) + return requireFileSync(file, { force: true }) + } + + /** 由于上游类型定义错误,导致下游需要手动对已设置的内容进行类型转换。。。 */ + private convertType () { + const pushList = this.getYaml('config', 'pushlist') + pushList.bilibili.map((item) => { + if (typeof item.host_mid === 'string') { + item.host_mid = parseInt(item.host_mid) + } + return item + }) + this.Modify('pushlist', 'bilibili', pushList.bilibili) } /** @@ -119,6 +132,72 @@ class Cfg { fs.writeFileSync(path, yamlData.toString({ lineWidth: -1 }), 'utf8') } + /** + * 修改整个配置文件,保留注释 + * @param name 文件名 + * @param config 完整的配置对象 + * @param type 配置文件类型,默认为用户配置文件 `config` + */ + ModifyPro ( + name: T, + config: ConfigType[T], + type: ConfigDirType = 'config' + ) { + const filePath = + type === 'config' + ? `${this.dirCfgPath}/${name}.yaml` + : `${this.defCfgPath}/${name}.yaml` + + try { + // 1. 读取现有文件(包含注释) + const existingContent = fs.readFileSync(filePath, 'utf8') + const doc = YAML.parseDocument(existingContent) + + // 2. 将新配置转换为YAML节点(不解析注释) + const newConfigNode = YAML.parseDocument(YAML.stringify(config)).contents + + // 3. 深度合并新配置到现有文档(保留注释结构) + this.deepMergeYaml(doc.contents, newConfigNode) + + // 4. 写回文件 + fs.writeFileSync(filePath, doc.toString({ lineWidth: -1 }), 'utf8') + return true + } catch (error) { + logger.error(`修改配置文件时发生错误:${error}`) + return false + } + } + + /** + * 深度合并YAML节点(保留目标注释) + * @param target 目标节点(保留注释的原始节点) + * @param source 源节点(提供新值的节点) + */ + private deepMergeYaml (target: any, source: any) { + if (YAML.isMap(target) && YAML.isMap(source)) { + // 遍历源节点的所有键 + for (const pair of source.items) { + const key = pair.key + const sourceVal = pair.value + const targetVal = target.get(key) + + if (targetVal === undefined) { + // 新增键:直接添加 + target.set(key, sourceVal) + } else if (YAML.isMap(targetVal) && YAML.isMap(sourceVal)) { + // 递归合并嵌套Map + this.deepMergeYaml(targetVal, sourceVal) + } else if (YAML.isSeq(targetVal) && YAML.isSeq(sourceVal)) { + // 替换序列内容但保留注释 + targetVal.items = sourceVal.items + } else { + // 覆盖标量值但保留原键的注释 + target.set(key, sourceVal) + } + } + } + } + /** * 在YAML映射中设置嵌套值 * @@ -196,7 +275,7 @@ class Cfg { } } -type Config = ConfigType & Pick +type Config = ConfigType & Pick export const Config: Config = new Proxy(new Cfg().initCfg(), { get (target, prop: string) { diff --git a/src/platform/bilibili/push.ts b/src/platform/bilibili/push.ts index 1402901..b0bade5 100644 --- a/src/platform/bilibili/push.ts +++ b/src/platform/bilibili/push.ts @@ -52,6 +52,7 @@ export class Bilibilipush extends Base { } this.force = force // 保存传入的强制执行标志 this.amagi = new Client({ bilibili: Config.cookies.bilibili }) + this.mergeBilibiliItems(Config.pushlist.bilibili) } /** @@ -62,7 +63,7 @@ export class Bilibilipush extends Base { if (await this.checkremark()) return true try { - const data = await this.getDynamicList(Config.pushlist.bilibili) + const data = await this.getDynamicList(this.mergeBilibiliItems(Config.pushlist.bilibili)) const pushdata = this.excludeAlreadyPushed(data.willbepushlist, data.DBdata) if (Object.keys(pushdata).length === 0) return true @@ -684,6 +685,28 @@ export class Bilibilipush extends Base { const img = await Render('bilibili/userlist', { renderOpt }) await this.e.reply(img) } + + /** 由于上游类型定义错误,导致下游需要手动对已设置的内容进行类型转换。。。 */ + mergeBilibiliItems (config: bilibiliPushItem[]) { + const mergedBilibili: bilibiliPushItem[] = [] + const midMap = new Map() + + config.forEach(item => { + const { host_mid } = item + if (midMap.has(host_mid)) { + const index = midMap.get(host_mid)! + mergedBilibili[index].group_id = [ + ...mergedBilibili[index].group_id, + ...item.group_id + ] + } else { + mergedBilibili.push({ ...item }) + midMap.set(host_mid, mergedBilibili.length - 1) + } + }) + Config.Modify('pushlist', 'bilibili', mergedBilibili) + return mergedBilibili + } } /** diff --git a/src/types/config/kuaishou.ts b/src/types/config/kuaishou.ts index 684d3df..407024e 100644 --- a/src/types/config/kuaishou.ts +++ b/src/types/config/kuaishou.ts @@ -6,6 +6,9 @@ export interface kuaishouConfig { /** 快手解析提示,发送提示信息:“检测到快手链接,开始解析” */ tip: boolean + /** 快手评论解析,发送快手作品评论图 */ + comment: boolean + /** 快手评论数量,范围1~30条 */ numcomment: number } diff --git a/src/types/config/upload.ts b/src/types/config/upload.ts index e055fd7..7c5872b 100644 --- a/src/types/config/upload.ts +++ b/src/types/config/upload.ts @@ -4,24 +4,24 @@ export interface uploadConfig { /** 发送视频经本插件转换为base64格式后再发送,适合Karin与机器人不在同一网络环境下开启 */ sendbase64: boolean - /** 视频文件上传限制,开启后会根据解析的视频文件大小判断是否需要上传 */ + /** 视频上传拦截,开启后会根据视频文件大小判断是否需要上传,需配置「视频拦截阈值」。 */ usefilelimit: boolean - /** 视频文件大小限制(填数字),视频文件大于该数值则不会上传 单位: MB,「usefilelimit」开启后才会生效 */ + /** 视频拦截阈值,视频文件大于该数值则直接结束任务,不会上传,单位: MB,「视频上传拦截」开启后才会生效。 */ filelimit: number - /** 压缩视频,开启后会将视频文件压缩后再上传,适合上传大文件 */ + /** 压缩视频,开启后会将视频文件压缩后再上传,适合上传大文件,任务过程中会吃满CPU,对低配服务器不友好。需配置「压缩触发阈值」与「压缩后的值」 */ compress: boolean - /** 压缩视频触发阈值,单位:MB。当文件大小超过该值时,才会压缩视频,「compress」开启后才会生效 */ + /** 压缩触发阈值,触发视频压缩的阈值,单位:MB。当文件大小超过该值时,才会压缩视频,「压缩视频」开启后才会生效 */ compresstrigger: number - /** 压缩后的视频大小,单位:MB。若下载的视频大于该值,则会由本插件进行压缩至该值(±10 %),「compress」开启后才会生效 */ + /** 压缩后的值,单位:MB。若视频文件大小大于「压缩触发阈值」的值,则会进行压缩至该值(±5%),「压缩视频」开启后才会生效 */ compressvalue: number - /** 使用群文件上传,开启后会将视频文件上传到群文件中 */ + /** 群文件上传,使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」 */ usegroupfile: boolean - /** 群文件上传阈值,当文件大小超过该值时将使用群文件上传,单位:MB,「usegroupfile」开启后才会生效 */ + /** 群文件上传阈值,当文件大小超过该值时将使用群文件上传,单位:MB,「使用群文件上传」开启后才会生效 */ groupfilevalue: number } diff --git a/src/web.config.ts b/src/web.config.ts index 141f7be..9f35027 100644 --- a/src/web.config.ts +++ b/src/web.config.ts @@ -1,4 +1,5 @@ import { components } from 'node-karin' +import _ from 'node-karin/lodash' import { Config } from '@/module' import { ConfigType } from '@/types' @@ -9,7 +10,10 @@ export default { info: {}, /** 动态渲染的组件 */ components: () => [ - // 手风琴 + components.divider.create('divider-1', { + description: 'Cookies 相关', + descPosition: 20 + }), components.accordion.create('cfg.cookies', { children: [ components.accordion.createItem('cookies', { @@ -19,87 +23,788 @@ export default { children: [ components.input.string('cfg.cookies.douyin', { label: '抖音', - isRequired: true, type: 'text', description: '请输入你的抖音Cookies,不输入则无法使用抖音相关功能噢', defaultValue: all.cookies.douyin, - isClearable: true, - placeholder: '' + placeholder: '', + rules: undefined }), components.input.string('cfg.cookies.bilibili', { label: 'B站', - fullWidth: true, - isRequired: true, - type: 'password', + type: 'text', description: '请输入你的B站Cookies,不输入则无法使用B站相关功能噢', defaultValue: all.cookies.bilibili, placeholder: '', - errorMessage: '请填写一个有效的 B站 Cookies' + rules: undefined }), components.input.string('cfg.cookies.kuaishou', { label: '快手', - isRequired: true, - type: 'password', + type: 'text', description: '请输入你的快手Cookies,不输入则无法使用快手相关功能噢', defaultValue: all.cookies.kuaishou, - isClearable: true, - placeholder: '' + placeholder: '', + rules: undefined }) ] }) ] }), - // 基础用法 - components.divider.create('divider-1', { + components.divider.create('divider-2', { description: '插件应用相关', - orientation: 'vertical' + descPosition: 20 }), - // 开关 - components.switch.create('cfg.app.rmmp4', { - startText: '缓存删除', - description: '缓存自动删除,非必要不修改!', - defaultSelected: all.app.rmmp4 + components.accordion.create('cfg.app', { + children: [ + components.accordion.createItem('app', { + title: '插件应用相关', + className: 'ml-4 mr-4', + subtitle: '此处用于管理插件的基本设置', + children: [ + components.switch.create('cfg.app.rmmp4', { + startText: '缓存删除', + description: '缓存自动删除,非必要不修改!', + defaultSelected: all.app.rmmp4 + }), + components.switch.create('cfg.app.defaulttool', { + startText: '默认解析', + description: '即识别最高优先级,修改后重启生效', + defaultSelected: all.app.defaulttool + }), + components.input.number('cfg.app.priority', { + label: '自定义优先级', + description: '自定义优先级,「默认解析」关闭后才会生效。修改后重启生效', + defaultValue: all.app.priority.toString(), + isDisabled: all.app.defaulttool, + rules: undefined + }), + components.input.number('cfg.app.renderScale', { + label: '渲染精度', + description: '可选值50~200,建议100。设置高精度会提高图片的精细度,过高可能会影响渲染与发送速度', + defaultValue: all.app.renderScale.toString(), + rules: [ + { + min: 50, + max: 200 + } + ] + }), + components.switch.create('cfg.app.APIServer', { + startText: 'API服务', + description: '本地部署一个视频解析API服务,接口范围为本插件用到的所有', + defaultSelected: all.app.APIServer + }), + components.input.number('cfg.app.APIServerPort', { + label: 'API服务端口', + defaultValue: all.app.APIServerPort.toString(), + rules: [ + { + min: 1024, + max: 65535, + error: '请输入一个范围在 1024 到 65535 之间的数字' + } + ] + }), + components.radio.group('cfg.app.Theme', { + label: '渲染图片的主题色', + orientation: 'horizontal', + defaultValue: all.app.Theme.toString(), + radio: [ + components.radio.create('cfg.app.Theme-1', { + label: '自动', + value: '0' + }), + components.radio.create('cfg.app.Theme-2', { + label: '浅色', + value: '1' + }), + components.radio.create('cfg.app.Theme-3', { + label: '深色', + value: '2' + }) + ] + }) + ] + }) + ] }), - components.switch.create('cfg.app.defaulttool', { - startText: '默认解析', - description: '即识别最高优先级,修改后重启生效', - defaultSelected: all.app.defaulttool + components.divider.create('divider-3', { + description: '抖音相关', + descPosition: 20 }), - components.input.number('cfg.app.defaulttool', { - label: '渲染精度', - description: '可选值50~200,建议100。设置高精度会提高图片的精细度,过高可能会影响渲染与发送速度', - defaultValue: all.app.renderScale.toString(), - rules: [ - { - min: 50, - max: 200 - } + components.accordion.create('cfg.douyin', { + children: [ + components.accordion.createItem('douyin', { + title: '抖音相关', + className: 'ml-4 mr-4', + subtitle: '此处为抖音相关的用户偏好设置', + children: [ + components.switch.create('cfg.douyin.switch', { + startText: '解析开关', + description: '抖音解析开关,此开关为单独开关', + defaultSelected: all.douyin.switch + }), + components.switch.create('cfg.douyin.tip', { + startText: '解析提示', + description: '抖音解析提示,发送提示信息:“检测到抖音链接,开始解析”', + defaultSelected: all.douyin.tip + }), + components.switch.create('cfg.douyin.comment', { + startText: '评论解析', + description: '抖音评论解析,开启后可发送抖音作品评论图', + defaultSelected: all.douyin.comment + }), + components.input.number('cfg.douyin.numcomment', { + label: '评论解析数量', + defaultValue: all.douyin.numcomment.toString(), + rules: [{ min: 1 }] + }), + components.switch.create('cfg.douyin.autoResolution', { + startText: '自动分辨率', + description: '根据「视频拦截阈值」自动选择合适的分辨率,关闭后默认选择最大分辨率进行下载', + defaultSelected: all.douyin.autoResolution + }), + components.divider.create('divider-dy-1', { + description: '抖音推送相关', + descPosition: 20 + }), + components.switch.create('cfg.douyin.push.switch', { + startText: '推送开关', + description: '推送开关,开启后需重启;使用「#设置抖音推送 + 抖音号」配置推送列表', + defaultSelected: all.douyin.push.switch + }), + components.input.group('cfg.douyin.push.banWords', { + label: '作品中有以下指定关键词时,不推送', + maxRows: 2, + itemsPerRow: 4, + data: all.douyin.push.banWords, + template: components.input.string('cfg.douyin.push.banWords', { + placeholder: '', + label: '', + color: 'success' + }) + }), + components.input.group('cfg.douyin.push.banTags', { + label: '作品中有指定标签时,不推送', + maxRows: 2, + itemsPerRow: 4, + data: all.douyin.push.banWords, + template: components.input.string('cfg.douyin.push.banTags', { + placeholder: '', + label: '', + color: 'success' + }) + }), + components.radio.group('cfg.douyin.push.permission', { + label: '谁可以设置推送', + orientation: 'horizontal', + defaultValue: all.douyin.push.permission, + radio: [ + components.radio.create('cfg.douyin.push.permission.radio-1', { + label: '所有人', + value: 'all' + }), + components.radio.create('cfg.douyin.push.permission.radio-2', { + label: '管理员', + value: 'admin' + }), + components.radio.create('cfg.douyin.push.permission.radio-3', { + label: '主人', + value: 'master' + }), + components.radio.create('cfg.douyin.push.permission.radio-4', { + label: '群主', + value: 'group.owner' + }), + components.radio.create('cfg.douyin.push.permission.radio-5', { + label: '群管理员', + value: 'group.admin' + }) + ] + }), + components.input.string('cfg.douyin.push.cron', { + label: '定时任务表达式', + defaultValue: all.douyin.push.cron + }), + components.switch.create('cfg.douyin.push.parsedynamic', { + startText: '作品解析', + description: '触发推送时是否一同解析该作品', + defaultSelected: all.douyin.push.parsedynamic + }), + components.switch.create('cfg.douyin.push.log', { + startText: '推送日志', + description: '是否打印推送日志(修改后需重启)', + defaultSelected: all.douyin.push.log + }) + ] + }) ] }), - components.switch.create('cfg.app.APIServer', { - startText: 'API服务', - description: '本地部署一个视频解析API服务,接口范围为本插件用到的所有', - defaultSelected: all.app.APIServer + components.divider.create('divider-4', { + description: 'B站相关', + descPosition: 20 }), - components.input.number('cfg.app.defaulttool', { - label: 'API服务端口', - defaultValue: all.app.APIServerPort.toString(), - rules: [ - { - min: 1024, - max: 65535, - error: '请输入一个范围在 1024 到 65535 之间的数字' - } + components.accordion.create('cfg.bilibili', { + children: [ + components.accordion.createItem('bilibili', { + title: 'B站相关', + className: 'ml-4 mr-4', + subtitle: '此处为B站相关的用户偏好设置', + children: [ + components.switch.create('cfg.bilibili.switch', { + startText: '解析开关', + description: 'B站解析开关,此开关为单独开关', + defaultSelected: all.bilibili.switch + }), + components.switch.create('cfg.bilibili.tip', { + startText: '解析提示', + description: 'B站解析提示,发送提示信息:“检测到B站链接,开始解析”', + defaultSelected: all.bilibili.tip + }), + components.switch.create('cfg.bilibili.comment', { + startText: '评论解析', + description: 'B站评论解析,开启后可发送B站作品评论图', + defaultSelected: all.bilibili.comment + }), + components.input.number('cfg.bilibili.numcomment', { + label: '评论解析数量', + defaultValue: all.bilibili.numcomment.toString(), + rules: [{ min: 1 }] + }), + components.switch.create('cfg.bilibili.videopriority', { + startText: '优先保内容', + description: '解析视频是否优先保内容,true为优先保证上传将使用最低分辨率,false为优先保清晰度将使用最高分辨率', + defaultSelected: all.bilibili.videopriority + }), + components.switch.create('cfg.bilibili.autoResolution', { + startText: '自动分辨率', + description: '根据「视频拦截阈值」自动选择合适的分辨率,关闭后默认选择最大分辨率进行下载', + defaultSelected: all.bilibili.autoResolution + }), + components.divider.create('divider-dy-1', { + description: 'B站推送相关', + descPosition: 20 + }), + components.switch.create('cfg.bilibili.push.switch', { + startText: '推送开关', + description: '推送开关,开启后需重启;使用「#设置B站推送 + UID」配置推送列表', + defaultSelected: all.bilibili.push.switch + }), + components.input.group('cfg.bilibili.push.banWords', { + label: '动态中有以下指定关键词时,不推送', + maxRows: 2, + itemsPerRow: 4, + data: all.bilibili.push.banWords, + template: components.input.string('cfg.bilibili.push.banWords', { + placeholder: '', + label: '', + color: 'success' + }) + }), + components.input.group('cfg.bilibili.push.banTags', { + label: '动态中有指定标签时,不推送', + maxRows: 2, + itemsPerRow: 4, + data: all.bilibili.push.banWords, + template: components.input.string('cfg.bilibili.push.banTags', { + placeholder: '', + label: '', + color: 'success' + }) + }), + components.radio.group('cfg.bilibili.push.permission', { + label: '谁可以设置推送', + orientation: 'horizontal', + defaultValue: all.bilibili.push.permission, + radio: [ + components.radio.create('cfg.bilibili.push.permission.radio-1', { + label: '所有人', + value: 'all' + }), + components.radio.create('cfg.bilibili.push.permission.radio-2', { + label: '管理员', + value: 'admin' + }), + components.radio.create('cfg.bilibili.push.permission.radio-3', { + label: '主人', + value: 'master' + }), + components.radio.create('cfg.bilibili.push.permission.radio-4', { + label: '群主', + value: 'group.owner' + }), + components.radio.create('cfg.bilibili.push.permission.radio-5', { + label: '群管理员', + value: 'group.admin' + }) + ] + }), + components.input.string('cfg.bilibili.push.cron', { + label: '定时任务表达式', + defaultValue: all.bilibili.push.cron + }), + components.switch.create('cfg.bilibili.push.parsedynamic', { + startText: '作品解析', + description: '触发推送时是否一同解析该作品', + defaultSelected: all.bilibili.push.parsedynamic + }), + components.switch.create('cfg.bilibili.push.log', { + startText: '推送日志', + description: '是否打印推送日志(修改后需重启)', + defaultSelected: all.bilibili.push.log + }) + ] + }) ] - }) + }), + components.divider.create('divider-5', { + description: '快手相关', + descPosition: 20 + }), + components.accordion.create('cfg.kuaishou', { + children: [ + components.accordion.createItem('kuaishou', { + title: '快手相关', + className: 'ml-4 mr-4', + subtitle: '此处为快手相关的用户偏好设置', + children: [ + components.switch.create('cfg.kuaishou.switch', { + startText: '解析开关', + description: '快手解析开关,此开关为单独开关', + defaultSelected: all.kuaishou.switch + }), + components.switch.create('cfg.kuaishou.tip', { + startText: '解析提示', + description: '抖音解析提示,发送提示信息:“检测到快手链接,开始解析”', + defaultSelected: all.kuaishou.tip + }), + components.switch.create('cfg.kuaishou.comment', { + startText: '评论解析', + description: '快手评论解析,开启后可发送抖音作品评论图', + defaultSelected: all.kuaishou.comment + }), + components.input.number('cfg.kuaishou.numcomment', { + label: '评论解析数量', + defaultValue: all.kuaishou.numcomment.toString(), + rules: [{ min: 1 }] + }) + ] + }) + ] + }), + components.divider.create('divider-6', { + description: '上传相关', + descPosition: 20 + }), + components.accordion.create('cfg.upload', { + children: [ + components.accordion.createItem('upload', { + title: '上传相关', + className: 'ml-4 mr-4', + subtitle: '此处为上传相关的用户偏好设置', + children: [ + components.switch.create('cfg.upload.sendbase64', { + startText: '转换Base64', + description: '发送视频经本插件转换为base64格式后再发送,适合Karin与机器人不在同一网络环境下开启', + defaultSelected: all.upload.swisendbase64tch + }), + components.switch.create('cfg.upload.usefilelimit', { + startText: '视频上传拦截', + description: '开启后会根据视频文件大小判断是否需要上传,需配置「视频拦截阈值」。', + defaultSelected: all.upload.usefilelimit + }), + components.input.number('cfg.upload.filelimit', { + label: '视频拦截阈值', + description: '视频文件大于该数值则直接结束任务,不会上传,单位: MB,「视频上传拦截」开启后才会生效。', + defaultValue: all.upload.filelimit.toString(), + rules: [{ min: 1 }], + isDisabled: !all.upload.usefilelimit + }), + components.switch.create('cfg.upload.compress', { + startText: '压缩视频', + description: '开启后会将视频文件压缩后再上传,适合上传大文件,任务过程中会吃满CPU,对低配服务器不友好。需配置「压缩触发阈值」与「压缩后的值」', + defaultSelected: all.upload.compress + }), + components.input.number('cfg.upload.compresstrigger', { + label: '压缩触发阈值', + description: '触发视频压缩的阈值,单位:MB。当文件大小超过该值时,才会压缩视频,「压缩视频」开启后才会生效', + defaultValue: all.upload.compresstrigger.toString(), + rules: [{ min: 1 }], + isDisabled: !all.upload.compress + }), + components.input.number('cfg.upload.compressvalue', { + label: '压缩后的值', + description: '单位:MB,若视频文件大小大于「压缩触发阈值」的值,则会进行压缩至该值(±5%),「压缩视频」开启后才会生效', + defaultValue: all.upload.compressvalue.toString(), + rules: [{ min: 1 }], + isDisabled: !all.upload.compress + }), + components.switch.create('cfg.upload.usegroupfile', { + startText: '群文件上传', + description: '使用群文件上传,开启后会将视频文件上传到群文件中,需配置「群文件上传阈值」', + defaultSelected: all.upload.usegroupfile + }), + components.input.number('cfg.upload.groupfilevalue', { + label: '群文件上传阈值', + description: '当文件大小超过该值时将使用群文件上传,单位:MB,「使用群文件上传」开启后才会生效', + defaultValue: all.upload.groupfilevalue.toString(), + rules: [{ min: 1 }], + isDisabled: !all.upload.usegroupfile + }) + ] + }) + ] + }), + components.divider.create('divider-7', { + description: '抖音推送列表相关', + descPosition: 20 + }), + components.accordionPro.create( + 'cfg.pushlist.douyin', + all.pushlist.douyin.map((item) => { + return { + ...item, + title: item.remark, + subtitle: item.short_id + } + }), + { + label: '抖音推送列表', + children: components.accordion.createItem('accordion-item-douyin', { + className: 'ml-4 mr-4', + children: [ + components.input.string('short_id', { + color: 'success', + placeholder: '', + label: '抖音号' + }), + components.input.group('group_id', { + label: '推送群号和机器人账号', + maxRows: 2, + data: [], + template: components.input.string('accordion-item-douyin.banTags', { + placeholder: '', + label: '', + color: 'success' + }) + }), + components.input.string('sec_uid', { + color: 'default', + placeholder: '', + label: 'UID', + isRequired: false + }), + components.input.string('remark', { + color: 'default', + placeholder: '', + label: '昵称', + isRequired: false + }) + ] + }) + } + ), + components.divider.create('divider-8', { + description: 'B站推送列表相关', + descPosition: 20 + }), + components.accordionPro.create( + 'cfg.pushlist.bilibili', + all.pushlist.bilibili.map((item) => { + return { + ...item, + title: item.remark, + subtitle: item.host_mid + } + }), + { + label: 'B站推送列表', + children: components.accordion.createItem('accordion-item-bilibili', { + className: 'ml-4 mr-4', + children: [ + components.input.number('host_mid', { + color: 'success', + placeholder: '', + label: 'UID', + rules: undefined + }), + components.input.group('group_id', { + label: '推送群号和机器人账号', + maxRows: 2, + data: [], + template: components.input.string('accordion-item-bilibili.banTags', { + placeholder: '', + label: '', + color: 'success' + }) + }), + components.input.string('remark', { + color: 'default', + placeholder: '', + label: '昵称', + isRequired: false + }) + ] + }) + } + ) ], /** 前端点击保存之后调用的方法 */ - save: (config: ConfigType) => { - console.log(config) + save: (config: any) => { + const formatCfg = processFrontendConfig(config) + const oldAllCfg = Config.All() + /** 合并旧新配置 */ + const fullData = _.mergeWith({}, oldAllCfg, formatCfg, customizer) + let success = false + let isChange = false; + + (Object.keys(fullData) as Array).forEach((key: keyof ConfigType) => { + isChange = deepEqual(fullData[key], oldAllCfg[key]) + if (isChange) { + success = Config.ModifyPro(key, fullData[key]) + } + }) + return { - success: true, - message: '还在写。。。Ciallo~(∠・ω< )⌒☆' + success, + message: success ? '保存成功 Ciallo~(∠・ω< )⌒☆' : '配置无变化,无需保存' + } + } +} + +/** + * 遇到数组时用新数组覆盖原始数组(而不是合并) + * @param value 原始内容 + * @param srcValue 新内容 + * @returns + */ +const customizer = (value: any, srcValue: any) => { + if (Array.isArray(srcValue)) { + return srcValue // 直接返回新数组(覆盖旧数组) + } +} + +/** + * 归递判断配置是否修改 + * @param a 前端传回来的配置 + * @param b 用户原本的配置 + * @returns 配置对象是否被修改 + */ +const deepEqual = (a: any, b: any): boolean => { + // 如果两个值严格相等,说明没有修改 + if (a === b) { + return false + } + + // 如果 a 和 b 都是字符串,比较是否相等 + if (typeof a === 'string' && typeof b === 'string') { + if (a !== b) return true + } + + // 如果 a 和 b 都是数字,比较是否相等 + if (typeof a === 'number' && typeof b === 'number') { + if (a !== b) return true + } + + // 如果 a 和 b 都是布尔值,比较是否相等 + if (typeof a === 'boolean' && typeof b === 'boolean') { + if (a !== b) return true + } + + // 如果其中一个为 null 或者不是对象/数组,说明有修改 + if (a === null || b === null || typeof a !== typeof b) { + return true + } + + // 如果 a 和 b 都是数组 + if (Array.isArray(a) && Array.isArray(b)) { + // 如果数组长度不同,说明有修改 + if (a.length !== b.length) { + return true + } + + // 递归比较数组中的每个元素 + for (let i = 0; i < a.length; i++) { + if (deepEqual(a[i], b[i])) { + return true // 如果某个元素有修改,返回 true + } + } + } + + let isChange = false + // 如果 a 和 b 都是对象 + if (typeof a === 'object' && typeof b === 'object') { + if (isChange) return true + // 获取两个对象的键 + const keysA = Object.keys(a) + const keysB = Object.keys(b) + + // 如果键的数量不同,说明对象结构有修改 + if (keysA.length !== keysB.length) { + return true + } + + // 遍历对象 a 的键 + for (const key of keysA) { + // 如果 b 中没有该键,或者递归比较 a[key] 和 b[key] 有修改 + if (!keysB.includes(key)) { + isChange = true + return true // 如果 b 中没有该键,返回 true + } + + // 如果 a[key] 和 b[key] 不相等,返回 true + if (deepEqual(a[key], b[key])) { + isChange = true + return true + } } } + + // 如果所有检查都没有发现修改,返回 false + return false +} + +/** + * str 转 num + * @param value 字符串或者数字 + * @returns + */ +const convertToNumber = (value: string | number): number => { + if (typeof value === 'string') { + return parseInt(value, 10) + } + return value +} + +/** + * 转换前端传回来的配置成定义的配置文件格式 + * @param frontendConfig 前端传回来的原始内容 + * @returns + */ +const processFrontendConfig = (frontendConfig: any): ConfigType => { + const processedConfig: ConfigType = { + app: {} as ConfigType['app'], + bilibili: {} as ConfigType['bilibili'], + douyin: {} as ConfigType['douyin'], + cookies: {} as ConfigType['cookies'], + pushlist: {} as ConfigType['pushlist'], + upload: {} as ConfigType['upload'], + kuaishou: {} as ConfigType['kuaishou'] + } + + // 处理cookies配置 + if (frontendConfig['cfg.cookies'] && frontendConfig['cfg.cookies'].length > 0) { + const cookiesData = frontendConfig['cfg.cookies'][0] + processedConfig.cookies = { + bilibili: cookiesData['cfg.cookies.bilibili'], + douyin: cookiesData['cfg.cookies.douyin'], + kuaishou: cookiesData['cfg.cookies.kuaishou'] + } + } + + // 处理app配置 + if (frontendConfig['cfg.app'] && frontendConfig['cfg.app'].length > 0) { + const appData = frontendConfig['cfg.app'][0] + processedConfig.app = { + defaulttool: appData['cfg.app.defaulttool'], + priority: convertToNumber(appData['cfg.app.priority']), + rmmp4: appData['cfg.app.rmmp4'], + renderScale: convertToNumber(appData['cfg.app.renderScale']), + APIServer: appData['cfg.app.APIServer'], + APIServerPort: convertToNumber(appData['cfg.app.APIServerPort']), + Theme: convertToNumber(appData['cfg.app.Theme']) + } + } + + // 处理douyin配置 + if (frontendConfig['cfg.douyin'] && frontendConfig['cfg.douyin'].length > 0) { + const douyinData = frontendConfig['cfg.douyin'][0] + processedConfig.douyin = { + switch: douyinData['cfg.douyin.switch'], + tip: douyinData['cfg.douyin.tip'], + comment: douyinData['cfg.douyin.comment'], + numcomment: convertToNumber(douyinData['cfg.douyin.numcomment']), + autoResolution: douyinData['cfg.douyin.autoResolution'], + push: { + switch: douyinData['cfg.douyin.push.switch'], + banWords: douyinData['cfg.douyin.push.banWords'], + banTags: douyinData['cfg.douyin.push.banTags'], + permission: douyinData['cfg.douyin.push.permission'] as 'all' | 'admin' | 'master' | 'group.owner' | 'group.admin', + cron: douyinData['cfg.douyin.push.cron'], + parsedynamic: douyinData['cfg.douyin.push.parsedynamic'], + log: douyinData['cfg.douyin.push.log'] + } + } + } + + // 处理bilibili配置 + if (frontendConfig['cfg.bilibili'] && frontendConfig['cfg.bilibili'].length > 0) { + const bilibiliData = frontendConfig['cfg.bilibili'][0] + processedConfig.bilibili = { + switch: bilibiliData['cfg.bilibili.switch'], + tip: bilibiliData['cfg.bilibili.tip'], + comment: bilibiliData['cfg.bilibili.comment'], + numcomment: convertToNumber(bilibiliData['cfg.bilibili.numcomment']), + videopriority: bilibiliData['cfg.bilibili.videopriority'], + autoResolution: bilibiliData['cfg.bilibili.autoResolution'], + push: { + switch: bilibiliData['cfg.bilibili.push.switch'], + banWords: bilibiliData['cfg.bilibili.push.banWords'], + banTags: bilibiliData['cfg.bilibili.push.banTags'], + permission: bilibiliData['cfg.bilibili.push.permission'] as 'all' | 'admin' | 'master' | 'group.owner' | 'group.admin', + cron: bilibiliData['cfg.bilibili.push.cron'], + parsedynamic: bilibiliData['cfg.bilibili.push.parsedynamic'], + log: bilibiliData['cfg.bilibili.push.log'] + } + } + } + + // 处理kuaishou配置 + if (frontendConfig['cfg.kuaishou'] && frontendConfig['cfg.kuaishou'].length > 0) { + const kuaishouData = frontendConfig['cfg.kuaishou'][0] + processedConfig.kuaishou = { + switch: kuaishouData['cfg.kuaishou.switch'], + tip: kuaishouData['cfg.kuaishou.tip'], + comment: kuaishouData['cfg.kuaishou.comment'], + numcomment: convertToNumber(kuaishouData['cfg.kuaishou.numcomment']) + } + } + + // 处理upload配置 + if (frontendConfig['cfg.upload'] && frontendConfig['cfg.upload'].length > 0) { + const uploadData = frontendConfig['cfg.upload'][0] + processedConfig.upload = { + sendbase64: uploadData['cfg.upload.sendbase64'], + usefilelimit: uploadData['cfg.upload.usefilelimit'], + filelimit: convertToNumber(uploadData['cfg.upload.filelimit']), + compress: uploadData['cfg.upload.compress'], + compresstrigger: convertToNumber(uploadData['cfg.upload.compresstrigger']), + compressvalue: convertToNumber(uploadData['cfg.upload.compressvalue']), + usegroupfile: uploadData['cfg.upload.usegroupfile'], + groupfilevalue: convertToNumber(uploadData['cfg.upload.groupfilevalue']) + } + } + + // 处理pushlist配置 + processedConfig.pushlist = { + douyin: [], + bilibili: [] + } + if (frontendConfig['cfg.pushlist.douyin'] && frontendConfig['cfg.pushlist.douyin'].length > 0) { + processedConfig.pushlist.douyin = frontendConfig['cfg.pushlist.douyin'].map((item: any) => ({ + sec_uid: item.sec_uid, + short_id: item.short_id, + group_id: item.group_id, + remark: item.remark + })) + } + if (frontendConfig['cfg.pushlist.bilibili'] && frontendConfig['cfg.pushlist.bilibili'].length > 0) { + processedConfig.pushlist.bilibili = frontendConfig['cfg.pushlist.bilibili'].map((item: any) => ({ + host_mid: convertToNumber(item.host_mid), + group_id: item.group_id, + remark: item.remark + })) + } + + return processedConfig }