feat:版本迭代

This commit is contained in:
jiangrui
2025-02-20 12:00:19 +08:00
parent fd110590af
commit 510fdc48f6
86 changed files with 5045 additions and 1161 deletions

View File

@@ -9,19 +9,25 @@
},
"dependencies": {
"axios": "^1.6.7",
"bcrypt": "^5.1.1",
"cheerio": "^1.0.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"jsonwebtoken": "^9.0.2",
"rss-parser": "^3.13.0",
"sequelize": "^6.37.5",
"socket.io": "^4.8.1",
"sqlite3": "^5.1.7",
"tunnel": "^0.0.6"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.7",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^20.11.25",
"@types/tunnel": "^0.0.7",
"nodemon": "^3.1.0",

View File

@@ -1,8 +1,14 @@
// filepath: /d:/code/CloudDiskDown/backend/src/app.ts
import "./types/express";
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import routes from "./routes/api";
import { errorHandler } from "./middleware/errorHandler";
import sequelize from "./config/database";
import { authMiddleware } from "./middleware/auth";
import GlobalSetting from "./models/GlobalSetting";
import Searcher from "./services/Searcher";
const app = express();
@@ -17,14 +23,54 @@ app.use(
app.use(cookieParser());
app.use(express.json());
// 应用 token 验证中间件,排除登录和注册接口
app.use((req, res, next) => {
if (
req.path === "/user/login" ||
req.path === "/user/register" ||
req.path.includes("tele-images")
) {
return next();
}
authMiddleware(req, res, next);
});
app.use("/", routes);
const initializeGlobalSettings = async () => {
const settings = await GlobalSetting.findOne();
if (!settings) {
await GlobalSetting.create({
httpProxyHost: "127.0.0.1",
httpProxyPort: 7890,
isProxyEnabled: true,
CommonUserCode: 9527,
AdminUserCode: 230713,
});
console.log("Global settings initialized with default values.");
}
await Searcher.updateAxiosInstance();
};
// 错误处理
app.use(errorHandler);
const PORT = process.env.PORT || 8009;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
// 在同步前禁用外键约束,同步后重新启用
sequelize
.query("PRAGMA foreign_keys = OFF") // 禁用外键
.then(() => sequelize.sync({ alter: true }))
.then(() => sequelize.query("PRAGMA foreign_keys = ON")) // 重新启用外键
.then(() => {
app.listen(PORT, async () => {
await initializeGlobalSettings();
console.log(`Server is running on port ${PORT}`);
});
})
.catch((error) => {
console.error("Database sync failed:", error);
});
export default app;

View File

@@ -0,0 +1,9 @@
// backend/src/config/database.ts
import { Sequelize } from "sequelize";
const sequelize = new Sequelize({
dialect: "sqlite",
storage: "./data/database.sqlite",
});
export default sequelize;

View File

@@ -30,6 +30,8 @@ interface HttpProxyConfig {
port: string;
}
interface Config {
jwtSecret: string;
registerCode: string;
rss: {
baseUrl: string;
channels: Channel[];
@@ -44,6 +46,7 @@ interface Config {
}
export const config: Config = {
jwtSecret: process.env.JWT_SECRET || "uV7Y$k92#LkF^q1b!",
rss: {
baseUrl: process.env.RSS_BASE_URL || "https://rsshub.rssforever.com/telegram/channel",
channels: [
@@ -61,14 +64,15 @@ export const config: Config = {
},
],
},
registerCode: process.env.REGISTER_CODE || "9527",
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",
host: process.env.HTTP_PROXY_HOST || "",
port: process.env.HTTP_PROXY_PORT || "",
},
cloudPatterns: {
@@ -77,7 +81,7 @@ export const config: Config = {
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,
pan115: /https?:\/\/(?:115|anxia|115cdn)\.com\/s\/[^\s<>"]+/g,
quark: /https?:\/\/pan\.quark\.cn\/[^\s<>"]+/g,
},

View File

@@ -1,46 +1,61 @@
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";
import { sendSuccess, sendError } from "../utils/response";
import UserSetting from "../models/UserSetting";
const { cookie } = config.cloud115;
const cloud115 = new Cloud115Service(cookie);
const cloud115 = new Cloud115Service();
const setCookie = async (req: Request) => {
const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({
where: { userId },
});
console.log(userSetting?.dataValues.cloud115Cookie);
if (userSetting && userSetting.dataValues.cloud115Cookie) {
cloud115.setCookie(userSetting.dataValues.cloud115Cookie);
} else {
throw new Error("请先设置115网盘cookie");
}
};
export const cloud115Controller = {
async getShareInfo(req: Request, res: Response, next: NextFunction) {
try {
const { shareCode, receiveCode } = req.query;
await setCookie(req);
const result = await cloud115.getShareInfo(shareCode as string, receiveCode as string);
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "获取分享信息失败", next);
console.log(error);
sendError(res, { message: (error as Error).message || "获取分享信息失败" });
}
},
async getFolderList(req: Request, res: Response, next: NextFunction) {
try {
const { parentCid } = req.query;
await setCookie(req);
const result = await cloud115.getFolderList(parentCid as string);
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "获取目录列表失败", next);
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
}
},
async saveFile(req: Request, res: Response, next: NextFunction) {
try {
const { shareCode, receiveCode, fileId, folderId } = req.body;
await setCookie(req);
const result = await cloud115.saveSharedFile({
shareCode,
receiveCode,
fileId,
cid: folderId,
});
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "保存文件失败", next);
sendError(res, { message: (error as Error).message || "保存文件失败" });
}
},
};
export const Cloud115ServiceInstance = cloud115;

View File

@@ -0,0 +1,22 @@
import { Request, Response, NextFunction } from "express";
import DoubanService from "../services/DoubanService";
import { sendSuccess, sendError } from "../utils/response";
const doubanService = new DoubanService();
export const doubanController = {
async getDoubanHotList(req: Request, res: Response, next: NextFunction) {
try {
const { type = "movie", tag = "热门", page_limit = "50", page_start = "0" } = req.query;
const result = await doubanService.getHotList({
type: type as string,
tag: tag as string,
page_limit: page_limit as string,
page_start: page_start as string,
});
sendSuccess(res, result);
} catch (error) {
sendError(res, { message: "获取热门列表失败" });
}
},
};

View File

@@ -1,40 +1,52 @@
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";
import { sendSuccess, sendError } from "../utils/response";
import UserSetting from "../models/UserSetting";
const { cookie } = config.quark;
const quark = new QuarkService();
const quark = new QuarkService(cookie);
const setCookie = async (req: Request) => {
const userId = req.user?.userId;
const userSetting = await UserSetting.findOne({
where: { userId },
});
if (userSetting && userSetting.dataValues.quarkCookie) {
quark.setCookie(userSetting.dataValues.quarkCookie);
} else {
throw new Error("请先设置夸克网盘cookie");
}
};
export const quarkController = {
async getShareInfo(req: Request, res: Response, next: NextFunction) {
try {
const { pwdId, passcode } = req.query;
await setCookie(req);
const result = await quark.getShareInfo(pwdId as string, passcode as string);
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "获取分享信息失败", next);
sendError(res, { message: "获取分享信息失败" });
}
},
async getFolderList(req: Request, res: Response, next: NextFunction) {
try {
const { parentCid } = req.query;
await setCookie(req);
const result = await quark.getFolderList(parentCid as string);
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "获取目录列表失败", next);
sendError(res, { message: (error as Error).message || "获取目录列表失败" });
}
},
async saveFile(req: Request, res: Response, next: NextFunction) {
try {
await setCookie(req);
const result = await quark.saveSharedFile(req.body);
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "保存文件失败", next);
sendError(res, { message: (error as Error).message || "保存文件失败" });
}
},
};

View File

@@ -1,8 +1,7 @@
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";
import Searcher from "../services/Searcher";
import { sendSuccess, sendError } from "../utils/response";
export const resourceController = {
async rssSearch(req: Request, res: Response, next: NextFunction) {
@@ -10,23 +9,26 @@ export const resourceController = {
const { keyword } = req.query;
const searcher = new RSSSearcher();
const result = await searcher.searchAll(keyword as string);
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "获取资源发生未知错误", next);
sendError(res, {
message: (error as Error).message || "RSS 搜索失败",
});
}
},
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(
const result = await Searcher.searchAll(
keyword as string,
channelId as string,
lastMessageId as string
);
handleResponse(res, result, true);
sendSuccess(res, result);
} catch (error) {
handleError(res, error, "获取资源发生未知错误", next);
sendError(res, {
message: (error as Error).message || "搜索资源失败",
});
}
},
};

