Compare commits
2 Commits
f68e19a3bd
...
b8279e4c45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8279e4c45 | ||
| f45a0068a2 |
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -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]
|
||||
- **浏览器**: [如适用]
|
||||
|
||||
## 截图或日志
|
||||
|
||||
如有截图或错误日志,请贴在这里。
|
||||
|
||||
## 补充说明
|
||||
|
||||
任何其他上下文信息。
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: 功能建议
|
||||
about: 为项目提出新功能或改进建议
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
## 功能描述
|
||||
|
||||
清晰简洁地描述你想要的功能或改进。
|
||||
|
||||
## 使用场景
|
||||
|
||||
描述这个功能会在什么场景下使用,解决什么问题。
|
||||
|
||||
## 可能的实现方案
|
||||
|
||||
如果你有实现思路,可以简要描述。
|
||||
|
||||
## 替代方案
|
||||
|
||||
是否考虑过其他替代方案?
|
||||
|
||||
## 补充说明
|
||||
|
||||
任何其他上下文信息。
|
||||
26
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
## 描述
|
||||
|
||||
请简要描述这个 PR 的内容和目的。
|
||||
|
||||
## 变更类型
|
||||
|
||||
- [ ] Bug 修复
|
||||
- [ ] 新功能
|
||||
- [ ] 文档更新
|
||||
- [ ] 代码重构
|
||||
- [ ] 性能优化
|
||||
- [ ] 其他
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 代码已测试,本地 `npm run dev` 可正常运行
|
||||
- [ ] 没有提交敏感信息(API Key、账号等)
|
||||
- [ ] 没有提交原始图片到根目录 `mobile-image/` / `desketop-image/`
|
||||
- [ ] 相关文档已更新(README / CONTRIBUTING)
|
||||
- [ ] 提交信息符合 Conventional Commits 规范
|
||||
|
||||
## 关联 Issue
|
||||
|
||||
如有,请填写:
|
||||
|
||||
Fixes #(issue 编号)
|
||||
64
.gitignore
vendored
@@ -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
|
||||
|
||||
100
BLOG.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# [萌芽妙妙工具]RandBG API:一个零成本、可自托管的随机背景图片API
|
||||
|
||||
> **开源地址**: [github.com/shumengya/randbg-api](https://github.com/shumengya/randbg-api)
|
||||
> **开源协议**: [MIT License](https://github.com/shumengya/randbg-api/blob/main/LICENSE)
|
||||
> **在线体验**: [randbg.smyhub.com](https://randbg.smyhub.com)
|
||||
> **作者**: *树萌芽*
|
||||
|
||||
---
|
||||
|
||||
## 前言
|
||||
|
||||
平时写博客、做落地页,总需要随机背景图。用别人的服务怕挂、怕限速;自己搭又嫌重。我想要一个**够轻、够快、够便宜**的方案——最好不用维护服务器,直接部署就能跑。
|
||||
|
||||
Cloudflare Workers 的 Free Tier 每月 10 万次请求,对个人项目完全够用。问题是:图片放哪儿?
|
||||
|
||||
直接把图片塞进 Worker 脚本?不行,Worker 有 4 MiB 体积限制,几十张图直接撑爆。
|
||||
|
||||
所以我把架构拆成两层:Worker 只跑 API 逻辑,图片和前端页面全部走 **Workers Static Assets**。
|
||||
|
||||
---
|
||||
|
||||
## 项目架构
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ 用户请求 │────▶│ Cloudflare Worker│────▶│ Static Assets│
|
||||
│ │ │ /api/random │ │ WebP 图片 │
|
||||
└─────────────┘ └──────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
**Worker 端**:不到 200 行 JS,只做三件事——读 `manifest.json`、识别设备、302 或 JSON 响应。
|
||||
|
||||
**静态端**:`public/` 目录下放 `index.html`、`manifest.json`、编号 WebP 图片。`wrangler deploy` 一次把前后端一起发布。
|
||||
|
||||
这样做的好处很直接:
|
||||
- Worker 脚本极小,冷启动几乎无感
|
||||
- 图片走 Cloudflare CDN,全球缓存
|
||||
- 不涉及数据库,无状态,不存在宕机
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 设备识别
|
||||
|
||||
根据 `User-Agent` 和 `Sec-CH-UA-Mobile` 自动判断移动端/桌面端,返回对应方向的图片。你也可以手动指定:
|
||||
|
||||
```bash
|
||||
curl https://randbg.smyhub.com/api/random?mode=mobile
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
默认 `302 Redirect` 直接跳转到图片地址,浏览器里一行就能用:
|
||||
|
||||
```html
|
||||
<img src="https://randbg.smyhub.com/api/random" />
|
||||
```
|
||||
|
||||
加上 `?format=json` 返回元数据,方便前端自己控制加载逻辑:
|
||||
|
||||
```json
|
||||
{
|
||||
"variant": "desktop",
|
||||
"folder": "desketop-image",
|
||||
"filename": "7.webp",
|
||||
"url": "https://randbg.smyhub.com/desketop-image/7.webp",
|
||||
"count": 19
|
||||
}
|
||||
```
|
||||
|
||||
### 构建流程
|
||||
|
||||
写了个 `build.py`,功能单一但够用:
|
||||
|
||||
1. 扫描 `mobile-image/` 和 `desketop-image/` 目录
|
||||
2. 用 Pillow 把原图转为编号 WebP(`1.webp`、`2.webp`...)
|
||||
3. 输出到 `public/` 并生成 `manifest.json`
|
||||
4. 如果已有 WebP 且没有原图,自动保留,不会误删
|
||||
|
||||
```bash
|
||||
python3 build.py
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部署
|
||||
|
||||
整个部署流程是:
|
||||
|
||||
1. 把原图扔进 `mobile-image/` 和 `desketop-image/`
|
||||
2. `python3 build.py` 转 WebP
|
||||
3. `npm run deploy` 推到 Cloudflare
|
||||
|
||||
从改图到上线,本地测试用 `npm run dev`,本地 8787 端口就能跑。没有 Docker,没有数据库,没有环境变量要填。
|
||||
|
||||
---
|
||||
|
||||
如果你也需要一个零成本、不用维护的随机背景服务,可以直接 Fork 用。代码量很小,改起来不费劲。
|
||||
67
CONTRIBUTING.md
Normal file
@@ -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。
|
||||
|
||||
## 行为准则
|
||||
|
||||
- 尊重他人,保持友善和建设性的沟通
|
||||
- 接受不同的观点和经验
|
||||
- 关注对社区最有利的事情
|
||||
|
||||
再次感谢你的贡献!
|
||||
21
LICENSE
Normal file
@@ -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.
|
||||
260
README.md
@@ -1,54 +1,236 @@
|
||||
# 随机背景API
|
||||
# RandBG API - 随机背景图片服务
|
||||
|
||||
一个可直接部署到 Cloudflare Pages 的静态站点 + Pages Functions API,用来按设备方向随机返回背景图。
|
||||
[](https://workers.cloudflare.com/)
|
||||
[](LICENSE)
|
||||
[](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
|
||||
<script src="https://random-background-api.pages.dev/widget.js"></script>
|
||||
<random-background-widget
|
||||
src="https://random-background-api.pages.dev"
|
||||
mode="auto"
|
||||
blur="12"
|
||||
radius="16px"
|
||||
height="260px">
|
||||
</random-background-widget>
|
||||
```
|
||||
本项目基于 [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 读取。
|
||||
<p align="center">
|
||||
Made with ❤️ by <a href="https://github.com/shumengya">shumengya</a>
|
||||
</p>
|
||||
|
||||
390
Typora_Hook_Log.txt
Normal file
@@ -0,0 +1,390 @@
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync 重定向 D:\WindowsApp\Typora\resources\app\atom.compiled.dist.jsc --> D:\WindowsApp\Typora\resources\app.bak\atom.compiled.dist.jsc
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.openSync D:\WindowsApp\Typora\resources\app.bak\atom.compiled.dist.jsc
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\hjson\lib\hjson.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\util\assign.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\fs\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\node_modules\universalify\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\graceful-fs\graceful-fs.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\graceful-fs\polyfills.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\graceful-fs\legacy-streams.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\graceful-fs\clone.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\copy\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\copy\copy.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\copy\ncp.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\util\utimes.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\mkdirs\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\mkdirs\mkdirs.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\mkdirs\win32.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\mkdirs\mkdirs-sync.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\path-exists\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\copy-sync\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\copy-sync\copy-sync.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\copy-sync\copy-file-sync.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\util\buffer.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\remove\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\remove\rimraf.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\json\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\json\jsonfile.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\node_modules\jsonfile\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\json\output-json-sync.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\json\output-json.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\move\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\move-sync\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\empty\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\ensure\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\ensure\file.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\ensure\link.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\ensure\symlink.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\ensure\symlink-paths.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\ensure\symlink-type.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-extra\lib\output\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\electron-fetch\lib\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\electron-fetch\node_modules\encoding\lib\encoding.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\iconv-lite\lib\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\iconv-lite\node_modules\safer-buffer\safer.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\iconv-lite\lib\bom-handling.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\iconv-lite\lib\streams.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync C:\Users\smy\AppData\Roaming\Typora/conf/conf.user.json
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\lib\fs-plus.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\underscore-plus\lib\underscore-plus.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\underscore\underscore-node.cjs
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\underscore\underscore-node-f.cjs
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\async\lib\async.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\mkdirp\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\rimraf\rimraf.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\glob\glob.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\fs.realpath\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\fs.realpath\old.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\minimatch\minimatch.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\brace-expansion\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\concat-map\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\balanced-match\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\inherits\inherits.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\path-is-absolute\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\glob\sync.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\glob\common.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\inflight\inflight.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\wrappy\wrappy.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\fs-plus\node_modules\once\once.js
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.statSync d:\windowsapp\typora\typora.exe
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.statSync D:\WINDOWSAPP\TYPORA\TYPORA.EXE
|
||||
------------------
|
||||
[2026/6/6 21:02:10] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/typora.log
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\lowdb\lib\main.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\lowdb\node_modules\lodash\lodash.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\lowdb\node_modules\is-promise\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\lowdb\adapters\FileSync.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\lowdb\adapters\Base.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\lowdb\adapters\_stringify.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync C:\Users\smy\AppData\Roaming\Typora\profile.data
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync C:\Users\smy\AppData\Roaming\Typora\history.data
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\native-reg\lib\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFile D:\WindowsApp\Typora\resources\locales\zh-Hans.lproj\Panel.json
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFile D:\WindowsApp\Typora\resources\locales\zh-Hans.lproj\Panel.json
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.statSync D:\SmyProjects\CloudFlare\CloudFlare Workers\randbg-api\BLOG.md
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFile D:\WindowsApp\Typora\resources\locales\zh-Hans.lproj\Menu.json
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\electron-progressbar\source\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFileSync D:\WindowsApp\Typora\resources\node_modules.asar\electron-progressbar\node_modules\extend\index.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFile D:\WindowsApp\Typora\resources\locales\zh-Hans.lproj\Menu.json
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.readFile C:\Users\smy\AppData\Roaming\Typora\typora-dictionaries/user-dict.json
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Local\Temp\Typora
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook/Promises] 程序试图 fs.promises.readFile 重定向 D:\WindowsApp\Typora\resources\app/package.json --> D:\WindowsApp\Typora\resources\app.bak\package.json
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook/Promises] 程序试图 fs.promises.readFile 重定向 D:\WindowsApp\Typora\resources\app/launch.dist.js --> D:\WindowsApp\Typora\resources\app.bak\launch.dist.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook/Promises] 程序试图 fs.promises.readFile 重定向 D:\WindowsApp\Typora\resources\app/../page-dist/license.html --> D:\WindowsApp\Typora\resources\app.bak\../page-dist/license.html
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook/Promises] 程序试图 fs.promises.readFile 重定向 D:\WindowsApp\Typora\resources\app/../page-dist/static/js/LicenseIndex.180dd4c7.5b58fa97.js --> D:\WindowsApp\Typora\resources\app.bak\../page-dist/static/js/LicenseIndex.180dd4c7.5b58fa97.js
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:02:11] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:02:41] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\draftsRecover
|
||||
------------------
|
||||
[2026/6/6 21:02:58] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:02:58] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:02] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:02] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:05] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:05] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:07] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:07] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:08] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:08] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:09] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:09] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:11] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:11] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:12] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:12] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:45] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:45] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:46] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:46] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:47] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:47] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:49] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:49] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:51] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:51] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:52] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:52] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:03:54] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:03:54] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:01] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:01] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:09] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:09] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:21] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:21] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:26] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:26] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:29] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:29] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:31] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:31] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:34] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:34] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:35] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:35] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:36] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:36] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:43] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:43] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:45] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:45] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:50] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:50] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:53] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:53] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:54] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:54] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:04:55] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:04:55] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:05:09] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:05:09] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:05:12] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:05:12] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:05:14] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:05:14] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:06:50] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:06:50] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:07:28] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:07:28] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:07:30] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:07:30] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:07:37] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:07:37] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:07:39] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:07:39] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:07:42] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:07:42] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:07:52] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:07:52] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:08:01] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:08:01] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:08:07] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:08:07] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:08:09] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:08:09] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:08:11] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:08:11] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:08:12] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:08:12] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:08:14] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:08:14] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:08:23] [Log] [🛡️ fsHook] 程序试图 fs.stat C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:08:23] [Log] [🛡️ fsHook] 程序试图 fs.open C:\Users\smy\AppData\Roaming\Typora/backups/sum
|
||||
------------------
|
||||
[2026/6/6 21:11:00] [Log] [🛡️ fsHook] 程序试图 fs.statSync C:\Users\smy\AppData\Roaming\Typora\backups
|
||||
------------------
|
||||
[2026/6/6 21:11:00] [Log] [🛡️ fsHook] 程序试图 fs.statSync C:\Users\smy\AppData\Roaming\Typora/typora.log
|
||||
------------------
|
||||
103
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}")
|
||||
|
||||
|
Before Width: | Height: | Size: 939 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 938 KiB |
|
Before Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 684 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 728 KiB |
|
Before Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 622 KiB |
|
Before Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 634 KiB |
|
Before Width: | Height: | Size: 345 KiB |
390
index.html
@@ -1,390 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>随机背景 API</title>
|
||||
<meta name="description" content="Cloudflare Pages 随机背景 API 文档与在线预览。">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f7fb;
|
||||
--panel: #fff;
|
||||
--text: #172033;
|
||||
--muted: #5b657a;
|
||||
--line: #e3e7ef;
|
||||
--soft: #f3f5f9;
|
||||
--accent: #2563eb;
|
||||
--code: #0b1220;
|
||||
--shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff 0%, var(--bg) 100%);
|
||||
color: var(--text);
|
||||
font: 14px/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
.wrap {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: minmax(0, 1fr) 250px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pad { padding: 12px; }
|
||||
|
||||
h1, h2, p { margin: 0; }
|
||||
h1 {
|
||||
font-size: clamp(24px, 3vw, 34px);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 9px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--soft);
|
||||
font-size: 11px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.links {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.endpoint strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.code {
|
||||
margin: 0;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--code);
|
||||
color: #e7eefc;
|
||||
overflow-x: auto;
|
||||
font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
select, button, input {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) 100px;
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.preview-box {
|
||||
aspect-ratio: 16 / 10;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
.preview-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.small {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.top, .main, .grid-2, .preview-grid, .row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="top">
|
||||
<div class="card pad">
|
||||
<h1>随机背景 API</h1>
|
||||
<p class="lead">Cloudflare Pages 随机背景接口。上传图片后先用 `build.py` 生成序号 `.webp`,然后可通过 `/api/random` 和 `/api/list` 调用。</p>
|
||||
<div class="meta">
|
||||
<span class="tag">竖屏:mobile-image</span>
|
||||
<span class="tag">横屏:desketop-image</span>
|
||||
<span class="tag">返回:302 / JSON</span>
|
||||
<span class="tag">组件:widget.js</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card pad">
|
||||
<h2>快速链接</h2>
|
||||
<div class="links">
|
||||
<a href="/api/list" target="_blank" rel="noreferrer">/api/list</a>
|
||||
<a href="/manifest.json" target="_blank" rel="noreferrer">manifest.json</a>
|
||||
<a href="/mobile-image/1.webp" target="_blank" rel="noreferrer">mobile-image/1.webp</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="main">
|
||||
<div class="card pad section" id="api">
|
||||
<h2>API 调用</h2>
|
||||
<div class="grid-2">
|
||||
<div class="endpoint">
|
||||
<strong>随机图片</strong>
|
||||
<div class="small">默认 302 跳转到随机图片。</div>
|
||||
<pre class="code">GET /api/random</pre>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<strong>返回 JSON</strong>
|
||||
<div class="small">适合前端自己处理图片地址。</div>
|
||||
<pre class="code">GET /api/random?format=json</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="endpoint">
|
||||
<strong>强制竖屏</strong>
|
||||
<pre class="code">GET /api/random?mode=mobile</pre>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<strong>强制横屏</strong>
|
||||
<pre class="code">GET /api/random?mode=desktop</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="small">支持参数:`mode=auto|mobile|desktop`、`orientation=portrait|landscape`、`device=mobile|desktop`、`format=redirect|json`。</p>
|
||||
</div>
|
||||
|
||||
<div class="card pad section" id="preview">
|
||||
<h2>在线预览</h2>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="modeSelect">mode</label>
|
||||
<select id="modeSelect">
|
||||
<option value="auto">auto</option>
|
||||
<option value="mobile">mobile</option>
|
||||
<option value="desktop">desktop</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="orientationSelect">orientation</label>
|
||||
<select id="orientationSelect">
|
||||
<option value="">auto</option>
|
||||
<option value="portrait">portrait</option>
|
||||
<option value="landscape">landscape</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formatSelect">format</label>
|
||||
<select id="formatSelect">
|
||||
<option value="json">json</option>
|
||||
<option value="redirect">redirect</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="refreshBtn" type="button">刷新</button>
|
||||
</div>
|
||||
|
||||
<div class="preview-grid">
|
||||
<div>
|
||||
<label for="apiUrl">当前地址</label>
|
||||
<input id="apiUrl" readonly value="">
|
||||
<div class="preview-box" style="margin-top:8px">
|
||||
<img id="previewImg" alt="随机背景预览" src="">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>返回结果</label>
|
||||
<pre class="code" id="jsonOutput">{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card pad section" style="margin-top:10px">
|
||||
<h2>嵌入组件</h2>
|
||||
<div class="grid-2">
|
||||
<div class="endpoint">
|
||||
<strong>直接引用</strong>
|
||||
<pre class="code"><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></pre>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<strong>说明</strong>
|
||||
<div class="small">组件只读静态 `manifest.json` 和图片,不消耗 Functions 额度;`blur` 控制高斯模糊像素值。</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="footer">
|
||||
构建命令:<code>python3 build.py</code>。这里保留 Pages Functions,所以可以直接提供 HTTP 随机 API。
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
const orientationSelect = document.getElementById('orientationSelect');
|
||||
const formatSelect = document.getElementById('formatSelect');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const apiUrl = document.getElementById('apiUrl');
|
||||
const previewImg = document.getElementById('previewImg');
|
||||
const jsonOutput = document.getElementById('jsonOutput');
|
||||
|
||||
function detectOrientation() {
|
||||
return window.matchMedia('(orientation: portrait)').matches ? 'portrait' : 'landscape';
|
||||
}
|
||||
|
||||
function 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';
|
||||
}
|
||||
|
||||
function buildApiPath() {
|
||||
const params = new URLSearchParams();
|
||||
params.set('mode', modeSelect.value);
|
||||
params.set('device', detectDevice());
|
||||
params.set('format', formatSelect.value);
|
||||
const orientation = orientationSelect.value || detectOrientation();
|
||||
params.set('orientation', orientation);
|
||||
return `/api/random?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const url = buildApiPath();
|
||||
apiUrl.value = url;
|
||||
previewImg.style.opacity = '0.72';
|
||||
|
||||
if (formatSelect.value === 'json') {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
jsonOutput.textContent = JSON.stringify(data, null, 2);
|
||||
previewImg.src = data.url || '';
|
||||
} else {
|
||||
jsonOutput.textContent = '{ "format": "redirect" }';
|
||||
previewImg.src = url;
|
||||
}
|
||||
}
|
||||
|
||||
previewImg.addEventListener('load', () => {
|
||||
previewImg.style.opacity = '1';
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener('click', refresh);
|
||||
modeSelect.addEventListener('change', refresh);
|
||||
orientationSelect.addEventListener('change', refresh);
|
||||
formatSelect.addEventListener('change', refresh);
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 310 KiB |
91
optimize_webp.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Re-compress existing .webp images with optimized parameters to reduce file size.
|
||||
|
||||
Usage:
|
||||
python optimize_webp.py
|
||||
|
||||
Backups originals to a .bak/ folder before overwriting.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
import shutil
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
PUBLIC_DIR = BASE_DIR / "public"
|
||||
|
||||
# Directories to optimize
|
||||
TARGET_DIRS = [
|
||||
PUBLIC_DIR / "mobile-image",
|
||||
PUBLIC_DIR / "desketop-image",
|
||||
]
|
||||
|
||||
# Compression parameters
|
||||
QUALITY = 80
|
||||
METHOD = 6
|
||||
|
||||
|
||||
def optimize_webp(src_path: Path, backup_dir: Path) -> tuple[int, int]:
|
||||
"""Re-encode a single WebP image with optimized quality settings.
|
||||
Returns (original_size, new_size) in bytes.
|
||||
"""
|
||||
original_size = src_path.stat().st_size
|
||||
backup_path = backup_dir / src_path.name
|
||||
|
||||
# Backup original
|
||||
shutil.copy2(src_path, backup_path)
|
||||
|
||||
# Re-encode
|
||||
img = Image.open(src_path)
|
||||
# For RGB/RGBA, keep as-is; for other modes, convert to RGBA
|
||||
if img.mode not in ("RGB", "RGBA"):
|
||||
img = img.convert("RGBA")
|
||||
img.save(src_path, format="WEBP", quality=QUALITY, method=METHOD)
|
||||
|
||||
new_size = src_path.stat().st_size
|
||||
return original_size, new_size
|
||||
|
||||
|
||||
def main():
|
||||
total_original = 0
|
||||
total_new = 0
|
||||
total_files = 0
|
||||
|
||||
for target_dir in TARGET_DIRS:
|
||||
if not target_dir.exists():
|
||||
print(f"Skip: {target_dir} not found")
|
||||
continue
|
||||
|
||||
backup_dir = target_dir.parent / (target_dir.name + ".bak")
|
||||
backup_dir.mkdir(exist_ok=True)
|
||||
|
||||
print(f"\n[DIR] Optimizing: {target_dir}")
|
||||
print("-" * 50)
|
||||
|
||||
for webp_file in sorted(target_dir.glob("*.webp"), key=lambda p: p.name.lower()):
|
||||
original_size, new_size = optimize_webp(webp_file, backup_dir)
|
||||
total_original += original_size
|
||||
total_new += new_size
|
||||
total_files += 1
|
||||
|
||||
reduction = original_size - new_size
|
||||
pct = (reduction / original_size) * 100 if original_size else 0
|
||||
status = "OK" if new_size < original_size else "WARN"
|
||||
print(
|
||||
f" [{status}] {webp_file.name:20s} "
|
||||
f"{original_size/1024:8.1f} KB -> {new_size/1024:8.1f} KB "
|
||||
f"(-{reduction/1024:8.1f} KB, {pct:5.1f}%)"
|
||||
)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f" Total files: {total_files}")
|
||||
print(f" Original size: {total_original/1024/1024:.2f} MB")
|
||||
print(f" New size: {total_new/1024/1024:.2f} MB")
|
||||
print(f" Saved: {(total_original - total_new)/1024/1024:.2f} MB ({(total_original - total_new)/total_original*100:.1f}%)")
|
||||
print("=" * 60)
|
||||
print(f"\nBackups saved to: {PUBLIC_DIR}/mobile-image.bak/ and desketop-image.bak/")
|
||||
print("If quality is not acceptable, restore from backup folders.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
11
package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "randbg-api",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
BIN
public/desketop-image.bak/1.webp
Normal file
|
After Width: | Height: | Size: 918 KiB |
BIN
public/desketop-image.bak/10.webp
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
public/desketop-image.bak/11.webp
Normal file
|
After Width: | Height: | Size: 647 KiB |
BIN
public/desketop-image.bak/12.webp
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
public/desketop-image.bak/13.webp
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
public/desketop-image.bak/14.webp
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/desketop-image.bak/15.webp
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/desketop-image.bak/16.webp
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
public/desketop-image.bak/17.webp
Normal file
|
After Width: | Height: | Size: 707 KiB |
BIN
public/desketop-image.bak/18.webp
Normal file
|
After Width: | Height: | Size: 609 KiB |
BIN
public/desketop-image.bak/19.webp
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
public/desketop-image.bak/2.webp
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/desketop-image.bak/3.webp
Normal file
|
After Width: | Height: | Size: 513 KiB |
BIN
public/desketop-image.bak/4.webp
Normal file
|
After Width: | Height: | Size: 918 KiB |
BIN
public/desketop-image.bak/5.webp
Normal file
|
After Width: | Height: | Size: 545 KiB |
BIN
public/desketop-image.bak/6.webp
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
public/desketop-image.bak/7.webp
Normal file
|
After Width: | Height: | Size: 608 KiB |
BIN
public/desketop-image.bak/8.webp
Normal file
|
After Width: | Height: | Size: 595 KiB |
BIN
public/desketop-image.bak/9.webp
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
public/desketop-image/1.webp
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
public/desketop-image/10.webp
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
public/desketop-image/11.webp
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
public/desketop-image/12.webp
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
public/desketop-image/13.webp
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
public/desketop-image/14.webp
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/desketop-image/15.webp
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
public/desketop-image/16.webp
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/desketop-image/17.webp
Normal file
|
After Width: | Height: | Size: 386 KiB |
BIN
public/desketop-image/18.webp
Normal file
|
After Width: | Height: | Size: 344 KiB |
BIN
public/desketop-image/19.webp
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
public/desketop-image/2.webp
Normal file
|
After Width: | Height: | Size: 563 KiB |
BIN
public/desketop-image/3.webp
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
public/desketop-image/4.webp
Normal file
|
After Width: | Height: | Size: 502 KiB |
BIN
public/desketop-image/5.webp
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
public/desketop-image/6.webp
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
public/desketop-image/7.webp
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
public/desketop-image/8.webp
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
public/desketop-image/9.webp
Normal file
|
After Width: | Height: | Size: 170 KiB |
258
public/index.html
Normal file
@@ -0,0 +1,258 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>随机背景API</title>
|
||||
<meta name="description" content="Cloudflare Workers:随机背景 API 说明与预览。">
|
||||
<style>
|
||||
:root {
|
||||
--text: #1a1a1a;
|
||||
--muted: #666;
|
||||
--border: #ddd;
|
||||
--code-bg: #f5f5f5;
|
||||
--accent: #06c;
|
||||
}
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 1.5rem 1.25rem 3rem;
|
||||
color: var(--text);
|
||||
font: 16px/1.6 ui-sans-serif, system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #fff;
|
||||
}
|
||||
a { color: var(--accent); }
|
||||
code {
|
||||
font: 0.92em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
background: var(--code-bg);
|
||||
padding: 0.15em 0.35em;
|
||||
}
|
||||
pre, .pre-like {
|
||||
margin: 0.5rem 0 1rem;
|
||||
padding: 0.65rem 0.85rem;
|
||||
background: var(--code-bg);
|
||||
font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.doc {
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
margin: 2rem 0 0.65rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
p { margin: 0 0 0.85rem; }
|
||||
.muted { color: var(--muted); font-size: 0.95rem; }
|
||||
ul {
|
||||
margin: 0 0 1rem;
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
li { margin: 0.2rem 0; }
|
||||
.api-item { margin-bottom: 1.1rem; }
|
||||
.api-item p { margin: 0.25rem 0 0.35rem; }
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1rem;
|
||||
align-items: flex-end;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
select {
|
||||
font: inherit;
|
||||
padding: 0.35rem 0.45rem;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
min-width: 6.5rem;
|
||||
}
|
||||
button {
|
||||
font: inherit;
|
||||
padding: 0.38rem 0.85rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { filter: brightness(1.05); }
|
||||
#apiUrl {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: 13px ui-monospace, Menlo, Consolas, monospace;
|
||||
padding: 0.45rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
background: #fafafa;
|
||||
}
|
||||
#previewImg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 14rem;
|
||||
object-fit: cover;
|
||||
margin-top: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--code-bg);
|
||||
}
|
||||
footer {
|
||||
margin-top: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--muted);
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.controls { flex-direction: column; align-items: stretch; }
|
||||
button { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="doc">
|
||||
<h1>随机背景 API</h1>
|
||||
<p>Cloudflare Workers 接口。先用 <code>build.py</code> 在 <code>public/</code> 下生成序号 <code>.webp</code> 与 <code>manifest.json</code>,再通过 <code>/api/random</code>、<code>/api/list</code> 调用。</p>
|
||||
<p class="muted">约定:<code>mobile-image</code> 竖屏,<code>desketop-image</code> 横屏。默认响应为 302;加 <code>format=json</code> 时返回 JSON。</p>
|
||||
|
||||
<h2 id="links">快捷链接</h2>
|
||||
<ul>
|
||||
<li><a href="/api/list" target="_blank" rel="noreferrer"><code>/api/list</code></a></li>
|
||||
<li><a href="/manifest.json" target="_blank" rel="noreferrer"><code>/manifest.json</code></a></li>
|
||||
<li><a href="/mobile-image/1.webp" target="_blank" rel="noreferrer"><code>/mobile-image/1.webp</code></a>(示例)</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="api">接口说明</h2>
|
||||
|
||||
<div class="api-item">
|
||||
<p><strong><code>GET /api/random</code></strong></p>
|
||||
<p class="muted">按设备/方向选一个文件名,302 跳到对应图片。</p>
|
||||
<pre>GET /api/random</pre>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<p><strong><code>GET /api/random?format=json</code></strong></p>
|
||||
<p class="muted">返回包含 <code>url</code> 等字段的 JSON。</p>
|
||||
<pre>GET /api/random?format=json</pre>
|
||||
</div>
|
||||
|
||||
<div class="api-item">
|
||||
<p><strong><code>GET /api/random?mode=mobile</code></strong> / <strong><code>?mode=desktop</code></strong></p>
|
||||
<p class="muted">强制使用竖屏或横屏素材目录。</p>
|
||||
<pre>GET /api/random?mode=mobile
|
||||
GET /api/random?mode=desktop</pre>
|
||||
</div>
|
||||
|
||||
<p class="muted">查询参数:<code>mode=auto|mobile|desktop</code>,<code>orientation=portrait|landscape</code>,<code>device=mobile|desktop</code>,<code>format=redirect|json</code>。</p>
|
||||
|
||||
<h2 id="preview">在线预览</h2>
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label for="modeSelect">mode</label>
|
||||
<select id="modeSelect">
|
||||
<option value="auto">auto</option>
|
||||
<option value="mobile">mobile</option>
|
||||
<option value="desktop">desktop</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="orientationSelect">orientation</label>
|
||||
<select id="orientationSelect">
|
||||
<option value="">auto</option>
|
||||
<option value="portrait">portrait</option>
|
||||
<option value="landscape">landscape</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="formatSelect">format</label>
|
||||
<select id="formatSelect">
|
||||
<option value="json">json</option>
|
||||
<option value="redirect">redirect</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="refreshBtn" type="button">刷新</button>
|
||||
</div>
|
||||
|
||||
<p><label for="apiUrl" class="muted" style="font-size: 0.75rem;">当前请求</label></p>
|
||||
<input id="apiUrl" readonly value="" autocomplete="off" spellcheck="false">
|
||||
<img id="previewImg" alt="随机背景预览" src="">
|
||||
<p><span class="muted" style="font-size: 0.75rem;">响应体</span></p>
|
||||
<pre class="pre-like" id="jsonOutput">{}</pre>
|
||||
|
||||
<footer>
|
||||
构建:<code>python3 build.py</code>。部署:<code>npm run deploy</code>(<code>wrangler deploy</code>)。Worker 仅含 API,静态页与图片来自 Assets。
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const modeSelect = document.getElementById('modeSelect');
|
||||
const orientationSelect = document.getElementById('orientationSelect');
|
||||
const formatSelect = document.getElementById('formatSelect');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const apiUrl = document.getElementById('apiUrl');
|
||||
const previewImg = document.getElementById('previewImg');
|
||||
const jsonOutput = document.getElementById('jsonOutput');
|
||||
|
||||
function detectOrientation() {
|
||||
return window.matchMedia('(orientation: portrait)').matches ? 'portrait' : 'landscape';
|
||||
}
|
||||
|
||||
function 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';
|
||||
}
|
||||
|
||||
function buildApiPath() {
|
||||
const params = new URLSearchParams();
|
||||
params.set('mode', modeSelect.value);
|
||||
params.set('device', detectDevice());
|
||||
params.set('format', formatSelect.value);
|
||||
const orientation = orientationSelect.value || detectOrientation();
|
||||
params.set('orientation', orientation);
|
||||
return `/api/random?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const url = buildApiPath();
|
||||
apiUrl.value = url;
|
||||
previewImg.style.opacity = '0.72';
|
||||
|
||||
if (formatSelect.value === 'json') {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
jsonOutput.textContent = JSON.stringify(data, null, 2);
|
||||
previewImg.src = data.url || '';
|
||||
} else {
|
||||
jsonOutput.textContent = '{ "format": "redirect" }';
|
||||
previewImg.src = url;
|
||||
}
|
||||
}
|
||||
|
||||
previewImg.addEventListener('load', () => {
|
||||
previewImg.style.opacity = '1';
|
||||
});
|
||||
|
||||
refreshBtn.addEventListener('click', refresh);
|
||||
modeSelect.addEventListener('change', refresh);
|
||||
orientationSelect.addEventListener('change', refresh);
|
||||
formatSelect.addEventListener('change', refresh);
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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",
|
||||
BIN
public/mobile-image.bak/1.webp
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
public/mobile-image.bak/2.webp
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
public/mobile-image.bak/3.webp
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
public/mobile-image.bak/4.webp
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
public/mobile-image.bak/5.webp
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
public/mobile-image.bak/6.webp
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
public/mobile-image/1.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/mobile-image/2.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
public/mobile-image/3.webp
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
public/mobile-image/4.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/mobile-image/5.webp
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
public/mobile-image/6.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
@@ -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<Response> } }} 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 };
|
||||
@@ -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<Response> } }} 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);
|
||||
|
||||
@@ -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 {
|
||||
38
src/worker.js
Normal file
@@ -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<Response> } }} 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<Response> } }} env
|
||||
* @param {(request: Request, env: object) => Promise<Response> | Response} handler
|
||||
*/
|
||||
async function routeApiGet(request, env, handler) {
|
||||
if (request.method !== 'GET') {
|
||||
return new Response('Method Not Allowed', { status: 405 });
|
||||
}
|
||||
return handler(request, env);
|
||||
}
|
||||
209
widget.js
@@ -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 = `
|
||||
<style>${STYLE}</style>
|
||||
<div class="frame">
|
||||
<img part="image" alt="random background">
|
||||
<div class="state">加载中</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
})();
|
||||
9
wrangler.toml
Normal file
@@ -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
|
||||