This commit is contained in:
Jerry 2025-09-11 13:10:29 +08:00
parent 55d4e17598
commit ed5b216eac
14 changed files with 212 additions and 1226 deletions

View File

@ -1,17 +1,6 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
// 常量提取
const mimeTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
// 添加其他文件类型和MIME类型的映射
};
/** /**
* 通用错误处理函数 * 通用错误处理函数
* @param err * @param err

View File

@ -1,214 +1,218 @@
import { import { GENERAL_REQ_LINK, GENERAL_REQ_LINK_3 } from '../constants/tools.js';
GENERAL_REQ_LINK,
GENERAL_REQ_LINK_2, GENERAL_REQ_LINK_3
} from "../constants/tools.js";
/** /**
* 第三方接口适配器用于大面积覆盖解析视频的内容 * 第三方接口适配器用于大面积覆盖解析视频的内容
*/ */
class GeneralLinkAdapter { class GeneralLinkAdapter {
constructor() {}
constructor() { /**
* 暂时用这个来处理短链接
* @param url
* @param includeRedirect
* @returns {Promise<string|Response>}
*/
async fetchUrl(url, includeRedirect = false) {
let response = await fetch(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
},
});
return includeRedirect ? response.url : response;
}
/**
* 辅助函数创造一个第三方接口的链接
* @param externalInterface 第三方接口这个链接来自常量 constants/tools.js @GENERAL_REQ_LINK / ...
* @param requestURL 请求的链接
* @returns {*}
*/
createReqLink(externalInterface, requestURL) {
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
let reqLink = { ...externalInterface };
reqLink.link = reqLink.link.replace('{}', requestURL);
return reqLink;
}
async ks(link) {
// 例子https://www.kuaishou.com/short-video/3xkfs8p4pnd67p4?authorId=3xkznsztpwetngu&streamSource=find&area=homexxbrilliant
// https://v.m.chenzhongtech.com/fw/photo/3xburnkmj3auazc
// https://v.kuaishou.com/1ff8QP
let msg =
/(?:https?:\/\/)?(www|v)\.(kuaishou|m\.chenzhongtech)\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(
link
)[0];
// 跳转短号
if (msg.includes('v.kuaishou')) {
msg = await this.fetchUrl(msg, true);
}
let video_id;
if (msg.includes('/fw/photo/')) {
video_id = msg.match(/\/fw\/photo\/([^/?]+)/)[1];
} else if (msg.includes('short-video')) {
video_id = msg.match(/short-video\/([^/?]+)/)[1];
} else {
throw Error('无法提取快手的信息,请重试或者换一个视频!');
}
const reqLink = this.createReqLink(
GENERAL_REQ_LINK,
`https://www.kuaishou.com/short-video/${video_id}`
);
// 提取视频
return {
name: '快手',
reqLink,
};
}
async xigua(link) {
// 1. https://v.ixigua.com/ienrQ5bR/
// 2. https://www.ixigua.com/7270448082586698281
// 3. https://m.ixigua.com/video/7270448082586698281
let msg = /(?:https?:\/\/)?(www|v|m)\.ixigua\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0];
// 跳转短号
if (msg.includes('v.ixigua')) {
msg = await this.fetchUrl(msg, true);
} }
/** const id = /ixigua\.com\/(\d+)/.exec(msg)[1] || /\/video\/(\d+)/.exec(msg)[1];
* 暂时用这个来处理短链接 const videoReq = `https://www.ixigua.com/${id}`;
* @param url const reqLink = this.createReqLink(GENERAL_REQ_LINK, videoReq);
* @param includeRedirect return { name: '西瓜', reqLink };
* @returns {Promise<string|Response>} }
*/
async fetchUrl(url, includeRedirect = false) {
let response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
}
});
return includeRedirect ? response.url : response;
}
/** async pipixia(link) {
* 辅助函数创造一个第三方接口的链接 const msg = /https:\/\/h5\.pipix\.com\/(s|item)\/[A-Za-z0-9]+/.exec(link)?.[0];
* @param externalInterface 第三方接口这个链接来自常量 constants/tools.js @GENERAL_REQ_LINK / ... const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
* @param requestURL 请求的链接 return { name: '皮皮虾', reqLink };
* @returns {*} }
*/
createReqLink(externalInterface, requestURL) {
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
let reqLink = { ...externalInterface };
reqLink.link = reqLink.link.replace("{}", requestURL);
return reqLink;
}
async ks(link) { async pipigx(link) {
// 例子https://www.kuaishou.com/short-video/3xkfs8p4pnd67p4?authorId=3xkznsztpwetngu&streamSource=find&area=homexxbrilliant const msg = /https:\/\/h5\.pipigx\.com\/pp\/post\/[A-Za-z0-9]+/.exec(link)?.[0];
// https://v.m.chenzhongtech.com/fw/photo/3xburnkmj3auazc const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
// https://v.kuaishou.com/1ff8QP return { name: '皮皮搞笑', reqLink };
let msg = /(?:https?:\/\/)?(www|v)\.(kuaishou|m\.chenzhongtech)\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0]; }
// 跳转短号
if (msg.includes("v.kuaishou")) { async tieba(link) {
msg = await this.fetchUrl(msg, true); const msg = /https:\/\/tieba\.baidu\.com\/p\/[A-Za-z0-9]+/.exec(link)?.[0];
} // 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
let video_id; // !!!这里加了 '\?' 是因为 API 问题
if (msg.includes('/fw/photo/')) { const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg + '"?');
video_id = msg.match(/\/fw\/photo\/([^/?]+)/)[1]; return { name: '贴吧', reqLink };
} else if (msg.includes("short-video")) { }
video_id = msg.match(/short-video\/([^/?]+)/)[1];
} else { async qqSmallWorld(link) {
throw Error("无法提取快手的信息,请重试或者换一个视频!"); const msg = /https:\/\/s.xsj\.qq\.com\/[A-Za-z0-9]+/.exec(link)?.[0];
} const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
const reqLink = this.createReqLink(GENERAL_REQ_LINK, `https://www.kuaishou.com/short-video/${ video_id }`); return { name: 'QQ小世界', reqLink };
// 提取视频 }
async jike(link) {
// https://m.okjike.com/originalPosts/6583b4421f0812cca58402a6?s=ewoidSI6ICI1YTgzMTY4ZmRmNDA2MDAwMTE5N2MwZmQiCn0=
const msg = /https:\/\/m.okjike.com\/originalPosts\/[A-Za-z0-9]+/.exec(link)?.[0];
const reqLink = this.createReqLink(GENERAL_REQ_LINK_3, msg);
return { name: '即刻', reqLink };
}
async douyinBackup(link) {
const msg = /(http:\/\/|https:\/\/)v.douyin.com\/[A-Za-z\d._?%&+\-=\/#]*/.exec(link)?.[0];
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
return { name: '抖音动图', reqLink };
}
/**
* 初始化通用适配器
* @param link 通用链接
* @returns {Promise<*>}
*/
async init(link) {
logger.mark('[R插件][通用解析]', link);
const handlers = new Map([
[/(kuaishou.com|chenzhongtech.com)/, this.ks.bind(this)],
[/ixigua.com/, this.xigua.bind(this)],
[/h5.pipix.com/, this.pipixia.bind(this)],
[/h5.pipigx.com/, this.pipigx.bind(this)],
[/tieba.baidu.com/, this.tieba.bind(this)],
[/xsj.qq.com/, this.qqSmallWorld.bind(this)],
[/m.okjike.com/, this.jike.bind(this)],
[/v.douyin.com/, this.douyinBackup.bind(this)],
]);
for (let [regex, handler] of handlers) {
if (regex.test(link)) {
return handler(link);
}
}
}
/**
* 通用解析适配器将其他的第三方接口转换为统一的接口
* @param adapter 通用解析适配器
* @param sign 通用解析标识12 在适配器的reqLink中
* @returns {Promise<void>}
*/
async resolve(adapter, sign) {
// 发送GET请求
return fetch(adapter.reqLink.link, {
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Pragma: 'no-cache',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
},
timeout: 10000,
}).then(async (resp) => {
const data = await resp.json();
if (sign === 1) {
// @link GENERAL_REQ_LINK
return { return {
name: "快手", name: adapter.name,
reqLink images: data.data?.imageUrl,
video: data.data?.url,
}; };
} } else if (sign === 2) {
// @link GENERAL_REQ_LINK_2
return {
name: adapter.name,
async xigua(link) { images: data.data?.images,
// 1. https://v.ixigua.com/ienrQ5bR/ video: data.data?.videoUrl,
// 2. https://www.ixigua.com/7270448082586698281 desc: data.data?.desc,
// 3. https://m.ixigua.com/video/7270448082586698281 };
let msg = /(?:https?:\/\/)?(www|v|m)\.ixigua\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0]; } else if (sign === 3) {
// 跳转短号 console.log(data);
if (msg.includes("v.ixigua")) { return {
msg = await this.fetchUrl(msg, true); name: adapter.name,
} images: data?.images.map((item) => item.url),
};
} else {
throw Error('[R插件][通用解析]错误Sign标识');
}
});
}
const id = /ixigua\.com\/(\d+)/.exec(msg)[1] || /\/video\/(\d+)/.exec(msg)[1]; /**
const videoReq = `https://www.ixigua.com/${ id }`; * 通过工厂方式创建一个通用解析的JSON对象
const reqLink = this.createReqLink(GENERAL_REQ_LINK, videoReq); * @param link
return { name: "西瓜", reqLink }; * @returns {Promise<*>}
} */
static async create(link) {
async pipixia(link) { // 先正则匹配到函数进行出策略处理
const msg = /https:\/\/h5\.pipix\.com\/(s|item)\/[A-Za-z0-9]+/.exec(link)?.[0]; const adapter = await new GeneralLinkAdapter();
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg); const adapterHandler = await adapter.init(link);
return { name: "皮皮虾", reqLink }; // 对处理完的信息进行通用解析
} return adapter.resolve(adapterHandler, adapterHandler.reqLink.sign);
}
async pipigx(link) {
const msg = /https:\/\/h5\.pipigx\.com\/pp\/post\/[A-Za-z0-9]+/.exec(link)?.[0];
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
return { name: "皮皮搞笑", reqLink };
}
async tieba(link) {
const msg = /https:\/\/tieba\.baidu\.com\/p\/[A-Za-z0-9]+/.exec(link)?.[0];
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
// !!!这里加了 '\?' 是因为 API 问题
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg + "\"?")
return { name: "贴吧", reqLink };
}
async qqSmallWorld(link) {
const msg = /https:\/\/s.xsj\.qq\.com\/[A-Za-z0-9]+/.exec(link)?.[0];
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
return { name: "QQ小世界", reqLink };
}
async jike(link) {
// https://m.okjike.com/originalPosts/6583b4421f0812cca58402a6?s=ewoidSI6ICI1YTgzMTY4ZmRmNDA2MDAwMTE5N2MwZmQiCn0=
const msg = /https:\/\/m.okjike.com\/originalPosts\/[A-Za-z0-9]+/.exec(link)?.[0];
const reqLink = this.createReqLink(GENERAL_REQ_LINK_3, msg);
return { name: "即刻", reqLink };
}
async douyinBackup(link) {
const msg = /(http:\/\/|https:\/\/)v.douyin.com\/[A-Za-z\d._?%&+\-=\/#]*/.exec(link)?.[0];
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
return { name: "抖音动图", reqLink };
}
/**
* 初始化通用适配器
* @param link 通用链接
* @returns {Promise<*>}
*/
async init(link) {
logger.mark("[R插件][通用解析]", link)
const handlers = new Map([
[/(kuaishou.com|chenzhongtech.com)/, this.ks.bind(this)],
[/ixigua.com/, this.xigua.bind(this)],
[/h5.pipix.com/, this.pipixia.bind(this)],
[/h5.pipigx.com/, this.pipigx.bind(this)],
[/tieba.baidu.com/, this.tieba.bind(this)],
[/xsj.qq.com/, this.qqSmallWorld.bind(this)],
[/m.okjike.com/, this.jike.bind(this)],
[/v.douyin.com/, this.douyinBackup.bind(this)],
]);
for (let [regex, handler] of handlers) {
if (regex.test(link)) {
return handler(link);
}
}
}
/**
* 通用解析适配器将其他的第三方接口转换为统一的接口
* @param adapter 通用解析适配器
* @param sign 通用解析标识12 在适配器的reqLink中
* @returns {Promise<void>}
*/
async resolve(adapter, sign) {
// 发送GET请求
return fetch(adapter.reqLink.link, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Pragma': 'no-cache',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
},
timeout: 10000
}).then(async resp => {
const data = await resp.json();
if (sign === 1) {
// @link GENERAL_REQ_LINK
return {
name: adapter.name,
images: data.data?.imageUrl,
video: data.data?.url,
}
} else if (sign === 2) {
// @link GENERAL_REQ_LINK_2
return {
name: adapter.name,
images: data.data?.images,
video: data.data?.videoUrl,
desc: data.data?.desc
}
} else if (sign === 3) {
console.log(data)
return {
name: adapter.name,
images: data?.images.map(item => item.url),
}
} else {
throw Error("[R插件][通用解析]错误Sign标识");
}
})
}
/**
* 通过工厂方式创建一个通用解析的JSON对象
* @param link
* @returns {Promise<*>}
*/
static async create(link) {
// 先正则匹配到函数进行出策略处理
const adapter = await new GeneralLinkAdapter();
const adapterHandler = await adapter.init(link);
// 对处理完的信息进行通用解析
return adapter.resolve(adapterHandler, adapterHandler.reqLink.sign);
}
} }
export default GeneralLinkAdapter export default GeneralLinkAdapter;

