From d0e6e6e5bdc264131aa6bd0af25be7925bf2a661 Mon Sep 17 00:00:00 2001 From: zhiyu <542716863@qq.com> Date: Fri, 26 Jan 2024 12:49:11 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=96=B0=E5=A2=9E=E5=93=94?= =?UTF-8?q?=E5=93=A9=E5=93=94=E5=93=A9=E5=AE=98=E6=96=B9AI=E6=80=BB?= =?UTF-8?q?=E7=BB=93=E3=80=81=E5=93=94=E5=93=A9=E5=93=94=E5=93=A9=E9=9F=B3?= =?UTF-8?q?=E4=B9=90=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增哔哩哔哩音乐提取,使用“bili音乐+链接”即可提取视频中的音乐 2. 重构部分代码 3. 更换tiktok API 4. 更换视频总结方式GPT为官方的视频摘要(免费使用) 5. 删除GPT相关内容 --- README.md | 38 ++--- apps/query.js | 2 +- apps/tools.js | 306 ++++++++++++++++++++++------------------ config/help.yaml | 3 + config/tools.yaml | 4 - config/version.yaml | 8 +- guoba.support.js | 13 +- utils/biliInfo.js | 7 +- utils/biliSummary.js | 208 --------------------------- utils/bilibili.js | 29 +++- utils/common.js | 40 +++++- utils/constant.js | 41 ------ utils/trans-strategy.js | 2 +- 13 files changed, 274 insertions(+), 427 deletions(-) delete mode 100644 utils/biliSummary.js delete mode 100644 utils/constant.js diff --git a/README.md b/README.md index cb03116..3d8ea19 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ sudo apt-get install ffmpeg 小程序解析 -5. 【可选】对哔哩哔哩解析进行总结:需要填写accessToken和哔哩哔哩的SESSDATA +5. 【可选】对哔哩哔哩解析进行总结:需要填写哔哩哔哩的SESSDATA 小程序解析 @@ -70,10 +70,28 @@ sudo apt-get install ffmpeg - 锅巴设置 ## 🐤 Q&A +### bilibili问题 > 哔哩哔哩的SESSDATA? > 进入哔哩哔哩网站 -- 打开F12开发者选项 -- 应用 -- 找到Cookie -- 找到SESSDATA -- 复制 -- 粘贴到plugins/rconsole-plugin/config/tools.yaml or 锅巴 > [图文教程【群友推荐!】](https://www.bilibili.com/read/cv12349604) +### 🎵 douyin问题 +由于douyin的解析变化莫测,现版本需要填入自己的cookie,具体步骤如下: +1. 打开`https://www.douyin.com/` 扫码登入自己的账号 +2. F12进入控制台,或者下载一个[Cookie-Editor](https://www.crxsoso.com/webstore/detail/hlkenndednhfkekhgcdicdfddnkalmdm) +3. 如果是F12,就将以下参数填入到`tools.yaml - douyinCookie`,或者使用锅巴 +> odin_tt=xxx;sessionid_ss=xxx;ttwid=xxx;passport_csrf_token=xxx;msToken=xxx; + +3. 如果是`Cookie-Editor`就直接到插件复制到`tools.yaml - douyinCookie`,或者锅巴 + +具体图示,找以下这几个: +- odin_tt +- sessionid_ss +- ttwid +- passport_csrf_token +- msToken + ![douyin_cookie](./img/douyin_cookie.webp) + ## 🤺 R插件交流群 扫码不行就:575663150 @@ -88,22 +106,10 @@ sudo apt-get install ffmpeg `proxyAddr: '127.0.0.1' # 魔法地址` `proxyPort: '7890' # 魔法端口` -## 🎵 douyin_cookie问题 -由于douyin的解析变化莫测,现版本需要填入自己的cookie,具体步骤如下: -1. 打开`https://www.douyin.com/` 扫码登入自己的账号 -2. F12进入控制台,或者下载一个[Cookie-Editor](https://www.crxsoso.com/webstore/detail/hlkenndednhfkekhgcdicdfddnkalmdm) -3. 如果是F12,就将以下参数填入到`tools.yaml - douyinCookie`,或者使用锅巴 -> odin_tt=xxx;sessionid_ss=xxx;ttwid=xxx;passport_csrf_token=xxx;msToken=xxx; +> 海外服务器示例: +`proxyAddr: '127.0.0.1' # 魔法地址` +`proxyPort: '80' # 魔法端口` -3. 如果是`Cookie-Editor`就直接到插件复制到`tools.yaml - douyinCookie`,或者锅巴 - -具体图示,找以下这几个: -- odin_tt -- sessionid_ss -- ttwid -- passport_csrf_token -- msToken -![douyin_cookie](./img/douyin_cookie.webp) ## 📦 业务 ![help](./img/help.webp) diff --git a/apps/query.js b/apps/query.js index a583955..f012c3f 100644 --- a/apps/query.js +++ b/apps/query.js @@ -6,7 +6,7 @@ import puppeteer from "../../../lib/puppeteer/puppeteer.js"; // http库 import axios from "axios"; // 常量 -import { CAT_LIMIT } from "../utils/constant.js"; +import { CAT_LIMIT } from "../constants/constant.js"; // 书库 import { getZHelper, getYiBook, getZBook } from "../utils/books.js"; // 工具类 diff --git a/apps/tools.js b/apps/tools.js index 0c1b5b0..da22ae5 100644 --- a/apps/tools.js +++ b/apps/tools.js @@ -7,10 +7,15 @@ import _ from "lodash"; import tunnel from "tunnel"; import HttpProxyAgent from "https-proxy-agent"; import { mkdirIfNotExists, checkAndRemoveFile, deleteFolderRecursive } from "../utils/file.js"; -import { downloadBFile, getDownloadUrl, mergeFileToMp4 } from "../utils/bilibili.js"; +import { downloadBFile, getAudioUrl, getDownloadUrl, mergeFileToMp4 } from "../utils/bilibili.js"; import { parseUrl, parseM3u8, downloadM3u8Videos, mergeAcFileToMp4 } from "../utils/acfun.js"; -import { transMap, douyinTypeMap, XHS_CK, TEN_THOUSAND } from "../utils/constant.js"; -import { getIdVideo } from "../utils/common.js"; +import { + transMap, + douyinTypeMap, + XHS_CK, + RESTRICTION_DESCRIPTION, +} from "../constants/constant.js"; +import { dataProcessing, formatBiliInfo, getIdVideo, secondsToTime } from "../utils/common.js"; import config from "../model/index.js"; import Translate from "../utils/trans-strategy.js"; import * as xBogus from "../utils/x-bogus.cjs"; @@ -21,6 +26,8 @@ import { ChatGPTBrowserClient, ChatGPTClient } from "@waylaidwanderer/chatgpt-ap import { av2BV } from "../utils/bilibili-bv-av-convert.js"; import querystring from "querystring"; import TokenBucket from "../utils/token-bucket.js"; +import { getWbi } from "../utils/biliWbi.js"; +import { BILI_SUMMARY } from "../constants/bili.js"; export class tools extends plugin { constructor() { @@ -31,7 +38,7 @@ export class tools extends plugin { priority: 300, rule: [ { - reg: `^(翻|trans)[${tools.Constants.existsTransKey}]`, + reg: `^(翻|trans)[${ tools.Constants.existsTransKey }]`, fnc: "trans", }, { @@ -92,30 +99,13 @@ export class tools extends plugin { // 代理接口 this.proxyAddr = this.toolsConfig.proxyAddr; this.proxyPort = this.toolsConfig.proxyPort; - this.myProxy = `http://${this.proxyAddr}:${this.proxyPort}`; + this.myProxy = `http://${ this.proxyAddr }:${ this.proxyPort }`; // 加载哔哩哔哩配置 this.biliSessData = this.toolsConfig.biliSessData; // 加载哔哩哔哩的限制时长 this.biliDuration = this.toolsConfig.biliDuration; // 加载抖音Cookie this.douyinCookie = this.toolsConfig.douyinCookie; - // 加载gpt配置:accessToken、apiKey、模型 - this.openaiAccessToken = this.toolsConfig.openaiAccessToken; - this.openaiApiKey = this.toolsConfig.openaiApiKey; - this.openaiModel = this.toolsConfig.openaiModel; - // 加载gpt客户端(默认加载sk,如果填了AccessToken就用AccessToken) - this.chatGptClient = this.openaiAccessToken === '' ? new ChatGPTClient(this.openaiApiKey, { - modelOptions: { - model: this.openaiModel, - temperature: 0, - }, - proxy: this.myProxy, - debug: false, - }) : new ChatGPTBrowserClient({ - reverseProxyUrl: "https://bypass.churchless.tech/api/conversation", - accessToken: this.openaiAccessToken, - model: this.openaiModel, - }) } // 翻译插件 @@ -164,15 +154,15 @@ export class tools extends plugin { Referer: "https://www.douyin.com/", cookie: this.douyinCookie, }; - const dyApi = `https://www.douyin.com/aweme/v1/web/aweme/detail/?device_platform=webapp&aid=6383&channel=channel_pc_web&aweme_id=${douId}&pc_client_type=1&version_code=190500&version_name=19.5.0&cookie_enabled=true&screen_width=1344&screen_height=756&browser_language=zh-CN&browser_platform=Win32&browser_name=Firefox&browser_version=118.0&browser_online=true&engine_name=Gecko&engine_version=109.0&os_name=Windows&os_version=10&cpu_core_num=16&device_memory=&platform=PC&webid=7284189800734082615&msToken=B1N9FM825TkvFbayDsDvZxM8r5suLrsfQbC93TciS0O9Iii8iJpAPd__FM2rpLUJi5xtMencSXLeNn8xmOS9q7bP0CUsrt9oVTL08YXLPRzZm0dHKLc9PGRlyEk=`; + const dyApi = `https://www.douyin.com/aweme/v1/web/aweme/detail/?device_platform=webapp&aid=6383&channel=channel_pc_web&aweme_id=${ douId }&pc_client_type=1&version_code=190500&version_name=19.5.0&cookie_enabled=true&screen_width=1344&screen_height=756&browser_language=zh-CN&browser_platform=Win32&browser_name=Firefox&browser_version=118.0&browser_online=true&engine_name=Gecko&engine_version=109.0&os_name=Windows&os_version=10&cpu_core_num=16&device_memory=&platform=PC&webid=7284189800734082615&msToken=B1N9FM825TkvFbayDsDvZxM8r5suLrsfQbC93TciS0O9Iii8iJpAPd__FM2rpLUJi5xtMencSXLeNn8xmOS9q7bP0CUsrt9oVTL08YXLPRzZm0dHKLc9PGRlyEk=`; // xg参数 const xbParam = xBogus.sign( new URLSearchParams(new URL(dyApi).search).toString(), headers["User-Agent"], ); // const param = resp.data.result[0].paramsencode; - const resDyApi = `${dyApi}&X-Bogus=${xbParam}`; - headers['Referer'] = `https://www.douyin.com/video/${douId}` + const resDyApi = `${ dyApi }&X-Bogus=${ xbParam }`; + headers['Referer'] = `https://www.douyin.com/video/${ douId }` axios .get(resDyApi, { headers, @@ -182,9 +172,9 @@ export class tools extends plugin { e.reply("解析失败,请重试!"); return; } - console.log(resp.data) + // console.log(resp.data) const item = resp.data.aweme_detail; - e.reply(`识别:抖音, ${item.desc}`); + e.reply(`识别:抖音, ${ item.desc }`); const urlTypeCode = item.aweme_type; const urlType = douyinTypeMap[urlTypeCode]; if (urlType === "video") { @@ -192,7 +182,7 @@ export class tools extends plugin { "http", "https", ); - const path = `${this.defaultPath}${ + const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/temp.mp4`; await this.downloadVideo(resUrl).then(() => { @@ -253,10 +243,10 @@ export class tools extends plugin { } else { url = urlRex.exec(url)[0]; } - let idVideo = await getIdVideo(url); - idVideo = idVideo.replace(/\//g, ""); + let tiktokVideoId = await getIdVideo(url); + tiktokVideoId = tiktokVideoId.replace(/\//g, ""); // API链接 - const API_URL = `https://api16-normal-c-useast1a.tiktokv.com/aweme/v1/feed/?aweme_id=${idVideo}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9`; + const API_URL = `https://api16-normal-c-useast1a.tiktokv.com/aweme/v1/feed/?aweme_id=${ tiktokVideoId }`; await axios .get(API_URL, { @@ -277,11 +267,11 @@ export class tools extends plugin { }) .then(resp => { const data = resp.data.aweme_list[0]; - e.reply(`识别:tiktok, ${data.desc}`); + e.reply(`识别:tiktok, ${ data.desc }`); this.downloadVideo(data.video.play_addr.url_list[0], true).then(video => { e.reply( segment.video( - `${this.defaultPath}${this.e.group_id || this.e.user_id}/temp.mp4`, + `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/temp.mp4`, ), ); }); @@ -295,6 +285,7 @@ export class tools extends plugin { this.biliCore(e); }); } + async biliCore(e) { const urlRex = /(?:https?:\/\/)?www\.bilibili\.com\/[A-Za-z\d._?%&+\-=\/#]*/g; const bShortRex = /(http:|https:)\/\/b23.tv\/[A-Za-z\d._?%&+\-=\/#]*/g; @@ -317,81 +308,68 @@ export class tools extends plugin { if (matched) { url = url.replace(matched[0], av2BV(Number(matched[2]))); } - // 动态 + // 动态处理 if (url.includes("t.bilibili.com")) { - // 去除多余参数 - if (url.includes("?")) { - url = url.substring(0, url.indexOf("?")); - } - const dynamicId = /[^/]+(?!.*\/)/.exec(url)[0]; - getDynamic(dynamicId).then(async resp => { - if (resp.dynamicSrc.length > 0) { - e.reply(`识别:哔哩哔哩动态, ${resp.dynamicDesc}`); - let dynamicSrcMsg = []; - resp.dynamicSrc.forEach(item => { - dynamicSrcMsg.push({ - message: segment.image(item), - nickname: e.sender.card || e.user_id, - user_id: e.user_id, - }); - }); - await this.reply(await Bot.makeForwardMsg(dynamicSrcMsg)); - } else { - e.reply(`识别:哔哩哔哩动态, 但是失败!`); - } - }); + url = this.biliDynamic(url, e); return true; } - // 视频信息获取例子:http://api.bilibili.com/x/web-interface/view?bvid=BV1hY411m7cB // 请求视频信息 const videoInfo = await getVideoInfo(url); - const { title, pic, desc, duration, dynamic, stat, aid, cid, pages } = videoInfo; + const { title, pic, desc, duration, dynamic, stat, bvid, aid, cid, owner, pages } = videoInfo; // 视频信息 let { view, danmaku, reply, favorite, coin, share, like } = stat; - // 数据处理 - const dataProcessing = data => { - return Number(data) >= TEN_THOUSAND ? (data / TEN_THOUSAND).toFixed(1) + "万" : data; - }; // 限制时长 & 考虑分页视频情况 const query = querystring.parse(url); const curPage = query?.p || 0; const curDuration = pages?.[curPage]?.duration || duration; const isLimitDuration = curDuration > this.biliDuration + // 构造一个可扩展的Map + const dataProcessMap = { + "点赞": like, + "硬币": coin, + "收藏": favorite, + "分享": share, + "总播放量": view, + "弹幕数量": danmaku, + "评论": reply + }; // 格式化数据 - const combineContent = - `\n点赞:${dataProcessing(like)} | 硬币:${dataProcessing( - coin, - )} | 收藏:${dataProcessing(favorite)} | 分享:${dataProcessing(share)}\n` + - `总播放量:${dataProcessing(view)} | 弹幕数量:${dataProcessing( - danmaku, - )} | 评论:${dataProcessing(reply)}\n` + - `简介:${desc}`; - let biliInfo = [`识别:哔哩哔哩:${title}`, combineContent] + const combineContent = `\n${ formatBiliInfo(dataProcessMap) }\n简介:${ desc }`; + let biliInfo = [`识别:哔哩哔哩:${ title }`, combineContent] + // 只提取音乐处理 + if (e.msg.includes("bili音乐")) { + return await this.biliMusic(url, e, biliInfo); + } + // 不提取音乐,正常处理 if (isLimitDuration) { // 加入图片 biliInfo.unshift(segment.image(pic)) // 限制视频解析 const durationInMinutes = (curDuration / 60).toFixed(0); - biliInfo.push(`\n-----------------------限制说明-----------------------\n当前视频时长约:${durationInMinutes}分钟,\n大于管理员设置的最大时长 ${this.biliDuration / 60} 分钟!`) + biliInfo.push(`${RESTRICTION_DESCRIPTION}\n当前视频时长约:${ durationInMinutes }分钟,\n大于管理员设置的最大时长 ${ this.biliDuration / 60 } 分钟!`) e.reply(biliInfo); // 总结 - const summary = await this.getBiliSummary(videoInfo); + const summary = await this.getBiliSummary(bvid, cid, owner.mid); summary && e.reply(summary); return true; } else { + // 总结 + const summary = await this.getBiliSummary(bvid, cid, owner.mid); + summary && biliInfo.push(`\n${summary}`) + // e.reply(biliInfo); } // 创建文件,如果不存在 - const path = `${this.defaultPath}${this.e.group_id || this.e.user_id}/`; + const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/`; await mkdirIfNotExists(path); // 下载文件 getDownloadUrl(url) .then(data => { - this.downBili(`${path}temp`, data.videoUrl, data.audioUrl) + this.downBili(`${ path }temp`, data.videoUrl, data.audioUrl) .then(_ => { - e.reply(segment.video(`${path}temp.mp4`)); + e.reply(segment.video(`${ path }temp.mp4`)); }) .catch(err => { logger.error(err); @@ -402,17 +380,95 @@ export class tools extends plugin { logger.error(err); e.reply("解析失败,请重试一下"); }); - // 总结 - const summary = await this.getBiliSummary(videoInfo); - summary && e.reply(summary); return true; } + async biliMusic(url, e, biliInfo) { + const { audioUrl, title } = await getAudioUrl(url); + e.reply(biliInfo) + e.reply(segment.record(audioUrl)) + return true + } + + // 发送哔哩哔哩动态的算法 + biliDynamic(url, e) { + // 去除多余参数 + if (url.includes("?")) { + url = url.substring(0, url.indexOf("?")); + } + const dynamicId = /[^/]+(?!.*\/)/.exec(url)[0]; + getDynamic(dynamicId).then(async resp => { + if (resp.dynamicSrc.length > 0) { + e.reply(`识别:哔哩哔哩动态, ${ resp.dynamicDesc }`); + let dynamicSrcMsg = []; + resp.dynamicSrc.forEach(item => { + dynamicSrcMsg.push({ + message: segment.image(item), + nickname: e.sender.card || e.user_id, + user_id: e.user_id, + }); + }); + await this.reply(await Bot.makeForwardMsg(dynamicSrcMsg)); + } else { + e.reply(`识别:哔哩哔哩动态, 但是失败!`); + } + }); + return url; + } + + /** + * 哔哩哔哩总结 + * @author zhiyu1998 + * @param bvid 稿件 + * @param cid 视频 cid + * @param up_mid UP主 mid + * @return {Promise} + */ + async getBiliSummary(bvid, cid, up_mid) { + // 这个有点用,但不多 + let wbi = "wts=1701546363&w_rid=1073871926b3ccd99bd790f0162af634" + if (!_.isEmpty(this.biliSessData)) { + wbi = await getWbi({ bvid, cid, up_mid }, this.biliSessData); + } + // 构造API + const summaryUrl = `${ BILI_SUMMARY }?${ wbi }`; + logger.info(summaryUrl) + // 构造结果:https://api.bilibili.com/x/web-interface/view/conclusion/get?bvid=BV1L94y1H7CV&cid=1335073288&up_mid=297242063&wts=1701546363&w_rid=1073871926b3ccd99bd790f0162af634 + return axios.get(summaryUrl) + .then(resp => { + const data = resp.data.data?.model_result; + // logger.info(data) + const summary = data?.summary; + const outline = data?.outline; + let resReply = ""; + // 总体总结 + if (summary) { + resReply = `摘要:${ summary }\n` + } + // 分段总结 + if (outline) { + const specificTimeSummary = outline.map(item => { + const smallTitle = item.title; + const keyPoint = item?.part_outline; + // 时间点的总结 + const specificContent = keyPoint.map(point => { + const { timestamp, content } = point + const specificTime = secondsToTime(timestamp) + return `${ specificTime } ${ content }\n`; + }).join(""); + return `- ${ smallTitle }\n${ specificContent }\n`; + }); + resReply += specificTimeSummary.join(""); + } + return resReply; + }) + } + // 百科 async wiki(e) { const key = e.msg.replace(/#|百科|wiki/g, "").trim(); - const url = `https://xiaoapi.cn/API/bk.php?m=json&type=sg&msg=${encodeURI(key)}`; - const bdUrl = `https://xiaoapi.cn/API/bk.php?m=json&type=bd&msg=${encodeURI(key)}`; + const url = `https://xiaoapi.cn/API/bk.php?m=json&type=sg&msg=${ encodeURI(key) }`; + const bdUrl = `https://xiaoapi.cn/API/bk.php?m=json&type=bd&msg=${ encodeURI(key) }`; const bkRes = await Promise.all([ axios .get(bdUrl, { @@ -440,8 +496,8 @@ export class tools extends plugin { return res.map(item => { return { message: ` - 解释:${_.get(item, "msg")}\n - 详情:${_.get(item, "more")}\n + 解释:${ _.get(item, "msg") }\n + 详情:${ _.get(item, "more") }\n `, nickname: e.sender.card || e.user_id, user_id: e.user_id, @@ -473,8 +529,8 @@ export class tools extends plugin { expansions: ["entities.mentions.username", "attachments.media_keys"], }) .then(async resp => { - e.reply(`识别:小蓝鸟学习版,${resp.data.text}`); - const downloadPath = `${this.defaultPath}${this.e.group_id || this.e.user_id}`; + e.reply(`识别:小蓝鸟学习版,${ resp.data.text }`); + const downloadPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`; // 创建文件夹(如果没有过这个群) if (!fs.existsSync(downloadPath)) { mkdirsSync(downloadPath); @@ -489,7 +545,7 @@ export class tools extends plugin { // 视频 await this.downloadVideo(resp.includes.media[0].variants[0].url, true).then( _ => { - e.reply(segment.video(`${downloadPath}/temp.mp4`)); + e.reply(segment.video(`${ downloadPath }/temp.mp4`)); }, ); } @@ -524,21 +580,21 @@ export class tools extends plugin { // acfun解析 async acfun(e) { - const path = `${this.defaultPath}${this.e.group_id || this.e.user_id}/temp/`; + const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/temp/`; await mkdirIfNotExists(path); let inputMsg = e.msg; // 适配手机分享:https://m.acfun.cn/v/?ac=32838812&sid=d2b0991bd6ad9c09 if (inputMsg.includes("m.acfun.cn")) { - inputMsg = `https://www.acfun.cn/v/ac${/ac=([^&?]*)/.exec(inputMsg)[1]}`; + inputMsg = `https://www.acfun.cn/v/ac${ /ac=([^&?]*)/.exec(inputMsg)[1] }`; } parseUrl(inputMsg).then(res => { - e.reply(`识别:猴山,${res.videoName}`); + e.reply(`识别:猴山,${ res.videoName }`); parseM3u8(res.urlM3u8s[res.urlM3u8s.length - 1]).then(res2 => { downloadM3u8Videos(res2.m3u8FullUrls, path).then(_ => { - mergeAcFileToMp4(res2.tsNames, path, `${path}out.mp4`).then(_ => { - e.reply(segment.video(`${path}out.mp4`)); + mergeAcFileToMp4(res2.tsNames, path, `${ path }out.mp4`).then(_ => { + e.reply(segment.video(`${ path }out.mp4`)); }); }); }); @@ -550,9 +606,9 @@ export class tools extends plugin { async redbook(e) { // 正则说明:匹配手机链接、匹配小程序、匹配PC链接 let msgUrl = - /(http:|https:)\/\/(xhslink|xiaohongshu).com\/[A-Za-z\d._?%&+\-=\/#@]*/.exec( - e.msg, - )?.[0] + /(http:|https:)\/\/(xhslink|xiaohongshu).com\/[A-Za-z\d._?%&+\-=\/#@]*/.exec( + e.msg, + )?.[0] || /(http:|https:)\/\/www\.xiaohongshu\.com\/discovery\/item\/(\w+)/.exec( e.message[0].data, )?.[0] @@ -574,9 +630,9 @@ export class tools extends plugin { } else { id = /explore\/(\w+)/.exec(msgUrl)?.[1] || /discovery\/item\/(\w+)/.exec(msgUrl)?.[1]; } - const downloadPath = `${this.defaultPath}${this.e.group_id || this.e.user_id}`; + const downloadPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`; // 获取信息 - fetch(`https://www.xiaohongshu.com/discovery/item/${id}`, { + fetch(`https://www.xiaohongshu.com/discovery/item/${ id }`, { headers: { "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/110.0.0.0", @@ -589,7 +645,7 @@ export class tools extends plugin { const res = JSON.parse(resJson.match(reg)[1]); const noteData = res.noteData.data.noteData; const { title, desc, type } = noteData; - e.reply(`识别:小红书, ${title}\n${desc}`); + e.reply(`识别:小红书, ${ title }\n${ desc }`); let imgPromise = []; if (type === "video") { const url = noteData.video.url; @@ -661,9 +717,9 @@ export class tools extends plugin { } const rTempFileLen = await deleteFolderRecursive(this.toolsConfig.defaultPath) e.reply( - `数据统计:\n`+ - `- 当前清理了${dataDirectory}下总计:${dataClearFileLen} 个垃圾文件\n`+ - `- 当前清理了${ this.toolsConfig.defaultPath}下文件夹:${rTempFileLen} 个群的所有临时文件` + `数据统计:\n` + + `- 当前清理了${ dataDirectory }下总计:${ dataClearFileLen } 个垃圾文件\n` + + `- 当前清理了${ this.toolsConfig.defaultPath }下文件夹:${ rTempFileLen } 个群的所有临时文件` ); } catch (err) { logger.error(err); @@ -677,10 +733,10 @@ export class tools extends plugin { if (suffix.startsWith("reel")) { suffix = suffix.replace("reel/", "p/"); } - const API = `https://imginn.com/${suffix}`; - logger.info(API); + const API = `https://imginn.com/${ suffix }`; + // logger.info(API); let imgPromise = []; - const downloadPath = `${this.defaultPath}${this.e.group_id || this.e.user_id}`; + const downloadPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`; // 简单封装图片下载 const downloadImg = (url, destination) => { return new Promise((resolve, reject) => { @@ -708,13 +764,13 @@ export class tools extends plugin { const desc = html.match(/(?<=content=").*?(?=\")/g)?.[2]; const images = html.match(/
/g); if (!_.isNull(images)) { - e.reply(`识别:Insta,${desc || "暂无描述"}\n`); + e.reply(`识别:Insta,${ desc || "暂无描述" }\n`); images.map((item, index) => { const imgUrl = /(?<=data-src=").*?(?=")/ .exec(item)[0] .replace(/#38/g, "") .replace(/;/g, ""); - imgPromise.push(downloadImg(imgUrl, `${downloadPath}/${index}.jpg`)); + imgPromise.push(downloadImg(imgUrl, `${ downloadPath }/${ index }.jpg`)); }); } // TODO 视频,会出bug暂时不做 @@ -755,18 +811,18 @@ export class tools extends plugin { /(?=mvId).*?(?=&)/.exec(e.msg.trim())?.[0].replace("mvId=", ""); const { name, album, artist, albumPic120, categorys } = await getBodianMusicInfo(id); e.reply([ - `识别:波点音乐,${name}-${album}-${artist}\n标签:${categorys + `识别:波点音乐,${ name }-${ album }-${ artist }\n标签:${ categorys .map(item => item.name) - .join(" | ")}`, + .join(" | ") }`, segment.image(albumPic120), ]); if (e.msg.includes("musicId")) { - const path = `${this.defaultPath}${this.e.group_id || this.e.user_id}`; + const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`; await getBodianAudio(id, path).then(_ => { Bot.acquireGfs(e.group_id).upload( fs.readFileSync(path + "/temp.mp3"), "/", - `${name}-${album}-${artist}.mp3`, + `${ name }-${ album }-${ artist }.mp3`, ); }); } else if (e.msg.includes("mvId")) { @@ -800,7 +856,7 @@ export class tools extends plugin { return } // 提取视频 - const videoUrl = `https://www.kuaishou.com/short-video/${video_id}`; + const videoUrl = `https://www.kuaishou.com/short-video/${ video_id }`; // 发送GET请求 const response = await axios.get(videoUrl, { @@ -870,32 +926,10 @@ export class tools extends plugin { ), ), ]).then(data => { - return mergeFileToMp4(data[0].fullFileName, data[1].fullFileName, `${title}.mp4`); + return mergeFileToMp4(data[0].fullFileName, data[1].fullFileName, `${ title }.mp4`); }); } - /** - * 哔哩哔哩总结 - * @returns Promise{string} - * @param videoInfo - */ - async getBiliSummary(videoInfo) { - if (this.biliSessData && this.openaiAccessToken) { - try { - const prompt = await getBiliGptInputText(videoInfo, this.biliSessData); - - const response = await this.chatGptClient.sendMessage(prompt); - // 暂时不设计上下文 - return response.response - } catch (err) { - logger.error("总结失败,可能是没有弹幕或者网络问题!\n", err); - return "" - } - } else { - return "" - } - } - /** * 下载一张网络图片(自动以url的最后一个为名字) * @param img @@ -908,7 +942,7 @@ export class tools extends plugin { if (fileName === "") { fileName = img.split("/").pop(); } - const filepath = `${dir}/${fileName}`; + const filepath = `${ dir }/${ fileName }`; await mkdirIfNotExists(dir) const writer = fs.createWriteStream(filepath); const axiosConfig = { @@ -976,8 +1010,8 @@ export class tools extends plugin { * @returns {{groupPath: string, target: string}} */ getGroupPathAndTarget() { - const groupPath = `${this.defaultPath}${this.e.group_id || this.e.user_id}`; - const target = `${groupPath}/temp.mp4`; + const groupPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`; + const target = `${ groupPath }/temp.mp4`; return { groupPath, target }; } @@ -1012,7 +1046,7 @@ export class tools extends plugin { await checkAndRemoveFile(target); const res = await axios.get(url, axiosConfig); - logger.mark(`开始下载: ${url}`); + logger.mark(`开始下载: ${ url }`); const writer = fs.createWriteStream(target); res.data.pipe(writer); diff --git a/config/help.yaml b/config/help.yaml index 9f3188e..d674494 100644 --- a/config/help.yaml +++ b/config/help.yaml @@ -38,6 +38,9 @@ - icon: bilibili title: "bilibili/b23" desc: 哔哩哔哩分享实时下载 + - icon: bilimusic + title: "bili音乐+链接" + desc: 哔哩哔哩音乐分享实时下载 - icon: 推特 title: "小蓝鸟" desc: 推特学习版分享实时下载 diff --git a/config/tools.yaml b/config/tools.yaml index 7f2a926..c741a8c 100644 --- a/config/tools.yaml +++ b/config/tools.yaml @@ -8,8 +8,4 @@ translateSecret: '' # 百度翻译密匙 biliSessData: '' # 哔哩哔哩的SESSDATA biliDuration: 480 # 哔哩哔哩限制的最大视频时长(默认8分钟),单位:秒 -openaiAccessToken: '' # 通过获取:https://chat.openai.com/api/auth/session -openaiApiKey: '' # sk... -openaiModel: 'gpt-3.5-turbo' # 目前gpt-3.5-turbo效果比较好,廉价,适合群友 - douyinCookie: '' # douyin's cookie, 格式:odin_tt=xxx;sessionid_ss=xxx;ttwid=xxx;passport_csrf_token=xxx;msToken=xxx; \ No newline at end of file diff --git a/config/version.yaml b/config/version.yaml index 970a54b..8ab169f 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,11 +1,11 @@ - { - version: 1.1.3, + version: 1.2.0, data: [ + 新增哔哩哔哩官方AI总结功能, + 新增哔哩哔哩音乐提取功能, 新增快手解析功能, - 新增竹白百科功能, - 重构翻译功能, - 适配锅巴插件,方便查看和修改配置, + 支持锅巴插件,方便查看和修改配置, 添加#R帮助获取插件帮助, 添加#R版本获取插件版本, ], diff --git a/guoba.support.js b/guoba.support.js index 8198d91..5b23af7 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -79,7 +79,7 @@ export function supportGuoba() { field: "tools.biliSessData", label: "哔哩哔哩SESSDATA", bottomHelpMessage: - "如何获取具体参考我的文档说明:https://gitee.com/kyrzy0416/rconsole-plugin", + "如何获取具体参考我的文档说明:https://gitee.com/kyrzy0416/rconsole-plugin#Q&A", component: "Input", required: false, componentProps: { @@ -97,17 +97,6 @@ export function supportGuoba() { placeholder: "请输入哔哩哔哩的视频最大限制时长(默认15分钟)", }, }, - { - field: "tools.openaiAccessToken", - label: "OpenAI的AccessToken", - bottomHelpMessage: - "ey....,先登录:https://chat.openai.com/,再复制里面的accessToken:https://chat.openai.com/api/auth/session", - component: "Input", - required: false, - componentProps: { - placeholder: "请输入OpenAI的AccessToken(ey.....)", - }, - }, { field: "tools.douyinCookie", label: "抖音的Cookie", diff --git a/utils/biliInfo.js b/utils/biliInfo.js index 1fa3caa..482b3fd 100644 --- a/utils/biliInfo.js +++ b/utils/biliInfo.js @@ -1,11 +1,12 @@ import fetch from "node-fetch"; import axios from "axios"; +import { BILI_VIDEO_INFO } from "../constants/bili.js"; async function getVideoInfo(url) { - const baseVideoInfo = "http://api.bilibili.com/x/web-interface/view"; + // const baseVideoInfo = "http://api.bilibili.com/x/web-interface/view"; const videoId = /video\/[^\?\/ ]+/.exec(url)[0].split("/")[1]; // 获取视频信息,然后发送 - return fetch(`${baseVideoInfo}?bvid=${videoId}`) + return fetch(`${BILI_VIDEO_INFO}?bvid=${videoId}`) .then(async resp => { const respJson = await resp.json(); const respData = respJson.data; @@ -16,8 +17,10 @@ async function getVideoInfo(url) { duration: respData.duration, dynamic: respJson.data.dynamic, stat: respData.stat, + bvid: respData.bvid, aid: respData.aid, cid: respData.pages?.[0].cid, + owner: respData.owner, pages: respData?.pages, }; }); diff --git a/utils/biliSummary.js b/utils/biliSummary.js deleted file mode 100644 index 73fe4a9..0000000 --- a/utils/biliSummary.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * 获取gpt提取视频信息的文字 - * @param videoInfo - * @param biliSessData - * @param shouldShowTimestamp 是否在每段字幕前面加入时间标识 - * @returns {Promise} - */ -export async function getBiliGptInputText(videoInfo, biliSessData, shouldShowTimestamp = false) { - const headers = { - Accept: "application/json", - "Content-Type": "application/json", - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", - Host: "api.bilibili.com", - Cookie: `SESSDATA=${biliSessData}`, - }; - const commonConfig = { - method: "GET", - cache: "no-cache", - headers, - referrerPolicy: "no-referrer", - }; - const { title, desc, dynamic, aid, cid } = videoInfo; - // https://api.bilibili.com/x/player/v2?aid=438937138&cid=1066979272 - const resp = await fetch( - `https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`, - commonConfig, - ); - const subtitles = (await resp.json()).data.subtitle.subtitles; - const subtitlesUrl = subtitles?.subtitle_url?.startsWith("//") - ? `https:${subtitles?.subtitle_url}` - : subtitles?.subtitle_url; - let inputText = ""; - logger.mark(subtitlesUrl); - if (subtitlesUrl !== undefined) { - const res = await fetch(subtitlesUrl); - const subtitlesData = (await res.json()).body; - const subtitleTimestamp = reduceBilibiliSubtitleTimestamp( - subtitlesData, - shouldShowTimestamp, - ); - inputText = getSmallSizeTranscripts(subtitleTimestamp, subtitleTimestamp); - } else { - inputText = `${desc} ${dynamic}`; - } - const videoConfig = { - showEmoji: true, - }; - return shouldShowTimestamp - ? getUserSubtitleWithTimestampPrompt(title, inputText, videoConfig) - : getUserSubtitlePrompt(title, inputText, videoConfig); -} - -// 以下拼接算法来自:https://github.com/JimmyLv/BibiGPT -function reduceBilibiliSubtitleTimestamp(subtitles = [], shouldShowTimestamp) { - return reduceSubtitleTimestamp( - subtitles, - i => i.from, - i => i.content, - shouldShowTimestamp, - ); -} -function reduceSubtitleTimestamp(subtitles, getStart, getText, shouldShowTimestamp) { - // 把字幕数组总共分成 20 组 - const TOTAL_GROUP_COUNT = 30; - // 如果字幕不够多,就每7句话合并一下 - const MINIMUM_COUNT_ONE_GROUP = 7; - const eachGroupCount = - subtitles.length > TOTAL_GROUP_COUNT - ? subtitles.length / TOTAL_GROUP_COUNT - : MINIMUM_COUNT_ONE_GROUP; - - return subtitles.reduce((accumulator, current, index) => { - // 计算当前元素在哪一组 - const groupIndex = Math.floor(index / MINIMUM_COUNT_ONE_GROUP); - - // 如果是当前组的第一个元素,初始化这一组的字符串 - if (!accumulator[groupIndex]) { - accumulator[groupIndex] = { - // 5.88 -> 5.9 - // text: current.start.toFixed() + ": ", - index: groupIndex, - s: getStart(current), - text: shouldShowTimestamp ? getStart(current) + " - " : "", - }; - } - - // 将当前元素添加到当前组的字符串末尾 - accumulator[groupIndex].text = accumulator[groupIndex].text + getText(current) + " "; - - return accumulator; - }, []); -} - -function getSmallSizeTranscripts(newTextData, oldTextData, byteLimit = 6200) { - const text = newTextData - .sort((a, b) => a.index - b.index) - .map(t => t.text) - .join(" "); - const byteLength = getByteLength(text); - - if (byteLength > byteLimit) { - const filtedData = filterHalfRandomly(newTextData); - return getSmallSizeTranscripts(filtedData, oldTextData, byteLimit); - } - - let resultData = newTextData.slice(); - let resultText = text; - let lastByteLength = byteLength; - - for (let i = 0; i < oldTextData.length; i++) { - const obj = oldTextData[i]; - if (itemInIt(newTextData, obj.text)) { - continue; - } - - const nextTextByteLength = getByteLength(obj.text); - const isOverLimit = lastByteLength + nextTextByteLength > byteLimit; - if (isOverLimit) { - const overRate = (lastByteLength + nextTextByteLength - byteLimit) / nextTextByteLength; - const chunkedText = obj.text.substring(0, Math.floor(obj.text.length * overRate)); - resultData.push({ text: chunkedText, index: obj.index }); - } else { - resultData.push(obj); - } - resultText = resultData - .sort((a, b) => a.index - b.index) - .map(t => t.text) - .join(" "); - lastByteLength = getByteLength(resultText); - } - - return resultText; -} - -function filterHalfRandomly(arr) { - const filteredArr = []; - const halfLength = Math.floor(arr.length / 2); - const indicesToFilter = new Set(); - - // 随机生成要过滤掉的元素的下标 - while (indicesToFilter.size < halfLength) { - const index = Math.floor(Math.random() * arr.length); - if (!indicesToFilter.has(index)) { - indicesToFilter.add(index); - } - } - - // 过滤掉要过滤的元素 - for (let i = 0; i < arr.length; i++) { - if (!indicesToFilter.has(i)) { - filteredArr.push(arr[i]); - } - } - - return filteredArr; -} - -function getByteLength(text) { - return unescape(encodeURIComponent(text)).length; -} - -function itemInIt(textData, text) { - return textData.find(t => t.text === text) !== undefined; -} - -function getUserSubtitlePrompt(title, transcript, videoConfig) { - const videoTitle = title?.replace(/\n+/g, " ").trim(); - const videoTranscript = limitTranscriptByteLength(transcript).replace(/\n+/g, " ").trim(); - const language = "zh-CN"; - const sentenceCount = videoConfig.sentenceNumber || 7; - const emojiTemplateText = videoConfig.showEmoji ? "[Emoji] " : ""; - const emojiDescriptionText = videoConfig.showEmoji - ? "Choose an appropriate emoji for each bullet point. " - : ""; - const shouldShowAsOutline = Number(videoConfig.outlineLevel) > 1; - const wordsCount = videoConfig.detailLevel ? (Number(videoConfig.detailLevel) / 100) * 2 : 15; - const outlineTemplateText = shouldShowAsOutline ? `\n - Child points` : ""; - const outlineDescriptionText = shouldShowAsOutline - ? `Use the outline list, which can have a hierarchical structure of up to ${videoConfig.outlineLevel} levels. ` - : ""; - const prompt = `Your output should use the following template:\n## Summary\n## Highlights\n- ${emojiTemplateText}Bulletpoint${outlineTemplateText}\n\nYour task is to summarise the text I have given you in up to ${sentenceCount} concise bullet points, starting with a short highlight, each bullet point is at least ${wordsCount} words. ${outlineDescriptionText}${emojiDescriptionText}Use the text above: {{Title}} {{Transcript}}.\n\nReply in ${language} Language.`; - - return `Title: "${videoTitle}"\nTranscript: "${videoTranscript}"\n\nInstructions: ${prompt}`; -} - -export function getUserSubtitleWithTimestampPrompt(title, transcript, videoConfig) { - const videoTitle = title?.replace(/\n+/g, " ").trim(); - const videoTranscript = limitTranscriptByteLength(transcript).replace(/\n+/g, " ").trim(); - const language = "zh-CN"; - const sentenceCount = videoConfig.sentenceNumber || 7; - const emojiTemplateText = videoConfig.showEmoji ? "[Emoji] " : ""; - const wordsCount = videoConfig.detailLevel ? (Number(videoConfig.detailLevel) / 100) * 2 : 15; - const promptWithTimestamp = `Act as the author and provide exactly ${sentenceCount} bullet points for the text transcript given in the format [seconds] - [text] \nMake sure that:\n - Please start by summarizing the whole video in one short sentence\n - Then, please summarize with each bullet_point is at least ${wordsCount} words\n - each bullet_point start with \"- \" or a number or a bullet point symbol\n - each bullet_point should has the start timestamp, use this template: - seconds - ${emojiTemplateText}[bullet_point]\n - there may be typos in the subtitles, please correct them\n - Reply all in ${language} Language.`; - const videoTranscripts = limitTranscriptByteLength(JSON.stringify(videoTranscript)); - return `Title: ${videoTitle}\nTranscript: ${videoTranscripts}\n\nInstructions: ${promptWithTimestamp}`; -} - -function limitTranscriptByteLength(str, byteLimit = 6200) { - const utf8str = unescape(encodeURIComponent(str)); - const byteLength = utf8str.length; - if (byteLength > byteLimit) { - const ratio = byteLimit / byteLength; - const newStr = str.substring(0, Math.floor(str.length * ratio)); - return newStr; - } - return str; -} diff --git a/utils/bilibili.js b/utils/bilibili.js index 0e2bcde..431f0a3 100644 --- a/utils/bilibili.js +++ b/utils/bilibili.js @@ -65,6 +65,33 @@ async function getDownloadUrl (url) { }); } +async function getAudioUrl (url) { + return axios + .get(url, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36', + referer: 'https://www.bilibili.com', + }, + }) + .then(({ data }) => { + const info = JSON.parse( + data.match(/