diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..ea33a4f --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,53 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + tags: + - "v*" + workflow_dispatch: + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/SearchGal-Api + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9935a1c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM node:20-bookworm-slim + +WORKDIR /app + +RUN if [ -f /etc/apt/sources.list ]; then \ + sed -i 's|http://deb.debian.org/debian|http://mirrors.aliyun.com/debian|g' /etc/apt/sources.list; \ + sed -i 's|http://security.debian.org/debian-security|http://mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list || true; \ + elif [ -f /etc/apt/sources.list.d/debian.sources ]; then \ + sed -i 's|http://deb.debian.org/debian|http://mirrors.aliyun.com/debian|g' /etc/apt/sources.list.d/debian.sources; \ + sed -i 's|http://security.debian.org/debian-security|http://mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list.d/debian.sources || true; \ + fi \ + && apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY package*.json ./ +RUN npm ci --include=optional + +COPY . . + +EXPOSE 8787 + +CMD ["npx", "wrangler", "dev", "--local", "--ip", "0.0.0.0", "--port", "8787"] diff --git a/README.md b/README.md index eb070c6..2fa9b75 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,93 @@ # Wrangler-API -Cloudflare Wrangler 实现的 SearchGal 后端搜索API + +Cloudflare Workers 版 SearchGal 聚合搜索 API。提供 `/gal` 和 `/patch` 两个入口,接收游戏名并以 SSE 流式返回各平台搜索结果。 + +## 准备 + +- Node.js(Win 需要) 与 pnpm +- Cloudflare 账号(发布时需要) + +## 安装 + +### POSIX + +```sh +## 安装 pnpm +curl -fsSL https://get.pnpm.io/install.sh | sh - + +## 安装依赖 +pnpm install +``` + +### Windows + +```sh +## 安装 Node.js +winget install -e --id OpenJS.NodeJS + +## 安装 pnpm +npx pnpm@latest-10 dlx @pnpm/exe@latest-10 setup + +## 安装依赖 +pnpm install +``` + +## 开发 + +```sh +# 纯本地(无 Cloudflare 登录) +npx wrangler dev --local + +# 实时连 Cloudflare +npx wrangler dev +``` + +## 运行 + +```sh +npx wrangler dev --ip 0.0.0.0 +``` + +## 发布 + +```sh +npx wrangler login # 首次需要 +npx wrangler publish +``` + +## API 使用 + +- 路径:`POST /gal` 或 `POST /patch` +- Content-Type:`multipart/form-data` +- 表单字段:`game` (string) +- 响应:`text/event-stream`,每行是一条 JSON,示例: + +```json +{"total":33} +{"progress":{"completed":1,"total":33}} +{"progress":{"completed":2,"total":33},"result":{"name":"某平台","color":"lime","tags":["NoReq"],"items":[{"name":"Title","url":"https://..."}]}} +{"done":true} +``` + +## 标签说明(tags) + +- `NoReq`:无需登录/回复即可拿到下载信息 +- `Login`:需登录后访问 +- `LoginPay`:需登录且支付积分 +- `LoginRep`:需登录并回复/评论解锁 +- `Rep`:需回复/评论但无需登录 +- `SuDrive`:自建网盘盘源 +- `NoSplDrive`:不限速网盘盘源(如Onedrive/Mega等) +- `SplDrive`:限速网盘盘源(如百度/夸克/天翼等) +- `MixDrive`:不限速与限速网盘盘源混合,可能提供多种下载形式 +- `BTmag`:BT或磁力链接 +- `magic`:站点需要代理访问 + +## 目录速览 + +- `src/index.ts` Worker 入口,路由 `/gal`、`/patch` +- `src/core.ts` 处理并行搜索与 SSE 组装 +- `src/platforms/gal` GAL 平台搜集器 +- `src/platforms/patch` 补丁平台搜集器 +- `src/utils/httpClient.ts` 统一请求封装 +- `scripts/generate-indices.js` 可选的索引生成脚本 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..b501112 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,896 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250821.0 + version: 4.20251125.0 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + wrangler: + specifier: ^4.31.0 + version: 4.50.0(@cloudflare/workers-types@4.20251125.0) + +packages: + + '@cloudflare/kv-asset-handler@0.4.0': + resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.7.11': + resolution: {integrity: sha512-se23f1D4PxKrMKOq+Stz+Yn7AJ9ITHcEecXo2Yjb+UgbUDCEBch1FXQC6hx6uT5fNA3kmX3mfzeZiUmpK1W9IQ==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20251106.1 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20251118.0': + resolution: {integrity: sha512-UmWmYEYS/LkK/4HFKN6xf3Hk8cw70PviR+ftr3hUvs9HYZS92IseZEp16pkL6ZBETrPRpZC7OrzoYF7ky6kHsg==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20251118.0': + resolution: {integrity: sha512-RockU7Qzf4rxNfY1lx3j4rvwutNLjTIX7rr2hogbQ4mzLo8Ea40/oZTzXVxl+on75joLBrt0YpenGW8o/r44QA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20251118.0': + resolution: {integrity: sha512-aT97GnOAbJDuuOG0zPVhgRk0xFtB1dzBMrxMZ09eubDLoU4djH4BuORaqvxNRMmHgKfa4T6drthckT0NjUvBdw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20251118.0': + resolution: {integrity: sha512-bXZPJcwlq00MPOXqP7DMWjr+goYj0+Fqyw6zgEC2M3FR1+SWla4yjghnZ4IdpN+H1t7VbUrsi5np2LzMUFs0NA==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20251118.0': + resolution: {integrity: sha512-2LV99AHSlpr8WcCb/BYbU2QsYkXLUL1izN6YKWkN9Eibv80JKX0RtgmD3dfmajE5sNvClavxZejgzVvHD9N9Ag==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20251125.0': + resolution: {integrity: sha512-YZdO/IX10DiHb2v+7H2CL5SKAbxJQIG22jNefgtW86YMf5LvQk6f75v5T/j1ju56MBwm6RzcqvsECF+cXs2h3g==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@poppinss/colors@4.1.5': + resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.2': + resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + + '@sindresorhus/is@7.1.1': + resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.12': + resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==} + + acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + + exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + miniflare@4.20251118.1: + resolution: {integrity: sha512-uLSAE/DvOm392fiaig4LOaatxLjM7xzIniFRG5Y3yF9IduOYLLK/pkCPQNCgKQH3ou0YJRHnTN+09LPfqYNTQQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + stoppable@1.1.0: + resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} + engines: {node: '>=4', npm: '>=6'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici@7.14.0: + resolution: {integrity: sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + workerd@1.20251118.0: + resolution: {integrity: sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.50.0: + resolution: {integrity: sha512-+nuZuHZxDdKmAyXOSrHlciGshCoAPiy5dM+t6mEohWm7HpXvTHmWQGUf/na9jjWlWJHCJYOWzkA1P5HBJqrIEA==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20251118.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + +snapshots: + + '@cloudflare/kv-asset-handler@0.4.0': + dependencies: + mime: 3.0.0 + + '@cloudflare/unenv-preset@2.7.11(unenv@2.0.0-rc.24)(workerd@1.20251118.0)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20251118.0 + + '@cloudflare/workerd-darwin-64@1.20251118.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20251118.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20251118.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20251118.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20251118.0': + optional: true + + '@cloudflare/workers-types@4.20251125.0': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.4': + optional: true + + '@esbuild/android-arm64@0.25.4': + optional: true + + '@esbuild/android-arm@0.25.4': + optional: true + + '@esbuild/android-x64@0.25.4': + optional: true + + '@esbuild/darwin-arm64@0.25.4': + optional: true + + '@esbuild/darwin-x64@0.25.4': + optional: true + + '@esbuild/freebsd-arm64@0.25.4': + optional: true + + '@esbuild/freebsd-x64@0.25.4': + optional: true + + '@esbuild/linux-arm64@0.25.4': + optional: true + + '@esbuild/linux-arm@0.25.4': + optional: true + + '@esbuild/linux-ia32@0.25.4': + optional: true + + '@esbuild/linux-loong64@0.25.4': + optional: true + + '@esbuild/linux-mips64el@0.25.4': + optional: true + + '@esbuild/linux-ppc64@0.25.4': + optional: true + + '@esbuild/linux-riscv64@0.25.4': + optional: true + + '@esbuild/linux-s390x@0.25.4': + optional: true + + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + + '@esbuild/openbsd-x64@0.25.4': + optional: true + + '@esbuild/sunos-x64@0.25.4': + optional: true + + '@esbuild/win32-arm64@0.25.4': + optional: true + + '@esbuild/win32-ia32@0.25.4': + optional: true + + '@esbuild/win32-x64@0.25.4': + optional: true + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@poppinss/colors@4.1.5': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.5 + '@sindresorhus/is': 7.1.1 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.2': {} + + '@sindresorhus/is@7.1.1': {} + + '@speed-highlight/core@1.2.12': {} + + acorn-walk@8.3.2: {} + + acorn@8.14.0: {} + + blake3-wasm@2.1.5: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + cookie@1.0.2: {} + + detect-libc@2.1.2: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + + exit-hook@2.2.1: {} + + fsevents@2.3.3: + optional: true + + glob-to-regexp@0.4.1: {} + + is-arrayish@0.3.4: {} + + kleur@4.1.5: {} + + mime@3.0.0: {} + + miniflare@4.20251118.1: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.14.0 + acorn-walk: 8.3.2 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + sharp: 0.33.5 + stoppable: 1.1.0 + undici: 7.14.0 + workerd: 1.20251118.0 + ws: 8.18.0 + youch: 4.1.0-beta.10 + zod: 3.22.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + semver@7.7.3: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + stoppable@1.1.0: {} + + supports-color@10.2.2: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + undici@7.14.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + workerd@1.20251118.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20251118.0 + '@cloudflare/workerd-darwin-arm64': 1.20251118.0 + '@cloudflare/workerd-linux-64': 1.20251118.0 + '@cloudflare/workerd-linux-arm64': 1.20251118.0 + '@cloudflare/workerd-windows-64': 1.20251118.0 + + wrangler@4.50.0(@cloudflare/workers-types@4.20251125.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.0 + '@cloudflare/unenv-preset': 2.7.11(unenv@2.0.0-rc.24)(workerd@1.20251118.0) + blake3-wasm: 2.1.5 + esbuild: 0.25.4 + miniflare: 4.20251118.1 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20251118.0 + optionalDependencies: + '@cloudflare/workers-types': 4.20251125.0 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.2 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.5 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.12 + cookie: 1.0.2 + youch-core: 0.3.3 + + zod@3.22.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..5ba62b2 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - esbuild + - sharp + - workerd diff --git a/src/core.ts b/src/core.ts index c69ec87..1114159 100644 --- a/src/core.ts +++ b/src/core.ts @@ -14,14 +14,17 @@ function formatStreamEvent(data: object): string { * @param game 要搜索的游戏名称。 * @param platforms 要使用的平台列表。 * @param writer 用于写入 SSE 事件的 WritableStreamDefaultWriter。 - * @param zypassword (可选) 访问某些平台可能需要的密码。 */ export async function handleSearchRequestStream( game: string, platforms: Platform[], writer: WritableStreamDefaultWriter, - zypassword: string = "" // 添加 zypassword 参数 ): Promise { + // 记录搜索关键词 + console.log(JSON.stringify({ + message: `搜索关键词: ${game}`, + level: "info", + })); const encoder = new TextEncoder(); const total = platforms.length; let completed = 0; @@ -31,16 +34,23 @@ export async function handleSearchRequestStream( const searchPromises = platforms.map(async (platform) => { try { - // 传递 zypassword 给平台搜索函数 - const result = await platform.search(game, zypassword); + const result = await platform.search(game); completed++; const progress: StreamProgress = { completed, total }; if (result.count > 0 || result.error) { + if (result.error) { + // 记录平台错误 + console.log(JSON.stringify({ + message: `平台 ${platform.name} 搜索错误: ${result.error}`, + level: "error", + })); + } const streamResult: StreamResult = { - name: result.name, + name: platform.name, color: result.error ? 'red' : platform.color, + tags: platform.tags, items: result.items, error: result.error, }; @@ -53,6 +63,11 @@ export async function handleSearchRequestStream( completed++; // 记录平台内部的未知错误 console.error(`Error searching platform ${platform.name}:`, e); + // 记录平台内部的未知错误 + console.log(JSON.stringify({ + message: `平台 ${platform.name} 内部错误: ${e instanceof Error ? e.message : String(e)}`, + level: "error", + })); const progress: StreamProgress = { completed, total }; await writer.write(encoder.encode(formatStreamEvent({ progress }))); } diff --git a/src/index.ts b/src/index.ts index 40948c8..1603ac1 100644 --- a/src/index.ts +++ b/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 = { @@ -15,22 +12,7 @@ async function handleSearch(request: Request, env: Env, ctx: ExecutionContext, p try { const formData = await request.formData(); 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" }), { @@ -44,7 +26,7 @@ async function handleSearch(request: Request, env: Env, ctx: ExecutionContext, p // 将异步任务交给 waitUntil 来处理,确保它能完整执行 ctx.waitUntil( - handleSearchRequestStream(game.trim(), platforms, writer, zypassword) // 传递 zypassword + handleSearchRequestStream(game.trim(), platforms, writer) .catch(err => console.error("Streaming error:", err)) .finally(() => writer.close()) ); @@ -71,6 +53,10 @@ export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); + if (url.pathname === '/') { + return Response.redirect('https://searchgal.homes', 302); + } + if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders }); } diff --git a/src/platforms/gal/ACGYingYingGuai.ts b/src/platforms/gal/ACGYingYingGuai.ts index de9b880..8d381be 100644 --- a/src/platforms/gal/ACGYingYingGuai.ts +++ b/src/platforms/gal/ACGYingYingGuai.ts @@ -6,7 +6,6 @@ const REGEX = / { const searchResult: PlatformSearchResult = { - name: "ACG嘤嘤怪", count: 0, items: [], }; @@ -17,7 +16,7 @@ async function searchACGYingYingGuai(game: string): Promise