View File

@@ -0,0 +1,57 @@
import { Request, Response, NextFunction } from "express";
import { sendSuccess, sendError } from "../utils/response";
import Searcher from "../services/Searcher";
import UserSetting from "../models/UserSetting";
import GlobalSetting from "../models/GlobalSetting";
export const settingController = {
async get(req: Request, res: Response) {
try {
const userId = req.user?.userId;
const role = req.user?.role;
if (userId !== null) {
let userSettings = await UserSetting.findOne({ where: { userId } });
if (!userSettings) {
userSettings = {
userId: userId,
cloud115Cookie: "",
quarkCookie: "",
} as UserSetting;
if (userSettings) {
await UserSetting.create(userSettings);
}
}
const globalSetting = await GlobalSetting.findOne();
sendSuccess(res, {
data: {
userSettings,
globalSetting: role === 1 ? globalSetting : null,
},
});
} else {
sendError(res, { message: "用户ID无效" });
}
} catch (error) {
console.log("获取设置失败:" + error);
sendError(res, { message: (error as Error).message || "获取设置失败" });
}
},
async save(req: Request, res: Response) {
try {
const userId = req.user?.userId;
const role = req.user?.role;
if (userId !== null) {
const { userSettings, globalSetting } = req.body;
await UserSetting.update(userSettings, { where: { userId } });
if (role === 1 && globalSetting) await GlobalSetting.update(globalSetting, { where: {} });
Searcher.updateAxiosInstance();
sendSuccess(res, {
message: "保存成功",
});
}
} catch (error) {
console.log("保存设置失败:" + error);
sendError(res, { message: (error as Error).message || "保存设置失败" });
}
},
};

