feat: 引入搜索日志和增强错误提示

*   移除旧版速率限制机制,简化服务部署与维护。(频率限制转移到CloudFlare中设置防护规则)
*   新增Cloudflare日志记录功能,提升搜索过程可观测性。
*   统一并细化了各资源平台的API错误响应信息。
*   修复Koyso和真红小站的URL编码及正则匹配问题。
*   优化紫缘社的访问密码错误提示。
This commit is contained in:
Jurangren
2025-08-22 00:24:39 +08:00
parent 83661b404a
commit 6a799ee897
34 changed files with 80 additions and 99 deletions

View File

@@ -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 })));
}

View File

@@ -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" }), {

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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,
});
}
}

View File

@@ -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[];

View File

@@ -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();

View File

@@ -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

View File

@@ -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();

View File

@@ -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;

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 }[] };

View File

@@ -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();

View File

@@ -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 => ({

View File

@@ -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();

View File

@@ -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 => ({

View File

@@ -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,
});
}
}

View File

@@ -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 => ({

View File

@@ -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 => ({

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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);
}
}

View File

@@ -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