View File

@ -1,153 +0,0 @@
// 获取MV信息的函数
export async function getKugouMv(msg, page_limit, count_limit, n) {
const url = `https://mobiles.kugou.com/api/v3/search/mv?format=json&keyword=${encodeURIComponent(
msg
)}&page=${page_limit}&pagesize=${count_limit}&showtype=1`;
try {
const response = await fetch(url);
const json = await response.json();
const info_list = json.data.info;
let data_list = [];
if (n !== "") {
const info = info_list[n];
const json_data2 = await getMvData(info.hash);
const mvdata_list = json_data2.mvdata;
let mvdata = null;
if ("sq" in mvdata_list) {
mvdata = mvdata_list["sq"];
} else if ("le" in mvdata_list) {
mvdata = mvdata_list["le"];
} else if ("rq" in mvdata_list) {
mvdata = mvdata_list["rq"];
}
data_list = [
{
name: info["filename"],
singername: info["singername"],
duration: new Date(info["duration"] * 1000)
.toISOString()
.substr(14, 5),
file_size: `${(mvdata["filesize"] / (1024 * 1024)).toFixed(2)} MB`,
mv_url: mvdata["downurl"],
cover_url: info["imgurl"].replace("/{size}", ""),
// 下面这些字段可能需要你从其他地方获取因为它们不是直接从这个API返回的
// "play_count": json.play_count,
// "like_count": json.like_count,
// "comment_count": json.comment_count,
// "collect_count": json.collect_count,
// "publish_date": json.publish_date
},
];
} else {
data_list = info_list.map((info) => ({
name: info["filename"],
singername: info["singername"],
duration: new Date(info["duration"] * 1000).toISOString().substr(14, 5),
cover_url: info["imgurl"].replace("/{size}", ""),
}));
}
return data_list;
} catch (error) {
console.error("Error fetching MV data:", error);
return [];
}
}
// 获取歌曲信息的函数
export async function getKugouSong(msg, page_limit, count_limit, n) {
const url = `https://mobiles.kugou.com/api/v3/search/song?format=json&keyword=${encodeURIComponent(
msg
)}&page=${page_limit}&pagesize=${count_limit}&showtype=1`;
try {
const response = await fetch(url);
const json = await response.json();
const info_list = json.data.info;
// console.log(info_list)
let data_list = [];
if (n !== "") {
const info = info_list[n];
const song_hash = info.hash;
let song_url = "付费歌曲暂时无法获取歌曲下载链接";
let json_data2 = {};
if (song_hash !== "") {
json_data2 = await getMp3Data(song_hash);
song_url = json_data2.error ? song_url : json_data2.url;
}
data_list = [
{
name: info.filename,
singername: info.singername,
duration: new Date(info.duration * 1000).toISOString().substr(14, 5),
file_size: `${(json_data2.fileSize / (1024 * 1024)).toFixed(2)} MB`,
song_url: song_url,
album_img: json_data2.album_img?.replace("/{size}", ""),
// "mv_url": await get_kugou_mv(msg, page_limit, count_limit, n) 这可能会导致递归调用,视具体情况而定
},
];
} else {
data_list = info_list.map((info) => ({
name: info.filename,
singername: info.singername,
duration: new Date(info.duration * 1000).toISOString().substr(14, 5),
hash: info.hash,
mvhash: info.mvhash ? info.mvhash : null,
}));
}
// 发送响应
return {
code: 200,
text: "解析成功",
type: "歌曲解析",
now: new Date().toISOString(),
data: data_list,
};
} catch (error) {
console.error("Error fetching song data:", error);
return { code: 500, text: "服务器内部错误" };
}
}
// 获取MP3数据的函数
async function getMp3Data(song_hash) {
const url = `https://m.kugou.com/app/i/getSongInfo.php?hash=${song_hash}&cmd=playInfo`;
try {
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/6.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36"
},
redirect: 'follow',
method: 'GET',
});
const json = await response.json();
return json;
} catch (error) {
console.error("Error fetching MP3 data:", error);
return {};
}
}
// 获取MV数据的函数
async function getMvData(mv_hash) {
const url = `http://m.kugou.com/app/i/mv.php?cmd=100&hash=${mv_hash}&ismp3=1&ext=mp4`;
try {
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/6.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36"
},
redirect: 'follow',
method: 'GET',
});
const json = await response.json();
return json;
} catch (error) {
console.error("Error fetching MV data:", error);
return {};
}
}

