diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..17d6fdd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug 报告 +about: 报告一个 Bug 或异常行为 +title: "[Bug] " +labels: bug +--- + +## 问题描述 + +清晰简洁地描述你遇到的 Bug。 + +## 复现步骤 + +1. 步骤一 +2. 步骤二 +3. 步骤三 + +## 预期行为 + +描述你期望发生的结果。 + +## 实际行为 + +描述实际发生的结果。 + +## 环境信息 + +- **操作系统**: [例如 Windows 11 / macOS / Ubuntu] +- **Node.js 版本**: [例如 20.0.0] +- **Python 版本**: [例如 3.12] +- **Wrangler 版本**: [例如 4.x] +- **浏览器**: [如适用] + +## 截图或日志 + +如有截图或错误日志,请贴在这里。 + +## 补充说明 + +任何其他上下文信息。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..e455e8b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: 功能建议 +about: 为项目提出新功能或改进建议 +title: "[Feature] " +labels: enhancement +--- + +## 功能描述 + +清晰简洁地描述你想要的功能或改进。 + +## 使用场景 + +描述这个功能会在什么场景下使用,解决什么问题。 + +## 可能的实现方案 + +如果你有实现思路,可以简要描述。 + +## 替代方案 + +是否考虑过其他替代方案? + +## 补充说明 + +任何其他上下文信息。 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..17e1196 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,26 @@ +## 描述 + +请简要描述这个 PR 的内容和目的。 + +## 变更类型 + +- [ ] Bug 修复 +- [ ] 新功能 +- [ ] 文档更新 +- [ ] 代码重构 +- [ ] 性能优化 +- [ ] 其他 + +## 检查清单 + +- [ ] 代码已测试,本地 `npm run dev` 可正常运行 +- [ ] 没有提交敏感信息(API Key、账号等) +- [ ] 没有提交原始图片到根目录 `mobile-image/` / `desketop-image/` +- [ ] 相关文档已更新(README / CONTRIBUTING) +- [ ] 提交信息符合 Conventional Commits 规范 + +## 关联 Issue + +如有,请填写: + +Fixes #(issue 编号) diff --git a/.gitignore b/.gitignore index 9a1cd7a..2b03e70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,68 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build artifacts .build-venv/ .wrangler/ __pycache__/ *.pyc + +# Logs +*.log +npm-debug.log* + +# Source images (only numbered .webp should be tracked; original images stay local) +# Ignore everything in source dirs +/mobile-image/* +/desketop-image/* + +# Keep .gitkeep to preserve empty directories +!/mobile-image/.gitkeep +!/desketop-image/.gitkeep + +# Keep only purely numbered .webp in source dirs (generated build artifacts) +# e.g. 1.webp, 10.webp, 123.webp — but NOT 3yfvfgrD.webp +!/mobile-image/[0-9].webp +!/mobile-image/[0-9][0-9].webp +!/mobile-image/[0-9][0-9][0-9].webp +!/mobile-image/[0-9][0-9][0-9][0-9].webp + +!/desketop-image/[0-9].webp +!/desketop-image/[0-9][0-9].webp +!/desketop-image/[0-9][0-9][0-9].webp +!/desketop-image/[0-9][0-9][0-9][0-9].webp + +# Ignore all original image formats (redundant safety net) +/mobile-image/*.jpg +/mobile-image/*.jpeg +/mobile-image/*.png +/mobile-image/*.gif +/mobile-image/*.bmp +/mobile-image/*.tif +/mobile-image/*.tiff + +/desketop-image/*.jpg +/desketop-image/*.jpeg +/desketop-image/*.png +/desketop-image/*.gif +/desketop-image/*.bmp +/desketop-image/*.tif +/desketop-image/*.tiff + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Editor / IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Wrangler local state +.dev.vars +.env +.env.local diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..394692c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# 贡献指南 + +感谢你对 RandBG API 项目的关注!我们欢迎任何形式的贡献,包括但不限于提交 Issue、修复 Bug、改进文档、新增功能等。 + +## 如何贡献 + +### 提交 Issue + +如果你发现了 Bug 或有新功能建议,请通过 [GitHub Issues](https://github.com/shumengya/randbg-api/issues) 提交。 + +提交 Issue 时,请尽可能包含以下信息: + +- **问题描述**:清晰描述你遇到的问题或建议 +- **复现步骤**:如果是 Bug,请提供详细的复现步骤 +- **环境信息**:操作系统、Node.js 版本、Python 版本等 +- **预期结果**与**实际结果** +- **截图或日志**(如有) + +### 提交 Pull Request + +1. **Fork 本仓库** 并克隆到本地 +2. **创建分支**:从 `main` 分支创建一个新的功能分支 + ```bash + git checkout -b feature/your-feature-name + ``` +3. **进行修改**:确保代码风格与现有项目保持一致 +4. **测试**:在本地运行 `npm run dev` 确保修改不会破坏现有功能 +5. **提交**:使用清晰的提交信息 + ```bash + git commit -m "feat: 添加 xxx 功能" + ``` +6. **推送并创建 PR**:推送到你的 Fork 仓库,然后向本仓库提交 Pull Request + +## 代码规范 + +- **JavaScript**:使用 ES Module,保持简洁,添加必要的 JSDoc 注释 +- **Python**:遵循 PEP 8 规范,使用类型注解(如有) +- **Git 提交信息**:参考 [Conventional Commits](https://www.conventionalcommits.org/) + - `feat:` 新功能 + - `fix:` 修复 Bug + - `docs:` 文档更新 + - `refactor:` 代码重构 + - `chore:` 构建/工具链更新 + +## 项目结构说明 + +- `src/`:Worker 运行时代码(API 逻辑) +- `public/`:静态资源(会被部署到 Cloudflare Assets) + - `index.html`:演示页面 + - `manifest.json`:图片清单(由 `build.py` 自动生成) + - `mobile-image/` / `desketop-image/`:构建后的 WebP 图片 +- `mobile-image/` / `desketop-image/`(根目录):**原始图片存放目录,不要提交到 Git** +- `build.py`:构建脚本,将原图转换为 WebP 并生成清单 + +## 注意事项 + +- **不要提交原始图片**:根目录下的 `mobile-image/` 和 `desketop-image/` 仅用于本地构建,已配置 `.gitignore` 忽略非编号文件。 +- **不要提交敏感信息**:`wrangler.toml` 中不应包含 `account_id` 或 API Token。 +- **保持 API 兼容性**:如果修改 API 接口,请确保向后兼容,或在文档中明确标注 Breaking Changes。 + +## 行为准则 + +- 尊重他人,保持友善和建设性的沟通 +- 接受不同的观点和经验 +- 关注对社区最有利的事情 + +再次感谢你的贡献! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d17fa7b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 shumengya + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d27d357..58b236b 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,236 @@ -# 随机背景API +# RandBG API - 随机背景图片服务 -一个可直接部署到 Cloudflare Pages 的静态站点 + Pages Functions API,用来按设备方向随机返回背景图。 +[![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare&logoColor=white)](https://workers.cloudflare.com/) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Demo](https://img.shields.io/badge/在线体验-randbg.smyhub.com-28a745)](https://randbg.smyhub.com) + +> 一个基于 **Cloudflare Workers** 部署的轻量级随机背景图片 API,支持自动识别设备类型(移动端/桌面端),并提供优雅的 Web 演示界面。 + +--- + +## 在线体验 + +- **在线演示**: [https://randbg.smyhub.com](https://randbg.smyhub.com) +- **GitHub 仓库**: [https://github.com/shumengya/randbg-api](https://github.com/shumengya/randbg-api) + +--- + +## 特性 + +- **自动设备识别**:根据 User-Agent 和 Client Hints 自动判断移动端/桌面端 +- **双重响应格式**:支持 `302 Redirect` 直接跳转图片,或 `JSON` 返回图片元数据 +- **静态资源优化**:使用 WebP 格式,配合 Cloudflare 静态资源缓存,加载极速 +- **智能构建流程**:`build.py` 自动将原图转换为编号 WebP 并生成 `manifest.json` +- **一体化部署**:Worker 负责 API 逻辑,静态页面与图片通过 Assets 提供,单次部署即发布前后端 +- **零成本部署**:基于 Cloudflare Workers Free Tier,完全免费 + +--- + +## API 文档 + +### `GET /api/random` + +随机返回一张背景图片。 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `mode` | string | `auto` | 强制指定类型:`auto` / `mobile` / `desktop` | +| `orientation` | string | `auto` | 屏幕方向:`portrait` / `landscape` | +| `device` | string | `auto` | 设备类型:`mobile` / `desktop` | +| `format` | string | `redirect` | 响应格式:`redirect`(302 跳转)/ `json` | + +**示例请求:** + +```bash +# 默认 302 跳转到随机图片 +curl https://randbg.smyhub.com/api/random + +# 返回 JSON +curl https://randbg.smyhub.com/api/random?format=json +# 响应: +# { +# "variant": "desktop", +# "folder": "desketop-image", +# "filename": "7.webp", +# "url": "https://randbg.smyhub.com/desketop-image/7.webp", +# "count": 19 +# } + +# 强制使用移动端图片 +curl https://randbg.smyhub.com/api/random?mode=mobile + +# 强制桌面端并返回 JSON +curl https://randbg.smyhub.com/api/random?mode=desktop&format=json +``` + +### `GET /api/list` + +返回各变体的图片清单与统计信息。 + +```bash +curl https://randbg.smyhub.com/api/list +``` + +**响应示例:** + +```json +{ + "generated_at": "2026-05-18T11:45:45.514521+00:00", + "variants": { + "mobile": { + "folder": "mobile-image", + "count": 6, + "images": ["1.webp", "2.webp", "3.webp", "4.webp", "5.webp", "6.webp"] + }, + "desktop": { + "folder": "desketop-image", + "count": 19, + "images": ["1.webp", "2.webp", ...] + } + } +} +``` + +### `GET /manifest.json` + +静态清单文件,由 `build.py` 自动生成,记录各变体的目录与文件名列表。 + +--- + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| Runtime | [Cloudflare Workers](https://workers.cloudflare.com/) | +| 静态托管 | Cloudflare Workers Static Assets | +| 构建脚本 | Python 3 + [Pillow](https://pillow.readthedocs.io/) | +| 前端 | Vanilla HTML / CSS / JS | + +--- + +## 项目结构 + +``` +randbg-api/ +├── src/ +│ ├── worker.js # Workers 入口路由 +│ ├── api/ +│ │ ├── random.js # /api/random 接口逻辑 +│ │ └── list.js # /api/list 接口逻辑 +│ └── lib/ +│ └── background.js # 通用工具(清单加载、设备识别、URL 构建) +├── public/ +│ ├── index.html # 演示与文档页面 +│ ├── manifest.json # 图片清单(build.py 生成) +│ ├── _headers # 静态资源 HTTP 响应头 +│ ├── mobile-image/ # 移动端 WebP 图片(构建产物) +│ └── desketop-image/ # 桌面端 WebP 图片(构建产物) +├── mobile-image/ # 原始图片(本地工作目录,不提交到 Git) +├── desketop-image/ # 原始图片(本地工作目录,不提交到 Git) +├── build.py # 构建脚本:原图 → WebP + manifest.json +├── wrangler.toml # Workers 部署配置 +├── package.json # 依赖与脚本 +└── README.md # 本文档 +``` + +> **注意**:根目录下的 `mobile-image/` 和 `desketop-image/` 仅用于本地存放原始素材,不应提交到 GitHub。`build.py` 会将转换后的编号 WebP 输出到 `public/` 目录,只有 `public/` 下的内容会被部署。 + +--- + +## 部署指南 + +### 前置条件 + +- [Cloudflare 账号](https://dash.cloudflare.com/) +- [Node.js](https://nodejs.org/) 18+ +- [Python 3](https://python.org/) 3.9+ +- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) + +### 1. 克隆项目 + +```bash +git clone https://github.com/shumengya/randbg-api.git +cd randbg-api +``` + +### 2. 安装依赖 + +```bash +npm install +``` + +### 3. 准备图片 + +将原始图片放入对应目录: + +``` +mobile-image/ # 竖屏图片(移动端) +desketop-image/ # 横屏图片(桌面端) +``` + +支持格式:`.jpg` `.jpeg` `.png` `.bmp` `.gif` `.tif` `.tiff` `.webp` + +### 4. 构建 WebP 与清单 + +```bash +python3 build.py +``` + +脚本会自动完成以下工作: +1. 将原图转换为高质量编号 WebP(`1.webp` `2.webp` ...) +2. 生成 `public/manifest.json` +3. 将转换后的文件输出到 `public/` 部署目录 + +如果 `public/` 下已有序号 WebP 且没有原图,脚本会**保留现有文件**,不会误删。 + +### 5. 本地预览 + +```bash +npm run dev +``` + +默认在 `http://localhost:8787` 启动预览。 + +### 6. 部署到 Cloudflare + +```bash +npm run deploy +``` + +Wrangler 会将 `src/worker.js` 打包为 Worker,同时将 `public/` 作为静态 Assets 一起发布。 + +--- ## 目录约定 -- `mobile-image/`:竖屏图片原图上传目录 -- `desketop-image/`:横屏图片原图上传目录 +- **`public/mobile-image/`**:竖屏图片(移动端) +- **`public/desketop-image/`**:横屏图片(桌面端) + - 注意拼写沿用历史目录名,API 内部已兼容 +- **`public/manifest.json`**:构建产物,API 运行时通过内部请求读取该清单 -## 使用流程 +--- -1. 把图片上传到对应目录。 -2. 运行构建脚本: - ```bash - python3 build.py - ``` -3. 把项目根目录作为 Cloudflare Pages 的发布目录部署。 -4. 打开: - - 页面:`/` - - API:`/api/random` +## 注意事项 -## API +1. **原图不上传**:根目录下的 `mobile-image/` 和 `desketop-image/` 仅用于本地构建,已配置 `.gitignore` 避免误提交。GitHub 仓库中只保留构建后的 `public/` 资源。 +2. **图片版权**:请确保上传的图片拥有合法使用权或来自版权友好的来源。 +3. **Worker 体积限制**:静态资源通过 Cloudflare Assets 提供,不占用 Worker 脚本 4 MiB 限制,可放心使用大量图片。 +4. **缓存策略**:`public/_headers` 已为图片配置了 `immutable` 长期缓存,为 `manifest.json` 配置了 5 分钟缓存。如需更新图片,请重新运行 `build.py` 并部署。 -- `GET /api/random` - - 默认按 `mode=auto` 随机返回图片重定向 -- `GET /api/random?format=json` - - 返回 JSON -- `GET /api/list` - - 返回图片清单 +--- -## 组件嵌入 +## 开源协议 -```html - - - -``` +本项目基于 [MIT License](LICENSE) 开源。 -`widget.js` 只读取静态的 `manifest.json` 和图片文件,不会调用 `api/random`。 +--- -## 构建说明 +## 贡献 -`build.py` 会把目录里的原图转换成同目录下的编号 `webp` 文件,例如: +欢迎提交 Issue 和 Pull Request!如果你有任何建议或发现问题,请随时反馈。 -- `1.webp` -- `2.webp` -- `55.webp` +--- -同时生成 `manifest.json`,供 Pages Functions 读取。 +

