# Wupfish 资源站 API 文档(v1.9) | 项 | 内容 | |---|---| | 当前版本 | v2.0.7(2026-10-12) | | 配套实现 | `backend/src/` 模块化后端(仅 Python 标准库,需 Python 3.7+)+ Cloudflare D1 | | 运行方式 | `python backend/src/server.py`,默认监听 `127.0.0.1:8000`(需先配置 `backend/cloudflare.json`) | | 数据存储 | 管理员 / 登录会话 / 统计 / 镜像 → **Cloudflare D1**(库 `wupfish`);公告 / 首栏 / 弹窗 / 管理日志 / 优选监测 → `backend/data/` | | 接口前缀 | `/api/v1`(与站点同源部署) | | 状态 | **已实现,可直接运行**;前端 `content.js` / `stats.js` 已全站接入;管理面板 `sy/admin`(经 `sy/login` 登录),支持公告发布 / 镜像发布 / 首栏公告 / 弹窗公告 / 加速链接 / 统计概览 / 收录推送 / 登录日志;导航栏新增「API」入口,镜像加速页 `sy/accel` 与开放 API 目录页 `sy/api` 公开可用;公开只读接口已放开跨域(CORS);页面地址无后缀,旧 `.html` 301 归一 | > 本文档是接口的唯一权威规范:字段、长度、错误码均与 `backend/src/` 实现一一对应。 > 阅读顺序建议:先看「二、快速开始」跑通全链路,再按需查阅各节。 --- ## 目录 - [一、概述](#一概述) - [二、快速开始](#二快速开始) - [三、通用约定](#三通用约定) - [四、鉴权与登录(管理接口)](#四鉴权与登录管理接口) - [五、公告(announcements)](#五公告announcements) - [六、镜像(mirrors)](#六镜像mirrors) - [七、统计(stats)](#七统计stats) - [八、数据存储与运维](#八数据存储与运维) - [九、前端接入指南](#九前端接入指南) - [十、部署](#十部署) - [十一、安全注意事项](#十一安全注意事项) - [十二、错误码速查表](#十二错误码速查表) - [十三、FAQ / 故障排查](#十三faq--故障排查) - [十四、相对 v1.0 的变更](#十四相对-v10-的变更) - [十五、加速与优选(公开工具接口)](#十五加速与优选公开工具接口) - [十六、搜索引擎收录](#十六搜索引擎收录) - [十七、变更记录](#十七变更记录) --- ## 一、概述 ### 1.1 架构 ``` ┌──────────────────────────── 浏览器 ────────────────────────────┐ │ sy/*.html 静态页面 │ │ ├─ content.js ── 只读拉取公告/镜像(GET,失败静默降级) │ │ ├─ stats.js ── 埋点上报(POST pageview/event/api-call) │ │ └─ login.html ── 管理登录页(用户名+口令,持有会话 Cookie) │ └──────────────────────────────┬─────────────────────────────────┘ │ 同源 HTTP(默认 8000 端口) ┌──────────────────────────────┴─────────────────────────────────┐ │ backend/src/(模块化后端,零第三方依赖) │ │ ├─ server.py 入口:静态托管(含黑名单)+ 路由 │ │ ├─ login.py 登录 / 会话 / 日志 / 失败限速 │ │ ├─ d1.py Cloudflare D1 客户端(REST/代理) │ │ ├─ config.py · store.py · content.py · stats.py · geo.py │ │ ├─ /api/v1 只读接口:无鉴权,失败不影响页面 │ │ ├─ /api/v1/auth/*:登录/登出/状态(公开) │ │ ├─ /api/v1/admin/*:登录会话 Cookie 鉴权 │ │ ├─ /api/v1/stats/*:公开上报 + 聚合查询 │ │ └─ 存储:Cloudflare D1(管理员/统计/镜像)+ 本地 JSON(公告) │ └─────────────────────────────────────────────────────────────────┘ ``` ### 1.2 设计原则 1. **同源部署**:前端 SDK 使用相对路径 `/api/v1`,API 与静态页面必须同源(同一域名同一端口),或由反向代理把 `/api/*` 转发到本服务。 2. **只读接口零门槛**:无鉴权、无 CORS 限制(同源),且公开只读接口(集合列表 / banner / domains / accel / ping 查询 / stats 查询 / health)响应头带 `Access-Control-Allow-Origin: *`,第三方网站与脚本可直接跨域调用(管理 `/admin/*` 与鉴权 `/auth/*` 不放行);后端不可用时前端**静默降级**——公告页保留内置内容、统计页显示占位符,页面功能完全不受影响。 3. **写接口收口**:所有写操作集中在 `/api/v1/admin/*`,需**登录会话 Cookie**(登录见 4.2);网页端提供管理面板(`sy/admin`,经 `sy/login` 登录),脚本用 curl 两步 cookie jar(见 4.4)。 4. **零依赖**:服务端只用 Python 标准库,模块化部署于 `backend/src/`,数据就是 JSON 文件,备份 = 复制 `backend/data/` 目录。 5. **隐私最小化**:统计只保留聚合计数与按天去重的匿名会话 ID(`sid`);完整 IP 只在内存中临时用于地理解析,**不落盘**。 ### 1.3 接口总览 | # | 方法 | 路径 | 鉴权 | 用途 | |---|---|---|---|---| | 1 | GET | `/api/v1/health` | 无 | 健康检查(探活 / 监控) | | 2 | GET | `/api/v1/announcements` | 无 | 公告列表(公告页使用) | | 3 | GET | `/api/v1/mirrors` | 无 | 镜像列表(镜像页使用) | | 4 | POST | `/api/v1/stats/pageview` | 无 | 页面访问上报(前端自动) | | 5 | POST | `/api/v1/stats/event` | 无 | 业务事件上报(如 download) | | 6 | POST | `/api/v1/stats/api-call` | 无 | 外部 API 调用上报(前端自动) | | 7 | GET | `/api/v1/stats/summary` | 无 | 统计总览(统计页使用) | | 8 | GET | `/api/v1/stats/regions` | 无 | 地区分布(统计页使用) | | 9 | POST | `/api/v1/stats/download` | 无 | 镜像下载计数上报(前端自动,按镜像 id) | | 10 | GET | `/api/v1/stats/downloads` | 无 | 各镜像下载量排行(管理面板使用) | | 11 | POST | `/api/v1/auth/login` | 无 | 管理登录(校验口令,发放会话 Cookie) | | 12 | POST | `/api/v1/auth/logout` | 无 | 登出(清除会话 Cookie) | | 13 | GET | `/api/v1/auth/status` | 无 | 会话状态查询(登录页使用) | | 14 | POST | `/api/v1/admin/announcements` | 登录会话 | 发布公告 | | 15 | PUT | `/api/v1/admin/announcements/{id}` | 登录会话 | 更新公告(部分更新) | | 16 | DELETE | `/api/v1/admin/announcements/{id}` | 登录会话 | 删除公告 | | 17 | GET | `/api/v1/admin/announcements` | 登录会话 | 公告管理列表(含未上架 / 已下架) | | 18 | POST | `/api/v1/admin/mirrors` | 登录会话 | 发布镜像 | | 19 | PUT | `/api/v1/admin/mirrors/{id}` | 登录会话 | 更新镜像 | | 20 | DELETE | `/api/v1/admin/mirrors/{id}` | 登录会话 | 删除镜像(硬删) | | 21 | GET | `/api/v1/admin/logs` | 登录会话 | 管理日志(登录 / 登出 / 登录失败,倒序) | | 22 | GET | `/api/v1/banner` | 无 | 首栏公告(首页顶部,已下架返回 null) | | 23 | GET | `/api/v1/admin/banner` | 登录会话 | 首栏公告配置读取(含已下架) | | 24 | PUT | `/api/v1/admin/banner` | 登录会话 | 首栏公告发布 / 更新 / 下架 | | 25 | GET | `/api/v1/accel` | 无 | 加速链接生成(GitHub 链接 → 多节点加速地址) | | 26 | GET | `/api/v1/domains` | 无 | 优选域名清单(本地配置直出,见 15.2) | | 27 | GET | `/api/v1/ping/board` | 无 | 优选监测看板(24 小时延迟 / 丢包格图,见 15.4) | | 28 | POST | `/api/v1/ping/report` | 无 | 优选监测样本上报(访客浏览器实测,开放能力) | | 29 | GET | `/api/v1/ping/summary` | 无 | 各优选域名最近延迟(开放接口,见 15.4) | | 30 | GET | `/api/v1/nodes` | 无 | 自建加速节点状态(在线 / 延迟 / 在线率,见 15.5) | | 31 | GET | `/api/v1/admin/seo` | 登录会话 | 收录配置概览(IndexNow 密钥 / sitemap 页数) | | 32 | POST | `/api/v1/admin/seo/indexnow` | 登录会话 | 一键把 sitemap 全量推送给 IndexNow | | 33 | GET | `/api/v1/popup` | 无 | 弹窗公告(全站弹窗,已下架返回 null) | | 34 | GET | `/api/v1/admin/popup` | 登录会话 | 弹窗公告配置读取(含已下架) | | 35 | PUT | `/api/v1/admin/popup` | 登录会话 | 弹窗公告发布 / 更新 / 上架下架 | 另:静态站点托管不是本 API 的一部分,但由同一进程提供——`GET /` 会 302 跳转到 `/sy/`;**页面地址统一无后缀**(如 `/sy/mirror`),旧 `*.html` 地址 301 归一(见 3.8);仓库内文件按路径直出(`backend/`、`data/`、`*.py` 等安全黑名单除外)。 --- ## 二、快速开始 ### 2.1 启动服务 ```bat cd H:\forge python backend\src\server.py ``` 启动前需完成 D1 配置(见 4.1),启动输出示例: ``` ============================================================== Wupfish 资源站后端 v1.4.0 站点入口: http://127.0.0.1:8000/sy/index.html 管理登录: http://127.0.0.1:8000/sy/login.html(登录后进入 admin.html 管理面板) API 健康: http://127.0.0.1:8000/api/v1/health 静态根: H:\forge 数据目录: H:\forge\backend\data(公告 / 会话日志) 云数据库: Cloudflare D1(管理员 / 统计 / 镜像) e05eeb0a-...(数据库 ID) 地理解析: 开启(ip-api.com,失败自动降级为未知) ============================================================== ``` 浏览器打开 即可访问整站;日常管理登录 (用户名 `wupfish`,口令见 4.1),登录成功自动进入管理面板 `sy/admin.html`。 ### 2.2 两分钟跑通全链路 管理操作只需**登录会话**:浏览器打开 `/sy/login.html` 输入管理员用户名 + 口令(用户名 `wupfish`,见 4.1),登录成功自动进入管理面板;脚本 / 自动化用 curl 两步 cookie jar(见 4.4)。以下用 curl 演示: **第 1 步:健康检查** ```bat curl http://127.0.0.1:8000/api/v1/health ``` ```json {"ok": true, "version": "1.4.0", "time": 1760000000000} ``` **第 2 步:登录并保存会话 Cookie** ```bat curl -c cookie.txt -X POST http://127.0.0.1:8000/api/v1/auth/login ^ -H "Content-Type: application/json" ^ -d "{\"username\":\"wupfish\",\"password\":\"你的口令\"}" ``` **第 3 步:发布一条测试公告** ```bat curl -b cookie.txt -X POST http://127.0.0.1:8000/api/v1/admin/announcements ^ -H "Content-Type: application/json" ^ -d @announce.json ``` **第 4 步:前端可见** 打开 —— 公告列表会替换为 API 返回的内容(置顶的测试公告 + 首次启动自动播种的示例公告 `a-welcome`)。 **第 5 步:删掉测试数据** ```bat curl -b cookie.txt -X DELETE http://127.0.0.1:8000/api/v1/admin/announcements/ ``` 返回 `204` 即删除成功。 ### 2.3 启动参数 | 参数 | 默认值 | 说明 | |---|---|---| | `--host` | `127.0.0.1` | 监听地址。仅本机用默认值;局域网 / 反向代理场景用 `0.0.0.0` | | `--port` | `8000` | 监听端口 | | `--root` | 仓库根目录 | 静态站点根目录(`backend` 的上级目录) | | `--data-dir` | `backend/data` | 本地数据目录(公告 / 日志;可指向其他磁盘 / 挂载点) | | `--set-admin` | — | 启动时写入 / 更新 D1 管理员(`用户名:口令`,哈希+盐),用于重置凭证 | | `--no-geo` | 关闭 | 加上后不做 IP 地理解析,地区一律记为「未知」 | 环境变量:D1 凭证可用 `WF_CF_WORKER_URL` / `WF_CF_AUTH_TOKEN`(Worker 代理)或 `WF_CF_ACCOUNT_ID` / `WF_CF_D1_DATABASE_ID` / `WF_CF_API_TOKEN`(直连)覆盖配置文件(见 4.1)。 --- ## 三、通用约定 ### 3.1 基础格式 - Base URL:`/api/v1`(同源;跨源部署见 10.3 的说明与限制) - 请求与响应主体均为 **JSON**,编码 **UTF-8** - 建议请求头显式声明 `Content-Type: application/json`(服务端不校验该头,只按 JSON 解析请求体) - API 响应头:`Content-Type: application/json; charset=utf-8`、`Cache-Control: no-store`、`X-Content-Type-Options: nosniff` - 请求体上限 **1MB**,超出返回 `413 payload_too_large` ### 3.2 时间格式 所有接口同时使用两种时间表示,字段名严格区分: | 字段名 | 格式 | 用途 | 示例 | |---|---|---|---| | `date` | `YYYY-MM-DD` | 展示日期(卡片上显示的日期) | `2026-09-10` | | `created` | 毫秒时间戳(number) | 排序用,创建时刻 | `1760000000000` | | `updated` | 毫秒时间戳(number) | 排序用,最后更新时刻(仅更新过的对象返回) | `1760000100000` | | `launchedAt` | 毫秒时间戳(number) | 建站时刻(统计) | `1756166400000` | | `updatedAt` | 毫秒时间戳(number) | 数据更新时刻(统计查询响应) | `1760000000000` | 日期均按**服务器本地时区**的自然日计算(容器部署注意时区,见 FAQ 13.3)。 ### 3.3 分页 适用于两个列表接口(announcements / mirrors): | 参数 | 类型 | 默认 | 约束 | 说明 | |---|---|---|---|---| | `limit` | int | 不传 = 返回全部 | `1 ~ 1000` | 每页条数 | | `offset` | int | `0` | `>= 0` | 起始下标 | - 响应固定为信封结构:`{"items": [...], "total": N}` - **`total` 是筛选后集合的全量条数,不受 `limit` / `offset` 影响**,可用于计算总页数 - 越界不报错:`offset >= total` 时返回空 `items` - 非法值(非整数 / 超范围)返回 `400 invalid_value` 示例:`GET /api/v1/announcements?limit=10&offset=20` ### 3.4 排序规则 两个列表接口统一按以下顺序排序(等效 SQL): ```sql ORDER BY pinned DESC, date DESC, created DESC ``` 即:置顶在最前 → 展示日期新的在前 → 同日期时后创建的在前。 `date` 是 `YYYY-MM-DD` 字符串,字典序即时间序,无需转换。 ### 3.5 对象公共字段 | 字段 | 类型 | 出现时机 | 说明 | |---|---|---|---| | `id` | string | 必有 | 服务端生成的唯一 ID,格式 `前缀-毫秒时间hex-6位随机hex`,如 `a-1984c2a1f3d-9f2e1a`。**视为不透明字符串**,不要解析其含义 | | `created` | number | 必有 | 创建毫秒时间戳 | | `updated` | number | PUT 过才出现 | 最后更新毫秒时间戳 | ID 前缀约定:公告 `a-`、镜像 `m-`。初始化播种的示例公告使用固定 ID `a-welcome`。 ### 3.6 错误响应 所有错误返回统一信封: ```json { "error": { "code": "invalid_value", "message": "字段 date 需为 YYYY-MM-DD 格式日期" } } ``` - `code`:机器可读,程序判断用(全表见 12 章) - `message`:人类可读中文,直接展示给操作者即可 - 状态码语义:`400` 参数问题 / `401` 鉴权失败 / `403` 路径越界 / `404` 资源或接口不存在 / `405` 方法不支持 / `413` 请求体过大 / `429` 触发限速 / `500` 服务器内部错误 ### 3.7 幂等性 | 方法 | 幂等 | 说明 | |---|---|---| | GET | 是 | 只读 | | POST | 否 | 每次调用都会创建新对象 | | PUT | 是 | 部分更新,重复提交结果一致 | | DELETE | 是 | 重复删除第二次返回 `404` | ### 3.8 静态托管行为(附带说明) 同一进程托管整站,规则如下(不属于 API,排障时可能用到): - `GET /` → 302 跳转 `/sy/` - **无后缀页面地址(SEO 友好)**:`/sy/mirror` 直接读取 `sy/mirror.html`;`/sy` 归一到 `/sy/`;`/sy/index` 归一到 `/sy/` - **旧地址 301 归一(唯一规范 URL)**:`/sy/mirror.html` → `/sy/mirror`,`/sy/index.html` → `/sy/`;查询串原样保留 - **安全黑名单**:`backend/`、`data/` 目录,以 `.` 开头的目录 / 文件(如 `.trae`),`*.py` / `*.pyc` / `*.sh` 一律返回 `404`——后端代码、部署脚本与数据不随静态站暴露 - 目录路径自动补尾斜杠并尝试 `index.html` - MIME 显式覆盖 `.html/.css/.js/.json/.svg/图片` 等(避免 Windows 注册表把 `.js` 识别成 `text/plain` 导致页面白屏) - 静态响应 `Cache-Control: no-cache`(开发友好;生产建议在反向代理层加缓存) - 路径穿越(`GET /../backend/src/server.py`)返回 `403 forbidden` - 仅支持 `GET`;API 路径支持 `GET/POST/PUT/DELETE/OPTIONS` - `OPTIONS` 返回 `204` + `Allow: GET, POST, PUT, DELETE, OPTIONS`(同源部署下不会触发预检,仅供自检) --- ## 四、鉴权与登录(管理接口) 管理操作**只认登录会话**:浏览器登录后自动携带会话 Cookie;脚本使用两步 cookie jar(见 4.4)。不再提供独立的 Bearer token 机制。 ### 4.1 管理员凭证与 D1 连接配置 **凭证存储**:管理员(用户名 + 口令)存于 Cloudflare D1 的 `admins` 表,只保存 **PBKDF2-HMAC-SHA256 哈希与随机盐**(20 万次迭代),不存明文;当前管理员用户名为 `wupfish`。 **D1 连接配置**(`backend/cloudflare.json`,本地文件、不对外托管),二选一: 方式 A(推荐,**Worker 代理**;适用于无法直连 `api.cloudflare.com` 或不便管理 API Token 的场景): ```json { "account_id": "你的 Cloudflare 账号 ID", "database_id": "D1 数据库 ID", "worker_url": "https://<绑定该 D1 库的 Worker 自定义域名>", "auth_token": "与 Worker 侧校验一致的随机密钥(32 字节十六进制)" } ``` - Worker 把 `{sql, params}` / `{"batch":[...]}` 原样转发给 D1,并按 D1 REST 的形状返回;请求需带 `Authorization: Bearer `(Worker 侧只存密钥的 SHA-256,不落明文) - `workers.dev` 域名在大陆网络不可达,必须给 Worker 绑定自定义域名(Zone 内加路由 + 一条代理 DNS 记录即可) 方式 B(直连 D1 REST API;需 `api.cloudflare.com` 可达且持有 D1 Edit Token): ```json { "account_id": "你的 Cloudflare 账号 ID", "database_id": "D1 数据库 ID", "api_token": "具备 D1 Edit 权限的 API Token" } ``` - 环境变量可覆盖:方式 A 用 `WF_CF_WORKER_URL` / `WF_CF_AUTH_TOKEN`,方式 B 用 `WF_CF_ACCOUNT_ID` / `WF_CF_D1_DATABASE_ID` / `WF_CF_API_TOKEN`;两模式均以 `account_id` / `database_id` 作为配置标识 - 缺失或连不上时服务**启动失败**并给出提示;运行中 D1 故障返回 `503 upstream_error` - 重置 / 新增管理员:`python backend/src/server.py --set-admin 用户名:新口令`(哈希+盐写入 D1,立即生效) **会话机制**:登录成功返回会话 Cookie: ``` Set-Cookie: wf_admin_session=<随机令牌>; Path=/; HttpOnly; SameSite=Lax; Max-Age=604800 ``` - 绝对有效期 **7 天**(固定,不滑动);HTTPS 反代(`X-Forwarded-Proto: https`)下自动附加 `Secure` - 会话保存在 **D1 `sessions` 表**(只存 token 的 SHA-256,不落明文):登出即删除记录(旧 Cookie 立即失效);**后端重启 / 重新部署后会话仍然有效**,不会再被静默登出 - 登录失败与 admin 鉴权失败**分开计数**:同一 IP 10 分钟内失败超过 10 次 → `429`(超出窗口后自动恢复) ### 4.2 登录 / 登出 / 状态(公开) **POST /api/v1/auth/login** —— 请求体 `{ "username": "...", "password": "..." }` - 成功:`200` + `{"ok": true, "expiresAt": 1760000000000}` + `Set-Cookie`;写入 `login` 日志 - 用户名或口令错误:`401 unauthorized`(同一文案,防用户名枚举)并写入 `login_failed` 日志;请求体非法:`400`;触发限速:`429`;D1 故障:`503` **POST /api/v1/auth/logout** —— 无请求体,`200 {"ok": true}`;服务端注销该会话、写入 `logout` 日志并清除 Cookie **GET /api/v1/auth/status** —— `200 {"authenticated": true, "expiresAt": ...}`,未登录为 `{"authenticated": false}` ### 4.3 管理日志(鉴权接口) 登录成功 / 登录失败 / 登出均追加写入 `backend/data/admin_log.jsonl`(仅存服务器本地,不对外托管): ```json {"ts": 1760000000000, "action": "login", "username": "wupfish", "ip": "203.0.113.7", "region": "福建", "ua": "Mozilla/5.0 ..."} ``` | action | 说明 | |---|---| | `login` | 登录成功 | | `login_failed` | 用户名或口令错误 | | `logout` | 登出 | - 文件超过 1MB 时自动截留最后 500 行(原子重写),不会无限增长;日志写入失败不影响登录本身 - 查询:`GET /api/v1/admin/logs?limit=100`(1–500,最新在前)→ `{"items": [...], "total": n}`;管理面板「日志」页即此接口 ### 4.4 调用方式(admin 接口) 所有 `/api/v1/admin/*` 接口需携带登录会话 Cookie: - **浏览器**:打开 `/sy/admin`(未登录 / 会话过期 / 接口异常会在页顶提示条写明原因,并可一键前往 `/sy/login`;登录成功直接进入面板) - **脚本(curl 两步)**:先登录保存 Cookie,再带 Cookie 调用: ```bash curl -c cookie.txt -X POST http://127.0.0.1:8000/api/v1/auth/login \ -H "Content-Type: application/json" -d '{"username":"wupfish","password":"你的口令"}' curl -b cookie.txt http://127.0.0.1:8000/api/v1/admin/logs ``` 校验规则: - 缺失 / 无效 / 已登出 / 已过期的会话 → `401 unauthorized`;连续失败超过 10 次 / 10 分钟 → `429` 限速 - 会话只在 `admin/*` 生效;只读接口与统计上报接口**不需要也无法**使用 ### 4.5 口令轮换与泄露处置 1. 换用新口令:`--set-admin 用户名:新口令`(或直接在 D1 `admins` 表更新哈希) 2. 旧口令立即失效(以 D1 当前值为准);如需让已登录会话全部下线:清空 D1 `sessions` 表(`DELETE FROM sessions;`)或等待 7 天自然过期 3. 怀疑泄露:轮换口令 → 检查 D1(`mirrors`)与 `backend/data`(公告 / 日志)是否被写入陌生内容 → 必要时回滚(见 8.3) --- ## 五、公告(announcements) 公告对象示例(列表接口返回的单个元素): ```json { "id": "a-1984c2a1f3d-9f2e1a", "pinned": true, "date": "2026-09-10", "title": "公告标题", "body": "