View File

@ -1,20 +0,0 @@
import { SUMMARY_CONTENT_ESTIMATOR_PATTERNS } from "../constants/constant.js";
/**
* 内容评估器
* @link {linkShareSummary}
* @param link 链接
*/
export function contentEstimator(link) {
for (const pattern of SUMMARY_CONTENT_ESTIMATOR_PATTERNS) {
if (pattern.reg.test(link)) {
return {
name: pattern.name,
summaryLink: pattern.reg.exec(link)?.[0]
};
}
}
logger.error("[R插件][总结模块] 内容评估出错...");
throw Error("内容评估出错...");
}

View File

@ -1,39 +0,0 @@
import { retryFetch } from "./common.js";
import { PearAPI_CRAWLER, PearAPI_DEEPSEEK } from "../constants/tools.js";
/**
* LLM 爬虫
* @param summaryLink
* @returns {Promise<string>}
*/
export async function llmRead(summaryLink) {
const llmCrawler = await retryFetch(PearAPI_CRAWLER.replace("{}", summaryLink));
return (await llmCrawler.json())?.data;
}
/**
* DeepSeek对话
* @param content
* @param prompt
* @returns {Promise<string>}
*/
export async function deepSeekChat(content, prompt) {
const deepseekFreeSummary = await fetch(PearAPI_DEEPSEEK, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"messages": [
{
"role": "system",
"content": prompt
},
{
"role": "user",
"content": content,
}]
}),
});
return (await deepseekFreeSummary.json())?.message;
}