+ Made with ❤️ by shumengya +

diff --git a/build.py b/build.py index 6b0859f..b680aad 100644 --- a/build.py +++ b/build.py @@ -4,6 +4,7 @@ from __future__ import annotations import json import os import re +import shutil import subprocess import sys import venv @@ -11,11 +12,24 @@ from datetime import datetime, timezone from pathlib import Path BASE_DIR = Path(__file__).resolve().parent -MANIFEST_PATH = BASE_DIR / "manifest.json" +PUBLIC_DIR = BASE_DIR / "public" +MANIFEST_PATH = PUBLIC_DIR / "manifest.json" VARIANTS = { - "mobile": [BASE_DIR / "mobile-image"], - "desktop": [BASE_DIR / "desketop-image"], + "mobile": { + "out": PUBLIC_DIR / "mobile-image", + "scan": [ + PUBLIC_DIR / "mobile-image", + BASE_DIR / "mobile-image", + ], + }, + "desktop": { + "out": PUBLIC_DIR / "desketop-image", + "scan": [ + PUBLIC_DIR / "desketop-image", + BASE_DIR / "desketop-image", + ], + }, } INPUT_EXTENSIONS = { @@ -81,14 +95,15 @@ def iter_source_files(directory: Path) -> list[Path]: return files -def select_source_directory(candidates: list[Path]) -> Path: - for directory in candidates: +def select_scan_directory(scan_candidates: list[Path]) -> Path: + """优先有原图的目录,其次有序号 webp;否则退回 scan 第一项(通常为 public下的输出目录)。""" + for directory in scan_candidates: if iter_source_files(directory): return directory - for directory in candidates: - if directory.exists(): + for directory in scan_candidates: + if sorted_generated_webp_names(directory): return directory - return candidates[0] + return scan_candidates[0] def clear_generated_files(directory: Path) -> None: @@ -100,6 +115,23 @@ def clear_generated_files(directory: Path) -> None: path.unlink() +def sorted_generated_webp_names(directory: Path) -> list[str]: + """已存在的序号 webp(无原图时再跑脚本不会清空)。""" + if not directory.exists(): + return [] + names = [ + path.name + for path in directory.iterdir() + if path.is_file() and GENERATED_NAME_RE.match(path.name) + ] + + def sort_key(entry: str) -> int: + return int(Path(entry).stem) + + names.sort(key=sort_key) + return names + + def convert_to_webp(source: Path, target: Path, image_mod, image_ops) -> None: with image_mod.open(source) as image: image = image_ops.exif_transpose(image) @@ -114,24 +146,47 @@ def convert_to_webp(source: Path, target: Path, image_mod, image_ops) -> None: image.save(target, format="WEBP", quality=92, method=6) -def build_variant(name: str, directories: list[Path], image_mod, image_ops) -> dict: - directory = select_source_directory(directories) - directory.mkdir(parents=True, exist_ok=True) - clear_generated_files(directory) +def copy_generated_webps(source_dir: Path, out_dir: Path, names: list[str]) -> None: + """将已有序号 webp 同步到 public 下的部署目录。""" + out_dir.mkdir(parents=True, exist_ok=True) + for entry in names: + shutil.copy2(source_dir / entry, out_dir / entry) + + +def build_variant(name: str, spec: dict[str, Path | list[Path]], image_mod, image_ops) -> dict: + scan_dir = select_scan_directory(list(spec["scan"])) + out_dir = spec["out"] + out_dir.mkdir(parents=True, exist_ok=True) + sources = iter_source_files(scan_dir) - sources = iter_source_files(directory) images: list[str] = [] - for index, source in enumerate(sources, start=1): - target = directory / f"{index}.webp" - try: - convert_to_webp(source, target, image_mod, image_ops) - images.append(target.name) - print(f"[{name}] {source.name} -> {target.name}") - except Exception as exc: - print(f"[{name}] skip {source.name}: {exc}", file=sys.stderr) + if sources: + clear_generated_files(out_dir) + for index, source in enumerate(sources, start=1): + target = out_dir / f"{index}.webp" + try: + convert_to_webp(source, target, image_mod, image_ops) + images.append(target.name) + rel_src = os.path.relpath(source.resolve(), BASE_DIR.resolve()) + print(f"[{name}] {rel_src} -> {target.name}") + except Exception as exc: + print(f"[{name}] skip {source.name}: {exc}", file=sys.stderr) + else: + images = sorted_generated_webp_names(out_dir) + if not images and scan_dir.resolve() != out_dir.resolve(): + legacy = sorted_generated_webp_names(scan_dir) + if legacy: + copy_generated_webps(scan_dir, out_dir, legacy) + images = legacy + print(f"[{name}] 已从 {scan_dir.relative_to(BASE_DIR)} 复制序号 webp ×{len(images)} -> public/") + if images: + if scan_dir.resolve() == out_dir.resolve(): + print(f"[{name}] 未检测到原图,保留 public 下序号 webp ×{len(images)}") + else: + print(f"[{name}] 跳过:未发现原图,且 public/ 与归档目录均无 1.webp 等序号文件") return { - "folder": directory.name, + "folder": out_dir.name, "count": len(images), "images": images, } @@ -149,8 +204,8 @@ def main() -> int: "variants": {}, } - for name, directories in VARIANTS.items(): - manifest["variants"][name] = build_variant(name, directories, image_mod, image_ops) + for name, variant_spec in VARIANTS.items(): + manifest["variants"][name] = build_variant(name, variant_spec, image_mod, image_ops) write_manifest(manifest) print(f"Wrote manifest: {MANIFEST_PATH}") diff --git a/desketop-image/3yfvfgrD.webp b/desketop-image/3yfvfgrD.webp deleted file mode 100644 index e8af8ec..0000000 Binary files a/desketop-image/3yfvfgrD.webp and /dev/null differ diff --git a/desketop-image/5CrdoShv.webp b/desketop-image/5CrdoShv.webp deleted file mode 100644 index 908add8..0000000 Binary files a/desketop-image/5CrdoShv.webp and /dev/null differ diff --git a/desketop-image/5E2wsNlc.webp b/desketop-image/5E2wsNlc.webp deleted file mode 100644 index e9adaea..0000000 Binary files a/desketop-image/5E2wsNlc.webp and /dev/null differ diff --git a/desketop-image/AGWXMclV.webp b/desketop-image/AGWXMclV.webp deleted file mode 100644 index c9463a9..0000000 Binary files a/desketop-image/AGWXMclV.webp and /dev/null differ diff --git a/desketop-image/IIbmur0t.webp b/desketop-image/IIbmur0t.webp deleted file mode 100644 index 0256344..0000000 Binary files a/desketop-image/IIbmur0t.webp and /dev/null differ diff --git a/desketop-image/ItOJOHST.webp b/desketop-image/ItOJOHST.webp deleted file mode 100644 index ec5ff63..0000000 Binary files a/desketop-image/ItOJOHST.webp and /dev/null differ diff --git a/desketop-image/L0nQHehz.webp b/desketop-image/L0nQHehz.webp deleted file mode 100644 index 1139765..0000000 Binary files a/desketop-image/L0nQHehz.webp and /dev/null differ diff --git a/desketop-image/RK0sjvKj.webp b/desketop-image/RK0sjvKj.webp deleted file mode 100644 index dfb950f..0000000 Binary files a/desketop-image/RK0sjvKj.webp and /dev/null differ diff --git a/desketop-image/XI78fkzD.webp b/desketop-image/XI78fkzD.webp deleted file mode 100644 index c70ad70..0000000 Binary files a/desketop-image/XI78fkzD.webp and /dev/null differ diff --git a/desketop-image/atm7rt6D.webp b/desketop-image/atm7rt6D.webp deleted file mode 100644 index bc3eaf7..0000000 Binary files a/desketop-image/atm7rt6D.webp and /dev/null differ diff --git a/desketop-image/c2HxMuGK.webp b/desketop-image/c2HxMuGK.webp deleted file mode 100644 index 32dae92..0000000 Binary files a/desketop-image/c2HxMuGK.webp and /dev/null differ diff --git a/desketop-image/cUDkKiOf.webp b/desketop-image/cUDkKiOf.webp deleted file mode 100644 index 90846fb..0000000 Binary files a/desketop-image/cUDkKiOf.webp and /dev/null differ diff --git a/desketop-image/cuSpSkq4.webp b/desketop-image/cuSpSkq4.webp deleted file mode 100644 index 4a6e914..0000000 Binary files a/desketop-image/cuSpSkq4.webp and /dev/null differ diff --git a/desketop-image/hj64Cqxn.webp b/desketop-image/hj64Cqxn.webp deleted file mode 100644 index d09984a..0000000 Binary files a/desketop-image/hj64Cqxn.webp and /dev/null differ diff --git a/desketop-image/mrSQGVzt.webp b/desketop-image/mrSQGVzt.webp deleted file mode 100644 index dcb0c23..0000000 Binary files a/desketop-image/mrSQGVzt.webp and /dev/null differ diff --git a/desketop-image/mtQKTMpU.webp b/desketop-image/mtQKTMpU.webp deleted file mode 100644 index b43d8dd..0000000 Binary files a/desketop-image/mtQKTMpU.webp and /dev/null differ diff --git a/desketop-image/nhS9Wnnh.webp b/desketop-image/nhS9Wnnh.webp deleted file mode 100644 index 9d9815c..0000000 Binary files a/desketop-image/nhS9Wnnh.webp and /dev/null differ diff --git a/desketop-image/xTsVkCli.webp b/desketop-image/xTsVkCli.webp deleted file mode 100644 index 2c6433f..0000000 Binary files a/desketop-image/xTsVkCli.webp and /dev/null differ diff --git a/desketop-image/zr6atT6m.webp b/desketop-image/zr6atT6m.webp deleted file mode 100644 index 028b9ea..0000000 Binary files a/desketop-image/zr6atT6m.webp and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index 93b95ed..0000000 --- a/index.html +++ /dev/null @@ -1,390 +0,0 @@ - - - - - - 随机背景 API - - - - -
-
-
-

