# ReleaseDock
**Repository Path**: ica520/release-dock
## Basic Information
- **Project Name**: ReleaseDock
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-11-08
- **Last Updated**: 2026-11-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# ReleaseDock · 软件分发台
内网自建、面向科室的软件分发台:管理员把安装包放到**中科院数据胶囊**,开启分发后拿到链接与二维码;
访客扫码填写**这份应用自己的核实信息**,校验通过后进入下载。
- 后端:Python 3.10+(开发机 3.13)+ FastAPI + SQLite,项目内 `.venv`,免安装
- 前端:React 19 + HeroUI v3(官方组件)+ Tailwind v4 + Vite,构建产物由 FastAPI 托管;
更新内容用 Markdown(`marked` 解析 + `DOMPurify` 消毒后渲染,防存储型 XSS)
- 云存储:**内置纯 Python S3 客户端**(`backend/s3.py`,标准库 SigV4 签名,读写 / 列举 / 校验 / 分段传输 / 流式转发),
预签名直链用同一套签名(`backend/presign.py`)——**不需要 rclone,也不需要任何外部可执行文件**
- 配置:除监听地址与端口外,全部存数据库、在网页上改,保存即生效
- 备份:`data/releasedock.db` 一个文件(含配置、名单令牌、访问日志)
> 状态(2026-11-08):`scripts/selftest.py` **155 项全通过**(全程离线,假胶囊也是 Python 写的);界面截图见 [docs/shots](docs/shots)。
> 真实数据胶囊已接通(列举 / HEAD / 上传 / 流式下载均实测通过),但该网关**拒绝预签名直链**,下载自动走服务端流式转发,详见 §5.3、§5.4。
---
## 1. 快速开始(Windows)
```bat
python -m venv .venv
.venv\Scripts\python.exe -m pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
cd frontend && npm install && npm run build && cd ..
.venv\Scripts\python.exe run.py
```
也可以双击 `start.bat`(会检查 venv、必要时构建前端并打开浏览器)。之后打开 。
Linux 部署见 §10.1,只有三步:拷代码、建 venv 装依赖、`python run.py`(没有别的组件要装)。
`run.py` 的常用参数:`--port`、`--host`、`--dev`(后端热重载)、`--init`(只生成配置)、`--selftest`。
## 2. 首次初始化
数据库为空时打开网页会进入初始化向导,**不需要改任何文件**:
1. **管理员账号**:设账号口令,确认「分发链接的对外地址」(默认取本机局域网 IP);
2. **数据胶囊**(可跳过):填 Endpoint、Bucket、AccessKey ID、Secret 四项,其余用默认;
3. **恢复密钥**:生成一次性的 `XXXX-XXXX-XXXX-XXXX-XXXX`,抄下来或下载 txt。忘口令全靠它。
完成后自动登录。演示数据(虚构,可清理):
```bat
.venv\Scripts\python.exe scripts\seed_demo.py --fake-capsule :: 起本地假胶囊 + 演示应用
.venv\Scripts\python.exe scripts\seed_demo.py --stop / --clean
```
自动化部署可用环境变量 `RELEASEDOCK_ADMIN_PASSWORD` 预建管理员;此时口令与恢复密钥会写入
`data/初始管理员与恢复密钥.txt`。
## 3. 配置放在哪
| 配置 | 位置 | 生效 |
|---|---|---|
| 数据胶囊(Endpoint / Bucket / AK / SK / 区域 / 前缀 / Path-Style) | 网页「设置 → 数据胶囊」 | 保存即生效 |
| 下载方式、预签名有效期、目录缓存 | 网页「设置 → 运行时开关」 | 立即 |
| 核实限流、登录保持、日志保留天数、对外地址 | 网页「设置 → 运行时开关」 | 立即 |
| 信任反向代理头、仅 HTTPS(Secure Cookie) | 网页「设置 → 运行时开关」 | 立即 |
| 管理员 / 口令 / 恢复密钥 | 网页「设置 → 管理员账号 / 我的口令」 | 立即 |
| 监听地址 `server.host`、端口 `server.port`、单次请求超时 `s3.timeout_seconds` | `config/设置.json` | 需重启 |
`config/设置.json` 首次启动自动生成,只有两块:
```json
{
"server": { "host": "0.0.0.0", "port": 8730 },
"s3": { "timeout_seconds": 600 }
}
```
> 从旧版升级:首次启动会把旧 `设置.json` 里已填的 capsule / security / 对外地址一次性迁入数据库,之后以数据库为准。
## 4. 目录结构
```
ReleaseDock/
├─ run.py / start.bat 启动入口(Windows 双击 start.bat)
├─ backend/ FastAPI、SQLite、S3 客户端与 SigV4 签名、名单加密、配置存储、日志保留
├─ frontend/ React 19 + HeroUI v3(构建到 frontend/dist)
├─ config/设置.json 只放监听地址/端口与请求超时(自动生成)
├─ data/releasedock.db 全部业务数据与运行时配置(含胶囊密钥)
├─ deploy/ systemd 单元(另附可选 Nginx 示例,本项目不需要)
├─ docs/shots/ 真机截图(无头 Chrome 实拍)
├─ docs/shots-jobs.example.json 截图任务示例(node scripts/shoot.mjs 用)
├─ requirements.txt 运行依赖;requirements-dev.txt 额外带 httpx(自检)
└─ scripts/ selftest / fake_s3(假胶囊)/ reset_admin / seed_demo / shoot
```
## 5. 数据胶囊接入
### 5.1 填什么
在数据胶囊网页「设置 → 开放接口 / S3 密钥」生成密钥,然后到「设置 → 数据胶囊」:
| 页面字段 | 填什么 |
|---|---|
| 接入点 / Endpoint | `s3.cstcloud.cn`(带不带 `https://` 都行) |
| 桶名 / Bucket | 网页上显示的数据集 id |
| AccessKey ID / Secret | 生成的密钥;保存后只显示打码,留空表示不修改 |
| 区域 / Region | `us-east-1`(默认,必须) |
| 路径寻址 Path-Style | 开启(默认,必须) |
| 签名版本 | v4(内置签名,无需填) |
| 客户端标识 User-Agent | 留空即可;数据胶囊只放行 UA 含 `rclone` 的请求,内置默认值已带上(见 §5.4) |
| 目录前缀 | `releases` 或留空(`/<前缀>/<应用slug>/`) |
点「保存并生效」→「连通性自检」。常见报错:
| 现象 | 原因 | 处理 |
|---|---|---|
| `InvalidAccessKeyId` | AK 错/失效 | 重新生成,别带空格 |
| `SignatureDoesNotMatch` | Secret 或 region 不对 | region 用 `us-east-1` |
| `NoSuchBucket` | 桶名错 | 用网页上那串 id |
| `403 Forbidden` | 密钥对该数据集无权限 | 换密钥/数据集 |
| 中文名下载 500 | 旧版签名编码 bug | 已修(`_encode` 只放行 ASCII) |
协议只支持 S3:数据胶囊就是 S3 服务,内置客户端也只实现 S3(原来靠 rclone 兜着的 WebDAV 备选已去掉)。
### 5.2 列举的两个网关怪癖(实测)
- **列目录的前缀要带结尾斜杠**:`prefix=releases` 返回空,`prefix=releases/` 才列出对象;
单文件用 HEAD 直查正常。代码统一走 `s3.as_dir()` + 先 HEAD 再列父目录,否则上传校验会误判 0 字节、
云端选文件会报"找不到"。
- **桶根本身不能带结尾斜杠**:请求 `/数据集id/` 一律 401,`/数据集id?prefix=...` 才正常(rclone 也是这么发的)。
- **不支持 ListObjectsV2**:带 `list-type=2` 的请求直接 401(空错误体),所以内置客户端用的是老版 ListObjects
(`marker` 分页),任何 S3 服务都支持。
### 5.3 预签名被拒 → 自动回退流式(实测)
拿同一把 AK/SK 生成的预签名 URL 一律 `401`(空包体),换任何 UA、换 boto3 生成都一样;
而头部签名的请求正常。因此程序内置**预签名探针**:
- 启动时后台探一次,结果缓存 10 分钟;
- 探测对象:`//.releasedock-presign-probe.txt`(不到 40 字节,可删,下次自检重建);
- **探不通就自动改为"服务器流式转发"**(`s3.cat_stream()` 边读边发给访客):功能正常,
代价是占本机带宽、浏览器不能断点续传;
- 设置页会显示「预签名不可用 · 走流式」及原因;服务商哪天支持了 query 签名,会自动切回直链。
### 5.4 网关按 User-Agent 放行(实测,重要)
数据胶囊的 S3 网关只放行 **User-Agent 里含 `rclone`** 的请求:空 UA、`Python-urllib`、`boto3`、`s3cmd`、
`minio-go`、`curl` 全部返回 `401`(错误体为空,光看状态码很难定位)。内置客户端因此默认发送:
```
User-Agent: ReleaseDock/0.1 (S3; rclone-compatible)
```
- 该值在「系统设置 → 数据胶囊 → 客户端标识」可改,留空表示沿用默认值;
- 只影响请求头,签名本身与它无关(UA 不进 SignedHeaders);
- 自检里的假胶囊也照这条规则设了 UA 白名单,改坏了会被 `scripts/selftest.py` 拦住。
## 6. 分发流程
### 管理员
1. **新建应用**:名称、平台、核实方式(授权名单 / 仅登记 / 口令 / 免核实)。版本号与安装包在「版本」页管理。
2. **核实信息**:每应用最多 8 个字段,可设类型、必填、最长字数与**归一化规则**(全角半角、中英文括号、
去 `-心内科` 后缀、只留数字……只影响比对)。每个字段下方有「名单标签与校验」两个开关:
- **标签**:这个字段照常出现在访客页、照常参与比对,导入名单时它的值同时写进名单「标签」列(明文),方便认人;
- **仅标签**:不参与核实、不出现在访客表单,只作为名单标签(此时「必填」自动置灰、归一化规则隐藏);
- 两个都不开就是普通核实字段。
**一个应用只能开启一个标签**:在新字段上开启标签时,会自动关掉其他字段的标签开关并提示「标签已换到这个字段」;
只剩一个核实字段时不允许把它改成「仅标签」(否则没人可核实);口令字段两个开关都是灰的(标签是明文,口令只存哈希)。
3. **授权名单**:模板下载 / 手工录入 / CSV·XLSX 导入(GBK 也认),支持搜索、状态筛选、翻页(50/100/200 条)。
详见 §7。
4. **版本 / 安装包**:在「版本」页上传新版本(两步上传:请求体先落暂存,再由内置客户端推到胶囊,超过 64 MiB 自动分段,带进度),
或直接引用云端已有文件;每个版本填版本号 + Markdown 更新内容,并可勾选「设为最新版」。
5. **开分发**:有效期、下载上限、每份核实信息可用次数、会话时长、设备绑定、下载方式。
**三个 0 都是不限**:有效期 0 = 长期(会清旧到期时间)、下载上限 0 = 不限、每份核实信息可用 0 = 不限。
保存后给出链接与二维码(可下载 PNG、打印海报)。
### 多版本与更新内容
- 一个应用可以有**多个版本**,每条版本记录 = 版本号 + 安装包(云端路径/大小/校验值)+ Markdown 更新内容 + 登记时间。
- **最新版**:标「最新」的那一条就是访客默认下载的版本;把任意旧版本「设为最新」会同步切换访客下载内容。
启用中的最新版不允许删除;**先停用、再强制删除**即可,删除时可选择是否**连带删除数据胶囊里的安装文件**
(不勾则只删版本记录、保留云端文件)。删掉最新版后会自动把最近一条启用版本提为最新,没有则清空应用快照。
- **更新内容**:Markdown 格式,管理台可编辑 + 实时预览;访客页在核实通过后展示**最新版**的更新内容。
渲染前经 DOMPurify 消毒,只保留安全 HTML。
- **历史版本**:访客页下载卡片下方列出所有**启用中**的版本(最新优先、登记时间倒序),每条可点开看该版本的更新内容;
核实通过后每条都可直接下载——下载请求带 `version_id` 精确指向那个版本的安装对象(不传则默认启用中的最新版)。
停用(is_active=0)的版本不列入、也不可下载。**配置了核实时,整块历史版本要核实通过后才展示**(免核实模式则直接展示)。
- 存量应用首次启动会自动把原有的版本号 + 安装包转成第一条「最新」版本,不丢数据。
### 访客
打开 `/d/<分享码>` → 填写该应用要求的核实项 → 通过后拿到**会话**(默认 30 分钟、绑定设备、额度不限/有限)
→ 点下载(直链或服务器转发)→ 刷新页面在会话有效期内无需再填;额度用尽或超时后自动回到核实表单。
下载卡片下方还列出**历史版本**:核实通过前每条「下载」是灰的,核实通过后即可逐条下载任意启用中的版本;
每条还能展开看该版本的更新内容。
### 兑换码分发(第二种分发方式)
面向苹果 App 商务等场景:分发的不是安装包,而是一次性的**兑换码 / 兑换链接**。
- **应用级模式**:新建应用时选「分发方式 = 兑换码」(`distribute_mode=codes`),与「安装包 / 版本」二选一。
兑换码应用的「版本 / 安装包」那套不适用,改由**兑换码池**管理。
- **码池**:`兑换码 / 链接 / 备注` 三列,可**逐条手工添加**,或**批量导入**(CSV / XLSX / 文本,列自动映射),
也能**下载模板**;重复码值自动跳过,「覆盖 / 追加」两种导入方式;已用码可**重置回可用**、可删除。
- **一次性**:访客**核实身份通过即自动领一个未使用码**,领过立即标记「已用、不可再用」,发完即止;
码池清空后该分发会被判定「未就绪」。核实机制与安装包应用完全一致(授权名单 / 仅登记 / 口令 / 免核实)。
- 详情页的标签页会随之变成「兑换码」;访客页核实通过后展示领取到的码值 / 链接 / 备注(可一键复制)。
## 7. 名单:加密、模板、手工录入、查询
- **只存令牌**:每行是 `HMAC-SHA256(主密钥, 应用盐 | 字段=归一化值…)`,导入后原文即焚;
每应用独立 24 位盐,跨应用不可比对。界面只显示令牌前 8 位与可选标签。
- **文件导入**:表头自动映射;解析结果只在服务进程内存(30 分钟、一次性);预览是脱敏的(中间打星)。
- **模板**:按当前字段标签生成 CSV(带 BOM,Excel 直接打开);表头里若有「备注/标签」列会自动认作标签。
- **手工录入**:逐条填写、加入列表、追加/覆盖,与文件导入走同一套归一化与 HMAC。
- **同标签自动覆盖**:导入(文件或手工)时若某行标签与已有名额相同,自动覆盖那条旧名额——替换核实值、
清零已用次数、解除吊销,不会留下重复标签;没有标签的行仍按核实值指纹去重。导入结果会报告覆盖了几条。
- **查询**:按**标签**或**令牌指纹**搜索(工号等只存哈希,**无法反查**),按状态筛选,分页。
- **标签是明文**:仅用于管理员辨认;参与核实的列仍然只存 HMAC 令牌。
- **什么时候要重导名单**:只拨「标签」开关(参与核实 ↔ 核实+标签)**不影响**已导入名单(参与比对的字段集合没变);
拨「仅标签」开关、调整归一化规则或字段标识才会改变令牌,需要重新导入。
- **名额操作**:吊销 / 恢复、单独设可用次数(0 = 跟随分发设置);**已吊销的可「删除」彻底抹掉这条令牌**
(未吊销的行没有删除按钮,防止手滑;删除不可撤销,历史使用次数一并消失,之后重新导入同一个人会当成新名额重新计数)。
删除只动名单表,已经发出去的会话仍按自己的额度用完为止;操作会写审计(`roster_delete`)。
- ⚠️ 修改字段角色或归一化规则会改变令牌,**已有名单需重新导入**(页面有红色提示)。
## 8. 口令与恢复密钥
- 忘记口令:登录页「用恢复密钥找回」→ 账号 + 恢复密钥 + 新口令;旧密钥立即作废并返回新密钥。
- 登录状态下可在「我的口令」改口令、重新生成恢复密钥。
- 恢复密钥只存 scrypt 散列,服务器无法还原;命令行兜底:`scripts/reset_admin.py --username admin`。
## 9. 安全设计
- **名单**:只存 HMAC 令牌 + 可选标签;原文只在处理瞬间存在于内存;不支持按核实值反查。
- **口令**:scrypt(16MB)散列;恢复密钥同样散列、一次性轮换。
- **会话**:管理员 Cookie `HttpOnly + SameSite=Lax`,默认 8 小时;访客会话按 `/api/portal/` 路径隔离;
**改口令 / 停用 / 删除账号会立即作废该用户全部旧会话**(防「改了口令旧会话还活着」)。
- **下载票据**:核实成功另发 `ticket`,下载必须带票据 + Cookie,防跨站 GET 触发下载。
- **限速**:同一 IP 对同一分发连续填错会锁定(默认 15 分钟 / 5 次);登录、找回口令、首次初始化同样限速。
- **响应头**:全站 `nosniff / X-Frame-Options: DENY / Referrer-Policy / COOP+CORP` + 严格 CSP
(内联主题脚本用 sha256 白名单,不用 `unsafe-inline`);`/api/` 一律 `no-store`。
- **接口文档**:`/api/docs` 与 `/api/openapi.json` 只对已登录管理员可见,匿名访问一律 404(不给外部递 API 清单)。
- **请求体**:JSON 接口统一 2MB 上限;名单导入 20MB、安装包上传 3GB 各自走流式限额,chunked 请求也拦得住(413)。
- **表格公式注入**:访问日志 CSV 导出与名单模板里,以 `= + - @ \t \r` 开头的单元格统一加前导单引号。
- **路径与注入**:云端目录清洗丢掉 `.`/`..` 段;静态托管有解析后白名单兜底;SQL 全参数化;前端无 innerHTML。
- **信息收敛**:`/api/health` 不回本机绝对路径;登录错误对「账号不存在 / 口令错误」一视同仁。
- **首启文件**:环境变量建号时把口令与恢复密钥写进 `data/初始管理员与恢复密钥.txt`,POSIX 上自动 `chmod 600`。
- **日志**:只记字段名、令牌前 8 位、脱敏 IP、结果;**不记录访客填写内容**;可导出带 BOM 的 CSV;
默认保留 90 天,每天首次访问时后台清理(0 = 永不清理,可手动「立即清理」)。
- **密钥**:胶囊 AK/SK 存本机 SQLite,界面只显示打码;备份文件与 `config/设置.json` 建议 `chmod 600` 保管。
## 10. 运维
### 10.1 部署到 Linux
适用于 Ubuntu 22.04+/Debian 12+/Rocky 9,要求只有 Python ≥3.10(构建前端另需 Node 20.19+)。
**没有别的组件要装**:不装 rclone、不装 nginx,Python 依赖只有 fastapi / uvicorn / openpyxl。
```bash
# 1) 拷代码
sudo useradd -r -s /usr/sbin/nologin -d /opt/releasedock releasedock
sudo mkdir -p /opt/releasedock && sudo chown releasedock: /opt/releasedock
rsync -a --exclude .venv --exclude frontend/node_modules --exclude data ./ user@server:/opt/releasedock/
# 2) 建 venv、装依赖、构建前端
cd /opt/releasedock
sudo -u releasedock python3 -m venv .venv
sudo -u releasedock .venv/bin/pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
cd frontend && npm install && npm run build && cd ..
# 3) 启动:先手工跑一次完成初始化(浏览器开 127.0.0.1:8730,或 ssh -L 8730:127.0.0.1:8730)
sudo -u releasedock .venv/bin/python run.py --host 127.0.0.1
# 想开机自启再装 systemd 单元(可选,不装就一直开着这个终端)
sudo cp deploy/releasedock.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now releasedock
sudo journalctl -u releasedock -f
```
要点:
- **只跑单进程**:监听套接字、传输任务、目录缓存都在进程内,不要 `--workers`、不要双实例。
- systemd 单元已设 `TZ=Asia/Shanghai`(日志与每日清理按本地日期)、非 root、只允许写 `data/` 与 `config/`。
- 权限:`chmod 600 config/设置.json data/releasedock.db`。
- Rocky/CentOS 写不了 `data/`:`chcon -R -t httpd_sys_rw_content_t /opt/releasedock/data`。
- 需要挂在域名/HTTPS 后面时,再自行配反向代理,并在设置里打开 **信任反向代理头**、
**仅 HTTPS(Secure Cookie)**,对外地址填 `https://域名`;`deploy/nginx.releasedock.conf` 只是个可选样例,
直接 `python run.py` 对外服务不需要它。
### 10.2 备份与升级
```bash
sqlite3 data/releasedock.db ".backup '/var/backups/rd-$(date +%F).db'"
```
换机器:`data/releasedock.db` + `config/设置.json` 一起拷(代码里有内置客户端,不再带工具目录)。
升级:替换代码 → `pip install -r requirements.txt` → `npm run build` → 重启;数据库自动补列,不丢数据。
### 10.3 自检
```bash
.venv/bin/python scripts/selftest.py # 用内置假胶囊(scripts/fake_s3.py),离线跑全链路
.venv/bin/python scripts/selftest.py --real # 用当前真实胶囊(只读为主)
```
自检覆盖:S3 客户端读写/校验/分段/列举、SigV4 预签名、Range、UA 白名单、名单加密(GBK/列映射/标签字段)、核实与限流、设备绑定、
次数与不限、流式下载、审计与导出、配置保存、日志保留、名单查询分页、
多版本(登记/设最新/编辑 Markdown/门户展示/删除保护)、名单同标签自动覆盖、
安全头与 CSP / 文档收敛 / 公式注入防护 / 路径清洗 / 改口令后会话作废 / 请求体上限等。
### 10.4 常见问题
| 现象 | 处理 |
|---|---|
| 启动报端口占用 | Windows 用独占绑定,第二个实例会直接失败;先停旧进程 |
| 页面能开但没样式 | 没构建前端:`cd frontend && npm run build` |
| 胶囊自检失败 | 看设置页每一步的 detail,对照 §5.1 报错表 |
| 下载 401 | 预签名被网关拒绝;确认探针结果为"走流式",或看 §5.3 |
| 所有云端操作都 401(错误体为空) | 多半是 User-Agent 被网关拦了,见 §5.4;把设置里的「客户端标识」改回含 `rclone` 的值 |
| 访客页提示"未就绪" | 应用缺安装包 / 名单为空 / 分发已停用或过期 |
| 忘记管理员口令 | 登录页用恢复密钥;或 `scripts/reset_admin.py` |
## 11. 已验证 / 未验证
**已验证(Windows 11 + Python 3.13 + Node 24,真机点击 + 截图)**
- 首启向导(建号 → 填胶囊 → 恢复密钥 → 自动登录)
- 恢复密钥:生成 / 重置口令 / 旧密钥作废 / 轮换
- 真实数据胶囊(改用内置 Python 客户端后重测):列举 / HEAD / 185MB 安装包流式下载 / MD5 取 ETag /
中文文件名 / 目录斜杠与桶根路径两个坑 / UA 白名单(§5.4)
- 名单:GBK CSV、表头映射、脱敏预览、标签字段唯一、导入后不含原文、一次性重放被拒、删除仅限已吊销名额且删后不再命中
- 核实:归一化命中、限流、会话额度、设备绑定、不限次数、次数用尽回到表单
- 两步上传 → 任务完成 → 登记大小/校验 → 从云端取回一致
- 审计:不含填写值、CSV 导出、90 天保留与手动清理
- 名单搜索/状态筛选/翻页(官方分页样式)
- 界面:亮/暗、1440 与 390 宽、二维码可扫
**未验证**
- Linux 实际运行(部署文件与命令已按真实路径写好,但本机是 Windows)
- 预签名直链(网关不支持 query 签名,已自动回退流式)
## 12. 已知限制
- **单进程**:不支持 `--workers` 多进程。
- 流式下载不支持 Range 断点续传;要恢复需服务商支持预签名。
- 单文件上传上限 3 GB(`routes_admin.MAX_UPLOAD_BYTES`)。
- S3 只提供 MD5(ETag);从云端挑的旧文件只在 ≤64MB 时自动算,自己上传的在暂存阶段算好 MD5+SHA1。
- 纯中文应用名会生成 `app-xxxxxx` 作为云端目录名(可在基本信息改成英文短名)。
- SQLite 删除日志后文件不会立刻变小(空间复用),需要时停机 `VACUUM`。
- 访客端按 Chrome 107+ 构建(覆盖 Win7 上的 Chrome/Edge 109);服务端需 Win10+/Linux。
## 13. 截图
| 文件 | 内容 |
|---|---|
| `01-setup-account.png` / `02-setup-capsule.png` / `03-setup-recovery.png` | 首次初始化三步 |
| `04-login.png` / `05-login-dark.png` | 登录(口令框带显示/隐藏图标) |
| `06-login-recovery.png` | 忘记口令 · 恢复密钥找回 |
| `07-dashboard.png` / `08-dashboard-light.png` | 概览 |
| `09-apps.png` / `24-apps-mobile.png` | 应用列表(桌面/手机) |
| `10-app-basic.png` | 应用详情 · 基本信息 |
| `11-app-verify.png` | 核实字段设计器(两个普通核实字段) |
| `25-label-field.png` | 「标签」开关开启:照常核实 + 写进名单标签 |
| `25b-label-only.png` | 「仅标签」开关开启:不参与核实、必填置灰 |
| `25c-label-light.png` | 同上(亮色) |
| `25d-label-swap.png` | 标签唯一:在新字段开启标签,其他字段自动关掉 |
| `25e-label-guard.png` | 只剩一个核实字段时不允许改成「仅标签」 |
| `25f-label-password.png` | 口令字段两个标签开关都置灰 |
| `26-label-manual.png` | 手工录入(标签单独一栏) |
| `12-app-roster.png` | 授权名单 |
| `27-roster-paging.png` / `28-roster-search.png` | 名单翻页 / 搜索 |
| `29-roster-delete.png` | 已吊销的行才有「删除」按钮(可用行只有吊销/次数) |
| `29b-roster-delete-confirm.png` | 删除确认(令牌指纹 + 标签 + 后果说明) |
| `29c-roster-deleted.png` | 删除后:名额与吊销计数同步减少 |
| `30-versions.png` | 版本管理:多版本列表、最新标记、设为最新 / 删除 |
| `30b-notes-preview.png` / `30c-notes-edit.png` | 更新内容 Markdown 预览 / 编辑 |
| `31-portal-notes.png` | 访客页展示最新版更新内容 |
| `p3-portal-verified.png` / `p4-portal-history-download.png`(`gui-test-screenshots/`,黑盒实拍) | 访客页历史版本列表:核实前「下载」置灰、核实后逐版本下载,每条可展开看该版本更新内容 |
| `13-app-share.png` / `14-app-share-light.png` | 分发 · 二维码(暗/亮) |
| `15-capsule.png` | 数据胶囊文件浏览(内置 Python 客户端,无外部程序) |
| `16-logs.png` | 访问记录 |
| `17-settings-capsule.png` | 系统设置 · 数据胶囊(含客户端标识 User-Agent) |
| `17c-capsule-selftest.png` | 连通性自检六个步骤(配置完整 / 访问方式 / 接入点格式 / 网络可达 / 列目录 / 预签名) |
| `18-settings-runtime.png` / `19-settings-account.png` | 系统设置 · 运行时开关 / 管理员账号 |
| `20-portal.png` / `21-portal-dark.png` / `22-portal-mobile.png` / `23-portal-verified.png` | 访客页与核实通过 |
截图由 `node scripts/shoot.mjs --base http://127.0.0.1:8730 --cookie "rd_admin=…" --out docs/shots --jobs docs/shots-jobs.example.json` 生成
(示例里的分享码/路径按你的环境改);`docs/shots-jobs-label.json` 是「标签/仅标签开关」那几张的作业,
用 `--jobs docs/shots-jobs-label.json` 复现,里面的 `/apps/1…4` 按你的应用 id 调整。