View File

@ -1,118 +0,0 @@
import axios from "axios";
export class OpenaiBuilder {
constructor() {
this.baseURL = "https://api.moonshot.cn"; // 默认模型
this.apiKey = ""; // 默认API密钥
this.prompt = "描述一下这个图片"; // 默认提示
this.model = 'claude-3-haiku-20240307'
this.provider = "kimi"; // 默认提供商
}
setBaseURL(baseURL) {
this.baseURL = baseURL;
return this;
}
setApiKey(apiKey) {
this.apiKey = apiKey;
return this;
}
setPrompt(prompt) {
this.prompt = prompt;
return this;
}
setModel(model) {
this.model = model;
return this;
}
setProvider(provider) {
this.provider = provider;
return this;
}
setPath(path) {
this.path = path;
return this;
}
async build() {
// logger.info(this.baseURL, this.apiKey)
// 创建客户端
this.client = axios.create({
baseURL: this.baseURL,
timeout: 100000,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + this.apiKey
}
});
return this;
}
/**
* 调用与OpenAI兼容的API如Kimi/Moonshot
* @param {Array<Object>} messages - 发送给模型的消息列表
* @param {Array<Object>} [tools=[]] - (可选) 一个描述可供模型使用的工具的数组
* @returns {Promise<Object>} 返回一个包含模型响应的对象如果模型决定调用工具则包含 'tool_calls' 字段否则包含 'ans' 文本响应
*/
async chat(messages, tools = []) {
if (this.provider === 'deepseek') {
const content = messages.find(m => m.role === 'user')?.content;
const ans = await deepSeekChat(content, this.prompt);
return {
"model": "deepseek",
"ans": ans
}
}
// 准备发送给API的消息
let requestMessages = [...messages];
// 检查是否已存在系统提示
const hasSystemPrompt = requestMessages.some(m => m.role === 'system');
// 如果没有系统提示并且builder中已设置则添加
if (!hasSystemPrompt && this.prompt) {
requestMessages.unshift({
role: 'system',
content: this.prompt,
});
}
// 构建API请求的负载
const payload = {
model: this.model, // 使用在builder中设置的模型
messages: requestMessages,
};
// 如果提供了工具,将其添加到负载中,并让模型自动决定是否使用
if (tools && tools.length > 0) {
payload.tools = tools;
payload.tool_choice = "auto";
}
// 发送POST请求到聊天完成端点
const completion = await this.client.post("/v1/chat/completions", payload);
const message = completion.data.choices[0].message;
// 从响应中获取实际使用的模型名称
const modelName = completion.data.model;
// 如果模型的响应中包含工具调用
if (message.tool_calls) {
return {
"model": modelName,
"tool_calls": message.tool_calls
}
}
// 否则,返回包含文本答案的响应
return {
"model": modelName,
"ans": message.content
}
}
}