View File

@@ -0,0 +1,57 @@
import axios, { AxiosInstance } from "axios";
import { Request, Response } from "express";
import tunnel from "tunnel";
import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting";
export class ImageControll {
private axiosInstance: AxiosInstance | null = null;
private isUpdate = false;
constructor() {
this.initializeAxiosInstance();
}
private async initializeAxiosInstance(isUpdate = false) {
let settings = null;
if (isUpdate) {
settings = await GlobalSetting.findOne();
this.isUpdate = isUpdate;
} else {
return;
}
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
this.axiosInstance = axios.create({
timeout: 3000,
httpsAgent: tunnel.httpsOverHttp({
proxy: {
host: globalSetting.httpProxyHost,
port: globalSetting.httpProxyPort,
headers: {
"Proxy-Authorization": "",
},
},
}),
withCredentials: true,
});
}
async getImages(req: Request, res: Response, url: string) {
try {
if (!this.isUpdate) await this.initializeAxiosInstance(true);
const response = await this.axiosInstance?.get(url, { responseType: "stream" });
res.set("Content-Type", response?.headers["content-type"]);
response?.data.pipe(res);
} catch (error) {
res.status(500).send("Image fetch error");
}
}
}
const iamgesInstance = new ImageControll();
export const imageControll = {
getImages: async (req: Request, res: Response) => {
const url = req.query.url as string;
iamgesInstance.getImages(req, res, url);
},
};

View File

