mirror of
https://github.com/jiangrui1994/CloudSaver.git
synced 2026-01-10 15:18:46 +08:00
Initial commit for open-source version
This commit is contained in:
1694
backend/package-lock.json
generated
Normal file
1694
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
backend/package.json
Normal file
31
backend/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "cloud-saver-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nodemon --exec ts-node src/app.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.7",
|
||||
"cheerio": "^1.0.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.3",
|
||||
"rss-parser": "^3.13.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"tunnel": "^0.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/tunnel": "^0.0.7",
|
||||
"nodemon": "^3.1.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
||||
30
backend/src/app.ts
Normal file
30
backend/src/app.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import routes from "./routes/api";
|
||||
import { errorHandler } from "./middleware/errorHandler";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "Cookie"],
|
||||
})
|
||||
);
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(express.json());
|
||||
app.use("/", routes);
|
||||
|
||||
// 错误处理
|
||||
app.use(errorHandler);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
92
backend/src/config/index.ts
Normal file
92
backend/src/config/index.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
// 加载.env文件
|
||||
dotenv.config();
|
||||
|
||||
interface Channel {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface CloudPatterns {
|
||||
baiduPan: RegExp;
|
||||
tianyi: RegExp;
|
||||
weiyun: RegExp;
|
||||
aliyun: RegExp;
|
||||
pan115: RegExp;
|
||||
quark: RegExp;
|
||||
}
|
||||
|
||||
interface Cloud115Config {
|
||||
userId: string;
|
||||
cookie: string;
|
||||
}
|
||||
interface QuarkConfig {
|
||||
userId: string;
|
||||
cookie: string;
|
||||
}
|
||||
interface HttpProxyConfig {
|
||||
host: string;
|
||||
port: string;
|
||||
}
|
||||
interface Config {
|
||||
rss: {
|
||||
baseUrl: string;
|
||||
channels: Channel[];
|
||||
};
|
||||
telegram: {
|
||||
baseUrl: string;
|
||||
};
|
||||
httpProxy: HttpProxyConfig;
|
||||
cloudPatterns: CloudPatterns;
|
||||
cloud115: Cloud115Config;
|
||||
quark: QuarkConfig;
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
rss: {
|
||||
baseUrl: process.env.RSS_BASE_URL || "https://rsshub.rssforever.com/telegram/channel",
|
||||
channels: [
|
||||
{
|
||||
id: "guaguale115",
|
||||
name: "115网盘资源分享",
|
||||
},
|
||||
{
|
||||
id: "hao115",
|
||||
name: "115网盘资源分享频道",
|
||||
},
|
||||
{
|
||||
id: "yunpanshare",
|
||||
name: "网盘资源收藏(夸克)",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
telegram: {
|
||||
baseUrl: process.env.TELEGRAM_BASE_URL || "https://t.me/s",
|
||||
},
|
||||
|
||||
httpProxy: {
|
||||
host: process.env.HTTP_PROXY_HOST || "127.0.0.1",
|
||||
port: process.env.HTTP_PROXY_PORT || "7890",
|
||||
},
|
||||
|
||||
cloudPatterns: {
|
||||
baiduPan: /https?:\/\/(?:pan|yun)\.baidu\.com\/[^\s<>"]+/g,
|
||||
tianyi: /https?:\/\/cloud\.189\.cn\/[^\s<>"]+/g,
|
||||
weiyun: /https?:\/\/share\.weiyun\.com\/[^\s<>"]+/g,
|
||||
aliyun: /https?:\/\/\w+\.aliyundrive\.com\/[^\s<>"]+/g,
|
||||
// pan115有两个域名 115.com 和 anxia.com
|
||||
pan115: /https?:\/\/(?:115|anxia)\.com\/s\/[^\s<>"]+/g,
|
||||
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
|
||||
},
|
||||
|
||||
cloud115: {
|
||||
userId: "",
|
||||
cookie: process.env.CLOUD115_COOKIE || "",
|
||||
},
|
||||
quark: {
|
||||
userId: process.env.QUARK_USER_ID || "",
|
||||
cookie: process.env.QUARK_COOKIE || "",
|
||||
},
|
||||
};
|
||||
46
backend/src/controllers/cloud115.ts
Normal file
46
backend/src/controllers/cloud115.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { Cloud115Service } from "../services/Cloud115Service";
|
||||
import { config } from "../config";
|
||||
import handleError from "../utils/handleError";
|
||||
import { handleResponse } from "../utils/responseHandler";
|
||||
|
||||
const { cookie } = config.cloud115;
|
||||
|
||||
const cloud115 = new Cloud115Service(cookie);
|
||||
|
||||
export const cloud115Controller = {
|
||||
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { shareCode, receiveCode } = req.query;
|
||||
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "获取分享信息失败", next);
|
||||
}
|
||||
},
|
||||
|
||||
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { parentCid } = req.query;
|
||||
const result = await cloud115.getFolderList(parentCid as string);
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "获取目录列表失败", next);
|
||||
}
|
||||
},
|
||||
|
||||
async saveFile(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { shareCode, receiveCode, fileId, folderId } = req.body;
|
||||
const result = await cloud115.saveSharedFile({
|
||||
shareCode,
|
||||
receiveCode,
|
||||
fileId,
|
||||
cid: folderId,
|
||||
});
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "保存文件失败", next);
|
||||
}
|
||||
},
|
||||
};
|
||||
40
backend/src/controllers/quark.ts
Normal file
40
backend/src/controllers/quark.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { QuarkService } from "../services/QuarkService";
|
||||
import { config } from "../config";
|
||||
import { handleResponse } from "../utils/responseHandler";
|
||||
import handleError from "../utils/handleError";
|
||||
|
||||
const { cookie } = config.quark;
|
||||
|
||||
const quark = new QuarkService(cookie);
|
||||
|
||||
export const quarkController = {
|
||||
async getShareInfo(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { pwdId, passcode } = req.query;
|
||||
const result = await quark.getShareInfo(pwdId as string, passcode as string);
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "获取分享信息失败", next);
|
||||
}
|
||||
},
|
||||
|
||||
async getFolderList(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { parentCid } = req.query;
|
||||
const result = await quark.getFolderList(parentCid as string);
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "获取目录列表失败", next);
|
||||
}
|
||||
},
|
||||
|
||||
async saveFile(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const result = await quark.saveSharedFile(req.body);
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "保存文件失败", next);
|
||||
}
|
||||
},
|
||||
};
|
||||
32
backend/src/controllers/resource.ts
Normal file
32
backend/src/controllers/resource.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { RSSSearcher } from "../services/RSSSearcher";
|
||||
import { Searcher } from "../services/Searcher";
|
||||
import { handleResponse } from "../utils/responseHandler";
|
||||
import handleError from "../utils/handleError";
|
||||
|
||||
export const resourceController = {
|
||||
async rssSearch(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { keyword } = req.query;
|
||||
const searcher = new RSSSearcher();
|
||||
const result = await searcher.searchAll(keyword as string);
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "获取资源发生未知错误", next);
|
||||
}
|
||||
},
|
||||
async search(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { keyword, channelId = "", lastMessageId = "" } = req.query; // Remove `: string` from here
|
||||
const searcher = new Searcher();
|
||||
const result = await searcher.searchAll(
|
||||
keyword as string,
|
||||
channelId as string,
|
||||
lastMessageId as string
|
||||
);
|
||||
handleResponse(res, result, true);
|
||||
} catch (error) {
|
||||
handleError(res, error, "获取资源发生未知错误", next);
|
||||
}
|
||||
},
|
||||
};
|
||||
9
backend/src/middleware/errorHandler.ts
Normal file
9
backend/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
console.error(err);
|
||||
res.status(err.status || 500).json({
|
||||
success: false,
|
||||
error: err.message || "服务器内部错误",
|
||||
});
|
||||
};
|
||||
14
backend/src/middleware/validateRequest.ts
Normal file
14
backend/src/middleware/validateRequest.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const validateRequest = (requiredParams: string[]) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const missingParams = requiredParams.filter((param) => !req.query[param] && !req.body[param]);
|
||||
if (missingParams.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `缺少必要的参数: ${missingParams.join(", ")}`,
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
22
backend/src/routes/api.ts
Normal file
22
backend/src/routes/api.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import express from "express";
|
||||
import { cloud115Controller } from "../controllers/cloud115";
|
||||
import { quarkController } from "../controllers/quark";
|
||||
import { resourceController } from "../controllers/resource";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// 资源搜索
|
||||
router.get("/search", resourceController.search);
|
||||
router.get("/rssSearch", resourceController.rssSearch);
|
||||
|
||||
// 115网盘相关
|
||||
router.get("/cloud115/share-info", cloud115Controller.getShareInfo);
|
||||
router.get("/cloud115/folders", cloud115Controller.getFolderList);
|
||||
router.post("/cloud115/save", cloud115Controller.saveFile);
|
||||
|
||||
// 夸克网盘相关
|
||||
router.get("/quark/share-info", quarkController.getShareInfo);
|
||||
router.get("/quark/folders", quarkController.getFolderList);
|
||||
router.post("/quark/save", quarkController.saveFile);
|
||||
|
||||
export default router;
|
||||
148
backend/src/services/Cloud115Service.ts
Normal file
148
backend/src/services/Cloud115Service.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { AxiosHeaders, AxiosInstance } from "axios"; // 导入 AxiosHeaders
|
||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { config } from "../config/index";
|
||||
import { ShareInfoResponse } from "../types/cloud115";
|
||||
|
||||
export class Cloud115Service {
|
||||
private api: AxiosInstance;
|
||||
|
||||
constructor(cookie: string) {
|
||||
if (!cookie) {
|
||||
throw new Error("115网盘需要提供cookie进行身份验证");
|
||||
}
|
||||
|
||||
this.api = createAxiosInstance(
|
||||
"https://webapi.115.com",
|
||||
AxiosHeaders.from({
|
||||
Host: "webapi.115.com",
|
||||
Connection: "keep-alive",
|
||||
xweb_xhr: "1",
|
||||
Origin: "",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 MicroMessenger/6.8.0(0x16080000) NetType/WIFI MiniProgramEnv/Mac MacWechat/WMPF MacWechat/3.8.9(0x13080910) XWEB/1227",
|
||||
Accept: "*/*",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
cookie: cookie,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getShareInfo(shareCode: string, receiveCode = ""): Promise<ShareInfoResponse> {
|
||||
try {
|
||||
const response = await this.api.get("/share/snap", {
|
||||
params: {
|
||||
share_code: shareCode,
|
||||
receive_code: receiveCode,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
cid: "",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data?.state && response.data.data?.list?.length > 0) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
list: response.data.data.list.map((item: any) => ({
|
||||
fileId: item.cid,
|
||||
fileName: item.n,
|
||||
fileSize: item.s,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "未找到文件信息",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getFolderList(parentCid = "0") {
|
||||
try {
|
||||
const response = await this.api.get("/files", {
|
||||
params: {
|
||||
aid: 1,
|
||||
cid: parentCid,
|
||||
o: "user_ptime",
|
||||
asc: 0,
|
||||
offset: 0,
|
||||
show_dir: 1,
|
||||
limit: 50,
|
||||
type: 0,
|
||||
format: "json",
|
||||
star: 0,
|
||||
suffix: "",
|
||||
natsort: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data?.state) {
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data
|
||||
.filter((item: any) => item.cid)
|
||||
.map((folder: any) => ({
|
||||
cid: folder.cid,
|
||||
name: folder.n,
|
||||
path: response.data.path,
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
Logger.error("获取目录列表失败:", response.data.error);
|
||||
return {
|
||||
success: false,
|
||||
error: "获取115pan目录列表失败:" + response.data.error,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("获取目录列表失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "获取115pan目录列表失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async saveSharedFile(params: {
|
||||
cid: string;
|
||||
shareCode: string;
|
||||
receiveCode: string;
|
||||
fileId: string;
|
||||
}) {
|
||||
try {
|
||||
const param = new URLSearchParams({
|
||||
cid: params.cid,
|
||||
user_id: config.cloud115.userId,
|
||||
share_code: params.shareCode,
|
||||
receive_code: params.receiveCode,
|
||||
file_id: params.fileId,
|
||||
});
|
||||
const response = await this.api.post("/share/receive", param.toString());
|
||||
|
||||
return {
|
||||
success: response.data.state,
|
||||
error: response.data.error,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
178
backend/src/services/QuarkService.ts
Normal file
178
backend/src/services/QuarkService.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||
|
||||
export class QuarkService {
|
||||
private api: AxiosInstance;
|
||||
|
||||
constructor(cookie: string) {
|
||||
if (!cookie) {
|
||||
throw new Error("115网盘需要提供cookie进行身份验证");
|
||||
}
|
||||
|
||||
this.api = createAxiosInstance(
|
||||
"https://drive-h.quark.cn",
|
||||
AxiosHeaders.from({
|
||||
cookie: cookie,
|
||||
accept: "application/json, text/plain, */*",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||
"content-type": "application/json",
|
||||
priority: "u=1, i",
|
||||
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-site",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getShareInfo(pwdId: string, passcode = "") {
|
||||
try {
|
||||
const response = await this.api.post(
|
||||
`/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc&uc_param_str=&__dt=994&__t=${Date.now()}`,
|
||||
{
|
||||
pwd_id: pwdId,
|
||||
passcode: "",
|
||||
}
|
||||
);
|
||||
if (response.data?.status === 200 && response.data.data) {
|
||||
const fileInfo = response.data.data;
|
||||
if (fileInfo.stoken) {
|
||||
let res = await this.getShareList(pwdId, fileInfo.stoken);
|
||||
return {
|
||||
success: true,
|
||||
data: res,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: "未找到文件信息",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getShareList(pwdId: string, stoken: string) {
|
||||
try {
|
||||
const response = await this.api.get("/1/clouddrive/share/sharepage/detail", {
|
||||
params: {
|
||||
pr: "ucpro",
|
||||
fr: "pc",
|
||||
uc_param_str: "",
|
||||
pwd_id: pwdId,
|
||||
stoken: stoken,
|
||||
pdir_fid: "0",
|
||||
force: "0",
|
||||
_page: "1",
|
||||
_size: "50",
|
||||
_fetch_banner: "1",
|
||||
_fetch_share: "1",
|
||||
_fetch_total: "1",
|
||||
_sort: "file_type:asc,updated_at:desc",
|
||||
__dt: "1589",
|
||||
__t: Date.now(),
|
||||
},
|
||||
});
|
||||
if (response.data?.data) {
|
||||
const list = response.data.data.list
|
||||
.filter((item: any) => item.fid)
|
||||
.map((folder: any) => ({
|
||||
fileId: folder.fid,
|
||||
fileName: folder.file_name,
|
||||
fileIdToken: folder.share_fid_token,
|
||||
}));
|
||||
return {
|
||||
list,
|
||||
pwdId,
|
||||
stoken: stoken,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
list: [],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("获取目录列表失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getFolderList(parentCid = "0") {
|
||||
try {
|
||||
const response = await this.api.get("/1/clouddrive/file/sort", {
|
||||
params: {
|
||||
pr: "ucpro",
|
||||
fr: "pc",
|
||||
uc_param_str: "",
|
||||
pdir_fid: parentCid,
|
||||
_page: "1",
|
||||
_size: "100",
|
||||
_fetch_total: "false",
|
||||
_fetch_sub_dirs: "1",
|
||||
_sort: "",
|
||||
__dt: "2093126",
|
||||
__t: Date.now(),
|
||||
},
|
||||
});
|
||||
if (response.data?.data && response.data.data.list.length) {
|
||||
return {
|
||||
success: true,
|
||||
data: response.data.data.list
|
||||
.filter((item: any) => item.fid)
|
||||
.map((folder: any) => ({
|
||||
cid: folder.fid,
|
||||
name: folder.file_name,
|
||||
path: [],
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
Logger.error("获取目录列表失败:", response.data.error);
|
||||
return {
|
||||
success: false,
|
||||
error: "获取夸克目录列表失败:" + response.data.error,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("获取目录列表失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "获取夸克目录列表失败",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async saveSharedFile(params: {
|
||||
fid_list: string[];
|
||||
fid_token_list: string[];
|
||||
to_pdir_fid: string;
|
||||
pwd_id: string;
|
||||
stoken: string;
|
||||
pdir_fid: string;
|
||||
scene: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await this.api.post(
|
||||
`/1/clouddrive/share/sharepage/save?pr=ucpro&fr=pc&uc_param_str=&__dt=208097&__t=${Date.now()}`,
|
||||
params
|
||||
);
|
||||
|
||||
return {
|
||||
success: response.data.code === 0,
|
||||
error: response.data.message,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "未知错误",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
112
backend/src/services/RSSSearcher.ts
Normal file
112
backend/src/services/RSSSearcher.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import RSSParser from "rss-parser";
|
||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
||||
import { config } from "../config";
|
||||
import { Logger } from "../utils/logger";
|
||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||
|
||||
interface RSSItem {
|
||||
title?: string;
|
||||
link?: string;
|
||||
pubDate?: string;
|
||||
content?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
cloudLinks?: string[];
|
||||
}
|
||||
|
||||
export class RSSSearcher {
|
||||
private parser: RSSParser;
|
||||
private axiosInstance: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.parser = new RSSParser({
|
||||
customFields: {
|
||||
item: [
|
||||
["content:encoded", "content"],
|
||||
["description", "description"],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
this.axiosInstance = createAxiosInstance(
|
||||
config.rss.baseUrl,
|
||||
AxiosHeaders.from({
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
Accept: "application/xml,application/xhtml+xml,text/html,application/rss+xml",
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
||||
const links: string[] = [];
|
||||
let cloudType = "";
|
||||
Object.values(config.cloudPatterns).forEach((pattern, index) => {
|
||||
const matches = text.match(pattern);
|
||||
if (matches) {
|
||||
links.push(...matches);
|
||||
cloudType = Object.keys(config.cloudPatterns)[index];
|
||||
}
|
||||
});
|
||||
return {
|
||||
links: [...new Set(links)],
|
||||
cloudType,
|
||||
};
|
||||
}
|
||||
|
||||
async searchAll(keyword: string) {
|
||||
const allResults = [];
|
||||
|
||||
for (let i = 0; i < config.rss.channels.length; i++) {
|
||||
const channel = config.rss.channels[i];
|
||||
try {
|
||||
const rssUrl = `${config.rss.baseUrl}/${
|
||||
channel.id
|
||||
}${keyword ? `/searchQuery=${encodeURIComponent(keyword)}` : ""}`;
|
||||
|
||||
const results = await this.searchInRSSFeed(rssUrl);
|
||||
if (results.items.length > 0) {
|
||||
const channelResults = results.items
|
||||
.filter((item: RSSItem) => item.cloudLinks && item.cloudLinks.length > 0)
|
||||
.map((item: RSSItem) => ({
|
||||
...item,
|
||||
channel: channel.name + "(" + channel.id + ")",
|
||||
}));
|
||||
|
||||
allResults.push(...channelResults);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
async searchInRSSFeed(rssUrl: string) {
|
||||
try {
|
||||
const response = await this.axiosInstance.get(rssUrl);
|
||||
const feed = await this.parser.parseString(response.data);
|
||||
|
||||
return {
|
||||
items: feed.items.map((item: RSSItem) => {
|
||||
const linkInfo = this.extractCloudLinks(item.content || item.description || "");
|
||||
return {
|
||||
title: item.title || "",
|
||||
link: item.link || "",
|
||||
pubDate: item.pubDate || "",
|
||||
image: item.image || "",
|
||||
cloudLinks: linkInfo.links,
|
||||
cloudType: linkInfo.cloudType,
|
||||
};
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.error(`RSS源解析错误: ${rssUrl}`, error);
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
159
backend/src/services/Searcher.ts
Normal file
159
backend/src/services/Searcher.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { AxiosInstance, AxiosHeaders } from "axios";
|
||||
import { createAxiosInstance } from "../utils/axiosInstance";
|
||||
import * as cheerio from "cheerio";
|
||||
import { config } from "../config";
|
||||
import { Logger } from "../utils/logger";
|
||||
|
||||
interface sourceItem {
|
||||
messageId?: string;
|
||||
title?: string;
|
||||
link?: string;
|
||||
pubDate?: string;
|
||||
content?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
cloudLinks?: string[];
|
||||
cloudType?: string;
|
||||
}
|
||||
|
||||
export class Searcher {
|
||||
private axiosInstance: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.axiosInstance = createAxiosInstance(
|
||||
config.telegram.baseUrl,
|
||||
AxiosHeaders.from({
|
||||
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,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||
"cache-control": "max-age=0",
|
||||
priority: "u=0, i",
|
||||
"sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"macOS"',
|
||||
"sec-fetch-dest": "document",
|
||||
"sec-fetch-mode": "navigate",
|
||||
"sec-fetch-site": "none",
|
||||
"sec-fetch-user": "?1",
|
||||
"upgrade-insecure-requests": "1",
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
|
||||
const links: string[] = [];
|
||||
let cloudType = "";
|
||||
Object.values(config.cloudPatterns).forEach((pattern, index) => {
|
||||
const matches = text.match(pattern);
|
||||
if (matches) {
|
||||
links.push(...matches);
|
||||
cloudType = Object.keys(config.cloudPatterns)[index];
|
||||
}
|
||||
});
|
||||
return {
|
||||
links: [...new Set(links)],
|
||||
cloudType,
|
||||
};
|
||||
}
|
||||
|
||||
async searchAll(keyword: string, channelId?: string, messageId?: string) {
|
||||
const allResults = [];
|
||||
const totalChannels = config.rss.channels.length;
|
||||
|
||||
const channelList = channelId
|
||||
? config.rss.channels.filter((channel) => channel.id === channelId)
|
||||
: config.rss.channels;
|
||||
|
||||
for (let i = 0; i < channelList.length; i++) {
|
||||
const channel = channelList[i];
|
||||
try {
|
||||
const messageIdparams = messageId ? `before=${messageId}` : "";
|
||||
const url = `/${channel.id}${keyword ? `?q=${encodeURIComponent(keyword)}&${messageIdparams}` : `?${messageIdparams}`}`;
|
||||
console.log(`Searching in channel ${channel.name} with URL: ${url}`);
|
||||
const results = await this.searchInWeb(url, channel.id);
|
||||
console.log(`Found ${results.items.length} items in channel ${channel.name}`);
|
||||
if (results.items.length > 0) {
|
||||
const channelResults = results.items
|
||||
.filter((item: sourceItem) => item.cloudLinks && item.cloudLinks.length > 0)
|
||||
.map((item: sourceItem) => ({
|
||||
...item,
|
||||
channel: channel.name,
|
||||
channelId: channel.id,
|
||||
}));
|
||||
|
||||
allResults.push(...channelResults);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
async searchInWeb(url: string, channelId: string) {
|
||||
try {
|
||||
const response = await this.axiosInstance.get(url);
|
||||
const html = response.data;
|
||||
const $ = cheerio.load(html);
|
||||
const items: sourceItem[] = [];
|
||||
// 遍历每个消息容器
|
||||
$(".tgme_widget_message_wrap").each((_, element) => {
|
||||
const messageEl = $(element);
|
||||
|
||||
// 通过 data-post 属性来获取消息的链接 去除channelId 获得消息id
|
||||
const messageId = messageEl
|
||||
.find(".tgme_widget_message")
|
||||
.data("post")
|
||||
?.toString()
|
||||
.split("/")[1];
|
||||
|
||||
// 提取标题 (消息截取100长度)
|
||||
const title = messageEl.find(".js-message_text").text().trim().substring(0, 50) + "...";
|
||||
|
||||
// 提取链接 (消息中的链接)
|
||||
// const link = messageEl.find('.tgme_widget_message').data('post');
|
||||
|
||||
// 提取发布时间
|
||||
const pubDate = messageEl.find("time").attr("datetime");
|
||||
|
||||
// 提取内容 (完整消息文本)
|
||||
const content = messageEl.find(".js-message_text").text();
|
||||
|
||||
// 提取描述 (消息文本中"描述:"后的内容)
|
||||
const description = content.split("描述:")[1]?.split("\n")[0]?.trim();
|
||||
|
||||
// 提取图片
|
||||
const image = messageEl
|
||||
.find(".tgme_widget_message_photo_wrap")
|
||||
.attr("style")
|
||||
?.match(/url\('(.+?)'\)/)?.[1];
|
||||
|
||||
// 提取云盘链接
|
||||
const links = messageEl
|
||||
.find(".tgme_widget_message_text a")
|
||||
.map((_, el) => $(el).attr("href"))
|
||||
.get();
|
||||
const cloudInfo = this.extractCloudLinks(links.join(" "));
|
||||
// 添加到数组第一位
|
||||
items.unshift({
|
||||
messageId,
|
||||
title,
|
||||
pubDate,
|
||||
content,
|
||||
description,
|
||||
image,
|
||||
cloudLinks: cloudInfo.links,
|
||||
cloudType: cloudInfo.cloudType,
|
||||
});
|
||||
});
|
||||
return { items };
|
||||
} catch (error) {
|
||||
Logger.error(`RSS源解析错误: ${url}`, error);
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
13
backend/src/types/cloud115.ts
Normal file
13
backend/src/types/cloud115.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface ShareInfo {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
}
|
||||
|
||||
export interface ShareInfoResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
list: ShareInfo[];
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
28
backend/src/utils/axiosInstance.ts
Normal file
28
backend/src/utils/axiosInstance.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
|
||||
import tunnel from "tunnel";
|
||||
import { config } from "../config";
|
||||
|
||||
export function createAxiosInstance(
|
||||
baseURL: string,
|
||||
headers: AxiosRequestHeaders,
|
||||
useProxy: boolean = false
|
||||
): AxiosInstance {
|
||||
let agent;
|
||||
|
||||
if (useProxy) {
|
||||
agent = tunnel.httpsOverHttp({
|
||||
proxy: {
|
||||
host: config.httpProxy.host,
|
||||
port: Number(config.httpProxy.port),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return axios.create({
|
||||
baseURL,
|
||||
timeout: 30000,
|
||||
headers,
|
||||
httpsAgent: useProxy ? agent : undefined,
|
||||
withCredentials: true,
|
||||
});
|
||||
}
|
||||
11
backend/src/utils/handleError.ts
Normal file
11
backend/src/utils/handleError.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Response, NextFunction } from "express";
|
||||
import { Logger } from "../utils/logger";
|
||||
export default function handleError(
|
||||
res: Response,
|
||||
error: any,
|
||||
message: string,
|
||||
next: NextFunction
|
||||
) {
|
||||
Logger.error(message, error);
|
||||
next(error || { success: false, message });
|
||||
}
|
||||
38
backend/src/utils/logger.ts
Normal file
38
backend/src/utils/logger.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
type LogLevel = "info" | "success" | "warn" | "error";
|
||||
|
||||
export const Logger = {
|
||||
info(...args: any[]) {
|
||||
this.log("info", ...args);
|
||||
},
|
||||
|
||||
success(...args: any[]) {
|
||||
this.log("success", ...args);
|
||||
},
|
||||
|
||||
warn(...args: any[]) {
|
||||
this.log("warn", ...args);
|
||||
},
|
||||
|
||||
error(...args: any[]) {
|
||||
this.log("error", ...args);
|
||||
},
|
||||
|
||||
log(level: LogLevel, ...args: any[]) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
||||
|
||||
switch (level) {
|
||||
case "success":
|
||||
console.log(prefix, ...args);
|
||||
break;
|
||||
case "warn":
|
||||
console.warn(prefix, ...args);
|
||||
break;
|
||||
case "error":
|
||||
console.error(prefix, ...args);
|
||||
break;
|
||||
default:
|
||||
console.log(prefix, ...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
5
backend/src/utils/responseHandler.ts
Normal file
5
backend/src/utils/responseHandler.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Response } from "express";
|
||||
|
||||
export const handleResponse = (res: Response, data: any, success: boolean) => {
|
||||
res.json({ success, data });
|
||||
};
|
||||
21
backend/tsconfig.json
Normal file
21
backend/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user