# OpenFmt 服务器接入方案

交付日期：2026-09-06。适用：OpenBox / OpenAddr / OpenFmt 同一站点。

## 当前已经上线的范围

OpenFmt 在浏览器内处理图片、PDF、文本和数据。转换文件不上传、不保存、不扣积分。账户资料与头像属于 OpenBox 账户服务，和转换文件分开存储。

Office 文档输入、音视频、OCR 均未接入。当前 `GET /api/openfmt/capabilities` 明确返回 `server.enabled=false`；`POST /api/openfmt/jobs` 返回 503，并且不读取或存储上传内容。未来接口的 TypeScript 类型位于 `modules/openfmt/server-contract.ts`。

## 建议部署结构

保留现有 Sites 网站、ChatGPT 认证、D1 账户与 R2 头像。另配置一台可运行容器的 Linux 服务器，部署任务网关、PostgreSQL 任务表、Redis 队列和转换 Worker。转换文件使用独立的私有对象存储，不复用头像桶；本站只负责身份、任务入口和状态展示。

第一阶段建议从 4 核 / 8 GB 内存、80 GB 临时磁盘起步，转换并发设为 1；这是待压测的初始配置，不是性能承诺。优先开通 Office → PDF，再根据实际任务内存和耗时增加媒体 Worker。不能把 LibreOffice、FFmpeg 等进程直接运行在当前 Sites Worker 中。

处理链路：浏览器 → OpenBox 已登录任务接口 → 私有任务网关 → 队列 → 隔离 Worker → 私有对象存储 → 本人下载。

## 转换引擎

| 能力 | 引擎 | 第一版开放范围 |
|---|---|---|
| Office → PDF | Gotenberg / LibreOffice | DOCX、XLSX、PPTX 到 PDF |
| 音视频 | FFmpeg | MP4/WebM、MP3/WAV；仅固定编码预设 |
| 文档 | Pandoc | Markdown、HTML、DOCX 等文档转换 |
| OCR | 后续评估 OCRmyPDF/Tesseract | 单独能力开关，未验收前不展示可用 |

按固定目标格式拼接参数数组，不接受用户提供的命令、滤镜表达式、远程 URL 或任意参数。Gotenberg 仅开放在私有网络；文件上传使用 `/forms/libreoffice/convert`。Pandoc 不允许用户提供 Lua filter、自定义可执行程序或 shell escape；`--sandbox` 是额外措施，不能替代容器隔离。FFmpeg 使用本地输入路径，限制协议与流数，明确映射音视频流并去除不必要的元数据。

部署当天选择受支持版本，固定镜像 digest，记录引擎版本和对应构建许可证。FFmpeg 具体许可取决于构建选项和编码器；不要直接把任意网上镜像当作可商用发行包。

## 身份与接口接入

现有 OpenBox `account()` 是身份来源。浏览器不可传入或决定 userId。站点服务端给私有网关签发 60 秒内有效的服务凭证，包含 issuer、audience、OpenBox userId、jti、iat、exp；网关校验签名、时钟和防重放。网关不信任外部传入的 `oai-authenticated-*` 头。

站点需配置新的服务端变量 `OPENFMT_GATEWAY_URL`、`OPENFMT_GATEWAY_TOKEN`（或者非对称签名私钥）和 `OPENFMT_SERVER_ENABLED`。变量不能使用 NEXT_PUBLIC 前缀。未配置、健康检查失败或能力列表为空时，保持现在的本地模式。

| 接口 | 行为 |
|---|---|
| `GET /api/openfmt/capabilities` | 返回已验收的操作、输入/输出类型、大小限制、是否收费 |
| `POST /api/openfmt/jobs` | 必须登录、同源；校验目标、配额、文件清单；创建 awaiting_upload 任务 |
| `POST /api/openfmt/jobs/{id}/upload` | 先验证任务所属账户；由服务端返回绑定单一对象键、短时有效的上传授权，或受限上传代理 |
| `POST /api/openfmt/jobs/{id}/commit` | 验证实际大小、哈希和文件签名；幂等地入队，禁止重复收费 |
| `GET /api/openfmt/jobs/{id}` | 校验所有权后返回状态；2 秒起轮询，退避到 10 秒 |
| `DELETE /api/openfmt/jobs/{id}` | 取消排队任务，或发出运行中终止信号；清理文件并返还未消费额度 |
| `GET /api/openfmt/jobs/{id}/download` | 校验所有权与有效期，返回最多 60 秒有效的下载地址 |