@@ -0,0 +1,62 @@
import { Request, Response } from "express";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import GlobalSetting from "../models/GlobalSetting";
import User from "../models/User";
import { config } from "../config";
import { sendSuccess, sendError } from "../utils/response";
const isValidInput = (input: string): boolean => {
// 检查是否包含空格或汉字
const regex = /^[^\s\u4e00-\u9fa5]+$/;
return regex.test(input);
};
export const userController = {
async register(req: Request, res: Response) {
const { username, password, registerCode } = req.body;
const globalSetting = await GlobalSetting.findOne();
const registerCodeList = [
globalSetting?.dataValues.CommonUserCode,
globalSetting?.dataValues.AdminUserCode,
];
if (!registerCode || !registerCodeList.includes(Number(registerCode))) {
return sendError(res, { message: "注册码错误" });
}
// 验证输入
if (!isValidInput(username) || !isValidInput(password)) {
return sendError(res, { message: "用户名、密码或注册码不能包含空格或汉字" });
}
// 检查用户名是否已存在
const existingUser = await User.findOne({ where: { username } });
if (existingUser) {
return sendError(res, { message: "用户名已存在" });
}
const hashedPassword = await bcrypt.hash(password, 10);
try {
const role = registerCodeList.findIndex((x) => x === Number(registerCode));
const user = await User.create({ username, password: hashedPassword, role });
sendSuccess(res, {
data: user,
message: "用户注册成功",
});
} catch (error: any) {
sendError(res, { message: error.message || "用户注册失败" });
}
},
async login(req: Request, res: Response) {
const { username, password } = req.body;
const user = await User.findOne({ where: { username } });
if (!user || !(await bcrypt.compare(password, user.password))) {
return sendError(res, { message: "用户名或密码错误" });
}
const token = jwt.sign({ userId: user.userId, role: user.role }, config.jwtSecret, {
expiresIn: "6h",
});
sendSuccess(res, {
data: {
token,
},
});
},
};

View File

@@ -0,0 +1,43 @@
// filepath: /D:/code/CloudDiskDown/backend/src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import jwt, { JwtPayload } from "jsonwebtoken";
import User from "../models/User";
import { config } from "../config";
interface AuthenticatedRequest extends Request {
user?: {
userId: string;
role: number;
};
}
export const authMiddleware = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
if (req.path === "/user/login" || req.path === "/user/register") {
return next();
}
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ message: "未提供 token" });
}
try {
const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;
req.user = {
userId: decoded.userId,
role: decoded.role,
};
const user = await User.findOne({ where: { userId: decoded.userId } });
if (!user) {
return res.status(401).json({ message: "无效的 token" });
}
next();
} catch (error) {
res.status(401).json({ message: "无效的 token" });
}
};

View File

@@ -0,0 +1,67 @@
import { DataTypes, Model, Optional } from "sequelize";
import sequelize from "../config/database";
export interface GlobalSettingAttributes {
id: number;
httpProxyHost: string;
httpProxyPort: number;
isProxyEnabled: boolean;
CommonUserCode: number;
AdminUserCode: number;
}
interface GlobalSettingCreationAttributes extends Optional<GlobalSettingAttributes, "id"> {}
class GlobalSetting
extends Model<GlobalSettingAttributes, GlobalSettingCreationAttributes>
implements GlobalSettingAttributes
{
public id!: number;
public httpProxyHost!: string;
public httpProxyPort!: number;
public isProxyEnabled!: boolean;
public CommonUserCode!: number;
public AdminUserCode!: number;
}
GlobalSetting.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
httpProxyHost: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: "127.0.0.1",
},
httpProxyPort: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 7890,
},
isProxyEnabled: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
CommonUserCode: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 9527,
},
AdminUserCode: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 230713,
},
},
{
sequelize,
modelName: "GlobalSetting",
tableName: "global_settings",
}
);
export default GlobalSetting;

View File

@@ -0,0 +1,62 @@
import { DataTypes, Model, Optional } from "sequelize";
import sequelize from "../config/database";
interface UserAttributes {
id: number;
userId?: number;
username: string;
password: string;
role: number; // 修改为数字类型
}
interface UserCreationAttributes extends Optional<UserAttributes, "id"> {}
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
public id!: number;
public userId!: number;
public username!: string;
public password!: string;
public role!: number; // 实现数字类型的角色属性
}
User.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false, // 显式设置为不可为空
},
userId: {
type: DataTypes.UUID, // 对外暴露的不可预测ID
defaultValue: DataTypes.UUIDV4,
unique: true,
allowNull: false, // 显式设置为不可为空
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
role: {
type: DataTypes.INTEGER, // 修改为数字类型
allowNull: false,
defaultValue: 0, // 默认值为普通用户
},
},
{
sequelize,
modelName: "User",
tableName: "users",
}
);
// 角色映射
// 0: 普通用户
// 1: 管理员
export default User;

