mirror of
https://github.com/Jerryplusy/crystelf-plugin.git
synced 2026-01-29 01:07:27 +00:00
🚀 chore(components): remove unused date component
🗑️ chore(modules): delete Fanqie app module 🗑️ chore(utils): remove Pinyin utility class 🛠️ refactor(components/tool): clean up tool functions 📦 update(package): add fs-extra and uuid dependencies
This commit is contained in:
parent
8c413949ac
commit
943a51be65
225
apps/fanqie.js
225
apps/fanqie.js
@ -1,225 +0,0 @@
|
|||||||
import fs from 'node:fs';
|
|
||||||
import path from 'path';
|
|
||||||
import chokidar from 'chokidar';
|
|
||||||
import ConfigControl from '../lib/config/configControl.js';
|
|
||||||
import Fanqie from '../modules/apps/fanqie/fanqie.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 本功能由 y68(github@yeqiu6080) 提供技术支持
|
|
||||||
*/
|
|
||||||
export default class FanqiePlugin extends plugin {
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
name: 'crystelf-fanqie',
|
|
||||||
dsc: '番茄小说下载器',
|
|
||||||
event: 'message',
|
|
||||||
priority: -114,
|
|
||||||
rule: [
|
|
||||||
{
|
|
||||||
reg: '(changdunovel.com/wap/share-v2.html|fanqienovel.com/page)',
|
|
||||||
fnc: 'handleFanqieLink',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: '#?fq下载(.*)',
|
|
||||||
fnc: 'downloadByBookId',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
reg: '^fq清(理|除|空)缓存$',
|
|
||||||
fnc: 'clearFanqieCache',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.initPromise = this.initFanqieConfig();
|
|
||||||
this.fanqieClient = null;
|
|
||||||
|
|
||||||
// 注册计划任务
|
|
||||||
this.task = {
|
|
||||||
cron: '0 0 16 * * ?',
|
|
||||||
name: '定时清理番茄缓存',
|
|
||||||
fnc: () => this.clearFanqieCache(false, true),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async initFanqieConfig() {
|
|
||||||
this.outDir = await ConfigControl.get('fanqieConfig')?.outDir;
|
|
||||||
this.apiUrl = await ConfigControl.get('fanqieConfig')?.url;
|
|
||||||
this.fanqieClient = new Fanqie(this.apiUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听下载输出目录
|
|
||||||
*/
|
|
||||||
async waitForOutputFile(dir, timeout = 30000) {
|
|
||||||
if (!dir) return false;
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const watcher = chokidar.watch(dir, {
|
|
||||||
persistent: true,
|
|
||||||
ignoreInitial: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
watcher.close();
|
|
||||||
resolve(false);
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
watcher.on('add', (filePath) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
watcher.close();
|
|
||||||
resolve(filePath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理缓存
|
|
||||||
*/
|
|
||||||
async clearFanqieCache(e, isScheduled = false, specificId = false) {
|
|
||||||
if (!isScheduled && e && !e.isMaster) {
|
|
||||||
e.reply('你没有权限使用此功能', true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.outDir) {
|
|
||||||
await this.initPromise;
|
|
||||||
if (!this.outDir) {
|
|
||||||
if (e) e.reply('缓存目录未初始化,无法清理', true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (specificId) {
|
|
||||||
const specificPath = path.join(this.outDir, 'files', specificId);
|
|
||||||
if (fs.existsSync(specificPath)) {
|
|
||||||
fs.rmSync(specificPath, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainCachePath = path.join(this.outDir, 'fanqie');
|
|
||||||
if (fs.existsSync(mainCachePath)) {
|
|
||||||
fs.readdirSync(mainCachePath).forEach((file) => {
|
|
||||||
const fullPath = path.join(mainCachePath, file);
|
|
||||||
const stat = fs.statSync(fullPath);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
||||||
} else {
|
|
||||||
fs.unlinkSync(fullPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isScheduled && e) e.reply('缓存清理完成', true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析网页链接中的 book_id
|
|
||||||
*/
|
|
||||||
async handleFanqieLink(e) {
|
|
||||||
const message = e.msg.trim();
|
|
||||||
let bookId = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (message.includes('changdunovel.com')) {
|
|
||||||
bookId = message.match(/book_id=(\d+)/)[1];
|
|
||||||
} else {
|
|
||||||
bookId = message.match(/page\/(\d+)/)[1];
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return e.reply('解析失败,请检查链接是否正确', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.downloadFanqieBook(e, bookId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用 #fq下载 命令下载
|
|
||||||
*/
|
|
||||||
async downloadByBookId(e) {
|
|
||||||
const bookId = e.msg.replace(/^#?fq下载/, '').trim();
|
|
||||||
return this.downloadFanqieBook(e, bookId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行下载并上传文件
|
|
||||||
*/
|
|
||||||
async downloadFanqieBook(e, bookId) {
|
|
||||||
await this.initPromise;
|
|
||||||
|
|
||||||
let bookInfo;
|
|
||||||
try {
|
|
||||||
bookInfo = await this.fanqieClient.get_info(bookId);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
return e.reply('获取小说信息失败', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bookInfo) return e.reply('获取失败,请稍后再试', true);
|
|
||||||
|
|
||||||
e.reply(
|
|
||||||
`识别小说:[番茄小说]《${bookInfo.book_name}》\n作者:${bookInfo.author}\n原名:${bookInfo.original_book_name}`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
e.reply('开始下载,请稍等片刻...', true);
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.fanqieClient.down(bookId, e.message_id);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
return e.reply('下载失败,请稍后重试', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outPath = path.join(this.outDir, 'files', String(e.message_id));
|
|
||||||
let finalFilePath = await this.waitForOutputFile(outPath);
|
|
||||||
if (!finalFilePath) return e.reply('下载超时', true);
|
|
||||||
|
|
||||||
// 文件重命名防止空格
|
|
||||||
const safePath = finalFilePath.replace(/ /g, '_');
|
|
||||||
if (finalFilePath !== safePath) {
|
|
||||||
try {
|
|
||||||
fs.renameSync(finalFilePath, safePath);
|
|
||||||
finalFilePath = safePath;
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`重命名失败:${err.stack}`);
|
|
||||||
return e.reply('重命名失败', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploaded = await this.sendFileToUser(e, finalFilePath);
|
|
||||||
await this.clearFanqieCache(false, true, String(e.message_id));
|
|
||||||
|
|
||||||
if (!uploaded) return e.reply('上传失败', true);
|
|
||||||
|
|
||||||
e.reply(`《${bookInfo.book_name}》上传成功,耗时 ${(Date.now() - startTime) / 1000}s`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传文件至群或私聊
|
|
||||||
*/
|
|
||||||
async sendFileToUser(e, filePath) {
|
|
||||||
try {
|
|
||||||
const fileName = path.basename(filePath);
|
|
||||||
if (e.isGroup) {
|
|
||||||
return await e.bot.sendApi('upload_group_file', {
|
|
||||||
group_id: e.group_id,
|
|
||||||
file: filePath,
|
|
||||||
name: fileName,
|
|
||||||
});
|
|
||||||
} else if (e.friend) {
|
|
||||||
return await e.bot.sendApi('upload_private_file', {
|
|
||||||
user_id: e.user_id,
|
|
||||||
file: filePath,
|
|
||||||
name: fileName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`文件上传失败:${logger.red(err.stack)}`);
|
|
||||||
e.reply(`上传失败:${err.message}`, true);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
let date = {}; //咕咕咕
|
|
||||||
|
|
||||||
export default date;
|
|
||||||
@ -7,15 +7,6 @@ let tools = {
|
|||||||
sleep(ms) {
|
sleep(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成指定范围内的随机整数
|
|
||||||
* @param {number} min - 最小值
|
|
||||||
* @param {number} max - 最大值
|
|
||||||
*/
|
|
||||||
randomInt(min, max) {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default tools;
|
export default tools;
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"url": "http://127.0.0.1:6868",
|
|
||||||
"outDir": "/home/user/debian/cache/Downloads"
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
class Fanqie {
|
|
||||||
constructor(apiurl) {
|
|
||||||
this.apiurl = apiurl;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_info(book_id) {
|
|
||||||
try {
|
|
||||||
let url = `${this.apiurl}/api/info?book_id=${book_id}&source=fanqie`;
|
|
||||||
let res = await axios.get(url);
|
|
||||||
if (res.status !== 200 || !res.data) throw new Error('请求失败或无数据');
|
|
||||||
let result = res.data['data'];
|
|
||||||
if (!result) throw new Error('data 字段不存在');
|
|
||||||
return {
|
|
||||||
author: result.author,
|
|
||||||
book_name: result.book_name,
|
|
||||||
original_book_name: result.original_book_name,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async down(book_id, msg_id) {
|
|
||||||
try {
|
|
||||||
let url = `${this.apiurl}/api/down?book_id=${book_id}&source=fanqie&type=txt&user_id=${msg_id}`;
|
|
||||||
// 发送get请求
|
|
||||||
await axios.get(url);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Fanqie;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import pinyin from 'pinyin-pro';
|
|
||||||
|
|
||||||
class PinyinUtils {
|
|
||||||
/**
|
|
||||||
* 将中文转化为拼音
|
|
||||||
* @param text 文本
|
|
||||||
* @param toneType none
|
|
||||||
* @returns {*|string}
|
|
||||||
*/
|
|
||||||
static toPinyin(text, toneType = 'none') {
|
|
||||||
try {
|
|
||||||
return pinyin.pinyin(text, {
|
|
||||||
toneType,
|
|
||||||
type: 'string',
|
|
||||||
nonZh: 'consecutive'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[crystelf-ai] 拼音转换失败: ${error.message}`);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查文本是否包含拼音关键词
|
|
||||||
* @param text
|
|
||||||
* @param pinyinKeywords
|
|
||||||
* @returns {{keyword: *, matched: boolean, type: string}|null}
|
|
||||||
*/
|
|
||||||
static matchPinyin(text, pinyinKeywords) {
|
|
||||||
if (!text || !pinyinKeywords || pinyinKeywords.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const textPinyin = this.toPinyin(text.toLowerCase());
|
|
||||||
for (const keyword of pinyinKeywords) {
|
|
||||||
if (textPinyin.includes(keyword.toLowerCase())) {
|
|
||||||
return {
|
|
||||||
keyword,
|
|
||||||
matched: true,
|
|
||||||
type: 'pinyin'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查文本是否包含关键词
|
|
||||||
* @param text 文本
|
|
||||||
* @param chineseKeywords 中文关键词数组
|
|
||||||
* @param pinyinKeywords 拼音关键词数组
|
|
||||||
* @returns {{keyword: *, matched: boolean, type: string}|null|{keyword: *, matched: boolean, type: string}}
|
|
||||||
*/
|
|
||||||
static matchKeywords(text, chineseKeywords = [], pinyinKeywords = []) {
|
|
||||||
if (!text) return null;
|
|
||||||
const lowerText = text.toLowerCase();
|
|
||||||
for (const keyword of chineseKeywords) {
|
|
||||||
if (lowerText.includes(keyword.toLowerCase())) {
|
|
||||||
return {
|
|
||||||
keyword,
|
|
||||||
matched: true,
|
|
||||||
type: 'chinese'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pinyinKeywords.length > 0) {
|
|
||||||
return this.matchPinyin(text, pinyinKeywords);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PinyinUtils;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user