View File

@ -7,7 +7,7 @@
* console.log(exists); // true or false * console.log(exists); // true or false
*/ */
export async function redisExistKey(key) { export async function redisExistKey(key) {
return redis.exists(key); return redis.exists(key);
} }
/** /**
@ -19,7 +19,7 @@ export async function redisExistKey(key) {
* console.log(value); // { ... } * console.log(value); // { ... }
*/ */
export async function redisGetKey(key) { export async function redisGetKey(key) {
return JSON.parse(await redis.get(key)); return JSON.parse(await redis.get(key));
} }
/** /**
@ -31,23 +31,5 @@ export async function redisGetKey(key) {
* await redisSetKey('myKey', { foo: 'bar' }); * await redisSetKey('myKey', { foo: 'bar' });
*/ */
export async function redisSetKey(key, value = {}) { export async function redisSetKey(key, value = {}) {
return redis.set( return redis.set(key, JSON.stringify(value));
key,
JSON.stringify(value),
);
} }
/**
* 判断是否存在这个key然后再取值如果没有就返回null
* @param key
* @returns {Promise<Object|Array>}
* @example
* const value = await redisExistAndGetKey('myKey');
* console.log(value); // { ... } or null
*/
export async function redisExistAndGetKey(key) {
if (await redisExistKey(key)) {
return redisGetKey(key);
}
return null;
}

View File

@ -1,57 +0,0 @@
import { exec } from 'child_process';
import path from 'path'
/**
* 执行 TDL 进行下载
* @param url
* @param curPath
* @param isOversea
* @param proxyAddr
* @param videoDownloadConcurrency
* @returns {Promise<string>}
*/
export async function startTDL(url, curPath, isOversea, proxyAddr, videoDownloadConcurrency = 1) {
return new Promise((resolve, reject) => {
curPath = path.resolve(curPath);
const proxyStr = isOversea ? `` : `--proxy ${ proxyAddr }`;
const concurrencyStr = videoDownloadConcurrency > 1 ? `-t ${ videoDownloadConcurrency } -l ${ videoDownloadConcurrency }` : '';
const command = `tdl dl -u ${ url } -d ${ curPath } ${ concurrencyStr } ${ proxyStr }`
logger.mark(`[R插件][TDL] ${ command }`);
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`[R插件][TDL]执行出错: ${ error.message }`);
return;
}
if (stderr) {
reject(`[R插件][TDL]错误信息: ${ stderr }`);
return;
}
resolve(stdout);
})
})
}
/**
* 保存小飞机内容到小飞机的收藏
* @param url
* @param isOversea
* @param proxyAddr
* @returns {Promise<unknown>}
*/
export async function saveTDL(url, isOversea, proxyAddr) {
return new Promise((resolve, reject) => {
const proxyStr = isOversea ? `` : `--proxy ${ proxyAddr }`;
const command = `tdl forward --from ${ url } ${ proxyStr }`
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`[R插件][TDL保存]执行出错: ${ error.message }`);
return;
}
if (stderr) {
reject(`[R插件][TDL保存]错误信息: ${ stderr }`);
return;
}
resolve(stdout);
})
})
}

View File

@ -1,36 +0,0 @@
export function genVerifyFp() {
const baseStr = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const t = baseStr.length;
let milliseconds = Date.now(); // 获取当前的时间戳(毫秒)
let base36 = "";
// 将时间戳转换为base36
while (milliseconds > 0) {
let remainder = milliseconds % 36;
if (remainder < 10) {
base36 = remainder.toString() + base36;
} else {
base36 = String.fromCharCode('a'.charCodeAt(0) + remainder - 10) + base36;
}
milliseconds = Math.floor(milliseconds / 36);
}
const r = base36;
let o = new Array(36).fill("");
o[8] = o[13] = o[18] = o[23] = "_";
o[14] = "4";
// 生成随机字符
for (let i = 0; i < 36; i++) {
if (!o[i]) {
let n = Math.floor(Math.random() * t);
if (i === 19) {
n = (3 & n) | 8;
}
o[i] = baseStr[n];
}
}
return "verify_" + r + "_" + o.join("");
}

View File