(?.*? async function searchBiAnXingLu(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "彼岸星露", count: 0, items: [], }; @@ -17,7 +16,7 @@ async function searchBiAnXingLu(game: string): Promise { 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(); @@ -51,6 +50,7 @@ async function searchBiAnXingLu(game: string): Promise { const BiAnXingLu: Platform = { name: "彼岸星露", color: "lime", + tags: [], magic: false, search: searchBiAnXingLu, }; diff --git a/src/platforms/gal/DaoHeGal.ts b/src/platforms/gal/DaoHeGal.ts index d4f591c..c2713e6 100644 --- a/src/platforms/gal/DaoHeGal.ts +++ b/src/platforms/gal/DaoHeGal.ts @@ -1,7 +1,7 @@ import { fetchClient } from "../../utils/httpClient"; import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types"; -const API_URL = "https://inarigal.com/api/home/list"; +const API_URL = "https://inarigal.com/api/search"; const BASE_URL = "https://inarigal.com/detail/"; interface DaoHeGalItem { @@ -18,20 +18,17 @@ interface DaoHeGalResponse { async function searchDaoHeGal(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "稻荷GAL", count: 0, items: [], }; try { const url = new URL(API_URL); - url.searchParams.set("page", "1"); - url.searchParams.set("pageSize", "18"); // Hardcoded as per original script - url.searchParams.set("search", game); + url.searchParams.set("keywords", game); 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; @@ -63,8 +60,9 @@ async function searchDaoHeGal(game: string): Promise { const DaoHeGal: Platform = { name: "稻荷GAL", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchDaoHeGal, }; -export default DaoHeGal; \ No newline at end of file +export default DaoHeGal; diff --git a/src/platforms/gal/FuFuACG.ts b/src/platforms/gal/FuFuACG.ts index e6ab779..640d20a 100644 --- a/src/platforms/gal/FuFuACG.ts +++ b/src/platforms/gal/FuFuACG.ts @@ -15,7 +15,6 @@ interface FuFuACGResponse { async function searchFuFuACG(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "FuFuACG", count: 0, items: [], }; @@ -31,7 +30,7 @@ async function searchFuFuACG(game: string): Promise { }); 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; @@ -59,6 +58,7 @@ async function searchFuFuACG(game: string): Promise { const FuFuACG: Platform = { name: "FuFuACG", color: "white", + tags: ["LoginPay"], magic: false, search: searchFuFuACG, }; diff --git a/src/platforms/gal/GGBases.ts b/src/platforms/gal/GGBases.ts index f13ee7f..63cf1b2 100644 --- a/src/platforms/gal/GGBases.ts +++ b/src/platforms/gal/GGBases.ts @@ -7,7 +7,6 @@ const REGEX = / { 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(); @@ -56,6 +55,7 @@ async function searchGGBases(game: string): Promise { const GGBases: Platform = { name: "GGBases", color: "lime", + tags: ["NoReq", "BTmag"], magic: false, search: searchGGBases, }; diff --git a/src/platforms/gal/GGS.ts b/src/platforms/gal/GGS.ts index d57f223..5a47e75 100644 --- a/src/platforms/gal/GGS.ts +++ b/src/platforms/gal/GGS.ts @@ -11,7 +11,6 @@ interface GgsItem { async function searchGGS(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "GGS", count: 0, items: [], }; @@ -49,6 +48,7 @@ async function searchGGS(game: string): Promise { const GGS: Platform = { name: "GGS", color: "lime", + tags: ["NoReq"], magic: false, search: searchGGS, }; diff --git a/src/platforms/gal/GalTuShuGuan.ts b/src/platforms/gal/GalTuShuGuan.ts index c48d902..5914f79 100644 --- a/src/platforms/gal/GalTuShuGuan.ts +++ b/src/platforms/gal/GalTuShuGuan.ts @@ -21,7 +21,6 @@ function stripHtml(html: string): string { async function searchGalTuShuGuan(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "GAL图书馆", count: 0, items: [], }; @@ -35,7 +34,7 @@ async function searchGalTuShuGuan(game: string): Promise { 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; @@ -81,6 +80,7 @@ async function searchGalTuShuGuan(game: string): Promise { const GalTuShuGuan: Platform = { name: "GAL图书馆", color: "lime", + tags: ["NoReq", "SplDrive"], magic: false, search: searchGalTuShuGuan, }; diff --git a/src/platforms/gal/GalgameX.ts b/src/platforms/gal/GalgameX.ts index f402a35..e35000e 100644 --- a/src/platforms/gal/GalgameX.ts +++ b/src/platforms/gal/GalgameX.ts @@ -15,7 +15,6 @@ interface GalgameXResponse { async function searchGalgameX(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "Galgamex", count: 0, items: [], }; @@ -48,7 +47,7 @@ async function searchGalgameX(game: string): Promise { }); 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; @@ -76,6 +75,7 @@ async function searchGalgameX(game: string): Promise { const GalgameX: Platform = { name: "Galgamex", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchGalgameX, }; diff --git a/src/platforms/gal/Hikarinagi.ts b/src/platforms/gal/Hikarinagi.ts deleted file mode 100644 index 8464e55..0000000 --- a/src/platforms/gal/Hikarinagi.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { fetchClient } from "../../utils/httpClient"; -import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types"; - -const API_URL = "https://www.hikarinagi.net/"; -const REGEX = /" class="lazyload fit-cover radius8">.*?

(?.*?)<\/a><\/h2>/gs; - -async function searchHikarinagi(game: string): Promise { - const searchResult: PlatformSearchResult = { - name: "Hikarinagi", - count: 0, - items: [], - }; - - try { - const url = new URL(API_URL); - url.searchParams.set("s", game); - - const response = await fetchClient(url); - if (!response.ok) { - throw new Error(`API response status code is ${response.status}`); - } - - const html = await response.text(); - const matches = html.matchAll(REGEX); - - const items: SearchResultItem[] = []; - for (const match of matches) { - if (match.groups?.NAME && match.groups?.URL) { - items.push({ - name: match.groups.NAME.trim(), - url: match.groups.URL, - }); - } - } - - searchResult.items = items; - searchResult.count = items.length; - - } catch (error) { - if (error instanceof Error) { - searchResult.error = error.message; - } else { - searchResult.error = "An unknown error occurred"; - } - searchResult.count = -1; - } - - return searchResult; -} - -const Hikarinagi: Platform = { - name: "Hikarinagi", - color: "white", - magic: false, - search: searchHikarinagi, -}; - -export default Hikarinagi; \ No newline at end of file diff --git a/src/platforms/gal/JiMengACG.ts b/src/platforms/gal/JiMengACG.ts index af8bd47..2d3a9bf 100644 --- a/src/platforms/gal/JiMengACG.ts +++ b/src/platforms/gal/JiMengACG.ts @@ -17,7 +17,6 @@ interface JiMengACGResponse { async function searchJiMengACG(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "绮梦ACG", count: 0, items: [], }; @@ -31,7 +30,7 @@ async function searchJiMengACG(game: string): Promise { 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; @@ -63,6 +62,7 @@ async function searchJiMengACG(game: string): Promise { const JiMengACG: Platform = { name: "绮梦ACG", color: "lime", + tags: ["Rep", "SuDrive"], magic: false, search: searchJiMengACG, }; diff --git a/src/platforms/gal/Koyso.ts b/src/platforms/gal/Koyso.ts index bc5a312..52722fe 100644 --- a/src/platforms/gal/Koyso.ts +++ b/src/platforms/gal/Koyso.ts @@ -3,11 +3,10 @@ import type { Platform, PlatformSearchResult, SearchResultItem } from "../../typ const API_URL = "https://koyso.to/"; const BASE_URL = "https://koyso.to"; -const REGEX = /.*?(?.+?)<\/span>/gs; +const REGEX = /.*?(?.+?)<\/span>/gs; async function searchKoyso(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "Koyso", count: 0, items: [], }; @@ -18,13 +17,10 @@ async function searchKoyso(game: string): Promise { 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 - // --- END DEBUGGING --- const matches = html.matchAll(REGEX); @@ -33,7 +29,7 @@ async function searchKoyso(game: string): Promise { 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, }); } } @@ -56,6 +52,7 @@ async function searchKoyso(game: string): Promise { const Koyso: Platform = { name: "Koyso", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchKoyso, }; diff --git a/src/platforms/gal/KunGalgame.ts b/src/platforms/gal/KunGalgame.ts index 225b783..720c4bd 100644 --- a/src/platforms/gal/KunGalgame.ts +++ b/src/platforms/gal/KunGalgame.ts @@ -14,7 +14,6 @@ interface KunGalgameItem { async function searchKunGalgame(game: string): Promise { const searchResult: PlatformSearchResult = { - name: "鲲Galgame", count: 0, items: [], }; @@ -28,7 +27,7 @@ async function searchKunGalgame(game: string): Promise { 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[]; @@ -60,6 +59,7 @@ async function searchKunGalgame(game: string): Promise { const KunGalgame: Platform = { name: "鲲Galgame", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchKunGalgame, }; diff --git a/src/platforms/gal/LiSiTanACG.ts b/src/platforms/gal/LiSiTanACG.ts index 6039c6b..e912210 100644 --- a/src/platforms/gal/LiSiTanACG.ts +++ b/src/platforms/gal/LiSiTanACG.ts @@ -7,7 +7,6 @@ const REGEX = /.*?(.*?)<\/title>.*?<url>(.*?)<\/url>.*?<\/entry>/g async function searchLiSiTanACG(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "莉斯坦ACG", count: 0, items: [], }; @@ -52,6 +51,7 @@ async function searchLiSiTanACG(game: string): Promise<PlatformSearchResult> { const LiSiTanACG: Platform = { name: "莉斯坦ACG", color: "lime", + tags: ["NoReq", "NoSplDrive"], magic: false, search: searchLiSiTanACG, }; diff --git a/src/platforms/gal/LiangZiACG.ts b/src/platforms/gal/LiangZiACG.ts index b8c8e00..1e82673 100644 --- a/src/platforms/gal/LiangZiACG.ts +++ b/src/platforms/gal/LiangZiACG.ts @@ -6,7 +6,6 @@ const REGEX = />\s*<h2 class="item-heading"><a target="_blank" href="(?<URL>.*?) async function searchLiangZiACG(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "量子acg", count: 0, items: [], }; @@ -17,7 +16,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(); @@ -50,7 +49,8 @@ async function searchLiangZiACG(game: string): Promise<PlatformSearchResult> { const LiangZiACG: Platform = { name: "量子acg", - color: "white", + color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchLiangZiACG, }; diff --git a/src/platforms/gal/MaoMaoWangPan.ts b/src/platforms/gal/MaoMaoWangPan.ts index 6b0490a..149a37b 100644 --- a/src/platforms/gal/MaoMaoWangPan.ts +++ b/src/platforms/gal/MaoMaoWangPan.ts @@ -19,7 +19,6 @@ interface MaoMaoResponse { async function searchMaoMaoWangPan(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "猫猫网盘", count: 0, items: [], }; @@ -43,13 +42,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 @@ -77,6 +76,7 @@ async function searchMaoMaoWangPan(game: string): Promise<PlatformSearchResult> const MaoMaoWangPan: Platform = { name: "猫猫网盘", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchMaoMaoWangPan, }; diff --git a/src/platforms/gal/MiaoYuanLingYu.ts b/src/platforms/gal/MiaoYuanLingYu.ts index 3504a2f..e805633 100644 --- a/src/platforms/gal/MiaoYuanLingYu.ts +++ b/src/platforms/gal/MiaoYuanLingYu.ts @@ -6,7 +6,6 @@ const REGEX = /<div class="item-thumbnail">\s*<a target="_blank" href="(?<URL>.* async function searchMiaoYuanLingYu(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "喵源领域", count: 0, items: [], }; @@ -18,7 +17,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(); @@ -56,6 +55,7 @@ async function searchMiaoYuanLingYu(game: string): Promise<PlatformSearchResult> const MiaoYuanLingYu: Platform = { name: "喵源领域", color: "white", + tags: ["Login", "SuDrive"], magic: false, search: searchMiaoYuanLingYu, }; diff --git a/src/platforms/gal/Nysoure.ts b/src/platforms/gal/Nysoure.ts index 0cf80ba..99e2fee 100644 --- a/src/platforms/gal/Nysoure.ts +++ b/src/platforms/gal/Nysoure.ts @@ -1,13 +1,13 @@ import { fetchClient } from "../../utils/httpClient"; import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types"; -const API_URL = "https://res.nyne.dev/api/resource/search"; -const BASE_URL = "https://res.nyne.dev/resources/"; +const API_URL = "https://nysoure.com/api/resource/search"; +const BASE_URL = "https://nysoure.com/resources/"; interface NysoureItem { id: number; title: string; -} +} interface NysoureResponse { success: boolean; @@ -16,7 +16,6 @@ interface NysoureResponse { async function searchNysoure(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "Nysoure", count: 0, items: [], }; @@ -28,7 +27,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; @@ -59,7 +58,8 @@ async function searchNysoure(game: string): Promise<PlatformSearchResult> { const Nysoure: Platform = { name: "Nysoure", - color: "gold", + color: "lime", + tags: ["NoReq", "magic", "SuDrive"], magic: true, search: searchNysoure, }; diff --git a/src/platforms/gal/QingJiACG.ts b/src/platforms/gal/QingJiACG.ts index 2ec160c..36ec732 100644 --- a/src/platforms/gal/QingJiACG.ts +++ b/src/platforms/gal/QingJiACG.ts @@ -6,7 +6,6 @@ const REGEX = /" class="lazyload fit-cover radius8">.*?<h2 class="item-heading"> async function searchQingJiACG(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "青桔ACG", count: 0, items: [], }; @@ -18,7 +17,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(); @@ -57,6 +56,7 @@ async function searchQingJiACG(game: string): Promise<PlatformSearchResult> { const QingJiACG: Platform = { name: "青桔ACG", color: "lime", + tags: ["NoReq", "SplDrive"], magic: false, search: searchQingJiACG, }; diff --git a/src/platforms/gal/ShenShiTianTang.ts b/src/platforms/gal/ShenShiTianTang.ts index 780f944..37171ac 100644 --- a/src/platforms/gal/ShenShiTianTang.ts +++ b/src/platforms/gal/ShenShiTianTang.ts @@ -6,7 +6,6 @@ const REGEX = /<h2 class="post-list-title">\s*<a href="(?<URL>.*?)" title=".+?" async function searchShenShiTianTang(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "绅仕天堂", count: 0, items: [], }; @@ -17,7 +16,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(); @@ -50,7 +49,8 @@ async function searchShenShiTianTang(game: string): Promise<PlatformSearchResult const ShenShiTianTang: Platform = { name: "绅仕天堂", - color: "gold", + color: "white", + tags: ["Login", "magic", "SuDrive"], magic: true, search: searchShenShiTianTang, }; diff --git a/src/platforms/gal/TaoHuaYuan.ts b/src/platforms/gal/TaoHuaYuan.ts deleted file mode 100644 index 125c3df..0000000 --- a/src/platforms/gal/TaoHuaYuan.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { fetchClient } from "../../utils/httpClient"; -import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types"; - -const DATA_URL = "https://peach.sslswwdx.top/page/search/index.json"; - -interface TaoHuaYuanItem { - title: string; - permalink: string; -} - -async function searchTaoHuaYuan(game: string): Promise<PlatformSearchResult> { - const searchResult: PlatformSearchResult = { - name: "桃花源", - count: 0, - items: [], - }; - - try { - const response = await fetchClient(DATA_URL); - if (!response.ok) { - throw new Error(`Failed to fetch data from ${DATA_URL}`); - } - - const data = await response.json() as TaoHuaYuanItem[]; - - const items: SearchResultItem[] = data - .filter(item => item.title.includes(game)) - .map(item => ({ - name: item.title.trim(), - url: item.permalink, - })); - - searchResult.items = items; - searchResult.count = items.length; - - } catch (error) { - if (error instanceof Error) { - searchResult.error = error.message; - } else { - searchResult.error = "An unknown error occurred"; - } - searchResult.count = -1; - } - - return searchResult; -} - -const TaoHuaYuan: Platform = { - name: "桃花源", - color: "lime", - magic: false, - search: searchTaoHuaYuan, -}; - -export default TaoHuaYuan; \ No newline at end of file diff --git a/src/platforms/gal/TianYouErCiYuan.ts b/src/platforms/gal/TianYouErCiYuan.ts index b2a58f8..40865be 100644 --- a/src/platforms/gal/TianYouErCiYuan.ts +++ b/src/platforms/gal/TianYouErCiYuan.ts @@ -2,11 +2,10 @@ import { fetchClient } from "../../utils/httpClient"; import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types"; const API_URL = "https://www.tiangal.com/search/"; -const REGEX = /<\/i><\/a><h2><a href="(?<URL>.*?)" title="(?<NAME>.*?)"/gs; +const REGEX = /<h2>\s*<a href="(?<URL>[^"]+)" title="(?<NAME>[^"]+)"/gs; async function searchTianYouErCiYuan(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "天游二次元", count: 0, items: [], }; @@ -16,7 +15,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(); @@ -27,7 +26,7 @@ async function searchTianYouErCiYuan(game: string): Promise<PlatformSearchResult if (match.groups?.NAME && match.groups?.URL) { items.push({ name: match.groups.NAME.trim(), - url: match.groups.URL, + url: new URL(match.groups.URL, API_URL).toString(), }); } } @@ -49,7 +48,8 @@ async function searchTianYouErCiYuan(game: string): Promise<PlatformSearchResult const TianYouErCiYuan: Platform = { name: "天游二次元", - color: "gold", + color: "white", + tags: ["LoginPay", "MixDrive"], magic: true, search: searchTianYouErCiYuan, }; diff --git a/src/platforms/gal/TouchGal.ts b/src/platforms/gal/TouchGal.ts index f302547..e6726ab 100644 --- a/src/platforms/gal/TouchGal.ts +++ b/src/platforms/gal/TouchGal.ts @@ -6,7 +6,6 @@ const BASE_URL = "https://www.touchgal.us/"; async function searchTouchGal(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "TouchGal", count: 0, items: [], }; @@ -39,7 +38,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 }[] }; @@ -67,6 +66,7 @@ async function searchTouchGal(game: string): Promise<PlatformSearchResult> { const TouchGal: Platform = { name: "TouchGal", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchTouchGal, }; diff --git a/src/platforms/gal/VikaACG.ts b/src/platforms/gal/VikaACG.ts index b67c674..d9caf39 100644 --- a/src/platforms/gal/VikaACG.ts +++ b/src/platforms/gal/VikaACG.ts @@ -1,86 +1,81 @@ import { fetchClient } from "../../utils/httpClient"; import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types"; -const API_URL = "https://www.vikacg.com/wp-json/b2/v1/getPostList"; +const API_URL = "https://www.vikacg.com/api/vikacg/v1/getPosts"; -// The Python code suggests the response text itself might be a JSON string -// that contains escaped HTML. Let's try to parse it as JSON first. -// The regex is applied to the *unescaped* string. -const REGEX = /<h2><a target="_blank" href="(?<URL>.*?)" title="(?<NAME>.*?)"/gs; +/** + * 响应体最小类型(按你贴的 JSON) + */ +type VikaGetPostsResponse = { + status?: string; + code?: number; + message?: string; + statusMessage?: string; + data?: { + list?: Array<{ + id: number; + title: string; + // 其他字段这里不需要就不展开了 + }>; + count?: number; + paged?: number; + page_count?: number; + pages?: number; + }; +}; + +const POST_URL = (id: number) => `https://www.vikacg.com/p/${id}`; async function searchVikaACG(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "VikaACG", count: 0, items: [], }; try { - const payload = { - paged: 1, - post_paged: 1, - post_count: 1000, // Corresponds to MAX_RESULTS - post_type: "post-1", - post_cat: [6], - post_order: "modified", - post_meta: [ - "user", "date", "des", "cats", "like", "comment", "views", "video", "download", "hide", - ], - metas: {}, - search: game, - }; - const response = await fetchClient(API_URL, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(payload), + body: JSON.stringify({ + order: "updated_at", + sort: "desc", + status: null, + search: game, + page_count: 50, + paged: 1, + category: null, + tag: null, + rating: null, + is_pinned: false, + user_id: null, + }), }); if (!response.ok) { - throw new Error(`API response status code is ${response.status}`); + throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`); } - const rawText = await response.text(); - let htmlContent: string; + const json: VikaGetPostsResponse = await response.json(); - try { - // Attempt to parse as JSON first. If it's a JSON string containing HTML, - // JSON.parse will handle standard escapes like \uXXXX. - const parsedJson = JSON.parse(rawText); - // Assuming the HTML content is directly the value of the JSON, or a specific field. - // The Python code implies the entire response text, after unescaping, is the HTML. - // So, if parsedJson is a string, use it. Otherwise, stringify it. - htmlContent = typeof parsedJson === 'string' ? parsedJson : JSON.stringify(parsedJson); - } catch (jsonError) { - // If JSON.parse fails, it might be due to non-standard Python escapes like \\/ or \\\\ - // Attempt a simple unescape for these specific cases. - // Note: This is a simplified unescape and might not cover all Python's unicode_escape nuances. - const unescapedText = rawText.replace(/\\(.)/g, '$1'); // Replaces \\/ with / and \\\\ with \ - try { - htmlContent = JSON.parse(unescapedText); // Try parsing as JSON again - } catch (finalError) { - // If still fails, assume it's raw HTML that just needs basic unescaping - htmlContent = unescapedText; - } + if (json.status !== "success" || !json.data) { + const msg = json.message || json.statusMessage || "接口返回非 success"; + throw new Error(`资源平台 SearchAPI 返回异常:${msg}`); } - const matches = htmlContent.matchAll(REGEX); - - const items: SearchResultItem[] = []; - for (const match of matches) { - if (match.groups?.NAME && match.groups?.URL) { - items.push({ - name: match.groups.NAME.trim(), - url: match.groups.URL, - }); - } - } + const list = json.data.list ?? []; + const items: SearchResultItem[] = list + .filter((p) => typeof p?.id === "number" && typeof p?.title === "string") + .map((p) => ({ + name: p.title.trim(), + url: POST_URL(p.id), + })); searchResult.items = items; - searchResult.count = items.length; + // ✅ 建议 count 用后端总数(data.count),否则用本页长度兜底 + searchResult.count = typeof json.data.count === "number" ? json.data.count : items.length; } catch (error) { if (error instanceof Error) { searchResult.error = error.message; @@ -95,9 +90,10 @@ async function searchVikaACG(game: string): Promise<PlatformSearchResult> { const VikaACG: Platform = { name: "VikaACG", - color: "gold", + color: "white", + tags: ["LoginPay", "magic", "MixDrive"], magic: true, search: searchVikaACG, }; -export default VikaACG; \ No newline at end of file +export default VikaACG; diff --git a/src/platforms/gal/WeiZhiYunPan.ts b/src/platforms/gal/WeiZhiYunPan.ts index 04d6b23..cda08e8 100644 --- a/src/platforms/gal/WeiZhiYunPan.ts +++ b/src/platforms/gal/WeiZhiYunPan.ts @@ -19,7 +19,6 @@ interface WeiZhiYunPanResponse { async function searchWeiZhiYunPan(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "未知云盘", count: 0, items: [], }; @@ -43,13 +42,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 => ({ @@ -75,6 +74,7 @@ async function searchWeiZhiYunPan(game: string): Promise<PlatformSearchResult> { const WeiZhiYunPan: Platform = { name: "未知云盘", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchWeiZhiYunPan, }; diff --git a/src/platforms/gal/YingZhiGuang.ts b/src/platforms/gal/YingZhiGuang.ts index db5845c..7a44b11 100644 --- a/src/platforms/gal/YingZhiGuang.ts +++ b/src/platforms/gal/YingZhiGuang.ts @@ -7,7 +7,6 @@ const REGEX = /<entry>.*?<title>(.*?)<\/title>.*?<url>(.*?)<\/url>.*?<\/entry>/g async function searchYingZhiGuang(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "萤ノ光", count: 0, items: [], }; @@ -52,6 +51,7 @@ async function searchYingZhiGuang(game: string): Promise<PlatformSearchResult> { const YingZhiGuang: Platform = { name: "萤ノ光", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchYingZhiGuang, }; diff --git a/src/platforms/gal/YouYuDeloli.ts b/src/platforms/gal/YouYuDeloli.ts index c6e2e52..44bd581 100644 --- a/src/platforms/gal/YouYuDeloli.ts +++ b/src/platforms/gal/YouYuDeloli.ts @@ -6,7 +6,6 @@ const REGEX = /<p style="text-align: center;"> <a href=".*?" target="_blank">.*? async function searchYouYuDeloli(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "忧郁的loli", count: 0, items: [], }; @@ -14,10 +13,11 @@ async function searchYouYuDeloli(game: string): Promise<PlatformSearchResult> { try { const url = new URL(API_URL); url.searchParams.set("s", game); + url.searchParams.set("submit", ''); 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(); @@ -54,6 +54,7 @@ async function searchYouYuDeloli(game: string): Promise<PlatformSearchResult> { const YouYuDeloli: Platform = { name: "忧郁的loli", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchYouYuDeloli, }; diff --git a/src/platforms/gal/YueYao.ts b/src/platforms/gal/YueYao.ts index c50ef2b..a490b1c 100644 --- a/src/platforms/gal/YueYao.ts +++ b/src/platforms/gal/YueYao.ts @@ -7,7 +7,6 @@ const REGEX = /<entry>.*?<title>(.*?)<\/title>.*?<url>(.*?)<\/url>.*?<\/entry>/g async function searchYueYao(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "月谣", count: 0, items: [], }; @@ -52,6 +51,7 @@ async function searchYueYao(game: string): Promise<PlatformSearchResult> { const YueYao: Platform = { name: "月谣", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchYueYao, }; diff --git a/src/platforms/gal/ZeroFive.ts b/src/platforms/gal/ZeroFive.ts index dda6cbe..2c03814 100644 --- a/src/platforms/gal/ZeroFive.ts +++ b/src/platforms/gal/ZeroFive.ts @@ -19,7 +19,6 @@ interface ZeroFiveResponse { async function searchZeroFive(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "05的资源小站", count: 0, items: [], }; @@ -43,13 +42,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 => ({ @@ -75,6 +74,7 @@ async function searchZeroFive(game: string): Promise<PlatformSearchResult> { const ZeroFive: Platform = { name: "05的资源小站", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchZeroFive, }; diff --git a/src/platforms/gal/ZhenHongXiaoZhan.ts b/src/platforms/gal/ZhenHongXiaoZhan.ts index a19e7e9..953f05b 100644 --- a/src/platforms/gal/ZhenHongXiaoZhan.ts +++ b/src/platforms/gal/ZhenHongXiaoZhan.ts @@ -7,7 +7,6 @@ const REGEX = /hover:underline" href="(?<URL>.+?)">\s*(?<NAME>.+?)\s*<\/a>/gs; async function searchZhenHongXiaoZhan(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "真红小站", count: 0, items: [], }; @@ -18,7 +17,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 +28,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, }); } } @@ -52,6 +51,7 @@ async function searchZhenHongXiaoZhan(game: string): Promise<PlatformSearchResul const ZhenHongXiaoZhan: Platform = { name: "真红小站", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchZhenHongXiaoZhan, }; diff --git a/src/platforms/gal/ZiLingDeMiaoMiaoWu.ts b/src/platforms/gal/ZiLingDeMiaoMiaoWu.ts index 2a6f168..75ae756 100644 --- a/src/platforms/gal/ZiLingDeMiaoMiaoWu.ts +++ b/src/platforms/gal/ZiLingDeMiaoMiaoWu.ts @@ -19,7 +19,6 @@ interface ZiLingDeMiaoMiaoWuResponse { async function searchZiLingDeMiaoMiaoWu(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "梓澪の妙妙屋", count: 0, items: [], }; @@ -43,13 +42,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 => ({ @@ -75,6 +74,7 @@ async function searchZiLingDeMiaoMiaoWu(game: string): Promise<PlatformSearchRes const ZiLingDeMiaoMiaoWu: Platform = { name: "梓澪の妙妙屋", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchZiLingDeMiaoMiaoWu, }; diff --git a/src/platforms/gal/ZiYuanShe.ts b/src/platforms/gal/ZiYuanShe.ts index 53b8c92..f425e7a 100644 --- a/src/platforms/gal/ZiYuanShe.ts +++ b/src/platforms/gal/ZiYuanShe.ts @@ -1,65 +1,50 @@ import { fetchClient } from "../../utils/httpClient"; import type { Platform, PlatformSearchResult, SearchResultItem } from "../../types"; -const API_URL = "https://galzy.eu.org/api/fs/search"; const BASE_URL = "https://galzy.eu.org"; -interface ZiYuanSheItem { - name: string; - parent: string; -} - -interface ZiYuanSheResponse { - message: string; - data: { - content: ZiYuanSheItem[]; - total: number; - }; -} - -async function searchZiYuanShe(game: string, zypassword: string = ""): Promise<PlatformSearchResult> { +async function searchZiYuanShe(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "紫缘Gal", count: 0, items: [], }; try { - const payload = { - parent: "/", - keywords: game, - scope: 0, - page: 1, - per_page: 999999, // Corresponds to MAX_RESULTS - password: zypassword, // Pass the zypassword here - }; - - const response = await fetchClient(API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - + const response = await fetchClient(`${BASE_URL}/api/search?q=${encodeURIComponent(game)}`); if (!response.ok) { - throw new Error(`API response status code is ${response.status}`); + throw new Error(`资源平台 SearchAPI 响应异常状态码 ${response.status}`); } + + const resJson: any = await response.json(); + + const gameListData = resJson.hits; - const data = await response.json() as ZiYuanSheResponse; - - if (data.message !== "success") { - throw new Error(`API returned an error: ${data.message}`); - } + if (gameListData) { + const items: SearchResultItem[] = gameListData.map((item: any) => { + let name: string = "未知"; + let firstTitle: string | undefined; - const items: SearchResultItem[] = data.data.content.map(item => ({ - name: item.name.trim(), - url: BASE_URL + item.parent + "/" + item.name, - })); - - searchResult.items = items; - searchResult.count = items.length; + for (const titleObj of item.titles) { + if (!firstTitle) { + firstTitle = titleObj.title; + } + if (["zh-Hans", "zh-Hant"].includes(titleObj.lang)) { + name = titleObj.title; + break; + } + } + if (name === "未知" && firstTitle) { + name = firstTitle; + } + return { + name: name.trim(), + url: `${BASE_URL}/${item.id}`, + }; + }); + searchResult.items = items; + searchResult.count = items.length; + } } catch (error) { if (error instanceof Error) { searchResult.error = error.message; @@ -74,7 +59,8 @@ async function searchZiYuanShe(game: string, zypassword: string = ""): Promise<P const ZiYuanShe: Platform = { name: "紫缘Gal", - color: "white", + color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchZiYuanShe, }; diff --git a/src/platforms/gal/index.ts b/src/platforms/gal/index.ts index 0ae4d8a..936d7ee 100644 --- a/src/platforms/gal/index.ts +++ b/src/platforms/gal/index.ts @@ -7,7 +7,6 @@ import GalgameX from "./GalgameX"; import GalTuShuGuan from "./GalTuShuGuan"; import GGBases from "./GGBases"; import GGS from "./GGS"; -import Hikarinagi from "./Hikarinagi"; import JiMengACG from "./JiMengACG"; import Koyso from "./Koyso"; import KunGalgame from "./KunGalgame"; @@ -18,7 +17,6 @@ import MiaoYuanLingYu from "./MiaoYuanLingYu"; import Nysoure from "./Nysoure"; import QingJiACG from "./QingJiACG"; import ShenShiTianTang from "./ShenShiTianTang"; -import TaoHuaYuan from "./TaoHuaYuan"; import TianYouErCiYuan from "./TianYouErCiYuan"; import TouchGal from "./TouchGal"; import VikaACG from "./VikaACG"; @@ -41,7 +39,6 @@ const platforms: Platform[] = [ GalTuShuGuan, GGBases, GGS, - Hikarinagi, JiMengACG, Koyso, KunGalgame, @@ -52,7 +49,6 @@ const platforms: Platform[] = [ Nysoure, QingJiACG, ShenShiTianTang, - TaoHuaYuan, TianYouErCiYuan, TouchGal, VikaACG, diff --git a/src/platforms/gal/xxacg.ts b/src/platforms/gal/xxacg.ts index 1ed1ce8..35af273 100644 --- a/src/platforms/gal/xxacg.ts +++ b/src/platforms/gal/xxacg.ts @@ -9,7 +9,6 @@ function stripHtml(html: string): string { async function searchXxacg(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "xxacg", count: 0, items: [], }; @@ -22,7 +21,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(); @@ -38,11 +37,6 @@ async function searchXxacg(game: string): Promise<PlatformSearchResult> { } } - if (items.length === 0 && html.length > 0) { - // 如果没有匹配项,但我们确实收到了 HTML,这可能意味着页面结构已更改。 - // 将部分 HTML 包含在错误中以供调试。 - throw new Error(`No matches found on page. HTML starts with: ${html.substring(0, 500)}`); - } searchResult.items = items; searchResult.count = items.length; @@ -61,7 +55,8 @@ async function searchXxacg(game: string): Promise<PlatformSearchResult> { const xxacg: Platform = { name: "xxacg", - color: "gold", + color: "white", + tags: ["Login", "magic", "NoSplDrive"], magic: true, search: searchXxacg, }; diff --git a/src/platforms/patch/KunGalgameBuDing.ts b/src/platforms/patch/KunGalgameBuDing.ts index e04c788..5bd3eb4 100644 --- a/src/platforms/patch/KunGalgameBuDing.ts +++ b/src/platforms/patch/KunGalgameBuDing.ts @@ -15,7 +15,6 @@ interface KunGalgameBuDingResponse { async function searchKunGalgameBuDing(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "鲲Galgame补丁", count: 0, items: [], }; @@ -41,7 +40,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; @@ -69,6 +68,7 @@ async function searchKunGalgameBuDing(game: string): Promise<PlatformSearchResul const KunGalgameBuDing: Platform = { name: "鲲Galgame补丁", color: "lime", + tags: ["NoReq", "SuDrive"], magic: false, search: searchKunGalgameBuDing, }; diff --git a/src/platforms/patch/TWOdfan.ts b/src/platforms/patch/TWOdfan.ts index c0b83de..d093850 100644 --- a/src/platforms/patch/TWOdfan.ts +++ b/src/platforms/patch/TWOdfan.ts @@ -5,13 +5,8 @@ const API_URL = "https://2dfan.com/subjects/search"; const BASE_URL = "https://2dfan.com"; const REGEX = /<h4 class="media-heading"><a target="_blank" href="(?<URL>.*?)">(?<NAME>.*?)<\/a><\/h4>/gs; -interface TwoDFanResponse { - subjects: string; // This is an HTML string -} - async function searchTWOdfan(game: string): Promise<PlatformSearchResult> { const searchResult: PlatformSearchResult = { - name: "2dfan", count: 0, items: [], }; @@ -22,11 +17,10 @@ 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; - const html = data.subjects; + + const html: string = await response.text(); const matches = html.matchAll(REGEX); @@ -57,8 +51,9 @@ async function searchTWOdfan(game: string): Promise<PlatformSearchResult> { const TWOdfan: Platform = { name: "2dfan", - color: "lime", - magic: false, + color: "white", + tags: ["LoginPay", "magic", "MixDrive"], + magic: true, search: searchTWOdfan, }; diff --git a/src/ratelimit.ts b/src/ratelimit.ts deleted file mode 100644 index 7220b9c..0000000 --- a/src/ratelimit.ts +++ /dev/null @@ -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, - }; -} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index c5b11e4..4142db3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,13 +2,13 @@ export interface SearchResultItem { name: string; url: string; + tags?: string[]; } // 平台搜索的返回值 export interface PlatformSearchResult { items: SearchResultItem[]; count: number; - name: string; error?: string; } @@ -16,6 +16,7 @@ export interface PlatformSearchResult { export interface Platform { name: string; color: string; + tags: string[]; magic: boolean; search: (game: string, ...args: any[]) => Promise<PlatformSearchResult>; } @@ -24,6 +25,7 @@ export interface Platform { export interface StreamResult { name: string; color: string; + tags: string[]; items: SearchResultItem[]; error?: string; } diff --git a/src/utils/httpClient.ts b/src/utils/httpClient.ts index 3f97360..81da63f 100644 --- a/src/utils/httpClient.ts +++ b/src/utils/httpClient.ts @@ -32,7 +32,7 @@ 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 { diff --git a/wrangler.toml b/wrangler.toml index ec54ceb..f93bab9 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -2,10 +2,5 @@ name = "searchgalapi" main = "src/index.ts" compatibility_date = "2023-10-30" -[vars] -# 如果有需要,可以在这里添加环境变量 - -# --- KV 命名空间绑定 --- -[[kv_namespaces]] -binding = "RATE_LIMIT_KV" -id = "7ed4c4f36baf419bb9ed54538a61f473" +[observability.logs] +enabled = true