公告内容,HTML 格式

", "cover": "https://example.com/cover.jpg", "created": 1760000000000, "updated": 1760000100000, "startAt": 1760000000000, "endAt": 1760086400000 } ``` ### 5.0 字段说明 | 字段 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---| | `title` | string | 创建必填;更新时传了就必须合法 | 1–120 字符 | 标题 | | `body` | string | 创建必填 | 1–200000 字符 | 正文,**受信任 HTML**(由管理后台富文本编辑器产出,前端直接渲染,不做转义;见 11.1) | | `pinned` | bool | 可选 | 缺省 `false` | 置顶,置顶排最前 | | `date` | string | 可选 | `YYYY-MM-DD`,必须是真实存在的日期 | 展示日期;缺省 = 服务器当日 | | `cover` | string | 可选 | `http(s)://` 开头完整 URL,≤2048 字符 | 卡片顶部配图;传空串 / `null` 表示清除,响应中该键被省略 | | `startAt` | number | 可选 | 毫秒时间戳(整数,≥0) | **定时上架**:早于该时刻时公告不对外可见;未设 = 立即上架;传 `null` / 空串清除 | | `endAt` | number | 可选 | 毫秒时间戳(整数,≥0) | **定时下架**:到达该时刻后公告从公开列表移除;未设 = 长期有效;传 `null` / 空串清除;与 `startAt` 同时存在时须晚于它 | **定时上/下架语义**(服务端按毫秒时间戳判定,与服务器时区无关): - 公开列表(`GET /api/v1/announcements`)**只返回当前处于生效窗口内**的公告(未到 `startAt`、已过 `endAt` 的都不返回) - 管理接口与后台管理列表不受影响:随时可查看、编辑、删除全部公告(含未上架 / 已下架) - 管理面板中此类公告带状态徽标:未上架 / 已下架;表单用 `datetime-local` 选择时间,留空即不限制 前端渲染约定(公告页 `notice.html`):全站玻璃底 + 白色边框卡片;`pinned` 显示「置顶」徽标,否则显示 `date`;`cover` 存在时渲染为顶部配图;`title` 与其他字段做 HTML 转义,仅 `body` 按原样渲染。 ### 5.1 GET /api/v1/announcements —— 公告列表(公开) 只返回**当前处于生效窗口内**的公告(定时上/下架过滤,见 5.0)。查询参数:`limit`、`offset`(见 3.3)。 ```bat curl "http://127.0.0.1:8000/api/v1/announcements?limit=5" ``` ```json { "items": [ { "...": "公告对象,见 5.0" } ], "total": 1 } ``` 错误情形:仅分页参数错误(`400`)。接口本身无业务错误态,后端未启动时前端拿到的只是网络错误,静默降级。 ### 5.2 POST /api/v1/admin/announcements —— 发布公告(鉴权) 请求体: ```json { "pinned": false, "date": "2026-09-10", "title": "标题", "body": "