@ -1,140 +0,0 @@
import { tencentTransMap } from "../constants/constant.js";
import fetch from "node-fetch";
import _ from 'lodash'
// 定义翻译策略接口
class TranslateStrategy {
async translate(query, targetLanguage) {
throw new Error("This method should be implemented by subclasses");
}
}
// 企鹅翻译策略
class TencentTranslateStrategy extends TranslateStrategy {
constructor(config) {
super();
this.config = config;
this.url = "https://transmart.qq.com/api/imt";
this.commonHeaders = {
"USER-AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0"
};
this.clientKey = "browser-firefox-111.0.0-Mac OS-d35fca23-eb48-45ba-9913-114f1177b02b-1679376552800";
}
async detectLanguage(query) {
try {
const response = await fetch(this.url, {
method: "POST",
headers: this.commonHeaders,
body: JSON.stringify({
"header": {
"fn": "text_analysis",
"client_key": this.clientKey
},
"text": query,
"type": "plain",
"normalize": {
"merge_broken_line": false
}
})
});
const data = await response.json();
return data.header.ret_code === 'succ' ? data.language : "en";
} catch (error) {
logger.error("Error detecting language:", error);
return "en";
}
}
async translate(query, targetLanguage) {
try {
const sourceLanguage = await this.detectLanguage(query);
const response = await fetch(this.url, {
method: "POST",
headers: this.commonHeaders,
body: JSON.stringify({
"header": {
"fn": "auto_translation",
"client_key": this.clientKey
},
"type": "plain",
"model_category": "normal",
"text_domain": "general",
"source": {
"lang": sourceLanguage,
"text_list": ["", query, ""]
},
"target": {
"lang": tencentTransMap[targetLanguage]
}
})
});
const data = await response.json();
return data.header.ret_code === 'succ' ? data.auto_translation?.[1] : "翻译失败";
} catch (error) {
logger.error("Error translating text:", error);
return "翻译失败";
}
}
}
// Deepl翻译策略
class DeeplTranslateStrategy extends TranslateStrategy {
constructor(config) {
super();
this.config = config;
this.deeplUrls = this.config.deeplApiUrls.includes(",") ? this.config.deeplApiUrls.split(",") : [this.config.deeplApiUrls];
}
async translate(query, targetLanguage) {
const url = this.deeplUrls[Math.floor(Math.random() * this.deeplUrls.length)];
logger.info(`[R插件][Deepl翻译]当前使用的API${url}`);
try {
const source_lang = await new TencentTranslateStrategy(this.config).detectLanguage(query);
logger.info(`[R插件][Deepl翻译]:检测到的源语言:${source_lang}`);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...this.commonHeaders
},
body: JSON.stringify({
text: query,
source_lang,
target_lang: tencentTransMap[targetLanguage]
}),
});
const data = await response.json();
return data.data;
} catch (error) {
logger.error("Error translating text:", error);
return "翻译失败";
}
}
}
// 主逻辑
export default class Translate {
constructor(config) {
this.config = config;
this.strategy = null;
}
selectStrategy() {
if (!_.isEmpty(this.config.deeplApiUrls)) {
logger.info("[R插件][翻译策略]:当前选择 Deepl翻译")
return new DeeplTranslateStrategy(this.config);
} else {
logger.info("[R插件][翻译策略]:当前选择 企鹅翻译")
return new TencentTranslateStrategy(this.config);
}
}
async translate(query, targetLanguage) {
if (!this.strategy) {
this.strategy = this.selectStrategy();
}
return this.strategy.translate(query, targetLanguage);
}
}

View File

@ -1,31 +0,0 @@
// Base62 encode function in JavaScript
const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const base62_encode = (number) => {
if (number === 0) return '0';
let result = '';
while (number > 0) {
result = ALPHABET[number % 62] + result;
number = Math.floor(number / 62);
}
return result;
};
// Convert mid to id
export const mid2id = (mid) => {
mid = mid.toString().split('').reverse().join(''); // Reverse the input string
const size = Math.ceil(mid.length / 7);
let result = [];
for (let i = 0; i < size; i++) {
let s = mid.slice(i * 7, (i + 1) * 7).split('').reverse().join(''); // Chunk and reverse each chunk
s = base62_encode(parseInt(s, 10)); // Encode each chunk using base62
if (i < size - 1 && s.length < 4) {
// Pad with leading zeros if necessary
s = '0'.repeat(4 - s.length) + s;
}
result.push(s);
}
result.reverse(); // Reverse the result array to maintain order
return result.join(''); // Join the array into a single string
};

View File