`POST /jobs` 接受 `Idempotency-Key`。在服务端以 `(user_id, idempotency_key)` 唯一约束去重，重复键不同内容返回 409。上传授权必须绑定服务器生成的对象键、大小和内容类型，客户端文件名只作为下载展示信息。任务 ID 即使不可预测，也不能替代所有权校验。

状态机：awaiting_upload → queued → running → succeeded / failed / cancelled；超时保留期后 → expired。progress 无法准确计算时返回 null，让界面显示处理中，不伪造百分比。

错误代码建议：unsupported_format、file_too_large、invalid_file、encrypted_file、quota_exceeded、conversion_timeout、engine_unavailable、cancelled、expired。对用户显示中文说明；底层日志不直接回传。

## 存储与隔离

- 初始上传上限 50 MB、每人最多 2 个未完成任务；媒体支持后另设大小/时长额度。网关以流式实际字节数校验，不只信 Content-Length。
- 文件签名检测与扩展名白名单同时通过才允许处理。拒绝归档炸弹、过多页面/流/像素、未知加密文件和异常解码结果。
- 每个任务独立临时目录、随机路径、非 root 身份、只读容器根文件系统、内存/CPU/PID/磁盘限制。禁止挂载宿主 Docker socket，禁止访问其他任务目录。
- 转换进程默认无外网、禁止访问元数据地址。Office 宏禁用，外部链接不自动获取。引擎不直接接收互联网上的回调 URL。
- 文档任务初始超时 120 秒；媒体任务 600 秒。超时必须杀死进程组。失败、取消后立即清理输入和部分输出。
- 成功文件最多保留 1 小时，后台每 5 分钟清理，对象存储另加 24 小时生命周期兜底。任务历史只保存格式、大小、状态、时间和错误码，默认不记录正文和原文件名。
- 新增服务的数据库、队列和引擎均不暴露公网端口；只有带 TLS 的任务网关提供有限入口。

## 积分和支付（仍为预留）

OpenBox 共用一个账户余额。本地转换继续免费。服务器转换在明确价格并完成支付接入之前也不能假扣费。

未来建议 D1 增加 `credit_ledger`、`credit_reservations`、`payment_events`，每次账本变动保留唯一事件 ID。创建任务时冻结预估积分，成功后按服务端规则结算，失败/取消释放；重试和回调不得重复扣费。数据库约束保证余额不为负数。付款以验签成功且事件幂等的服务端 webhook 为准，不以浏览器跳转为准。现有 `lib/billing.ts` 与 checkout/webhook 入口可扩展；目前仍返回未配置状态。

## 推荐接入步骤与验收

1. 准备服务器、域名和私有对象存储；部署网关与一个 Office Worker，先在测试环境运行。
2. 实现上述任务状态机与服务间认证，新增数据库迁移；严格保持老账户、收藏、API 密钥不变。
3. 在独立功能开关下接入 UI：选择服务器格式时清楚说明文件将上传及保留时间；当前本地路径保持不变。
4. 验收 DOCX/XLSX/PPTX → PDF 内容、页数、中文字体与嵌入图片。对不支持的特性显示明确错误，不承诺完全还原。
5. 验证 A 用户不能查询、取消或下载 B 用户任务；匿名、伪造头、过期凭证、重复回调、超额输入和跨站提交被拒绝。
6. 验证进程超时、取消、服务重启、队列重复投递、存储故障和清理故障不会导致重复执行/计费或永久遗留文件。
7. 在桌面与手机上验证上传进度、后台切换、失败重试、下载与到期提示；压测峰值内存后调整并发。
8. 所有验收通过后，更新 capabilities 并开启相应格式。未通过的能力继续隐藏。回滚时关掉服务器能力开关，本地转换仍可用。

## 交接文件与官方参考

- 前端队列：`components/converter.tsx`
- 本地能力：`modules/openfmt/registry.ts`、`engine.ts`
- 服务器类型：`modules/openfmt/server-contract.ts`
- 当前能力/任务占位接口：`app/api/openfmt/`
- 账户、授权和支付：`lib/server.ts`、`lib/billing.ts`、`app/api/v1/[...path]/route.ts`
- Gotenberg：https://gotenberg.dev/docs/convert-with-libreoffice/convert-to-pdf
- FFmpeg：https://ffmpeg.org/ffmpeg.html
- Pandoc：https://pandoc.org/MANUAL.html

这份方案提供可实施的接口、隔离和验收要求；未包含已经部署的服务器，也不宣称重型转换已可用。