View File

@@ -0,0 +1,72 @@
import { DataTypes, Model, Optional } from "sequelize";
import sequelize from "../config/database";
import User from "./User";
interface UserSettingAttributes {
id: number;
userId: string;
cloud115UserId?: string;
cloud115Cookie: string;
quarkCookie: string;
}
interface UserSettingCreationAttributes extends Optional<UserSettingAttributes, "id"> {}
class UserSetting
extends Model<UserSettingAttributes, UserSettingCreationAttributes>
implements UserSettingAttributes
{
public id!: number;
public userId!: string;
public cloud115UserId?: string;
public cloud115Cookie!: string;
public quarkCookie!: string;
}
UserSetting.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
userId: {
type: DataTypes.UUID,
allowNull: false,
unique: true,
references: {
model: User,
key: "userId",
},
onDelete: "CASCADE",
},
cloud115UserId: {
type: DataTypes.STRING,
allowNull: true,
},
cloud115Cookie: {
type: DataTypes.STRING,
allowNull: true,
},
quarkCookie: {
type: DataTypes.STRING,
allowNull: true,
},
},
{
sequelize,
modelName: "UserSetting",
tableName: "user_settings",
}
);
User.hasOne(UserSetting, {
foreignKey: "userId",
as: "settings",
});
UserSetting.belongsTo(User, {
foreignKey: "userId",
as: "user",
});
export default UserSetting;

View File

@@ -2,9 +2,21 @@ import express from "express";
import { cloud115Controller } from "../controllers/cloud115";
import { quarkController } from "../controllers/quark";
import { resourceController } from "../controllers/resource";
import { doubanController } from "../controllers/douban";
import { imageControll } from "../controllers/teleImages";
import settingRoutes from "./setting";
import userRoutes from "./user";
const router = express.Router();
// 用户相关路由
router.use("/user", userRoutes);
router.use("/tele-images", imageControll.getImages);
// 设置相关路由
router.use("/setting", settingRoutes);
// 资源搜索
router.get("/search", resourceController.search);
router.get("/rssSearch", resourceController.rssSearch);
@@ -19,4 +31,7 @@ router.get("/quark/share-info", quarkController.getShareInfo);
router.get("/quark/folders", quarkController.getFolderList);
router.post("/quark/save", quarkController.saveFile);
// 获取豆瓣热门列表
router.get("/douban/hot", doubanController.getDoubanHotList);
export default router;

View File

@@ -0,0 +1,9 @@
import express from "express";
import { settingController } from "../controllers/setting";
const router = express.Router();
router.get("/get", settingController.get);
router.post("/save", settingController.save);
export default router;

View File

@@ -0,0 +1,10 @@
// backend/src/routes/user.ts
import express from "express";
import { userController } from "../controllers/user";
const router = express.Router();
router.post("/register", userController.register);
router.post("/login", userController.login);
export default router;

View File

@@ -6,12 +6,9 @@ import { ShareInfoResponse } from "../types/cloud115";
export class Cloud115Service {
private api: AxiosInstance;
private cookie: string = "";
constructor(cookie: string) {
if (!cookie) {
throw new Error("115网盘需要提供cookie进行身份验证");
}
constructor(cookie?: string) {
this.api = createAxiosInstance(
"https://webapi.115.com",
AxiosHeaders.from({
@@ -29,91 +26,77 @@ export class Cloud115Service {
Referer: "https://servicewechat.com/wx2c744c010a61b0fa/94/page-frame.html",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
cookie: cookie,
})
);
if (cookie) {
this.setCookie(cookie);
} else {
console.log("请注意:115网盘需要提供cookie进行身份验证");
}
this.api.interceptors.request.use((config) => {
config.headers.cookie = cookie || this.cookie;
return config;
});
}
public setCookie(cookie: string) {
this.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,
})),
},
};
}
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: false,
error: "未找到文件信息",
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
data: response.data.data.list.map((item: any) => ({
fileId: item.cid,
fileName: item.n,
fileSize: item.s,
})),
};
}
throw new Error("未找到文件信息");
}
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,
},
});
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);
if (response.data?.state) {
return {
success: false,
error: "获取115pan目录列表失败",
data: response.data.data
.filter((item: any) => item.cid && !!item.ns)
.map((folder: any) => ({
cid: folder.cid,
name: folder.n,
path: response.data.path,
})),
};
} else {
Logger.error("获取目录列表失败:", response.data.error);
throw new Error("获取115pan目录列表失败:" + response.data.error);
}
}
@@ -123,26 +106,23 @@ export class Cloud115Service {
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());
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());
Logger.info("保存文件:", response.data);
if (response.data.state) {
return {
success: response.data.state,
error: response.data.error,
data: response.data,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
message: response.data.error,
data: response.data.data,
};
} else {
Logger.error("保存文件失败:", response.data.error);
throw new Error("保存115pan文件失败:" + response.data.error);
}
}
}

