mirror of
https://github.com/Moe-Sakura/Wrangler-API.git
synced 2026-03-21 05:19:47 +08:00
feat: 引入搜索日志和增强错误提示
* 移除旧版速率限制机制,简化服务部署与维护。(频率限制转移到CloudFlare中设置防护规则) * 新增Cloudflare日志记录功能,提升搜索过程可观测性。 * 统一并细化了各资源平台的API错误响应信息。 * 修复Koyso和真红小站的URL编码及正则匹配问题。 * 优化紫缘社的访问密码错误提示。
This commit is contained in:
18
src/core.ts
18
src/core.ts
@@ -1,3 +1,4 @@
|
||||
import { logToCF } from "./utils/httpClient";
|
||||
import type { Platform, PlatformSearchResult, StreamProgress, StreamResult } from "./types";
|
||||
import platformsGal from "./platforms/gal";
|
||||
import platformsPatch from "./platforms/patch";
|
||||
@@ -22,6 +23,11 @@ export async function handleSearchRequestStream(
|
||||
writer: WritableStreamDefaultWriter<Uint8Array>,
|
||||
zypassword: string = "" // 添加 zypassword 参数
|
||||
): Promise<void> {
|
||||
// 记录搜索关键词
|
||||
logToCF({
|
||||
message: `搜索关键词: ${game}`,
|
||||
level: "info",
|
||||
});
|
||||
const encoder = new TextEncoder();
|
||||
const total = platforms.length;
|
||||
let completed = 0;
|
||||
@@ -38,6 +44,13 @@ export async function handleSearchRequestStream(
|
||||
const progress: StreamProgress = { completed, total };
|
||||
|
||||
if (result.count > 0 || result.error) {
|
||||
if (result.error) {
|
||||
// 记录平台错误
|
||||
logToCF({
|
||||
message: `平台 ${result.name} 搜索错误: ${result.error}`,
|
||||
level: "error",
|
||||
});
|
||||
}
|
||||
const streamResult: StreamResult = {
|
||||
name: result.name,
|
||||
color: result.error ? 'red' : platform.color,
|
||||
@@ -53,6 +66,11 @@ export async function handleSearchRequestStream(
|
||||
completed++;
|
||||
// 记录平台内部的未知错误
|
||||
console.error(`Error searching platform ${platform.name}:`, e);
|
||||
// 记录平台内部的未知错误
|
||||
logToCF({
|
||||
message: `平台 ${platform.name} 内部错误: ${e instanceof Error ? e.message : String(e)}`,
|
||||
level: "error",
|
||||
});
|
||||
const progress: StreamProgress = { completed, total };
|
||||
await writer.write(encoder.encode(formatStreamEvent({ progress })));
|
||||
}
|
||||
|
||||
17
src/index.ts
17
src/index.ts
@@ -1,8 +1,5 @@
|
||||
import { handleSearchRequestStream, PLATFORMS_GAL, PLATFORMS_PATCH } from "./core";
|
||||
import { checkRateLimit } from "./ratelimit";
|
||||
|
||||
export interface Env {
|
||||
RATE_LIMIT_KV: KVNamespace;
|
||||
}
|
||||
|
||||
const corsHeaders = {
|
||||
@@ -17,20 +14,6 @@ async function handleSearch(request: Request, env: Env, ctx: ExecutionContext, p
|
||||
const game = formData.get("game") as string;
|
||||
const zypassword = formData.get("zypassword") as string || ""; // 获取 zypassword
|
||||
|
||||
if (env.RATE_LIMIT_KV) {
|
||||
const ip = request.headers.get("CF-Connecting-IP") || "unknown";
|
||||
const { allowed, retryAfter } = await checkRateLimit(ip, env.RATE_LIMIT_KV);
|
||||
|
||||
if (!allowed) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `搜索过于频繁, 请 ${retryAfter} 秒后再试` }),
|
||||
{
|
||||
status: 429,
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!game || typeof game !== 'string') {
|
||||
return new Response(JSON.stringify({ error: "Game name is required" }), {
|
||||
|
||||
@@ -17,7 +17,7 @@ async function searchACGYingYingGuai(game: string): Promise<PlatformSearchResult
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -17,7 +17,7 @@ async function searchBiAnXingLu(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -31,7 +31,7 @@ async function searchDaoHeGal(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as DaoHeGalResponse;
|
||||
|
||||
@@ -31,7 +31,7 @@ async function searchFuFuACG(game: string): Promise<PlatformSearchResult> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as FuFuACGResponse;
|
||||
|
||||
@@ -19,7 +19,7 @@ async function searchGGBases(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -35,7 +35,7 @@ async function searchGalTuShuGuan(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as GalTuShuGuanResponse;
|
||||
|
||||
@@ -48,7 +48,7 @@ async function searchGalgameX(game: string): Promise<PlatformSearchResult> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as GalgameXResponse;
|
||||
|
||||
@@ -17,7 +17,7 @@ async function searchHikarinagi(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -31,7 +31,7 @@ async function searchJiMengACG(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as JiMengACGResponse;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Platform, PlatformSearchResult, SearchResultItem } from "../../typ
|
||||
|
||||
const API_URL = "https://koyso.to/";
|
||||
const BASE_URL = "https://koyso.to";
|
||||
const REGEX = /<a class="game_item" href="(?<URL>.+?)"\s*>.*?<span style="background-color: rgba\(128,128,128,0\)">(?<NAME>.+?)<\/span>/gs;
|
||||
const REGEX = /<a class="game_item"\s+href="(?<URL>.+?)"\s*>.*?<span style="background-color: rgba\(128,128,128,0\)">(?<NAME>.+?)<\/span>/gs;
|
||||
|
||||
async function searchKoyso(game: string): Promise<PlatformSearchResult> {
|
||||
const searchResult: PlatformSearchResult = {
|
||||
@@ -18,12 +18,12 @@ async function searchKoyso(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
// --- DEBUGGING: Print the full HTML content ---
|
||||
// console.log("Koyso API HTML Response:", html.substring(0, 1000)); // Print first 1000 chars
|
||||
console.log("Koyso API HTML Response:", html);
|
||||
// --- END DEBUGGING ---
|
||||
|
||||
const matches = html.matchAll(REGEX);
|
||||
@@ -33,7 +33,7 @@ async function searchKoyso(game: string): Promise<PlatformSearchResult> {
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: BASE_URL + encodeURIComponent(match.groups.URL),
|
||||
url: BASE_URL + match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ async function searchKunGalgame(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as KunGalgameItem[];
|
||||
|
||||
@@ -17,7 +17,7 @@ async function searchLiangZiACG(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -43,13 +43,13 @@ async function searchMaoMaoWangPan(game: string): Promise<PlatformSearchResult>
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as MaoMaoResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
throw new Error(`${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content
|
||||
|
||||
@@ -18,7 +18,7 @@ async function searchMiaoYuanLingYu(game: string): Promise<PlatformSearchResult>
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -28,7 +28,7 @@ async function searchNysoure(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as NysoureResponse;
|
||||
|
||||
@@ -18,7 +18,7 @@ async function searchQingJiACG(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -17,7 +17,7 @@ async function searchShenShiTianTang(game: string): Promise<PlatformSearchResult
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -16,7 +16,7 @@ async function searchTianYouErCiYuan(game: string): Promise<PlatformSearchResult
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -39,7 +39,7 @@ async function searchTouchGal(game: string): Promise<PlatformSearchResult> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as { galgames: { name: string; uniqueId: string }[] };
|
||||
|
||||
@@ -39,7 +39,7 @@ async function searchVikaACG(game: string): Promise<PlatformSearchResult> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const rawText = await response.text();
|
||||
|
||||
@@ -43,13 +43,13 @@ async function searchWeiZhiYunPan(game: string): Promise<PlatformSearchResult> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as WeiZhiYunPanResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
throw new Error(`${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
|
||||
@@ -17,7 +17,7 @@ async function searchYouYuDeloli(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
@@ -43,13 +43,13 @@ async function searchZeroFive(game: string): Promise<PlatformSearchResult> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ZeroFiveResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
throw new Error(`${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
|
||||
@@ -18,7 +18,7 @@ async function searchZhenHongXiaoZhan(game: string): Promise<PlatformSearchResul
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
@@ -29,7 +29,7 @@ async function searchZhenHongXiaoZhan(game: string): Promise<PlatformSearchResul
|
||||
if (match.groups?.NAME && match.groups?.URL) {
|
||||
items.push({
|
||||
name: match.groups.NAME.trim(),
|
||||
url: BASE_URL + encodeURIComponent(match.groups.URL),
|
||||
url: BASE_URL + match.groups.URL,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,13 @@ async function searchZiLingDeMiaoMiaoWu(game: string): Promise<PlatformSearchRes
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ZiLingDeMiaoMiaoWuResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
throw new Error(`${data.message}`);
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
|
||||
@@ -43,13 +43,17 @@ async function searchZiYuanShe(game: string, zypassword: string = ""): Promise<P
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as ZiYuanSheResponse;
|
||||
|
||||
if (data.message !== "success") {
|
||||
throw new Error(`API returned an error: ${data.message}`);
|
||||
throw new Error(`${data.message}`);
|
||||
}
|
||||
|
||||
if (data.data.total !== data.data.content.length) {
|
||||
throw new Error("访问密码错误");
|
||||
}
|
||||
|
||||
const items: SearchResultItem[] = data.data.content.map(item => ({
|
||||
|
||||
@@ -22,7 +22,7 @@ async function searchXxacg(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search API response status code is ${response.status}`);
|
||||
throw new Error(`Search 资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
html = await response.text();
|
||||
|
||||
@@ -41,7 +41,7 @@ async function searchKunGalgameBuDing(game: string): Promise<PlatformSearchResul
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as KunGalgameBuDingResponse;
|
||||
|
||||
@@ -22,7 +22,7 @@ async function searchTWOdfan(game: string): Promise<PlatformSearchResult> {
|
||||
|
||||
const response = await fetchClient(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`API response status code is ${response.status}`);
|
||||
throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as TwoDFanResponse;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// --- 速率限制常量 ---
|
||||
const SEARCH_INTERVAL_SECONDS = 15;
|
||||
// KV 条目将在此秒数后自动过期,以防止存储膨胀
|
||||
const IP_ENTRY_TTL_SECONDS = 60;
|
||||
|
||||
/**
|
||||
* 检查给定 IP 地址是否超出了速率限制。
|
||||
* @param ip 客户端的 IP 地址。
|
||||
* @param kvNamespace 用于存储 IP 时间戳的 KV 命名空间。
|
||||
* @returns 返回一个对象,包含是否允许请求以及剩余的等待秒数。
|
||||
*/
|
||||
export async function checkRateLimit(
|
||||
ip: string,
|
||||
kvNamespace: KVNamespace
|
||||
): Promise<{ allowed: boolean; retryAfter: number }> {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
const lastSearchTimeStr = await kvNamespace.get(ip);
|
||||
const lastSearchTime = lastSearchTimeStr ? parseInt(lastSearchTimeStr, 10) : 0;
|
||||
|
||||
if (lastSearchTime && (currentTime - lastSearchTime) < SEARCH_INTERVAL_SECONDS) {
|
||||
return {
|
||||
allowed: false,
|
||||
retryAfter: SEARCH_INTERVAL_SECONDS - (currentTime - lastSearchTime),
|
||||
};
|
||||
}
|
||||
|
||||
// 更新 IP 的最后搜索时间,并设置 TTL
|
||||
await kvNamespace.put(ip, currentTime.toString(), {
|
||||
expirationTtl: IP_ENTRY_TTL_SECONDS,
|
||||
});
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
retryAfter: 0,
|
||||
};
|
||||
}
|
||||
@@ -32,10 +32,27 @@ export async function fetchClient(
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error(`Request timed out after ${TIMEOUT_SECONDS} seconds`);
|
||||
throw new Error(`资源平台 SearchAPI 请求超时`);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 向 Cloudflare 发送日志。
|
||||
* @param data 要记录的数据对象。
|
||||
*/
|
||||
export async function logToCF(data: object) {
|
||||
try {
|
||||
await fetch("https://log.gal.homes", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to log to Cloudflare:", error);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,5 @@ name = "searchgal-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2023-10-30"
|
||||
|
||||
[vars]
|
||||
# 如果有需要,可以在这里添加环境变量
|
||||
|
||||
# --- KV 命名空间绑定 ---
|
||||
[[kv_namespaces]]
|
||||
binding = "RATE_LIMIT_KV"
|
||||
id = "7ed4c4f36baf419bb9ed54538a61f473"
|
||||
[observability.logs]
|
||||
enabled = true
|
||||
|
||||
Reference in New Issue
Block a user