2026-01-10 21:21:08 +08:00
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
|
|
|
|
|
|
|
|
|
|
// Token 内存缓存
|
|
|
|
|
|
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
|
|
|
|
|
|
|
|
|
|
|
export interface XiaoyaFile {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
size: number;
|
|
|
|
|
|
is_dir: boolean;
|
|
|
|
|
|
modified: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface XiaoyaListResponse {
|
|
|
|
|
|
content: XiaoyaFile[];
|
|
|
|
|
|
total: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class XiaoyaClient {
|
2026-01-23 16:41:49 +08:00
|
|
|
|
private token = '';
|
2026-01-10 21:21:08 +08:00
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
private baseURL: string,
|
|
|
|
|
|
private username?: string,
|
|
|
|
|
|
private password?: string,
|
|
|
|
|
|
private configToken?: string
|
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 使用账号密码登录获取Token
|
|
|
|
|
|
*/
|
|
|
|
|
|
static async login(
|
|
|
|
|
|
baseURL: string,
|
|
|
|
|
|
username: string,
|
|
|
|
|
|
password: string
|
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
|
const response = await fetch(`${baseURL}/api/auth/login`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
username,
|
|
|
|
|
|
password,
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`小雅登录失败: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code !== 200 || !data.data?.token) {
|
|
|
|
|
|
throw new Error('小雅登录失败: 未获取到Token');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return data.data.token;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取缓存的 Token 或重新登录
|
|
|
|
|
|
*/
|
2026-01-11 00:52:52 +08:00
|
|
|
|
async getToken(): Promise<string> {
|
2026-01-10 21:21:08 +08:00
|
|
|
|
// 如果配置了 Token,直接使用
|
|
|
|
|
|
if (this.configToken) {
|
|
|
|
|
|
return this.configToken;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有配置用户名密码,返回空字符串(guest 模式)
|
|
|
|
|
|
if (!this.username || !this.password) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cacheKey = `${this.baseURL}:${this.username}`;
|
|
|
|
|
|
const cached = tokenCache.get(cacheKey);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有缓存且未过期,直接返回
|
|
|
|
|
|
if (cached && cached.expiresAt > Date.now()) {
|
|
|
|
|
|
this.token = cached.token;
|
|
|
|
|
|
return this.token;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 否则重新登录
|
|
|
|
|
|
console.log('[XiaoyaClient] Token 不存在或已过期,重新登录');
|
|
|
|
|
|
this.token = await XiaoyaClient.login(
|
|
|
|
|
|
this.baseURL,
|
|
|
|
|
|
this.username,
|
|
|
|
|
|
this.password
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 缓存 Token,设置 1 小时过期
|
|
|
|
|
|
tokenCache.set(cacheKey, {
|
|
|
|
|
|
token: this.token,
|
|
|
|
|
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return this.token;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取基础 URL
|
|
|
|
|
|
*/
|
|
|
|
|
|
getBaseURL(): string {
|
|
|
|
|
|
return this.baseURL;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 列出目录内容
|
|
|
|
|
|
*/
|
2026-01-15 23:48:13 +08:00
|
|
|
|
async listDirectory(path: string, page = 1, perPage = 100, refresh = false): Promise<XiaoyaListResponse> {
|
2026-01-10 21:21:08 +08:00
|
|
|
|
const token = await this.getToken();
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`${this.baseURL}/api/fs/list`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': token,
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
path,
|
|
|
|
|
|
page,
|
|
|
|
|
|
per_page: perPage,
|
2026-01-15 23:48:13 +08:00
|
|
|
|
refresh,
|
2026-01-10 21:21:08 +08:00
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`小雅列表获取失败: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code !== 200) {
|
|
|
|
|
|
throw new Error(`小雅列表获取失败: ${data.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
content: data.data.content || [],
|
|
|
|
|
|
total: data.data.total || 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 搜索文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
async search(keyword: string, page = 1, perPage = 100): Promise<XiaoyaListResponse> {
|
|
|
|
|
|
const token = await this.getToken();
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`${this.baseURL}/api/fs/search`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': token,
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
parent: '/',
|
|
|
|
|
|
keywords: keyword,
|
|
|
|
|
|
scope: 1, // 递归搜索
|
|
|
|
|
|
page,
|
|
|
|
|
|
per_page: perPage,
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`小雅搜索失败: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code !== 200) {
|
|
|
|
|
|
throw new Error(`小雅搜索失败: ${data.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
content: data.data.content || [],
|
|
|
|
|
|
total: data.data.total || 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取文件信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getFileInfo(path: string): Promise<XiaoyaFile> {
|
|
|
|
|
|
const token = await this.getToken();
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`${this.baseURL}/api/fs/get`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': token,
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
path,
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`小雅文件信息获取失败: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.code !== 200) {
|
|
|
|
|
|
throw new Error(`小雅文件信息获取失败: ${data.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return data.data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取文件下载链接
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getDownloadUrl(path: string): Promise<string> {
|
|
|
|
|
|
// Alist 的直接下载链接格式
|
|
|
|
|
|
return `${this.baseURL}/d${path}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取文件内容(用于读取 NFO 等文本文件)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getFileContent(path: string): Promise<string> {
|
|
|
|
|
|
const downloadUrl = await this.getDownloadUrl(path);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(downloadUrl, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`文件读取失败: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return await response.text();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查文件是否存在
|
|
|
|
|
|
*/
|
|
|
|
|
|
async fileExists(path: string): Promise<boolean> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.getFileInfo(path);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|