View File

@@ -0,0 +1,52 @@
import { AxiosHeaders, AxiosInstance } from "axios";
import { createAxiosInstance } from "../utils/axiosInstance";
class DoubanService {
private baseUrl: string;
private api: AxiosInstance;
constructor() {
this.baseUrl = "https://movie.douban.com/j";
this.api = createAxiosInstance(
this.baseUrl,
AxiosHeaders.from({
accept: "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
priority: "u=1, i",
"sec-ch-ua": '"Not A(Brand";v="8", "Chromium";v="132", "Microsoft Edge";v="132"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-requested-with": "XMLHttpRequest",
cookie:
'll="118282"; bid=StA6AQFsAWQ; _pk_id.100001.4cf6=6448be57b1b5ca7e.1723172321.; _vwo_uuid_v2=DC15B8183560FF1E538FFE1D480723310|c08e2d213ecb5510005f90a6ff332121; __utmv=30149280.6282; _vwo_uuid_v2=DC15B8183560FF1E538FFE1D480723310|c08e2d213ecb5510005f90a6ff332121; __utmz=30149280.1731915179.21.6.utmcsr=search.douban.com|utmccn=(referral)|utmcmd=referral|utmcct=/movie/subject_search; __utmz=223695111.1731915179.21.6.utmcsr=search.douban.com|utmccn=(referral)|utmcmd=referral|utmcct=/movie/subject_search; douban-fav-remind=1; __utmc=30149280; __utmc=223695111; _pk_ref.100001.4cf6=%5B%22%22%2C%22%22%2C1739176523%2C%22https%3A%2F%2Fsearch.douban.com%2Fmovie%2Fsubject_search%3Fsearch_text%3D%E8%84%91%E6%B4%9E%E5%A4%A7%E5%BC%80%26cat%3D1002%22%5D; _pk_ses.100001.4cf6=1; ap_v=0,6.0; __utma=30149280.859303574.1723448979.1739167503.1739176523.42; __utmb=30149280.0.10.1739176523; __utma=223695111.1882744177.1723448979.1739167503.1739176523.42; __utmb=223695111.0.10.1739176523',
Referer: "https://movie.douban.com/",
"Referrer-Policy": "unsafe-url",
})
);
}
async getHotList(params: { type: string; tag: string; page_limit: string; page_start: string }) {
try {
const response = await this.api.get("/search_subjects", {
params: params,
});
if (response.data && response.data.subjects) {
return {
data: response.data.subjects,
};
} else {
return {
data: [],
};
}
} catch (error) {
console.error("Error fetching hot list:", error);
throw error;
}
}
}
export default DoubanService;

View File