正文

", "cover": "" } ``` 成功:**`201`**,返回**完整创建后的对象**(含服务端生成的 `id` / `created`)。 ```json { "id": "a-1984c2a1f3d-9f2e1a", "pinned": false, "date": "2026-09-10", "title": "标题", "body": "

正文

", "created": 1760000000000 } ``` 失败:`400 missing_field`(缺 title/body)、`400 invalid_value`(超长 / date 非法 / cover 非 URL / startAt·endAt 非毫秒时间戳 / 下架时间不晚于上架时间)、`401`、`429`、`413`。 ### 5.3 PUT /api/v1/admin/announcements/{id} —— 更新公告(鉴权) **部分更新语义**:只传需要修改的字段,未传字段保持不变;`id` / `created` 不可修改。定时字段传 `null` / 空串表示清除(如 `{"startAt": null}` = 取消定时上架,立即生效)。 请求体(示例只改置顶与正文): ```json { "pinned": true, "body": "

新正文

" } ``` 成功:`200`,返回更新后的完整对象(带 `updated` 时间戳)。`404` = ID 不存在。 ### 5.4 DELETE /api/v1/admin/announcements/{id} —— 删除公告(鉴权) 成功:**`204`**,无响应体。重复删除同一 ID 返回 `404`。 ### 5.5 GET /api/v1/admin/announcements —— 公告管理列表(鉴权) 返回**全部**公告(含未上架 / 已下架,不做定时过滤),供管理面板使用;查询参数与响应信封同 5.1。 --- ### 5.6 首栏公告(banner) 首页顶部「首栏」公告栏:单对象、与公告列表相互独立(不会出现在公告页),由管理面板发布 / 下架。 | 方法 | 路径 | 鉴权 | 说明 | |---|---|---|---| | GET | `/api/v1/banner` | 无 | 公开读取;已下架(`enabled=false`)或未设置时返回 `{"banner": null}` | | GET | `/api/v1/admin/banner` | 登录会话 | 读取当前配置(含已下架状态) | | PUT | `/api/v1/admin/banner` | 登录会话 | 发布 / 更新 / 下架 | `PUT` body 字段: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `text` | string | 首次发布必填(1-300 字) | 公告文字;已有内容时可只传 `enabled` 更新状态 | | `link` | string | 否 | 跳转链接(仅 `http(s)://`;留空 / null 清除) | | `enabled` | bool | 否(默认 true) | `false` = 下架,公开接口不再下发 | 公开响应示例:`{"banner": {"text": "本站即将加入自建 GitHub 加速节点…", "link": "", "updated": 1791290000000}}` 存储:`backend/data/banner.json`(本地 JSON,原子写,与公告同机制);首次启动自动播种一条默认公告。 ### 5.7 弹窗公告(popup) 全站弹窗:访客打开任意页面时弹出(管理 / 登录 / 跳转页除外),**同一会话只弹一次**,可随时关闭;由管理面板「弹窗」页签发布 / 上架 / 下架。与首栏公告相互独立。 | 方法 | 路径 | 鉴权 | 说明 | |---|---|---|---| | GET | `/api/v1/popup` | 无 | 公开读取;已下架(`enabled=false`)或未设置时返回 `{"popup": null}` | | GET | `/api/v1/admin/popup` | 登录会话 | 读取当前配置(含已下架状态) | | PUT | `/api/v1/admin/popup` | 登录会话 | 发布 / 更新 / 上架下架 | `PUT` body 字段: | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `title` | string | 首次发布必填(1-80 字) | 弹窗标题 | | `body` | string | 首次发布必填(1-2000 字) | 弹窗内容(纯文本,换行原样保留) | | `linkText` | string | 否(≤40 字) | 链接显示文字(留空则显示链接本身) | | `link` | string | 否 | 链接(仅 `http(s)://`;留空 / null 清除) | | `enabled` | bool | 否(默认 true) | `false` = 下架,公开接口不再下发 | 公开响应示例:`{"popup": {"title": "节点线路更新", "body": "新增 日本-1 / 台湾-1 节点…", "linkText": "", "link": "", "updated": 1791290000000}}` 存储:`backend/data/popup.json`(本地 JSON,原子写)。前端由 `nav.js` 拉取并以纯文本渲染(先转义再换行),链接仅放行 `http(s)`。 --- ## 六、镜像(mirrors) 镜像是「外部 URL + 简介」模式:资源在别人服务器上,本站只登记名称、简介与项目地址。镜像存于 **Cloudflare D1**(读取走内存缓存);**下载加速与历史版本由前端镜像页实时对接 GitHub 生成,不落数据库**。 - 登记 GitHub 项目(仓库地址或 Release 下载直链):前端自动生成 **gh-proxy.com 主加速 + ghfast.top 备用**下载链接,并在页面加载时跟随官方最新 Release 自动更新(见 6.0 渲染约定) - 「历史版本」入口实时读取该项目的 GitHub Releases,不经过本站服务器、不产生任何存储 - 非 GitHub 链接:卡片退化为普通「打开链接」,经 `redirect.html?url=` 中转页跳转 镜像对象示例: ```json { "id": "m-1984c2b2c4e-1a2b3c", "name": "LocalSend", "desc": "开源跨平台隔空投送工具", "url": "https://github.com/localsend/localsend", "cat": "file", "asset": "windows-x86-64.zip", "tag": "v1.18.2", "cover": "https://example.com/icon.jpg", "pinned": false, "date": "2026-09-10", "created": 1760000000000 } ``` ### 6.0 字段说明 | 字段 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---| | `name` | string | 创建必填 | 1–100 字符 | 显示名称 | | `desc` | string | 创建必填 | 1–300 字符 | 简介(**纯文本**,前端做转义渲染) | | `url` | string | 创建必填 | `http(s)://` 开头完整 URL,≤2048 字符 | GitHub 仓库地址或下载直链;非 GitHub 链接经 `redirect.html?url=` 中转 | | `cat` | string | 可选 | `download` / `ai` / `file` / `media` / `efficiency`,缺省 `""` | 分类(与镜像页分类栏一致);留空仅出现在「全部」 | | `asset` | string | 可选 | ≤40 字符,缺省 `""` | GitHub Release 资产名关键词(如 `win-x64`);留空时自动挑选最新版直装包(`.exe/.zip/.dmg` 优先,跳过源码包) | | `tag` | string | 可选 | ≤30 字符,缺省 `""` | 版本 / 标记(GitHub 卡片自动更新后显示最新 Release 版本号) | | `cover` | string | 可选 | URL,≤2048 字符 | 图标位配图,当前前端未渲染,预留 | | `pinned` | bool | 可选 | 缺省 `false` | 置顶 | | `date` | string | 可选 | `YYYY-MM-DD` | 收录 / 更新日期,缺省 = 服务器当日 | 前端渲染约定(`mirror.html`):卡片插入工具列表**最顶部**,参与分类筛选与搜索(关键词 = `name + tag + desc + cat`);GitHub 卡片主按钮为 `gh-proxy.com` 加速链接、备用按钮为 `ghfast.top` 加速链接,并自动改写为最新版下载;历史版本弹窗实时读取 Releases。用户点击主按钮 / 备用节点 / 历史版本下载时,前端自动上报 `POST /stats/download`(id = 卡片元素 id,如 `tool-localsend`、`tool-api-<镜像id>`)。 ### 6.1 GET /api/v1/mirrors —— 镜像列表(公开) 查询参数与响应信封同 5.1。前端 5 分钟内存缓存,失败返回 `null` 并保持内置卡片。 ### 6.2 POST /api/v1/admin/mirrors —— 发布镜像(鉴权) 请求体: ```json { "name": "LocalSend", "desc": "开源跨平台隔空投送工具", "url": "https://github.com/localsend/localsend", "cat": "file", "asset": "windows-x86-64.zip", "tag": "v1.18.2", "pinned": false } ``` 成功:**`201`** + 完整对象。失败:同 5.2(`400 missing_field` = 缺 name / desc / url;`400 invalid_value` = `cat` 不在白名单 / URL 非法 / 字段超长)。 ### 6.3 PUT /api/v1/admin/mirrors/{id} —— 更新镜像(鉴权) 部分更新,语义同 5.3。`200` + 完整对象;`404` = ID 不存在。 ### 6.4 DELETE /api/v1/admin/mirrors/{id} —— 删除镜像(鉴权) **硬删**:记录直接从 D1 与内存移除。`204`;重复删除返回 `404`。 --- ## 七、统计(stats) ### 7.1 四个上报接口(公开,前端自动调用) 均为 `POST`,请求体 JSON,成功统一返回 `200 {"ok": true}`。上报失败不影响任何页面功能(前端 fire-and-forget,不重试、不弹窗)。 **① POST /api/v1/stats/pageview —— 页面访问** ```json { "sid": "wf-lz3k9x2a-1a2b3c4d5e", "ts": 1760000000000, "page": "/sy/mod.html", "ref": "https://example.com/" } ``` | 字段 | 必填 | 约束 | 说明 | |---|---|---|---| | `sid` | 可选 | ≤64 字符 | 匿名会话 ID,前端本地生成、**每日重置**;缺省按空处理(只计 PV 不计 UV) | | `page` | 可选 | ≤300 字符 | 页面路径;**仅做长度校验,不存储** | | `ref` | 可选 | ≤300 字符 | 来源;**仅做长度校验,不存储**(避免把 URL 中的搜索词等用户输入落盘) | | `ts` | 可选 | — | 客户端时间戳;服务端不使用(以服务器接收时刻为准),不存储 | **② POST /api/v1/stats/event —— 业务事件** ```json { "sid": "wf-...", "ts": 1760000000000, "event": "download", "data": { "source": "modrinth", "id": "AANobbMI", "url": "..." } } ``` | 字段 | 必填 | 约束 | 说明 | |---|---|---|---| | `event` | **必填** | `^[A-Za-z0-9_-]{1,40}$` | 事件名,如 `download` | | `data` | 可选 | 任意 JSON 对象 | **不参与聚合计数、不存储**;为未来扩展预留 | 聚合规则:仅按事件名计数;`download` 事件数计入 summary 的 `downloads`(另见 ④)。 **③ POST /api/v1/stats/api-call —— 外部 API 调用** ```json { "sid": "wf-...", "ts": 1760000000000, "api": "https://api.modrinth.com/v2/search", "method": "GET" } ``` | 字段 | 必填 | 约束 | 说明 | |---|---|---|---| | `api` | **必填** | URL,≤2048 字符 | 被调用的外部接口地址 | | `method` | 可选 | — | 不参与聚合、不存储 | 聚合规则:按**域名(hostname,小写)**计数,查询串在客户端就已剥离;URL 解析不出域名时按原文前 80 字符聚合。 **④ POST /api/v1/stats/download —— 镜像下载计数(按镜像 id)** ```json { "sid": "wf-...", "ts": 1760000000000, "id": "tool-localsend" } ``` | 字段 | 必填 | 约束 | 说明 | |---|---|---|---| | `id` | **必填** | `^[A-Za-z0-9_-]{1,64}$` | 镜像卡片 id(内置卡片如 `tool-localsend`;后台镜像为 `tool-api-<镜像id>`) | 聚合规则:按 `id` 计数(存 D1 `stats_downloads` 表),**只存计数,不存任何请求明细**;非法 id 返回 `400 invalid_value`。查询排行见 7.5。 ### 7.2 统计口径定义(重要,展示层以此为准) | 指标 | 精确定义 | |---|---| | `pv` | 页面访问总量(pageview 上报次数累计) | | `uv` | 累计访客数 = **各自然日去重 sid 数之和**(同一访客跨天再访计为新的"日访客") | | `today.uv` / `today.pv` | 服务器本地时区**当日**的去重访客数 / 页面访问数 | | `month.uv` / `month.pv` | 服务器本地时区**当月**的去重访客数 / 页面访问数(sid 在当月内去重,与"按日累加"口径独立) | | `downloads` | 下载次数 = `stats_downloads` 表各镜像计数之和 + `event=download` 累计次数(mod 页上报) | | `daysOnline` | 已上线天数 = `今天 - launchedAt 的自然日差 + 1`(含建站当日,最小值 1) | | `launchedAt` | 建站毫秒时间戳 = D1 `stats_global.launched_at` 首次写入时刻 | | `apiCalls` | 对象 `{ "域名": 次数 }`,按外部 API 域名聚合 | | `regions` | 地区 → 访客数,与「当日首次来访」对齐计数(保证 Σregions 与当日新增 UV 一致) | | `updatedAt` | 本次查询响应的生成时刻(毫秒) | > 单日去重集合上限 20000、单月上限 100000;达到上限后该日 / 该月 UV 不再增长(PV 照常)。逐日明细保留最近 400 天、逐月保留最近 25 个月,超出自动裁剪最旧(裁剪不影响 `uv` / `pv` 累计值)。 ### 7.3 GET /api/v1/stats/summary —— 总览(公开) ```json { "uv": 1234, "pv": 5678, "downloads": 321, "today": { "uv": 10, "pv": 52 }, "month": { "uv": 260, "pv": 1200 }, "daysOnline": 14, "launchedAt": 1756166400000, "apiCalls": { "api.modrinth.com": 100, "thunderstore.io": 35 }, "updatedAt": 1760000000000 } ``` 统计页(`stats.html`)的消费方式:六卡片显示 `today.pv`(每日访问)、`month.pv`(本月访问)、`pv`(总访问量)、`daysOnline`(已上线天数)、`downloads`(下载次数)、ΣapiCalls(API 调用);`apiCalls` 取次数前 8 名画条形图;`updatedAt` 显示在页脚。 ### 7.4 GET /api/v1/stats/regions —— 地区分布(公开) ```json { "regions": [ { "region": "广东", "count": 123 }, { "region": "内网/本地", "count": 45 } ], "updatedAt": 1760000000000 } ``` 按 `count` 降序返回;统计页取前 12 名画条形图。 **地区解析规则**(按优先级): 1. 请求头 `X-WF-Region` 非空 → 直接采用(截断至 30 字符)。自建反代时可注入精确地区,也是集成测试的注入口。 2. 取访客 IP(优先级:`CF-Connecting-IP` > `X-Real-IP` > `X-Forwarded-For` 首段 > TCP 对端地址);IP 无法解析或属于内网 / 环回等私有地址 → 记为 **「内网/本地」**。 3. 否则调用 **ip-api.com** 免费接口(`lang=zh-CN`,返回省级行政区中文名,如「广东」);超时 / 失败 → **「未知」**。 缓存与开关:解析结果只存**内存缓存**(成功 24 小时、失败 10 分钟,容量上限 5000 条,LRU 淘汰),**完整 IP 不落盘**,重启即清空;`--no-geo` 启动参数可整体关闭(全部记「未知」)。 ### 7.5 GET /api/v1/stats/downloads —— 各镜像下载量(公开) ```json { "items": [ { "id": "tool-localsend", "count": 128 }, { "id": "tool-api-m-1984c2b2c4e-1a2b3c", "count": 12 } ], "total": 140, "updatedAt": 1760000000000 } ``` 按 `count` 降序返回;`total` 为全部镜像下载量之和(同一份计数也计入 summary 的 `downloads`)。**只读聚合计数,不涉及任何个人数据**;管理面板「统计」页以排行列表展示。镜像页**不**展示单卡计数(保持卡片简洁)。 ### 7.6 落盘与一致性(Cloudflare D1) - 上报先写内存并立即对查询可见;**每 2 秒批量写入 D1**(计数绝对值 UPSERT + 新 sid `INSERT OR IGNORE`,单批原子提交),避免高频写库 - 写入失败会自动保留待写集合、下一轮重试,不丢数;服务正常退出(Ctrl+C)时强制落盘一次 - 启动时从 D1 加载聚合值,当日 / 当月 UV 以 `COUNT(sids)` 校准(可自愈中断导致的偏差);逐日保留 400 天、逐月 25 个月(启动时自动裁剪) - 单实例约束不变:同一后端同时只应运行一个实例 ### 7.6 隐私说明 - 不存储完整 IP、UA、精确时间线;`page` / `ref` / `data` 仅做校验不落盘 - `sid` 是前端随机生成、按日重置的匿名 ID,不含任何身份信息 - 地理解析的 IP 缓存仅存内存(重启即清),D1 中持久化的只有 `地区名 → 计数` - 统计数据存于 Cloudflare D1,内容仍仅为聚合计数与匿名 sid - 对外展示的只有聚合计数(与 stats.html 页面所见一致) --- ## 八、数据存储与运维 ### 8.1 数据位置 **Cloudflare D1(库 `wupfish`,通过 `backend/cloudflare.json` 连接)**: | 表 | 内容 | |---|---| | `admins` | 管理员(用户名 + PBKDF2 哈希 + 随机盐 + 迭代数) | | `sessions` | 登录会话(仅存 token 的 SHA-256 + 用户名 + IP + 创建/过期时间;重启不失效) | | `stats_*` | 统计聚合:全局计数、逐日 / 逐月 PV·UV 与去重 sid、事件、API 调用、地区、**各镜像下载量(`stats_downloads`)** | | `mirrors` | 镜像展示字段(GitHub 加速与历史版本不落库,由前端实时生成) | **本地 `backend/data/`(原子写:先写 `.tmp` 再 `os.replace`,进程任何时刻被杀都不会留下半截文件)**: | 文件 | 内容 | 写入时机 | |---|---|---| | `announcements.json` | 公告数组 | 管理写操作后即时 | | `admin_log.jsonl` | 管理端日志(登录 / 登出 / 登录失败,JSONL 行) | 每次登录、失败、登出 | ### 8.2 文件格式示例 `announcements.json`: ```json [ { "id": "a-welcome", "pinned": true, "date": "2026-09-10", "title": "Wupfish 资源站 API 服务已上线", "body": "