随机背景 API

-

Cloudflare Pages 随机背景接口。上传图片后先用 `build.py` 生成序号 `.webp`,然后可通过 `/api/random` 和 `/api/list` 调用。

-
- 竖屏:mobile-image - 横屏:desketop-image - 返回:302 / JSON - 组件:widget.js -
-
- -
-

快速链接

- -
-
- -
-
-

API 调用

-
-
- 随机图片 -
默认 302 跳转到随机图片。
-
GET /api/random
-
-
- 返回 JSON -
适合前端自己处理图片地址。
-
GET /api/random?format=json
-
-
- -
-
- 强制竖屏 -
GET /api/random?mode=mobile
-
-
- 强制横屏 -
GET /api/random?mode=desktop
-
-
- -

支持参数:`mode=auto|mobile|desktop`、`orientation=portrait|landscape`、`device=mobile|desktop`、`format=redirect|json`。

-
- -
-

在线预览

-
-
- - -
-
- - -
-
- - -
- -
- -
-
- - -
- 随机背景预览 -
-
-
- -
{}
-
-
-
-
- -
-

嵌入组件

-
-
- 直接引用 -
<script src="https://random-background-api.pages.dev/widget.js"></script>
-<random-background-widget
-  src="https://random-background-api.pages.dev"
-  blur="12"
-  radius="16px"
-  height="260px">
-</random-background-widget>
-
-
- 说明 -
组件只读静态 `manifest.json` 和图片,不消耗 Functions 额度;`blur` 控制高斯模糊像素值。
-
-
-
- - -
- - - - diff --git a/mobile-image/1772108021881_44374ab6c1daa54e0204bca48ac382f2.png b/mobile-image/1772108021881_44374ab6c1daa54e0204bca48ac382f2.png deleted file mode 100644 index c2bdc79..0000000 Binary files a/mobile-image/1772108021881_44374ab6c1daa54e0204bca48ac382f2.png and /dev/null differ diff --git a/mobile-image/1772108021977_8020902a0c8788538eee1cd06e784c6a.png b/mobile-image/1772108021977_8020902a0c8788538eee1cd06e784c6a.png deleted file mode 100644 index 8ba092f..0000000 Binary files a/mobile-image/1772108021977_8020902a0c8788538eee1cd06e784c6a.png and /dev/null differ diff --git a/mobile-image/1772108022800_f945774e0a45f7a4afdc3da2b112025f.png b/mobile-image/1772108022800_f945774e0a45f7a4afdc3da2b112025f.png deleted file mode 100644 index fd16663..0000000 Binary files a/mobile-image/1772108022800_f945774e0a45f7a4afdc3da2b112025f.png and /dev/null differ diff --git a/mobile-image/1772108024006_3f9030ba77e355869115bc90fe019d53.png b/mobile-image/1772108024006_3f9030ba77e355869115bc90fe019d53.png deleted file mode 100644 index a514b02..0000000 Binary files a/mobile-image/1772108024006_3f9030ba77e355869115bc90fe019d53.png and /dev/null differ diff --git a/mobile-image/1772108030393_159bbf61f88b38475ee9144a2e8e4956.png b/mobile-image/1772108030393_159bbf61f88b38475ee9144a2e8e4956.png deleted file mode 100644 index 228aa28..0000000 Binary files a/mobile-image/1772108030393_159bbf61f88b38475ee9144a2e8e4956.png and /dev/null differ diff --git a/mobile-image/1772108123232_VJ86r.jpg b/mobile-image/1772108123232_VJ86r.jpg deleted file mode 100644 index 2814cd3..0000000 Binary files a/mobile-image/1772108123232_VJ86r.jpg and /dev/null differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..0ad85da --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "randbg-api", + "private": true, + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "devDependencies": { + "wrangler": "^4" + } +} diff --git a/_headers b/public/_headers similarity index 73% rename from _headers rename to public/_headers index 844f195..cb53805 100644 --- a/_headers +++ b/public/_headers @@ -2,10 +2,6 @@ Access-Control-Allow-Origin: * Cache-Control: public, max-age=300 -/widget.js - Access-Control-Allow-Origin: * - Cache-Control: public, max-age=300 - /mobile-image/* Cache-Control: public, max-age=31536000, immutable diff --git a/public/desketop-image/1.webp b/public/desketop-image/1.webp new file mode 100644 index 0000000..2c22953 Binary files /dev/null and b/public/desketop-image/1.webp differ diff --git a/public/desketop-image/10.webp b/public/desketop-image/10.webp new file mode 100644 index 0000000..6c8eb80 Binary files /dev/null and b/public/desketop-image/10.webp differ diff --git a/public/desketop-image/11.webp b/public/desketop-image/11.webp new file mode 100644 index 0000000..07a4d25 Binary files /dev/null and b/public/desketop-image/11.webp differ diff --git a/public/desketop-image/12.webp b/public/desketop-image/12.webp new file mode 100644 index 0000000..7ed3417 Binary files /dev/null and b/public/desketop-image/12.webp differ diff --git a/public/desketop-image/13.webp b/public/desketop-image/13.webp new file mode 100644 index 0000000..b587ec4 Binary files /dev/null and b/public/desketop-image/13.webp differ diff --git a/public/desketop-image/14.webp b/public/desketop-image/14.webp new file mode 100644 index 0000000..5afd06e Binary files /dev/null and b/public/desketop-image/14.webp differ diff --git a/public/desketop-image/15.webp b/public/desketop-image/15.webp new file mode 100644 index 0000000..7ee04a9 Binary files /dev/null and b/public/desketop-image/15.webp differ diff --git a/public/desketop-image/16.webp b/public/desketop-image/16.webp new file mode 100644 index 0000000..d239c20 Binary files /dev/null and b/public/desketop-image/16.webp differ diff --git a/public/desketop-image/17.webp b/public/desketop-image/17.webp new file mode 100644 index 0000000..0f27713 Binary files /dev/null and b/public/desketop-image/17.webp differ diff --git a/public/desketop-image/18.webp b/public/desketop-image/18.webp new file mode 100644 index 0000000..d281c39 Binary files /dev/null and b/public/desketop-image/18.webp differ diff --git a/public/desketop-image/19.webp b/public/desketop-image/19.webp new file mode 100644 index 0000000..1997f9f Binary files /dev/null and b/public/desketop-image/19.webp differ diff --git a/public/desketop-image/2.webp b/public/desketop-image/2.webp new file mode 100644 index 0000000..6825a63 Binary files /dev/null and b/public/desketop-image/2.webp differ diff --git a/public/desketop-image/3.webp b/public/desketop-image/3.webp new file mode 100644 index 0000000..a699097 Binary files /dev/null and b/public/desketop-image/3.webp differ diff --git a/public/desketop-image/4.webp b/public/desketop-image/4.webp new file mode 100644 index 0000000..21af0f9 Binary files /dev/null and b/public/desketop-image/4.webp differ diff --git a/public/desketop-image/5.webp b/public/desketop-image/5.webp new file mode 100644 index 0000000..d8ecf88 Binary files /dev/null and b/public/desketop-image/5.webp differ diff --git a/public/desketop-image/6.webp b/public/desketop-image/6.webp new file mode 100644 index 0000000..e1ef0d5 Binary files /dev/null and b/public/desketop-image/6.webp differ diff --git a/public/desketop-image/7.webp b/public/desketop-image/7.webp new file mode 100644 index 0000000..5d79439 Binary files /dev/null and b/public/desketop-image/7.webp differ diff --git a/public/desketop-image/8.webp b/public/desketop-image/8.webp new file mode 100644 index 0000000..daef5ea Binary files /dev/null and b/public/desketop-image/8.webp differ diff --git a/public/desketop-image/9.webp b/public/desketop-image/9.webp new file mode 100644 index 0000000..889147d Binary files /dev/null and b/public/desketop-image/9.webp differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..90a034e --- /dev/null +++ b/public/index.html @@ -0,0 +1,258 @@ + + + + + + 随机背景API + + + + +
+

随机背景 API

+

Cloudflare Workers 接口。先用 build.pypublic/ 下生成序号 .webpmanifest.json,再通过 /api/random/api/list 调用。

+

约定:mobile-image 竖屏,desketop-image 横屏。默认响应为 302;加 format=json 时返回 JSON。

+ + + + +

接口说明

+ +
+

GET /api/random

+

按设备/方向选一个文件名,302 跳到对应图片。

+
GET /api/random
+
+ +
+

GET /api/random?format=json

+

返回包含 url 等字段的 JSON。

+
GET /api/random?format=json
+
+ +
+

GET /api/random?mode=mobile / ?mode=desktop

+

强制使用竖屏或横屏素材目录。

+
GET /api/random?mode=mobile
+GET /api/random?mode=desktop
+
+ +

查询参数:mode=auto|mobile|desktoporientation=portrait|landscapedevice=mobile|desktopformat=redirect|json

+ +

在线预览

+
+
+ + +
+
+ + +
+
+ + +
+ +
+ +

+ + 随机背景预览 +

响应体

+
{}
+ + +
+ + + + diff --git a/manifest.json b/public/manifest.json similarity index 92% rename from manifest.json rename to public/manifest.json index fffe585..69c75a9 100644 --- a/manifest.json +++ b/public/manifest.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-03-28T07:14:07.314011+00:00", + "generated_at": "2026-05-18T11:45:45.514521+00:00", "variants": { "mobile": { "folder": "mobile-image", diff --git a/public/mobile-image/1.webp b/public/mobile-image/1.webp new file mode 100644 index 0000000..35d69e3 Binary files /dev/null and b/public/mobile-image/1.webp differ diff --git a/public/mobile-image/2.webp b/public/mobile-image/2.webp new file mode 100644 index 0000000..689e07f Binary files /dev/null and b/public/mobile-image/2.webp differ diff --git a/public/mobile-image/3.webp b/public/mobile-image/3.webp new file mode 100644 index 0000000..74f13f7 Binary files /dev/null and b/public/mobile-image/3.webp differ diff --git a/public/mobile-image/4.webp b/public/mobile-image/4.webp new file mode 100644 index 0000000..95dd2e1 Binary files /dev/null and b/public/mobile-image/4.webp differ diff --git a/public/mobile-image/5.webp b/public/mobile-image/5.webp new file mode 100644 index 0000000..f10b9a2 Binary files /dev/null and b/public/mobile-image/5.webp differ diff --git a/public/mobile-image/6.webp b/public/mobile-image/6.webp new file mode 100644 index 0000000..a37bf8a Binary files /dev/null and b/public/mobile-image/6.webp differ diff --git a/functions/api/list.js b/src/api/list.js similarity index 65% rename from functions/api/list.js rename to src/api/list.js index f3c34bb..35da14e 100644 --- a/functions/api/list.js +++ b/src/api/list.js @@ -1,8 +1,8 @@ import { getVariantPayload, loadManifest } from '../lib/background.js'; -export async function onRequestGet(context) { - const { request } = context; - const manifest = await loadManifest(request); +/** @param {Request} request @param {{ ASSETS?: { fetch(input: RequestInfo, init?: RequestInit): Promise } }} env */ +export async function handleListGet(request, env) { + const manifest = await loadManifest(request, env); return Response.json( { @@ -16,6 +16,7 @@ export async function onRequestGet(context) { ); } +/** @param {unknown} manifest @param {'mobile' | 'desktop'} variant */ function summarizeVariant(manifest, variant) { const { folder, images } = getVariantPayload(manifest, variant); return { folder, count: images.length, images }; diff --git a/functions/api/random.js b/src/api/random.js similarity index 82% rename from functions/api/random.js rename to src/api/random.js index cff8383..a2dddd8 100644 --- a/functions/api/random.js +++ b/src/api/random.js @@ -1,10 +1,10 @@ import { buildAssetUrl, detectVariant, getVariantPayload, loadManifest, pickRandom } from '../lib/background.js'; -export async function onRequestGet(context) { - const { request } = context; +/** @param {Request} request @param {{ ASSETS?: { fetch(input: RequestInfo, init?: RequestInit): Promise } }} env */ +export async function handleRandomGet(request, env) { const url = new URL(request.url); const format = (url.searchParams.get('format') || 'redirect').toLowerCase(); - const manifest = await loadManifest(request); + const manifest = await loadManifest(request, env); const variant = detectVariant(request); const { folder, images } = getVariantPayload(manifest, variant); diff --git a/functions/lib/background.js b/src/lib/background.js similarity index 89% rename from functions/lib/background.js rename to src/lib/background.js index f254805..f6a345b 100644 --- a/functions/lib/background.js +++ b/src/lib/background.js @@ -5,9 +5,11 @@ const FALLBACK_MANIFEST = { }, }; -export async function loadManifest(request) { +export async function loadManifest(request, env) { try { - const response = await fetch(new URL('/manifest.json', request.url)); + const url = new URL('/manifest.json', request.url); + const response = + env && env.ASSETS ? await env.ASSETS.fetch(new Request(url.toString())) : await fetch(url); if (!response.ok) return FALLBACK_MANIFEST; return await response.json(); } catch { diff --git a/src/worker.js b/src/worker.js new file mode 100644 index 0000000..d97b5c6 --- /dev/null +++ b/src/worker.js @@ -0,0 +1,38 @@ +import { handleListGet } from './api/list.js'; +import { handleRandomGet } from './api/random.js'; + +export default { + /** + * @param {Request} request + * @param {{ ASSETS: { fetch(input: Request | string, init?: RequestInit): Promise } }} env + */ + async fetch(request, env) { + const url = new URL(request.url); + const pathname = normalizePath(url.pathname); + + if (pathname === '/api/random') return routeApiGet(request, env, handleRandomGet); + if (pathname === '/api/list') return routeApiGet(request, env, handleListGet); + + return env.ASSETS.fetch(request); + }, +}; + +/** + * @param {string} pathname + */ +function normalizePath(pathname) { + if (pathname.length > 1 && pathname.endsWith('/')) return pathname.replace(/\/+$/, ''); + return pathname; +} + +/** + * @param {Request} request + * @param {{ ASSETS?: { fetch(input: RequestInfo, init?: RequestInit): Promise } }} env + * @param {(request: Request, env: object) => Promise | Response} handler + */ +async function routeApiGet(request, env, handler) { + if (request.method !== 'GET') { + return new Response('Method Not Allowed', { status: 405 }); + } + return handler(request, env); +} diff --git a/widget.js b/widget.js deleted file mode 100644 index 8f088ce..0000000 --- a/widget.js +++ /dev/null @@ -1,209 +0,0 @@ -(() => { - const DEFAULT_BASE_URL = (() => { - try { - return new URL(document.currentScript?.src || window.location.href).origin; - } catch { - return window.location.origin; - } - })(); - - const STYLE = ` - :host { - display: block; - width: var(--rbw-width, 100%); - height: var(--rbw-height, 260px); - } - .frame { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - border-radius: var(--rbw-radius, 14px); - background: #eef2f7; - border: 1px solid rgba(15, 23, 42, 0.08); - box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06); - } - img { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - object-fit: var(--rbw-fit, cover); - filter: blur(var(--rbw-blur, 0px)); - transform: scale(1.04); - opacity: 0; - transition: opacity 180ms ease; - will-change: filter, opacity, transform; - } - .state { - position: absolute; - inset: 0; - display: grid; - place-items: center; - padding: 16px; - text-align: center; - color: #5b657a; - font: 13px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; - background: linear-gradient(180deg, rgba(255,255,255,.45), rgba(255,255,255,.2)); - backdrop-filter: blur(4px); - } - :host([loaded]) .state, - :host([error]) .state { - opacity: 0; - pointer-events: none; - } - :host([loaded]) img { - opacity: 1; - } - `; - - const DEFAULTS = { - mode: 'auto', - blur: '0', - radius: '14px', - fit: 'cover', - height: '260px', - width: '100%', - }; - - class RandomBackgroundWidget extends HTMLElement { - static observedAttributes = ['src', 'mode', 'blur', 'radius', 'fit', 'height', 'width']; - - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - this._manifest = null; - this._requestSeq = 0; - this._boundRefresh = this.refresh.bind(this); - } - - connectedCallback() { - this.render(); - this.refresh(); - window.addEventListener('resize', this._boundRefresh, { passive: true }); - } - - disconnectedCallback() { - window.removeEventListener('resize', this._boundRefresh); - } - - attributeChangedCallback() { - if (this.isConnected) { - this.render(); - this.refresh(); - } - } - - get baseUrl() { - return (this.getAttribute('src') || DEFAULT_BASE_URL).replace(/\/+$/, ''); - } - - get mode() { - return (this.getAttribute('mode') || DEFAULTS.mode).toLowerCase(); - } - - get blur() { - const raw = Number.parseFloat(this.getAttribute('blur') || DEFAULTS.blur); - return Number.isFinite(raw) && raw > 0 ? raw : 0; - } - - get radius() { - return this.getAttribute('radius') || DEFAULTS.radius; - } - - get fit() { - return this.getAttribute('fit') || DEFAULTS.fit; - } - - get height() { - return this.getAttribute('height') || DEFAULTS.height; - } - - get width() { - return this.getAttribute('width') || DEFAULTS.width; - } - - render() { - if (!this.shadowRoot) return; - this.style.setProperty('--rbw-width', this.width); - this.style.setProperty('--rbw-height', this.height); - this.style.setProperty('--rbw-radius', this.radius); - this.style.setProperty('--rbw-fit', this.fit); - this.style.setProperty('--rbw-blur', `${this.blur}px`); - - this.shadowRoot.innerHTML = ` - -
- random background -
加载中
-
- `; - } - - async refresh() { - const seq = ++this._requestSeq; - const img = this.shadowRoot.querySelector('img'); - const state = this.shadowRoot.querySelector('.state'); - - try { - if (!this._manifest) { - this._manifest = await this.fetchManifest(); - } - if (seq !== this._requestSeq) return; - - const variant = this.resolveVariant(); - const data = this._manifest?.variants?.[variant] || {}; - const folder = data.folder || (variant === 'mobile' ? 'mobile-image' : 'desketop-image'); - const images = Array.isArray(data.images) ? data.images.filter((item) => typeof item === 'string') : []; - - if (!images.length) { - throw new Error(`no images in ${folder}`); - } - - const filename = images[Math.floor(Math.random() * images.length)]; - img.src = `${this.baseUrl}/${folder}/${filename}`; - img.alt = `random background ${variant}`; - this.setAttribute('loaded', ''); - this.removeAttribute('error'); - state.textContent = ''; - } catch (error) { - this.removeAttribute('loaded'); - this.setAttribute('error', ''); - state.textContent = error instanceof Error ? error.message : 'load failed'; - } - } - - resolveVariant() { - if (this.mode === 'mobile' || this.mode === 'desktop') return this.mode; - - const orientation = window.matchMedia('(orientation: portrait)').matches ? 'portrait' : 'landscape'; - const device = this.detectDevice(); - - if (orientation === 'portrait') return 'mobile'; - if (orientation === 'landscape') return 'desktop'; - return device; - } - - detectDevice() { - if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') { - return navigator.userAgentData.mobile ? 'mobile' : 'desktop'; - } - return /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ? 'mobile' : 'desktop'; - } - - async fetchManifest() { - const url = new URL('manifest.json', `${this.baseUrl}/`); - const response = await fetch(url.toString(), { mode: 'cors', cache: 'no-store' }); - if (!response.ok) { - throw new Error(`manifest ${response.status}`); - } - return await response.json(); - } - } - - if (!customElements.get('random-background-widget')) { - customElements.define('random-background-widget', RandomBackgroundWidget); - } - - window.RandomBackgroundWidget = RandomBackgroundWidget; -})(); diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..f24c20c --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,9 @@ +name = "randbg-api" +main = "src/worker.js" +compatibility_date = "2026-03-01" + +# 静态前端与 manifest / 图片均放在 public;仅 Worker 脚本被打包(API 体量极小) +[assets] +directory = "public" +binding = "ASSETS" +run_worker_first = true