@@ -4,16 +4,13 @@ import { createAxiosInstance } from "../utils/axiosInstance";
export class QuarkService {
private api: AxiosInstance;
private cookie: string = "";
constructor(cookie: string) {
if (!cookie) {
throw new Error("115网盘需要提供cookie进行身份验证");
}
constructor(cookie?: string) {
this.api = createAxiosInstance(
"https://drive-h.quark.cn",
AxiosHeaders.from({
cookie: cookie,
cookie: this.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",
@@ -26,125 +23,112 @@ export class QuarkService {
"sec-fetch-site": "same-site",
})
);
if (cookie) {
this.setCookie(cookie);
} else {
console.log("请注意:夸克网盘需要提供cookie进行身份验证");
}
this.api.interceptors.request.use((config) => {
config.headers.cookie = cookie || this.cookie;
return config;
});
}
public setCookie(cookie: string) {
this.cookie = cookie;
}
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,
};
}
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 {
data: res,
};
}
return {
success: false,
error: "未找到文件信息",
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
};
}
throw new Error("获取夸克分享信息失败");
}
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 [];
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: [],
};
}
}
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);
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) {
const data = response.data.data.list
.filter((item: any) => item.fid && item.file_type === 0)
.map((folder: any) => ({
cid: folder.fid,
name: folder.file_name,
path: [],
}));
return {
success: false,
error: "获取夸克目录列表失败",
data,
};
} else {
const message = "获取夸克目录列表失败:" + response.data.error;
Logger.error(message);
throw new Error(message);
}
}
@@ -164,15 +148,11 @@ export class QuarkService {
);
return {
success: response.data.code === 0,
error: response.data.message,
message: response.data.message,
data: response.data.data,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "未知错误",
};
throw new Error(error instanceof Error ? error.message : "未知错误");
}
}
}

View File