@ -1,46 +0,0 @@
/**
* 用于YouTube的格式化
* @param seconds
* @returns {string}
*/
export function ytbFormatTime(seconds) {
// 计算小时、分钟和秒
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
// 将小时、分钟和秒格式化为两位数
const formattedHours = String(hours).padStart(2, '0');
const formattedMinutes = String(minutes).padStart(2, '0');
const formattedSeconds = String(secs).padStart(2, '0');
// 构造时间范围字符串
return `00:00:00-${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
}
/**
* 移除链接中的不需要的参数
* @param url
* @returns {*}
*/
export function removeParams(url) {
return url
.replace(/&list=[^&]*/g, '')
.replace(/&start_radio=[^&]*/g, '')
.replace(/&index=[^&]*/g, '')
.replace(/&si=[^&]*/g, '');
}
export function convertToSeconds(timeStr) {
const parts = timeStr.split(':').map(Number);
if (parts.length === 2) {
const [minutes, seconds] = parts;
return minutes * 60 + seconds;
} else if (parts.length === 3) {
const [hours, minutes, seconds] = parts;
return hours * 3600 + minutes * 60 + seconds;
}
return timeStr;
}
export async function autoSelectMusicOrVideoSend() {
}

View File

@ -1,194 +0,0 @@
import { exec } from 'child_process';
/**
* 构建梯子参数
* @param isOversea
* @param proxy
* @returns {string|string}
*/
function constructProxyParam(isOversea, proxy) {
return isOversea ? '' : `--proxy ${proxy}`;
}
/**
* 构造cookie参数
* 目前只支持YouTube构造cookie否则就必须修改`url.includes("youtu")`
* @param url
* @param cookiePath
* @returns {string}
*/
function constructCookiePath(url, cookiePath) {
return cookiePath !== '' && url.includes('youtu') ? `--cookies ${cookiePath}` : '';
}
/**
* yt-dlp获取标题的时候可能需要的一个编码参数也在一定程度上解决部分window系统乱码问题
* @param url
* @returns {string}
*/
function constructEncodingParam(url) {
return '--encoding UTF-8'; // 始终为标题获取使用 UTF-8 编码
}
/**
* 获取时长
* @param url
* @param isOversea
* @param proxy
* @param cookiePath
* @returns string
*/
export function ytDlpGetDuration(url, isOversea, proxy, cookiePath = '') {
return new Promise((resolve, reject) => {
// 构造 cookie 参数
const cookieParam = constructCookiePath(url, cookiePath);
const command = `yt-dlp --get-duration --skip-download ${cookieParam} ${constructProxyParam(isOversea, proxy)} "${url}"`;
exec(command, (error, stdout, stderr) => {
if (error) {
logger.error(
`[R插件][yt-dlp审计] Error executing ytDlpGetDuration: ${error}. Stderr: ${stderr}`
);
reject(error);
} else {
resolve(stdout.trim());
}
});
});
}
/**
* 获取标题
* @param url
* @param isOversea
* @param proxy
* @param cookiePath
* @returns string
*/
export async function ytDlpGetTilt(url, isOversea, proxy, cookiePath = '') {
return new Promise((resolve, reject) => {
// 构造 cookie 参数
const cookieParam = constructCookiePath(url, cookiePath);
// 构造 编码 参数
const encodingParam = constructEncodingParam(url);
const command = `yt-dlp --get-title --skip-download ${cookieParam} ${constructProxyParam(isOversea, proxy)} "${url}" ${encodingParam}`;
exec(command, (error, stdout, stderr) => {
if (error) {
logger.error(
`[R插件][yt-dlp审计] Error executing ytDlpGetTilt: ${error}. Stderr: ${stderr}`
);
reject(error);
} else {
resolve(stdout.trim());
}
});
});
}
/**
* 获取封面
* @param path
* @param url
* @param isOversea
* @param proxy
* @param cookiePath
* @param thumbnailFilenamePrefix 缩略图文件名前缀 (不含扩展名)
*/
export function ytDlpGetThumbnail(
path,
url,
isOversea,
proxy,
cookiePath = '',
thumbnailFilenamePrefix = 'thumbnail'
) {
return new Promise((resolve, reject) => {
const cookieParam = constructCookiePath(url, cookiePath);
const finalThumbnailName = thumbnailFilenamePrefix || 'thumbnail';
const command = `yt-dlp --write-thumbnail --convert-thumbnails png --skip-download ${cookieParam} ${constructProxyParam(isOversea, proxy)} "${url}" -P "${path}" -o "${finalThumbnailName}.%(ext)s"`;
exec(command, (error, stdout, stderr) => {
if (error) {
logger.error(
`[R插件][yt-dlp审计] Error executing ytDlpGetThumbnail: ${error}. Stderr: ${stderr}`
);
return reject(error);
}
// 从yt-dlp的输出中提取文件名
const match = stdout.match(/Writing thumbnail to: (.*)/);
if (match && match[1]) {
const thumbnailPath = match[1].trim();
// 只返回文件名部分
const thumbnailFilename = thumbnailPath.split(/[\\/]/).pop();
logger.info(`[R插件][yt-dlp审计] Thumbnail downloaded: ${thumbnailFilename}`);
resolve(thumbnailFilename);
} else {
// 兜底方案:如果无法从输出中解析,则按原逻辑拼接
logger.warn(
'[R插件][yt-dlp审计] Could not parse thumbnail filename from stdout. Falling back to default.'
);
// 尝试查找文件因为yt-dlp可能没有输出我们期望的格式
const expectedPngPath = `${finalThumbnailName}.png`;
resolve(expectedPngPath);
}
});
});
}
/**
* yt-dlp 工具类
* @returns {Promise<void>}
* @param path 下载路径
* @param url 下载链接
* @param isOversea 是否是海外用户
* @param proxy 代理地址
* @param merge 是否合并输出为 mp4 格式 (仅适用于视频合并需求)
* @param graphics YouTube画质参数
* @param timeRange 截取时间段
* @param maxThreads 最大并发
* @param outputFilename 输出文件名 (不含扩展名)
* @param cookiePath Cookie所在位置
*/
export async function ytDlpHelper(
path,
url,
isOversea,
proxy,
maxThreads,
outputFilename,
merge = false,
graphics,
timeRange,
cookiePath = ''
) {
return new Promise((resolve, reject) => {
let command = '';
// 构造 cookie 参数
const cookieParam = constructCookiePath(url, cookiePath);
// 确保 outputFilename 不为空,提供一个默认值以防万一
const finalOutputFilename = outputFilename || 'temp_download';
if (url.includes('music')) {
// 这里是 YouTube Music的处理逻辑
// e.g yt-dlp -x --audio-format mp3 https://youtu.be/5wEtefq9VzM -o test.mp3
command = `yt-dlp -x --audio-format flac -f ba ${cookieParam} ${constructProxyParam(isOversea, proxy)} -P "${path}" -o "${finalOutputFilename}.flac" "${url}"`;
} else {
// 正常情况下的处理逻辑
const fParam = url.includes('youtu')
? `--download-sections "*${timeRange}" -f "bv${graphics}[ext=mp4]+ba[ext=m4a]" `
: '';
command = `yt-dlp -N ${maxThreads} ${fParam} --concurrent-fragments ${maxThreads} ${cookieParam} ${constructProxyParam(isOversea, proxy)} -P "${path}" -o "${finalOutputFilename}.%(ext)s" "${url}"`;
}
logger.info(`[R插件][yt-dlp审计] ${command}`);
exec(command, (error, stdout) => {
if (error) {
logger.error(`[R插件][yt-dlp审计] 执行命令时出错: ${error}`);
reject(error);
} else {
resolve(stdout);
}
});
});
}

View File

@ -1,169 +1,14 @@
import os from "os";
/** /**
* 将只有 text 类型的数组转换为原生的 {Bot.makeForwardMsg} * 将只有 text 类型的数组转换为原生的 {Bot.makeForwardMsg}
* @param e * @param e
* @param textArray {string[]} * @param textArray {string[]}
*/ */
export function textArrayToMakeForward(e, textArray) { export function textArrayToMakeForward(e, textArray) {
return textArray.map(item => { return textArray.map((item) => {
return { return {
message: { type: "text", text: item }, message: { type: 'text', text: item },
nickname: e.sender.card || e.user_id, nickname: e.sender.card || e.user_id,
user_id: e.user_id, user_id: e.user_id,
}; };
}) });
}
/**
* 发送群组音乐卡片
* @param e
* @param platformType 音乐平台
* @param musicId 音乐id
*/
export async function sendMusicCard(e, platformType, musicId) {
await e.bot.sendApi('send_group_msg', {
group_id: e.group_id,
message: [
{
type: 'music',
data: {
type: platformType,
id: musicId
}
}
]
});
}
/**
* 获取群文件最新的图片
* @param e
* @param count 获取群聊条数
* @returns {Promise<*|string>}
*/
export async function getLatestImage(e, count = 10) {
// 获取最新的聊天记录阈值为5
const latestChat = await e.bot.sendApi("get_group_msg_history", {
"group_id": e.group_id,
"count": count
});
const messages = latestChat.data.messages;
// 找到最新的图片
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages?.[i]?.message;
if (message?.[0]?.type === "image") {
return message?.[0].data?.url;
}
}
return "";
}
/**
* 获取群文件Url地址
* @param e
* @param count 获取群聊条数
*/
export async function getGroupFileUrl(e, count = 10) {
const latestChat = await e.bot.sendApi("get_group_msg_history", {
"group_id": e.group_id,
"count": count
});
const messages = latestChat.data.messages;
let file_id = "";
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages?.[i]?.message;
if (message?.[0]?.type === "file") {
file_id = message?.[0].data?.file_id;
break;
}
}
if (file_id === "") {
logger.info('未找到群文件')
return "";
}
// 获取文件信息
let latestFileUrl = await e.bot.sendApi("get_group_file_url", {
"group_id": e.group_id,
"file_id": file_id
});
let cleanPath = decodeURIComponent(latestFileUrl.data.url)
// 适配 低版本 Napcat 例如3.6.4
if (cleanPath.startsWith("https")) {
// https://njc-download.ftn.qq.com/....
const urlObj = new URL(cleanPath);
// 检查URL中是否包含 fname 参数
if (urlObj.searchParams.has('fname')) {
// 获取 fname 参数的值
const originalFname = urlObj.searchParams.get('fname');
// 提取 file_id第一个"."后面的内容)
const fileId = file_id.split('.').slice(1).join('.'); // 分割并去掉第一个部分
urlObj.searchParams.set('fname', `${originalFname}${fileId}`);
return {
cleanPath: urlObj.toString(),
file_id
};
}
} else if (cleanPath.startsWith('file:///')) {
cleanPath = cleanPath.replace('file:///', '')
}
return { cleanPath, file_id };
}
/**
* 获取群回复
* @param e
*/
export async function getReplyMsg(e) {
const msgList = await e.bot.sendApi("get_group_msg_history", {
"group_id": e.group_id,
"count": 1
});
let msgId = msgList.data.messages[0]?.message[0]?.data.id
let msg = await e.bot.sendApi("get_msg",{
"message_id" : msgId
})
return msg.data
}
/**
* 获取机器人信息
* @param e
* @returns {Promise<*>}
*/
export async function getBotLoginInfo(e) {
return await e.bot.sendApi("get_login_info");
}
/**
* 获取运行状态
* @param e
* @returns {Promise<*>}
*/
export async function getBotStatus(e) {
return await e.bot.sendApi("get_status");
}
/**
* 获取版本信息
* @param e
* @returns {Promise<*>}
*/
export async function getBotVersionInfo(e) {
return await e.bot.sendApi("get_version_info");
}
/**
* 发送私聊消息
* @param e
* @param message
* @returns {Promise<void>}
*/
export async function sendPrivateMsg(e, message) {
e.bot.sendApi("send_private_msg", {
user_id: e.user_id,
message: message,
})
} }