...

", "created": 1760000000000 } ] ``` 统计与镜像存于 D1,可直接用 SQL 查询(控制台 / `wrangler d1 execute` / MCP 均可): ```sql SELECT key, value FROM stats_global; -- pv / uv_total / launched_at SELECT day, pv, uv FROM stats_daily ORDER BY day DESC LIMIT 7; SELECT id, name, url, cat, asset FROM mirrors; ``` 管理日志是本地 JSONL(非 D1 表),直接查看:`Get-Content backend\data\admin_log.jsonl -Tail 20`(Windows)或 `tail -n 20 backend/data/admin_log.jsonl`,也可用管理面板「日志」页 / `GET /api/v1/admin/logs`。 ### 8.3 运维操作 | 操作 | 方法 | |---|---| | 备份 | D1 自带 Time Travel(免费版 7 天)可按时间点恢复,完整导出用 `wrangler d1 export`;本地 `backend/data/` 直接复制 | | 恢复 | D1 控制台按时间点恢复;本地目录还原后重启 | | 重置统计 | 在 D1 删除 `stats_global` / `stats_daily*` / `stats_monthly*` / `stats_events` / `stats_api_calls` / `stats_regions` 的对应行后重启,统计从零开始 | | 重置登录会话 | D1 执行 `DELETE FROM sessions;`(全员立即下线);等待 7 天自然过期亦可 | | 重置登录口令 | `python backend/src/server.py --set-admin 用户名:新口令` | | 清空某类内容 | 公告:删除 `announcements.json` 重启(会重新播种 `a-welcome` 示例)或走 DELETE 接口;镜像:走 DELETE 接口(硬删) | | 手工编辑数据 | 优先走管理接口;D1 可直接用 SQL,改完重启后端让内存缓存生效 | ### 8.4 容错行为 - 某个本地文件损坏(JSON 解析失败)时,启动**不会失败**:坏文件被备份为 `*.corrupt`,用默认值继续运行 - D1 凭证缺失或连不上:**启动直接失败并给出明确提示**(避免半可用状态);运行中 D1 故障:公开读接口继续用内存缓存服务,登录 / 写入返回 `503`,不产生脏数据 - 单实例约束:同一后端同时只应运行一个服务实例(无跨进程锁);多实例请各自指定 `--data-dir` 并各自使用独立数据库 --- ## 九、前端接入指南 ### 9.1 content.js —— 内容拉取 SDK(`sy/content.js`) 全局对象 `window.WupfishContent`,全部方法返回 `Promise`,**失败一律 resolve `null`**(由页面自行回退内置内容): ```js // 公告列表(5 分钟内存缓存) WupfishContent.getAnnouncements().then(function (items) { ... }); // 镜像列表 WupfishContent.getMirrors().then(function (items) { ... }); ``` 行为细节: - 缓存 TTL 5 分钟,只缓存成功且 `items` 非空的结果;空列表与失败不缓存 - 基准路径常量 `BASE = '/api/v1'`——**跨源部署必须改这一行,或用反代把 `/api` 转到本服务** - 消费方:公告页 `notice.html`(有数据时**整体替换**内置公告;空列表 / 失败则保留内置)、镜像页 `mirror.html`(有数据时把镜像卡片**插入列表顶部**,内置工具卡保留) ### 9.2 stats.js —— 埋点 SDK(`sy/stats.js`) 自动行为(无需页面参与): 1. **页面访问**:加载即上报 `pageview`(带 `page` / `ref`) 2. **fetch 拦截**:包装 `window.fetch`,对**绝对地址**(`http(s)://` 开头)且不含 `/api/v1` 的请求上报 `api-call`;上报前剥离查询参数(避免把搜索词等用户输入发往后端);相对路径请求(如本站 SDK 自身)天然不统计 3. **会话 ID**:`sid` 存 localStorage,按自然日自动重置,格式 `wf-<时间36进制>-<随机串>` 手动接口: ```js // 业务事件上报(如 mod 页下载按钮点击) WupfishStats.track('download', { source: 'modrinth', id: 'AANobbMI' }); // 查询(供统计页使用,失败 resolve null;走未被包装的原始 fetch,不会递归上报) WupfishStats.getSummary().then(function (s) { ... }); WupfishStats.getRegions().then(function (r) { ... }); ``` > 注意:拦截器统计的是"浏览器发出的外部 API 调用",与后端实际转发无关;`gh-proxy.com` 等加速节点的下载是 `` 链接直跳,不经过 fetch,因此不会计入 `apiCalls`(下载行为请用 `track('download', ...)` 上报)。 ### 9.3 后端未上线时的表现(兼容旧部署) - 上报:`fetch` 失败被静默吞掉,控制台无报错、页面无感知 - 查询:`getSummary` / `getRegions` resolve `null`,统计页显示占位提示,「已上线天数」按内置建站日(2026-08-26)兜底计算 - 内容:`getAnnouncements` 等返回 `null`,各页保持内置内容 即:**先上前端、后上后端**或**只上前端**都是合法状态,无需任何开关。 --- ## 十、部署 ### 10.1 拓扑一:单进程直出(最简) ```bat python backend/src/server.py --host 0.0.0.0 --port 8000 ``` 防火墙放行 8000 端口即可;`http://服务器IP:8000/` 直接是站点。适合内网 / 小流量。**公网建议至少套一层 HTTPS(见 10.2)。** ### 10.2 拓扑二:Nginx 反向代理(生产推荐) 静态文件可继续由后端进程提供,也可由 Nginx 直出、仅转发 API: ```nginx server { listen 443 ssl; server_name example.com; # 方案 A:静态也走 server.py(全部转发) # location / { proxy_pass http://127.0.0.1:8000; } # 方案 B:Nginx 直出静态,仅转发 API(推荐) root /var/www/forge; location /sy/ { try_files $uri =404; } location /pngdp/ { try_files $uri =404; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` `X-Real-IP` / `X-Forwarded-For` 必须配置,否则地区统计会把所有访客记为「内网/本地」(服务端已自动解析这两种头)。经 Cloudflare 代理时服务端还会识别 `CF-Connecting-IP`。若反代终止 TLS,建议同时传 `X-Forwarded-Proto $scheme`,登录 Cookie 会自动附加 `Secure`。 方案 B 只放行 `/sy/` 与 `/pngdp/`,后端目录(`backend/`、`data/`)天然不被 Nginx 暴露;若改为全站转发(方案 A),后端内置黑名单同样会拦截这些路径。 ### 10.3 同源要求与跨源限制 前端 SDK 以相对路径 `/api/v1` 请求,**站点与 API 必须同源**(同协议 + 同域名 + 同端口)。若确实需要跨源(如站点在 CDN、API 在另一台机器): - 需自行给后端加 `Access-Control-Allow-Origin` 等响应头(当前版本不提供,是有意收窄攻击面) - 或在 CDN / 反代层把 `/api/*` 回源到 API 域名,维持浏览器视角的同源(推荐) ### 10.4 进程守护 - **Linux systemd**: ```ini [Unit] Description=Wupfish resource site backend After=network.target [Service] WorkingDirectory=/opt/forge ExecStart=/usr/bin/python3 /opt/forge/backend/src/server.py --host 127.0.0.1 --port 8000 Environment=WF_ADMIN_TOKEN=你的密钥 Environment=WF_CF_WORKER_URL=https:// Environment=WF_CF_AUTH_TOKEN=你的代理密钥 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ``` 后端启动与运行需要能访问 D1 端点(直连模式为 `api.cloudflare.com`,代理模式为 Worker 自定义域名);account_id / database_id 从 `backend/cloudflare.json` 读取,密钥建议用环境变量注入(如上)。 - **Windows**:任务计划程序(开机触发,操作=`python H:\forge\backend\src\server.py --host 0.0.0.0`)或 NSSM 注册为服务 - **Docker**:挂载 `/app/backend/data` 卷持久化本地数据,并注入 D1 凭证(`WF_CF_*` 环境变量或挂载 `cloudflare.json`);容器需能访问 D1 端点(`api.cloudflare.com` 或 Worker 自定义域名);注意默认 UTC 时区会改变「当日 / 当月」边界,设置 `TZ=Asia/Shanghai`(见 FAQ 13.3) --- ## 十一、安全注意事项 ### 11.1 `body` 是受信任 HTML(XSS 边界) 公告 `body` 由管理后台产出,前端**不转义直接渲染**——这是设计而非疏漏。安全边界 = 登录口令: - 持有凭证的人可以往页面注入任意 HTML / 脚本(存储型 XSS),因此 **登录口令必须按凭据管理**(不入库、不入 git、不进日志) - 其余所有字段(`title` / `desc` / `name` / `tag` 等)在前端一律 HTML 转义后才渲染 - 公开只读接口**不可能**写入内容,普通访客无法利用此面 ### 11.2 其他要点 - 口令校验为常量时间比较(`hmac.compare_digest`),登录与 admin 鉴权失败**分开限速**(按真实客户端 IP 计数:CF-Connecting-IP → X-Real-IP → XFF 首段,10 次 / 10 分钟 → 429);公网部署仍建议把 `admin/*` 在反代层限制来源 IP - 会话为服务端内存中的随机令牌(非签名 Cookie),登出即服务端注销;重启服务会让全部会话失效 - 登录对「用户名不存在 / 口令错误」返回同一 401 文案,并对未知用户名做等价哈希,防用户名枚举 - **D1 凭证**:直连模式的 API Token 按最小权限创建(仅 Account → D1 → Edit);代理模式的 `auth_token` 为随机密钥,Worker 侧仅存其 SHA-256。两者均存于 `backend/cloudflare.json`(本地、不对外;勿提交任何仓库);泄露后轮换重建 - **静态黑名单**:`backend/`、`data/`、点开头目录、`*.py` / `*.pyc` 一律 `404`(v1.2.0 起;旧版曾可直接下载 `server.py` 与 `data/*`,升级后自动修复) - 登录会话 Cookie 为 `HttpOnly + SameSite=Lax` 随机令牌(服务端内存会话),HTTPS(反代 `X-Forwarded-Proto: https`)下自动附加 `Secure`;明文 HTTP 部署仅建议内网使用 - 请求体 1MB 上限;统计上报接口只写聚合计数,被刷最多污染统计数字(可在反代层对 `/api/v1/stats/*` 做限速) - 本 API 不接收文件上传;站点不提供任何上传类功能 - 静态托管有路径穿越防护;不要把 `--root` 指向包含敏感文件的目录 - 上线请走 HTTPS(反代终止 TLS 即可),口令不明文过公网 ### 11.3 防中间人攻击(MITM)方案 分层清单(✅=现状已具备,◻=建议加固): 1. ✅ **全站 HTTPS + CDN 代理**:访客 → Cloudflare 边缘、边缘 → 源站两段均加密,浏览器地址栏始终为 https 2. ◻ **SSL/TLS 模式 Full (strict)**:让边缘回源时校验证书,防「边缘 → 源站」段被截获 / 伪造(仅 Flexible / Full 时该段可被中间人解密) 3. ◻ **Always Use HTTPS + HSTS**(`includeSubDomains`;`preload` 谨慎开启):把 HTTP 强制跳转 + 浏览器记住只走 HTTPS,防 SSL 剥离 / 协议降级 4. ◻ **证书透明度(CT)监控**:开启告警后,他人为你的域名私签证书可第一时间发现 5. ◻ **源站只允许 Cloudflare 回源**:防火墙白名单 CF 回源 IP(或改用 Tunnel)——既防绕过 CDN 直连源站打源,也防伪造 `CF-Connecting-IP` / XFF 头污染限速与地理解析 6. ✅ **后端仅监听 127.0.0.1**(systemd 内 `--host 127.0.0.1`):公网不存在直连面,唯一入口是本机反代 7. ✅ **会话 Cookie `HttpOnly` + `SameSite=Lax`**,HTTPS 下自动 `Secure`:配合 HSTS 同时防嗅探与固定 8. ◻ **管理入口二次防护**:`/api/v1/admin/*` 与 `sy/login` 叠加 Cloudflare Access / IP 白名单 / 反代 Basic Auth 任一层 9. ✅ **前端零第三方脚本**:静态资源全部自托管(仅自站 `nav.js` / `stats.js` / `content.js`),没有供应链注入面 10. ◻ **域名与账号侧**:注册商 / Cloudflare 账号开 2FA 与注册锁(Registrar Lock);域名支持时开 DNSSEC,防 DNS 劫持导致的证书误发 11. ◻ **部署通道**:仅走 1Panel 网页(HTTPS)+ 服务器端 `deploy.sh`(包结构校验),不用明文 FTP / 未加密传输;关闭服务器口令 SSH,只用密钥 12. ✅ **缓存不可被长期投毒**:静态资源引用带 `?v=` 版本号、发版即递增,旧版本 URL 被边缘长期缓存也不会被替换为新内容;HTML / API 走 `no-cache` ### 11.4 后端安全加固方案 同样分「现状(✅)/ 建议(◻)」: 1. ✅ **口令存储**:PBKDF2-HMAC-SHA256 + 随机盐 + 20 万次迭代;常量时间比较;不使用可逆 / 明文存储 2. ✅ **会话**:服务端随机令牌,库中仅存 SHA-256(重启 / 部署不掉线);登出即注销;固定 7 天绝对过期(可调) 3. ✅ **防枚举与限速**:未知用户名做等价哈希;登录 / 鉴权失败按真实 IP 限速(10 次 / 10 分钟);公开上报接口另有速率限制 4. ✅ **输入校验**:全字段服务端校验(类型 / 必填 / 长度 / 白名单枚举);请求体 1MB 上限;JSON 顶层必须为对象 5. ✅ **输出安全**:统一错误信封不吐堆栈;响应带 `X-Content-Type-Options: nosniff`、`X-Frame-Options`、`Referrer-Policy` 6. ✅ **公告正文白名单净化**:写库时 + 公开读时双重净化,防存储型 XSS(与 11.1 的信任边界配套) 7. ✅ **静态黑名单 + 路径穿越防护**:`backend/`、`data/`、点开头目录、`*.py` / `*.sh` 一律 404 8. ✅ **零第三方依赖**:纯标准库,无包管理器 / 供应链风险;系统 Python 保持安全更新即可 9. ✅ **密钥管理**:D1 凭证仅存 `backend/cloudflare.json`(本地、随包下发但不外露),泄露即轮换重建;D1 Token 按最小权限(仅 D1 Edit);IndexNow 密钥可随时更换 10. ✅ **审计日志**:登录 / 登出 / 登录失败已落盘可查(管理面板);◻ 建议补管理写操作日志(发布 / 更新 / 删除对象) 11. ◻ **备份**:`backend/data/` 与 D1 定期导出;备份加密存放、异地保存;定期做恢复演练(闲时) 12. ◻ **主机层**:1Panel 面板 2FA、仅开放 80 / 443(管理端口只对白名单)、系统仅装安全更新、关闭 root 口令登录 13. ◻ **监控与告警**:`/api/v1/health` 外部探活;登录失败 / 429 突增、磁盘占用异常时告警;日志设置保留周期与磁盘限额 14. ✅ **隐私最小化**:统计仅聚合计数、完整 IP 仅在内存解析不落盘(与 1.2 设计原则第五条一致) 15. ✅ **报错不泄露内部信息**:错误响应只回机器码与通用中文提示;数据库 / 网络报错的原文、配置文件路径等技术细节写入 `ApiError.detail`,**仅进服务端 stderr 日志**,绝不返回给客户端(避免攻击者从报错里反推实现与配置) 16. ✅ **日志不落敏感串**:登录日志按「去控制字符 + 限长 64」净化写入的用户名字段(防止有人把口令误敲进用户名框、或被超长内容灌日志,把敏感串写进 `admin_log.jsonl`) ### 11.5 SSRF 与出站安全(重要) 本站全部出站请求的目标主机都是**代码内写死的常量**,不接受用户输入拼进 URL: | 出站位置 | 目标主机 | 用户可控输入 | 防护 | |---|---|---|---| | `geo.py` 地理解析 | `ip-api.com`(固定) | 访客 IP | 先经 `safe_public_ip()` 规整为**公网 IP 字面量**(`ipaddress` 解析,拒绝域名 / 带路径 / 带端口 / 内网 / 回环 / 保留段),再拼进路径 | | `nodes.py` 节点状态 | `cf-v2.uapis.cn`(固定) | 无(节点名来自配置) | 参数仅来自 `config.NODES` | | `monitor.py` 优选探测 | `config.PING_TARGETS`(固定) | 无 | 探测目标仅在配置中维护 | | `d1.py` 数据库 | 配置的 CF 端点 | 无 | 端点来自环境变量 / 配置文件 | | `seo.py` 收录推送 | `api.indexnow.org`(固定) | 无 | sitemap 来自本地文件 | 两条关键说明: 1. **代理头不可信**:`CF-Connecting-IP` / `X-Real-IP` / `X-Forwarded-For` 完全由客户端提供。仅在 `config.TRUST_PROXY_HEADERS = True`(站点确实位于可信反向代理 / CDN 之后)时才采信,否则一律使用 TCP 对端 IP —— 避免攻击者伪造头来污染地理解析或绕过按 IP 的限速。 2. **节点侧 SSRF 由本站入口校验兜住**:`GET /api/v1/accel` 拼出的地址会被**加速节点所在的机器**真正发起请求,因此接口对目标 URL 做严格校验(仅 GitHub 系域名白名单、拒绝 `@` userinfo 伪装、拒绝空白 / 控制字符、限定协议与长度),确保节点永远不会被诱导去访问内网或云元数据地址(如 `169.254.169.254`)。 ### 11.6 下载入口限速 - **本站能限的部分**:`GET /api/v1/accel` 每 IP 每分钟 **30 次**(`config.ACCEL_RATE_LIMIT`),超出返回 `429 rate_limited`;公开上报接口(统计 / 监测)每 IP 每分钟 120 次,超限静默丢弃;登录与 admin 鉴权失败各 10 次 / 10 分钟。 - **本站限不到的部分(务必知悉)**:镜像页的下载链接是**直连加速节点**的(`http://<节点>/`),流量不经过本站服务器,因此**按 IP 的真正下载限速必须由节点所在机器上的代理程序实施**。建议在节点侧按 IP 做并发 / 频率限制(如单 IP 每分钟 N 次、单 IP 并发 ≤ M),否则节点带宽会被刷干 —— 这是自建节点最容易被打的点。 --- ## 十二、错误码速查表 | HTTP | code | 触发场景 | 排查建议 | |---|---|---|---| | 400 | `invalid_json` | 请求体缺失 / 非 JSON / 顶层不是对象 | 检查 `-d` 数据与 `Content-Type`;文件方式提交可避开转义坑 | | 400 | `missing_field` | 缺必填字段(title / body / name / desc / url / api / event) | 对照各节字段表 | | 400 | `invalid_value` | 超长、date 非法、cover/url 非法、limit/offset 越界、event 名不合法 | 错误 message 会指明具体字段 | | 401 | `unauthorized` | admin 接口未登录 / 会话失效,或登录用户名 / 口令错误 | 先登录取得会话 Cookie(见 4.2 / 4.4) | | 403 | `forbidden` | 静态路径越界(穿越攻击) | 正常使用不应出现 | | 404 | `not_found` | 接口路径错误,或对象 ID 不存在 | 核对路径拼写与 ID | | 405 | `method_not_allowed` | 对静态资源使用非 GET 等 | 改用正确方法 | | 413 | `payload_too_large` | 请求体 > 1MB | 拆分或压缩内容 | | 429 | `rate_limited` | 登录 / admin 鉴权连续失败超限(按 IP 计数) | 等 10 分钟或重启服务(内存计数) | | 500 | `internal_error` | 未预期异常 | 看服务端 stderr 日志堆栈 | | 503 | `upstream_error` | Cloudflare D1 未配置 / 不可用(登录、写操作) | 检查 `backend/cloudflare.json` 与网络(见 4.1 / FAQ 13.11) | --- ## 十三、FAQ / 故障排查 **13.1 端口被占用** ``` netstat -ano | findstr :8000 → 找到 PID taskkill /PID /F → 结束占用进程 python backend\src\server.py --port 8001 → 或直接换端口(前端同源,需从新端口访问) ``` **13.2 登录口令忘了 / 想重置** - 重置登录口令:`python backend\src\server.py --set-admin wupfish:新口令`(哈希+盐写入 D1,立即生效)。 - 会话问题(登录后又被要求登录):v1.6.0 起会话存于 D1,**重启不再失效**;若仍被弹回登录页,先看 `/sy/admin` 页顶提示条(会写明「未登录 / 接口异常」的具体原因),再重新登录即可。 控制台会打印 D1 状态与当前版本。 **13.3 Docker 里 today/month 边界不对** 容器默认 UTC,「当日」「当月」按服务器本地时区切分。设置 `TZ=Asia/Shanghai` 环境变量(Debian 基础镜像需 `apt-get install tzdata`)。 **13.4 反代后地区全是「内网/本地」** 反代没有传真实 IP 头。确认 Nginx 配了 `proxy_set_header X-Real-IP $remote_addr;` 与 `X-Forwarded-For`(见 10.2)。 **13.5 地区全是「未知」** 服务器无法访问 `ip-api.com`(境外服务器被墙 / 出网受限)时,解析会失败并负缓存 10 分钟。可用 `--no-geo` 关闭,或自建反代注入 `X-WF-Region` 头。 **13.6 双击打开 HTML(file://)时接口全失败** 浏览器对 `file://` 页面的 fetch 有限制,且 SDK 的 `/api/v1` 相对路径也无处解析。请通过服务访问:`http://127.0.0.1:8000/sy/xxx`(无后缀地址,旧 `.html` 会自动 301)。 **13.7 想同时跑测试和正式数据** 用不同 `--data-dir` 与 `--port` 起两个实例;同一数据目录不要多实例(无跨进程文件锁)。 **13.8 升级后端会丢数据吗** 不会。管理员 / 统计 / 镜像在 Cloudflare D1,公告与凭证在 `backend/data/`;覆盖 `backend/src/` 代码后重启即可。本地文件损坏时会自动备份为 `.corrupt` 并用默认值继续。 **13.9 首页 / 公告页看不到 API 数据** 依次检查:① `curl /api/v1/health` 是否 200;② 浏览器地址是否同源(不是 file://);③ 公告列表为空时页面**故意**保留内置内容(设计行为,不是故障);④ F12 Network 看 `/api/v1/announcements` 的实际响应。 **13.10 旧命令 `python server.py` 失效** v1.2.0 起后端入口迁移为 `python backend/src/server.py`(见 2.1),根目录旧 `server.py` 已移除;数据也统一迁至 `backend/data/`。 **13.11 D1 报错 / 启动失败(Cloudflare D1 未配置或连不上)** 检查 `backend\cloudflare.json`:代理模式查 worker_url / auth_token,直连模式查 account_id / database_id / api_token(Token 需 D1 Edit 权限);确认机器能访问对应端点(`api.cloudflare.com` 或 Worker 自定义域名,对 Worker 域名返回 `403 forbidden` 属正常密钥拦截)。运行中出现 `503 upstream_error` 也是同一原因,修复后重启即可。 --- ## 十四、相对 v1.0 的变更 v1.0 为纯设计稿(无服务端实现)。v1.1 在**保持只读接口对前端 100% 兼容**的前提下落地实现,并做了如下明确化: | 变更点 | v1.0 | v1.1 | |---|---|---| | 服务端实现 | 无(仅接口设计) | `server.py` 单文件实现,可直接运行 | | 错误响应 | 未定义 | 统一 `{ "error": { "code", "message" } }` + 错误码表 | | POST 管理接口响应 | `{ "id", "created" }` | **`201` + 完整对象**(超集,向后兼容) | | PUT 语义 | 未定义 | 明确为部分更新,响应带 `updated` | | 新增 | — | `GET /api/v1/health` 健康检查 | | 新增 | — | 首次启动播种示例公告 `a-welcome` | | 新增 | — | 静态托管(单进程同源整站)、启动参数、token 文件自动生成 | | 统计口径 | 概念性描述 | 精确定义(UV 按日去重累加、地区与 UV 对齐计数、保留策略、落盘策略) | | 地理解析 | 「由后端解析」一笔带过 | 明确解析链、缓存策略、`X-WF-Region` 注入口、`--no-geo` 开关 | | 字段约束 | 未定义 | 全部字段给出类型 / 必填 / 长度约束,并在服务端强制校验 | | 镜像页接入 | 声称「已接入」(实际未接) | 已真实接入:API 镜像卡片插入 `mirror.html` 列表顶部 | | 修改 | — | mod 页开发提示从 `python -m http.server` 改为 `python server.py` | ## 十五、加速与优选(公开工具接口) 面向外部工具 / 脚本的公开接口,无需鉴权(有速率限制);数据归属见 15.3 第三方声明。 ### 15.1 GET /api/v1/accel —— 加速链接生成 把一个 GitHub 链接按**自建加速节点**拼装成可直接使用的加速地址。 - 查询参数:`url`(必填,`http(s)://` 开头的 **GitHub 系**链接:github.com / raw.githubusercontent.com / codeload.github.com / objects.githubusercontent.com / *.githubusercontent.com;其它域名返回 400) - 响应:`{"target": "...", "nodes": [{"name": "日本-4", "url": "http://jp-4.qwq.fan:44532/<原链接>"}, …]}` - 节点:**9 个自建节点**(日本-4 / 韩国-1 / 美国-3 / 中国香港-plus / 德国法兰克福 / 美国洛杉矶-3 / 立陶宛维尔纽斯 / 台湾-台北 / 美国-2),承载于第三方 FRP 隧道,清单见 `backend/src/config.py` 的 `NODES`(唯一数据源);其中台湾-台北、美国-2 每日限 3G - 速率限制:每 IP 每分钟 120 次,超限返回 `429 rate_limited` 示例:`GET /api/v1/accel?url=https://github.com/localsend/localsend/releases/download/v1.18.2/LocalSend-1.18.2-windows-x86-64.zip` ### 15.2 GET /api/v1/domains —— 优选域名清单(配置直出) 优选域名 / 优选 IP 的**唯一数据源**在 `backend/src/config.py` 的 `PREFERRED_DOMAINS` —— 优选页渲染、首页「今日优选」轮换、实时监测目标全部由此派生,新增或更换域名只改这一处;本接口直接返回该清单,**不依赖任何上游**,不存在「上游过期」问题。 - 响应:`{"updated": 毫秒时间戳, "items": [{"platform": "CloudFlare", "code": "CF", "base": "cloudflare.19931110.xyz", "prefix": "cf", "tag": "third", "host": "cf.cloudflare.19931110.xyz", "probe": "cf.cloudflare.19931110.xyz", "browser": false}], "ips": ["108.162.192.147", "108.162.192.149", "108.162.192.235"]}` - 字段:`platform` / `code` 平台名与简称;`base` 基础域名(页面展示 `*.base`);`prefix` 示例前缀(`host` = `prefix.base`,复制用);`tag`:`third`(第三方公益,19931110.xyz 系列)/ `self`(站长自建,18615615.xyz 系列);`probe` 实时监测宿主(可能为裸域名);`browser` 浏览器能否直连测量(见 15.4);`ips` CloudFlare 优选 IP(实测解析) - 消费方:`sy/domain`(完整清单)与首页「今日优选」轮换;接口不可用时页面给出提示 - 数据归属与免责见 15.3 与「版权声明」页。 ### 15.3 第三方数据声明(重要) - **优选域名(15.2,`*.19931110.xyz` 系列)为第三方公益提供**:为公开分享的优选域名服务,本站仅做整理展示与在线监测,**不运营、不担保**,可用性请自行评估。 - **优选域名(`*.18615615.xyz` 系列)为站长自建**:由本站站长自行部署维护,同样按「尽力而为」提供,不承诺 SLA。 - **加速节点(15.1)为本站自建**:承载于第三方免费的 FRP 隧道服务(qwq.fan / ChmlFrp / MEFrp),节点本身由本站站长配置与维护;隧道服务本身可用性由对应提供方决定,失效时可在 `config.py` 的 `NODES` 中调整。 - 其它第三方数据源(GitHub、Modrinth、Thunderstore、miawa.cn 等)的归属与用途见「致谢名单」页(`sy/thanks`)与第六章说明。 ### 15.4 优选监测与延迟(/ping/board · /ping/report · /ping/summary) 优选监测相关公开接口:`/ping/summary`(各域名最近延迟)/ `/ping/board`(24 小时格图)/ `/ping/report`(访客实测上报),均为**开放能力**,供第三方使用;站内页面已不展示监测数据。数据存 `backend/data/ping.json`(本地原子写,不入 D1)。 服务器探测:**每 5 分钟一轮**,判定为「TCP 可达或能出任意 HTTP 状态码即视为在线」(不要求 200 与证书匹配;443 不通自动回退 80),全部优选域名(含 CNAME 目标类)均纳入;格图按 20 分钟一格聚合同一组数据。 **GET /api/v1/ping/board?host=<域名>** —— 读取监测数据(公开,免鉴权)。 | 字段 | 类型 | 说明 | |---|---|---| | `host` | string | 查询参数;留空或非法值回落为监测目标列表的第一个(响应 `host` 为实际使用的域名) | | `slot` | number | 一格时长(**秒**;当前 1200 = 20 分钟) | | `slots` | number | 总格数(当前 72) | | `end` | number | 当前格编号(`epoch 秒 ÷ slot`,非时间戳) | | `now` | number | 当前毫秒时间戳 | | `targets` | string[] | 全部监测目标(= `PREFERRED_DOMAINS` 的 `probe` 列;新增域名只改配置) | | `rows` | object | 固定四行:`电信` / `移动` / `联通` / `海外`;行内字段见下 | | `rows[ISP].cells` | number[] | 长度 = `slots`(下标 0 = 最早一格);取值:`0` 零丢包 · `1` 丢包 ≤10% · `2` ≤30% · `3` >30% · `4` 全失联 · `-1` 无数据 | | `rows[ISP].latency` | number \| null | 窗口内平均延迟(毫秒;无成功样本为 `null`) | | `rows[ISP].loss` | number \| null | 窗口内丢包率,0~1(无样本为 `null`) | **GET /api/v1/ping/summary** —— 各监测目标最近一轮探测(公开,免鉴权)。 - 响应:`{"updated": 毫秒时间戳, "items": [{"host": "cf.cloudflare.19931110.xyz", "latency": 160, "loss": 0.0, "online": true}]}` - 字段:`latency` 最近一格内的平均延迟(毫秒;无成功样本为 `null`);`loss` 丢包率(0~1);`online`:最近一格是否有成功探测(`true` / `false`;从未探测为 `null`) **POST /api/v1/ping/report** —— 上报一条访客实测样本(开放能力,每 IP 限速,超限静默丢弃)。 - 请求体:`{"host": "vercel.18615615.xyz", "ms": 123}`;`host` 必须在 `targets` 内,`ms` 为整数毫秒(**≤0 表示失败**); - 响应:`{"ok": true}`;参数非法返回 `400 invalid_value`。 > 说明:站内页面不做访客实测也不展示监测数据;三个 ping 接口均为开放能力,第三方可自行使用(`GET /api/v1/domains` 的 `browser` 字段标明哪些域名可被浏览器直连测量)。 ### 15.5 GET /api/v1/nodes —— 自建加速节点状态 统计页底部「加速节点」看板的数据来源:服务器每 5 分钟对全部节点做一轮 TCP 探测(隧道在线才连得通), chmlfrp 节点另取其公开接口的近 7 天在线率(无需令牌)。 - 响应:`{"updated": 毫秒时间戳, "online": 7, "total": 9, "items": [{"id":"kr1","name":"韩国-1","area":"韩国","carrier":"qwq.fan","host":"kr-1.qwq.fan","port":16984,"online":true,"latency":97,"uptime":null,"limit":""}]}` - 字段:`online` 在线节点数 / `total` 总数;`items[].online` 本轮 TCP 探测是否可达;`latency` 探测耗时(毫秒,不可达为 `null`);`uptime` 近 7 天平均在线率(%,仅 chmlfrp 节点);`limit` 流量限制说明(如 `每日限 3G`) - 节点清单与 `GET /api/v1/accel` 同源(`config.py` 的 `NODES`) --- ## 十六、搜索引擎收录 ### 16.1 收录(管理接口,IndexNow) - `GET /api/v1/admin/seo` → `{"key","keyLocation","sitemapUrls","endpoint"}`; - `POST /api/v1/admin/seo/indexnow` → 读取站点根 `sitemap.xml` 全量 URL,提交给 `https://api.indexnow.org/indexnow`;返回 `{"ok","status","submitted","message"}`; - 归属校验:站点根目录 `.txt`(内容即密钥);免费、无需账号,覆盖 Bing / Copilot / Yandex / Naver / Seznam 等参与方; - 另有 `robots.txt`(放行全部主流 AI 爬虫)与 `llms.txt`(AI 助手可读站点摘要)。 --- ## 十七、变更记录 | 版本 | 日期 | 说明 | |---|---|---| | v2.0.7 | 2026-10-12 | **节点替换 + 弹窗公告 + SEO 深化**:① 两个下线节点(日本-4 / 韩国-1)替换为自建新线路 **日本-1(jp-1.qwq.fan:30469)/ 台湾-1(tw-1.qwq.fan:61260)**(config.py 的 NODES 唯一数据源 + 镜像页选单同步 + `frp/github7/8.toml` 写入新隧道配置;新增 `frp/reload.sh` 一键重载:改完配置在 1Panel 点一次任务即可,内置 CANARY 防自杀 + setsid 防连带回收);② **弹窗公告**上线:`GET /api/v1/popup`(公开,下架返回 null)+ `GET/PUT /api/v1/admin/popup`(管理);存储 `backend/data/popup.json`;`nav.js` 全站弹窗(管理/登录/跳转页除外,同一会话只弹一次,纯文本转义渲染 + 仅放行 http(s) 链接,淡入缩放 0.22s 且尊重 reduced-motion);管理面板新增「弹窗」页签(上架 / 更新 / 下架);③ **SEO 深化(第二批)**:镜像页「GitHub加速 / GitHub镜像 / GitHub资源」、代理页「免费公共代理 / 公共代理 / HTTP代理」、首页「免费资源 / 资源站 / 小众资源站」标题 / 描述 / 关键词 + 静态文字区(重复且连续)+ og 同步更新;sitemap 三页 lastmod 更新;④ 公告页新增 2026-10-12 条目;静态资源版本号递增为 `nav.js?v=2.0.7` | | v2.0.6 | 2026-10-12 | **镜像页节点自由选择 + 统计精简 + SEO 深化**:① 镜像页顶部新增「下载节点」下拉,访客可从 **9 条自建线路自由切换**(选择记在本地,改主下载按钮指向),默认主节点由 日本-4 换为更稳的 **韩国-1**、备用固定 **美国-3**(实测 9 条隧道已全部上线,`proxyProtocolVersion='v2'` 已开,按真实访客 IP 限速生效);② **去掉第三方服务 API 的流量统计**:移除 `nodes.py` 的 chmlfrp 令牌/今日流量/每日快照与 `config.CHMLFRP_TOKEN`,节点看板只保留「服务器探测 + 近 7 天在线率」;③ **SEO 深化**:优选页标题/描述/关键词改为「Cloudflare优选 / EdgeOne优选 / Vercel优选 / Netlify优选」高频组合词,新增静态文字区(四平台各自成句,重复且连续)与 FAQPage 结构化数据,sitemap 该页 priority 提至 0.9;④ 代码清理(删除流量相关死代码);静态资源版本号递增为 `nav.js?v=2.0.6` | | v2.0.5 | 2026-10-12 | **安全加固**:① 配置 chmlfrp 用户令牌(`config.CHMLFRP_TOKEN`);② **SSRF 防护**:新增 `geo.safe_public_ip()`(出站前把访客 IP 规整为公网字面量,拒绝域名 / 带路径 / 带端口 / 内网 / 回环 / 保留段)并新增 `config.TRUST_PROXY_HEADERS` 开关(不可信代理头不再被采信,防伪造头绕过限速 / 污染解析);`accel` 目标 URL 校验加严(拒绝 `@` userinfo 伪装、空白 / 控制字符、缺主机名);③ **报错脱敏**:`ApiError` 新增 `detail` 字段,数据库 / 网络报错原文与配置路径只进服务端日志,客户端只看到「数据库暂时不可用」等通用提示(原来会把 Cloudflare 报错原文与配置文件路径回给调用方);④ **日志脱敏**:登录日志的用户名去控制字符并限长 64(防口令误入用户名框被写进 `admin_log.jsonl`);⑤ **下载入口限速**:`/api/v1/accel` 由 120 次/分收紧到 **30 次/分**(`config.ACCEL_RATE_LIMIT`);文档新增 11.5 SSRF 与出站安全、11.6 下载入口限速(说明直连节点流量需在节点侧限速);静态资源版本号递增为 `nav.js?v=2.0.5` | | v2.0.4 | 2026-10-12 | **加速节点全面自建**:移除全部第三方公益加速节点(镜像页下载链接 / 加速页 / 管理面板 URL 加速 / 首页 FAQ / 致谢名单同步替换),改为 **9 个自建节点**(日本-4 / 韩国-1 / 美国-3 / 中国香港-plus / 德国法兰克福 / 美国洛杉矶-3 / 立陶宛维尔纽斯 / 台湾-台北 / 美国-2,承载于 qwq.fan / ChmlFrp / MEFrp 第三方 FRP 隧道;后两条每日限 3G),主用 日本-4、备用 中国香港-plus;节点清单收敛为唯一数据源 `config.py:NODES`(`ACCEL_NODES` 由其派生);新增 `GET /api/v1/nodes`(节点在线状态 / 延迟 / 7 日在线率 / 今日流量,chmlfrp 节点走其公开 API,其余由本站服务器探测)与统计页底部「加速节点」看板;静态黑名单新增 `frp/`(FRP 配置含令牌,不外托管);新增 qwq.fan 三个节点的 frpc 配置(`frp/github7~9.toml`);静态资源版本号递增为 `nav.js?v=2.0.4` | | v2.0.3 | 2026-10-12 | 优选页进一步精简:**移除逐域名延迟展示**(v2.0.2 上线)与相关文案 / 样式 / 脚本,页面回到纯清单(域名 + 归属标签 + 复制);三个 ping 接口(`summary` / `board` / `report`)保留为开放能力;静态资源版本号递增为 `nav.js?v=2.0.3` | | v2.0.2 | 2026-10-10 | 优选页**监测图下线,改为逐域名延迟**:服务器探测提速为**每 5 分钟一轮**(数据仍按 20 分钟一格聚合供格图接口),新增 `GET /api/v1/ping/summary`(各域名最近延迟 / 丢包 / 在线态),页面每个域名行内直接显示延迟(≤200ms 绿 / ≤500ms 黄 / 更高红 / 失联灰,3 分钟自动刷新);页面不再做访客实测上报(`POST /ping/report` 作为开放能力保留);移除监测图相关样式与全部说明文案,页面精简;静态资源版本号递增为 `nav.js?v=2.0.2` | | v2.0.1 | 2026-10-09 | **监测判定放宽 + 去硬编码**:服务器探测不再要求 200 / 证书匹配——**TCP 可达或能出任意 HTTP 状态码即视为在线**(443 不通自动回退 80,TLS 异常不再误判为失联),全部优选域名纳入「海外」行;优选清单收敛到服务端**唯一数据源** `config.py:PREFERRED_DOMAINS`(监测目标由其派生),新增 `GET /api/v1/domains` 配置直出,`sy/domain` 与首页「今日优选」改为动态渲染(页面不再内置副本);浏览器实测仅对可直连域名采样(`browser` 字段标注可测性,避免证书不匹配误记为线路故障);接口文档新增「防中间人攻击(MITM)」与「后端安全加固」方案(十一章节);精简死代码;静态资源版本号递增为 `nav.js?v=2.0.1`、`common.css?v=1.9.8` | | v2.0.0 | 2026-10-09 | **精简改革**:移除「IP 查询」「图床」「One IP」工具(nav 下拉、页面、站点地图与文档同步清理);**优选整合**:四平台优选(Cloudflare / EdgeOne / Netlify / Vercel,含站长自建 `*.18615615.xyz`)合并为站内「优选域名」页 `sy/domain`,不再跳转外部网站,首页「今日优选」改内置清单;彻底移除 CNAE 上游依赖(下线 `GET /api/v1/domains`、`backend/src/upstream.py` 与 `backend/upstream.json`);新增**优选实时监测**:`GET /api/v1/ping/board`(24 小时 × 20 分钟一格,电信 / 移动 / 联通 + 海外四行)+ `POST /api/v1/ping/report`(访客浏览器实测上报,按运营商归档),由 `backend/src/monitor.py` 实现(数据存 `backend/data/ping.json`,服务器每 20 分钟对目标做 TCP+TLS 握手探测归入「海外」行);静态资源版本号递增为 `nav.js?v=2.0.0` | | v1.10.0 | 2026-10-07 | 按需求移除「萌ICP备20260939号」(站内页脚、关于下拉、登录 / 跳转 / 代理页静态页脚、版权声明页第 4 节),恢复原「星ICP备2026001号」展示与版权页趣味说明;静态资源版本号递增为 `?v=1.10.0` | | v1.9.8 | 2026-10-07 | 页脚加入「萌ICP备20260939号」备案链接(指向 icp.gov.moe,新窗口打开);静态资源版本号递增为 `nav.js?v=1.9.8` | | v1.9.7 | 2026-10-07 | QQ 交流群面板改为**淡入 + 从右侧轻滑的开合动画**(0.22s,`visibility` 延迟过渡,支持 `prefers-reduced-motion`;此前为硬切显得突兀);镜像卡片修复**圆角与阴影**:统一 14px 圆角、替换面板默认大投影为轻量阴影(悬停时轻微加深 + 上浮),并去掉 `.panel` 默认外边距让网格间距均匀;静态资源版本号递增为 `?v=1.9.7`;公开加速节点从 4 个扩充为 **15 个**(网络收集整合,镜像加速页与 `/api/v1/accel` 访客自由选择) | | v1.9.6 | 2026-10-07 | 镜像页改为**卡片网格布局**(与 Mod 页一致,窄屏自动单列;描述最多 5 行,不再一条长龙往下滑);镜像英文工具名补**中文名标签**(内置 12 款 + 柠泽 12 款启动器/驱动/插件本地映射,免跳外站查询);Mod 卡片「译」按钮升级为**标题 + 描述一起翻译**(标题显示为「中文(原名)」,MyMemory 按需触发,不自动翻译以免耗尽配额);`GET /api/v1/stats/regions` 新增 `detail=1` 参数(返回含国外原始地区名的明细,**需管理会话**,未登录自动回落合并视图——公开页仍显示「海外」),管理面板地区分布改用明细(前 12) | | v1.9.5 | 2026-10-07 | **统计页重做为「数据洞察」**:站点概览(总访问/下载/独立访客/近 30 天/今日/上线天数)、**本站 API 调用**与外部接口调用(新增计数:服务端统计全部 `/api/v1` 请求,上报类 POST 除外,经事件表持久化)、近 30 天趋势 SVG 图(逐日 pv/uv,`summary` 新增 `trend` 字段)、热门资源排行、**国内访问分布**(内嵌简化省界,自托管 `sy/china-map.js`,不依赖外部 CDN,悬停显示访问量与占比)与**省份占比玫瑰图**;地区名归一修复:省份统一为规范全称(如 广东→广东省)、国外州名等合并为「海外」(`geo.normalize_stored`,历史数据读时归一);QQ 交流群悬浮入口改为点击弹出加群小面板(「用 QQ 打开」mqqapi + 「复制群号」兜底,修复网页加群入口不可用的问题);公告排序修复:置顶公告永远在最上方,非置顶公告插入内置置顶之后,不再把置顶挤下去;静态资源版本号递增为 `?v=1.9.5` | | v1.9.4 | 2026-10-07 | 新增**镜像加速页** `sy/accel`(粘贴 GitHub 链接一键生成多条加速地址,支持 `?url=` 预填、一键复制、中英双语)与**开放 API 目录页** `sy/api`(接口清单 + 复制/试调按钮 + 调用须知,文档指向根目录 API.md);导航栏新增「API」入口、工具菜单新增「镜像加速」;公开只读接口放开 **CORS**(`Access-Control-Allow-Origin: *`,管理 / 鉴权接口不放行;OPTIONS 预检返回 204);静态资源版本号递增为 `nav.js?v=1.9.4` | | v1.9.3 | 2026-10-07 | 按需求整体移除**节点榜单**:榜单页 `sy/rank`、公开接口 `GET /api/v1/rank` 与 `POST /api/v1/rank/report`、后台 `probe.py` 自探测与众测全部下线(首页「查看完整榜单」入口与工具菜单入口一并移除);QQ 交流群悬浮入口改用与站内白框面板一致的配色(渐变 + 白描边,浅色 / 护眼主题同步适配)并去掉图标,仅保留「QQ群」文字;静态资源版本号递增为 `?v=1.9.3` | | v1.9.2 | 2026-10-07 | 首页优选提示自适应:数据为通配域名(`*`)时提示"把 * 替换为子域",为 CNAE 优选 IP 时提示"直接填入域名解析 / hosts"(此前 IP 也显示"替换 *",文案对不上);语言切换时首页动态提示同步重绘(nav.js 新增 `wf-lang` 事件);移除旧版播种的"API 服务已上线"示例公告(启动自动清理,首启不再播种);静态资源版本号递增为 `nav.js?v=1.9.2` | | v1.9.1 | 2026-10-07 | 修复「部署后页面看似没更新」:Cloudflare 会给 `.js/.css` 下发 4 小时浏览器缓存(实测 `max-age=14400`),旧 `nav.js` / `content.js` 与新页面混用会让首页优选、首栏公告、工具卡空白,页脚致谢与榜单入口缺失——现全站静态资源引用统一带版本号(`?v=1.9.1`,发版时递增)击穿缓存;首栏公告容器补 `[hidden]` 隐藏样式(无数据时不再露出空条);内容脚本调用加 `typeof` 防御,接口或脚本缺失不再中断整页脚本 | | v1.9.0 | 2026-10-07 | 新增**节点榜单**:公开 `GET /api/v1/rank`(本站服务器 DNS/TCP/TLS/首字节四段探测 + 访客匿名众测,24h 滚动窗口,记录存 `backend/data/probe.json`)+ `POST /api/v1/rank/report`(每 IP 60 次/分钟);新增榜单页 `sy/rank`(排名 / 健康状态 / 分段耗时 / 迷你趋势 / 「测我的网络」众测 / 宝塔拨测·KKCE·boce·ITDOG 一键复核,声明归属);新增**收录**:管理接口 `GET /api/v1/admin/seo` 与 `POST /api/v1/admin/seo/indexnow`(IndexNow 一键推送 sitemap,免费无账号),管理面板新增「收录」页签,站点根新增 `favicon.ico` / `llms.txt` / IndexNow 密钥文件,`robots.txt` 显式放行 GPTBot·ClaudeBot·PerplexityBot 等 AI 爬虫;全站新增 QQ 交流群右侧悬浮入口;性能优化:移动端关闭背景模糊与动画、logo 字体改本地自托管(移除 Google Fonts 阻塞引用)、精简无效 CSS | | v1.8.1 | 2026-10-06 | 新增公开工具接口:`GET /api/v1/accel`(GitHub 链接一键生成多节点加速地址,含每 IP 速率限制)+ `GET /api/v1/domains`(优选数据:后端 10 分钟缓存转发 **CNAE 公布的公开 API**;未配置 `backend/upstream.json` 时前端自动回退内置快照),首页「今日优选」接入实时数据;API.md 新增「十五、加速与优选」章节并明确声明该优选 API 归属 CNAE(nexariust);英文翻译扩展:各页面标题/描述、首页要点与页脚致谢随中英切换 | | v1.8.0 | 2026-10-06 | 新增**首栏公告**:首页顶部公告栏(独立于公告列表,不进入公告页),`GET /api/v1/banner`(公开;下架返回 `null`)+ `GET/PUT /api/v1/admin/banner`(鉴权;发布 / 更新 / 下架),管理面板新增「首栏」页签,首次启动自动播种默认公告;页脚新增致谢(蓝点云服务器 / Cloudflare CDN,带 logo);致谢名单逐项标注名称与网址;移除各页面分散的数据来源与免责声明(统一收敛到致谢名单);内置公告重写为精确到日 | | v1.7.2 | 2026-10-06 | 公告页修复:后台公告改为**追加**在内置更新日志之上——此前逻辑是"后台一旦有公告就整体替换内置内容",导致内置公告在后台有数据后全部消失;现在内置内容仅在接口故障时作为兜底,后台公告永远置顶展示 | | v1.7.1 | 2026-10-06 | 管理面板类名由 `ad-` 前缀改为 `mg-`(广告拦截插件会隐藏 `[class^="ad-"]` 元素,导致面板整体不可见——纯类名调整,外观与功能不变);公告页元素类名由 `notice-*` 改为 `bulletin-*`、管理面板公告区 ID 同步改名(拦截插件同样按 notice 规则隐藏公告列表);镜像页新增**「柠泽」分类**:前端实时读取柠泽资源站(miawa.cn)公开 v2 接口(`/api/v2/launchers`,CORS 开放、自带 5 分钟缓存),展示 12 个 Minecraft 启动器/驱动/插件的最新版本与日期,其文件直链带 PoW 防滥用校验,下载跳转其站内页面(`/files/`),点击计入下载统计;公告正文新增**白名单净化**(写库时净化 + 公开读时二次净化,防存储型 XSS);后端安全加固:限速与地理解析改用**真实客户端 IP**(CF-Connecting-IP → X-Real-IP → XFF 首段;原先取 TCP 对端,经 Cloudflare + 本机反代后是共享 IP,会把登录限速变成"全局桶"),公开统计上报加每 IP 限速(120 次/分钟,超限静默丢弃),响应头补 `X-Frame-Options` / `Referrer-Policy`,`--set-admin` 改为交互式口令输入(不再接受命令行明文口令) | | v1.7.0 | 2026-10-06 | 新增**镜像下载量统计**:`POST /stats/download`(按镜像 id 计数,存 D1 `stats_downloads`)+ `GET /stats/downloads`(排行),镜像页主按钮 / 备用节点 / 历史版本下载自动上报,管理面板「统计」页新增下载排行;`summary.downloads` 口径调整为「镜像下载 + 事件下载」之和;镜像页备用加速节点由失效的 `github.moeyy.xyz` 更换为 `ghfast.top`(实测可用),管理面板新增「URL 加速」页签(一键生成多个第三方加速节点链接、可复制);镜像页与首页新增「常见问题 / 下载说明」内容与 FAQ 结构化数据(SEO);静态黑名单补上 `*.sh`(部署脚本随包放在站点目录也不对外暴露) | | v1.6.0 | 2026-10-05 | 登录会话改存 D1 `sessions` 表(仅存 token 的 SHA-256):后端重启 / 重新部署不再全员登出;登录链与面板异常可视化——`sy/admin` 页顶提示条写明「未登录 / 接口异常 / 脚本错误」具体原因,不再白屏或静默跳转,登录成功后复核会话生效、失败给出明确原因;页面地址全面无后缀(`/sy/mirror` 等),旧 `.html` 地址 301 归一为唯一规范 URL;新增 `robots.txt` 与 `sitemap.xml`,主要页面补充 description / canonical / OG / JSON-LD 结构化数据 | | v1.5.0 | 2026-10-04 | 公告支持**定时上/下架**:新增 `startAt` / `endAt` 毫秒时间戳字段(可清除),公开列表只返回生效窗口内的公告;新增 `GET /admin/announcements` 管理列表(含未上架/已下架);管理面板表单新增定时选择与状态徽标 | | v1.4.0 | 2026-10-04 | 新增管理面板 `sy/admin.html`(公告 / 镜像 / 统计 / 日志,登录后进入);镜像改造:新增 `cat` / `asset` 字段,下载加速与历史版本改为前端实时对接 GitHub(**不再落库**,删除 `mirror_versions` 与 `version`),删除改为硬删,`GET /admin/mirrors/{id}/versions` 下线;登录链简化:去掉 HMAC 签名 Cookie 与独立 Bearer token,改为服务端内存会话(登出即失效、重启需重登),新增登录 / 失败 / 登出日志(`admin_log.jsonl`)与 `GET /admin/logs`;统计仍存 D1 不变 | | v1.3.0 | 2026-10-04 | 接入 Cloudflare D1(库 `wupfish`):管理员(用户名 + 口令,PBKDF2 哈希 + 随机盐)、统计、镜像改存 D1;镜像新增自动版本历史与 `GET /admin/mirrors/{id}/versions`(删除为软删);登录升级为用户名 + 口令(`--set-admin` 重置);新增 `backend/cloudflare.json` 凭证配置(Worker 代理 / 直连 D1 REST 双模式);错误码新增 `503 upstream_error` | | v1.2.1 | 2026-10-04 | 移除文章(articles)功能:接口、SDK 预留方法与文档章节一并删除;内容类型现为公告与镜像 | | v1.2.0 | 2026-10-04 | 新增管理登录(`sy/login.html` + `/api/v1/auth/*`,PBKDF2 口令、HMAC 签名会话 Cookie);后端重构为 `backend/src/` 模块化多文件,数据迁至 `backend/data/`;静态托管新增安全黑名单(修复旧版可下载 `server.py` 与 `data/*` 的暴露问题);启动方式变更为 `python backend/src/server.py` | | v1.1.0 | 2026-09-10 | 服务端实现落地(server.py);统一错误信封;新增 health / 种子数据 / 静态托管 / 地理解析;补全全部字段约束与运维说明;镜像页真实接入 API | | v1.0 | 2026-09(初稿) | 首版接口设计:公告 / 镜像 / 文章 / 统计四组接口与 content.js、stats.js SDK 约定 |