@@ -3,6 +3,7 @@ import { AxiosInstance, AxiosHeaders } from "axios";
import { config } from "../config";
import { Logger } from "../utils/logger";
import { createAxiosInstance } from "../utils/axiosInstance";
import { data } from "cheerio/dist/commonjs/api/attributes";
interface RSSItem {
title?: string;
@@ -81,7 +82,10 @@ export class RSSSearcher {
}
}
return allResults;
return {
data: allResults,
message: "搜索成功",
};
}
async searchInRSSFeed(rssUrl: string) {

View File

@@ -1,5 +1,7 @@
import { AxiosInstance, AxiosHeaders } from "axios";
import { createAxiosInstance } from "../utils/axiosInstance";
import GlobalSetting from "../models/GlobalSetting";
import { GlobalSettingAttributes } from "../models/GlobalSetting";
import * as cheerio from "cheerio";
import { config } from "../config";
import { Logger } from "../utils/logger";
@@ -7,19 +9,30 @@ import { Logger } from "../utils/logger";
interface sourceItem {
messageId?: string;
title?: string;
completeTitle?: string;
link?: string;
pubDate?: string;
content?: string;
description?: string;
image?: string;
cloudLinks?: string[];
tags?: string[];
cloudType?: string;
}
export class Searcher {
private axiosInstance: AxiosInstance;
private axiosInstance: AxiosInstance | null = null;
constructor() {
this.initializeAxiosInstance();
}
private async initializeAxiosInstance(isUpdate = false) {
let settings = null;
if (isUpdate) {
settings = await GlobalSetting.findOne();
}
const globalSetting = settings?.dataValues || ({} as GlobalSettingAttributes);
this.axiosInstance = createAxiosInstance(
config.telegram.baseUrl,
AxiosHeaders.from({
@@ -37,9 +50,15 @@ export class Searcher {
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
}),
true
globalSetting?.isProxyEnabled,
globalSetting?.isProxyEnabled
? { host: globalSetting?.httpProxyHost, port: globalSetting?.httpProxyPort }
: undefined
);
}
public async updateAxiosInstance() {
await this.initializeAxiosInstance(true);
}
private extractCloudLinks(text: string): { links: string[]; cloudType: string } {
const links: string[] = [];
@@ -82,22 +101,38 @@ export class Searcher {
channelId: channel.id,
}));
allResults.push(...channelResults);
allResults.push({
list: channelResults,
channelInfo: {
...channel,
channelLogo: results.channelLogo,
},
id: channel.id,
});
}
} catch (error) {
Logger.error(`搜索频道 ${channel.name} 失败:`, error);
}
}
return allResults;
return {
data: allResults,
};
}
async searchInWeb(url: string, channelId: string) {
try {
if (!this.axiosInstance) {
throw new Error("Axios instance is not initialized");
}
const response = await this.axiosInstance.get(url);
const html = response.data;
const $ = cheerio.load(html);
const items: sourceItem[] = [];
let channelLogo = "";
$(".tgme_header_link").each((_, element) => {
channelLogo = $(element).find("img").attr("src") || "";
});
// 遍历每个消息容器
$(".tgme_widget_message_wrap").each((_, element) => {
const messageEl = $(element);
@@ -109,8 +144,24 @@ export class Searcher {
?.toString()
.split("/")[1];
// 提取标题 (消息截取100长度)
const title = messageEl.find(".js-message_text").text().trim().substring(0, 50) + "...";
// 提取标题 (第一个<br>标签前的内容)
const title =
messageEl
.find(".js-message_text")
.html()
?.split("<br>")[0]
.replace(/<[^>]+>/g, "")
.replace(/\n/g, "") || "";
// 提取描述 (第一个<a>标签前面的内容,不包含标题)
const content =
messageEl
.find(".js-message_text")
.html()
?.replace(title, "")
.split("<a")[0]
.replace(/<br>/g, "")
.trim() || "";
// 提取链接 (消息中的链接)
// const link = messageEl.find('.tgme_widget_message').data('post');
@@ -118,23 +169,24 @@ export class Searcher {
// 提取发布时间
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 tags: string[] = [];
// 提取云盘链接
const links = messageEl
.find(".tgme_widget_message_text a")
.map((_, el) => $(el).attr("href"))
.get();
messageEl.find(".tgme_widget_message_text a").each((index, element) => {
const tagText = $(element).text();
if (tagText && tagText.startsWith("#")) {
tags.push(tagText);
}
});
const cloudInfo = this.extractCloudLinks(links.join(" "));
// 添加到数组第一位
items.unshift({
@@ -142,18 +194,21 @@ export class Searcher {
title,
pubDate,
content,
description,
image,
cloudLinks: cloudInfo.links,
cloudType: cloudInfo.cloudType,
tags,
});
});
return { items };
return { items: items, channelLogo };
} catch (error) {
Logger.error(`RSS源解析错误: ${url}`, error);
Logger.error(`搜索错误: ${url}`, error);
return {
items: [],
channelLogo: "",
};
}
}
}
export default new Searcher();

View File

@@ -5,9 +5,6 @@ export interface ShareInfo {
}
export interface ShareInfoResponse {
success: boolean;
data?: {
list: ShareInfo[];
};
error?: string;
data?: ShareInfo[];
message?: string;
}

View File

@@ -0,0 +1,10 @@
import { Request } from "express";
declare module "express" {
interface Request {
user?: {
userId: string;
role: number;
};
}
}

View File

@@ -1,20 +1,24 @@
import axios, { AxiosInstance, AxiosRequestHeaders } from "axios";
import tunnel from "tunnel";
import { config } from "../config";
import GlobalSetting from "../models/GlobalSetting";
interface ProxyConfig {
host: string;
port: number;
}
export function createAxiosInstance(
baseURL: string,
headers: AxiosRequestHeaders,
useProxy: boolean = false
useProxy: boolean = false,
proxyConfig?: ProxyConfig
): AxiosInstance {
let agent;
if (useProxy) {
console.log(proxyConfig);
if (useProxy && proxyConfig) {
agent = tunnel.httpsOverHttp({
proxy: {
host: config.httpProxy.host,
port: Number(config.httpProxy.port),
},
proxy: proxyConfig,
});
}

View File

@@ -0,0 +1,21 @@
import jwt from "jsonwebtoken";
import { Request } from "express";
import { config } from "../config";
interface JwtPayload {
userId: string;
}
export function getUserIdFromToken(req: Request): string | null {
try {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
throw new Error("Token not found");
}
const decoded = jwt.verify(token, config.jwtSecret) as JwtPayload;
return decoded.userId;
} catch (error) {
console.error("Invalid token:", error);
return null;
}
}

View File

@@ -0,0 +1,17 @@
import { Response } from "express";
interface ResponseData {
code?: number; // 业务状态码
message?: string;
data?: any;
}
export const sendSuccess = (res: Response, response: ResponseData, businessCode: number = 0) => {
response.code = businessCode;
res.status(200).json(response);
};
export const sendError = (res: Response, response: ResponseData, businessCode: number = 10000) => {
response.code = businessCode;
res.status(200).json(response);
};

View File

@@ -12,6 +12,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": ".",
"typeRoots": ["./node_modules/@types", "./src/types"],
"paths": {
"@/*": ["src/*"]
}