跳转到正文
nexusPPT开发者文档

接入指南

后端使用 API Key 签发文稿授权,前端通过 iframe 嵌入预览、编辑与协作界面。

HTTP API · v1

服务地址:https://slides.example.com。以下接口路径均相对于服务地址;示例中的 Client ID、Key、文件 ID 和业务网址需替换为你的配置。

快速开始

1. 创建调用方

进入管理控制台 → API Key,点击“创建 API Key”。设置调用方名称和唯一 Client ID,在“允许嵌入地址”填写业务前端的网址,例如 https://app.example.com,每行一个。填写协议、域名和端口,不含页面路径;保存后立即生效。

将生成的 Key 保存到业务后端。Key 仅显示一次,丢失后在控制台轮换。若选择调用方自有文件方式,还需配置文件接口地址。

2. 上传一份文稿

在后端终端设置调用参数。此处的 API Key 是控制台分发给该业务系统的 Key。

Shell · 调用配置
export NEXUS_URL="https://slides.example.com"
export NEXUS_CLIENT_ID="platform-a"
export NEXUS_API_KEY="替换为调用方的 API Key"
cURL · 上传 PPTX
curl --fail-with-body "$NEXUS_URL/api/v1/files" \
  -H "X-Client-Id: $NEXUS_CLIENT_ID" \
  -H "Authorization: Bearer $NEXUS_API_KEY" \
  -F 'file=@./季度报告.pptx'

成功返回 HTTP 201 和文稿元数据。保存响应中的 id,用作后续的 fileId。已上传的文稿再次打开时复用这个 ID。

3. 为当前用户签发链接

cURL · 签发编辑授权
export NEXUS_FILE_ID="替换为上传返回的 id"

curl --fail-with-body "$NEXUS_URL/api/v1/files/$NEXUS_FILE_ID/grants" \
  -H "X-Client-Id: $NEXUS_CLIENT_ID" \
  -H "Authorization: Bearer $NEXUS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "mode": "edit",
    "userId": "user-1001",
    "displayName": "Alice",
    "parentOrigin": "https://app.example.com",
    "expiresIn": 14400,
    "allowDownload": false
  }'

返回 HTTP 201,将响应的 url 交给已登录且有文稿权限的业务前端。将 mode 改为 view 即可签发只读预览链接。

JSON · 授权响应示例
{
  "id": "0123456789abcdef0123456789abcdef",
  "expiresAt": 1791540000,
  "ticketExpiresIn": 60,
  "url": "https://slides.example.com/embed/0123456789abcdef0123456789abcdef#ticket=一次性票据"
}

4. 在业务页面中打开

HTML · 文稿容器
<iframe id="nexus-ppt" title="演示文稿"
  width="100%" height="760" frameborder="0"
  allow="fullscreen; clipboard-read; clipboard-write">
</iframe>
JavaScript · 从业务后端取得链接
const frame = document.querySelector('#nexus-ppt');

// /api/presentations/123/open 是你自己实现的业务后端接口。
// 该接口验证登录身份和文稿权限,再向 Nexus PPT 签发授权。
const response = await fetch('/api/presentations/123/open', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({mode: 'edit'})
});
if (!response.ok) throw new Error('无法取得文稿访问授权');
const grant = await response.json();
frame.src = grant.url;

一次性链接须在 60 秒内打开。每位用户、每次进入分别签发。授权有效期与票据兑换时限不同:票据兑换后,文稿会话可以持续到 expiresAt。

鉴权与权限

一个业务调用方使用一枚长期 Key。每次后端 API 请求携带以下两个请求头,服务按 Client ID 隔离文稿与授权:

HTTP · 后端鉴权头
X-Client-Id: platform-a
Authorization: Bearer <该调用方的 API Key>

API Key 不能放入前端代码、iframe URL 或 localStorage。业务前端只拿到短时文稿链接;票据兑换、浏览器会话和 WOPI 凭证由嵌入页面内部处理,调用方无需自己实现这些接口。

凭据 / 字段 用途 由谁持有
Client ID + API Key 上传、登记文件、签发与撤销授权 业务后端
userId 当前业务用户的稳定唯一标识 后端从已验证登录会话取得
displayName 编辑器中的参与者名称 后端传入,如 Alice / 张三
mode view 只读;edit 共同编辑 后端按业务权限决定
grant.url 本次单文稿、单用户的 iframe 入口 获授权的业务前端

parentOrigin 必须与业务前端实际的协议、域名和端口一致,并已加入该调用方的“允许嵌入地址”。例如 http://localhost:8890 与 http://127.0.0.1:8890 是两个不同地址。控制台中的设置直接保存到 SQLite,无需重启。

业务后端需要校验用户是否可以访问该业务文件、是否可以编辑或下载,并维护业务文件 ID 与 Nexus 文件 ID 的映射。不要直接信任前端提交的用户名称、文件 ID 或编辑权限。

Node.js 后端签发示例

下面的函数使用 Node.js 内置 fetch。参数 user、permission 和 nexusFileId 须由业务后端验证并解析后传入;返回的授权对象可以交给前端。

JavaScript · 业务后端
export async function issuePptGrant(nexusFileId, user, permission, mode = 'view') {
  if (!['view', 'edit'].includes(mode)) throw new Error('INVALID_MODE');
  if (mode === 'edit' && !permission.canEdit) throw new Error('EDIT_FORBIDDEN');
  const response = await fetch(
    `${process.env.NEXUS_URL}/api/v1/files/${encodeURIComponent(nexusFileId)}/grants`,
    {
      method: 'POST',
      headers: {
        'X-Client-Id': process.env.NEXUS_CLIENT_ID,
        'Authorization': `Bearer ${process.env.NEXUS_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        mode,
        userId: String(user.id),
        displayName: user.name,
        parentOrigin: process.env.BUSINESS_ORIGIN,
        allowDownload: Boolean(permission.canDownload)
      })
    }
  );
  const result = await response.json();
  if (!response.ok) throw new Error(result.error || 'NEXUS_REQUEST_FAILED');
  return result;
}

业务后端自行处理登录 Cookie、CSRF 和用户权限。调用 Nexus 的后端请求通常不带浏览器 Origin 头;不要原样转发浏览器请求头。长期 Key 的跨域浏览器请求会被服务拒绝。

文稿接口

支持两种文件来源:上传后由 Nexus 托管,或通过文件 API 读取与回存到调用方。两者都使用同一套授权和 iframe 入口。

方法 路径 用途 / 成功状态
POST /api/v1/files 上传托管文稿,201
POST /api/v1/files/{fileId}/copy 复制最近已保存的内容为独立托管文稿,201;无需请求体
POST /api/v1/files/external 登记或更新调用方自有文稿,201
GET /api/v1/files 当前调用方的文稿列表,200
GET /api/v1/files/{fileId} 文稿元数据,200
GET /api/v1/files/{fileId}/download 当前 PPTX 内容,200
GET /api/v1/files/{fileId}/download?original=true 托管文稿的上传原件,200
DELETE /api/v1/files/{fileId} 移入回收站并撤销授权,204
PATCH /api/v1/files/{fileId} 重命名,200;JSON:{"name":"报告.pptx"}
GET /api/v1/files/{fileId}/versions 托管文稿历史,200
GET /api/v1/files/{fileId}/versions/{versionId}/download 下载历史内容,200
POST /api/v1/files/{fileId}/versions/{versionId}/restore 恢复历史版本,200
POST /api/v1/files/{fileId}/restore 从回收站恢复,200
DELETE /api/v1/files/{fileId}/purge 永久删除回收站文稿,204

上传参数与文稿响应

创建副本返回与上传相同的文稿元数据,名称自动添加“-副本”,重名时追加序号。 副本归属当前认证调用方;管理员操作时归入本地文稿。 副本不继承源文稿的历史、访问授权、协作会话、恢复副本或外部回写配置, 不包含尚未保存的编辑。外部文稿须能通过现有读取接口取得登记版本的完整内容; 授权失效、版本冲突或读取失败时不创建副本。复制遵守目标调用方的文稿数量与存储配额。

POST /api/v1/files 使用 multipart/form-data,文件字段为 file,文件名以 .pptx 结尾。当前大小限制可从 GET /api/v1/config 的 maxUploadBytes 读取,默认 30 MB;上传限制可由管理员调整。

JSON · 文稿元数据示例
{
  "id": "fedcba9876543210fedcba9876543210",
  "name": "季度报告.pptx",
  "version": "9c5b…当前版本摘要…",
  "savedAt": 1791520000,
  "saveCount": 0,
  "size": 123456,
  "source": "managed",
  "canEdit": true
}
字段 说明
id Nexus 文件 ID。后续打开同一文件时复用。
version 当前版本标识。托管文稿为内容 SHA-256;外部文稿为调用方版本。应按不透明字符串处理。
savedAt 最近保存的 Unix 时间戳,单位秒,可带小数。
saveCount 成功保存次数;刚上传时为 0。
size 当前文件字节数。
source managed 托管;external 调用方自有文件。
canEdit 文件是否具备编辑回存能力。用户是否有编辑权限仍由调用方决定。
cURL · 下载当前结果
curl --fail-with-body "$NEXUS_URL/api/v1/files/$NEXUS_FILE_ID/download" \
  -H "X-Client-Id: $NEXUS_CLIENT_ID" \
  -H "Authorization: Bearer $NEXUS_API_KEY" \
  -o './编辑结果.pptx'

下载接口返回 PPTX 字节,不是 JSON。托管文稿保存原件与历史内容;外部文稿由调用方保存历史。移入回收站会立即撤销访问,永久删除外部记录不会删除调用方原文件。

历史与回收站

查询与分页

GET /api/v1/files 默认返回当前调用方的有效文稿数组。传入 page 后返回分页对象;管理员工作台可以查询全部调用方。

查询参数 规则
page / pageSize 页码从 1 开始,每页默认 30,最大 100。
search 按文稿名称搜索。
clientId 调用方筛选;业务 Key 仍只能查询自己的文稿。
sort saved 最近保存(默认)、created 最近上传、name 名称。
deleted=true 查询回收站;默认只查询有效文稿。
JSON · 分页响应
{"items": [], "total": 0, "page": 1, "pageSize": 20}

文稿元数据另包含 createdAt 上传时间与 deletedAt 删除时间,单位为 Unix 秒。早期数据未记录上传时间时 createdAt 为 0;有效文稿的 deletedAt 为 null。

历史版本

每次内容发生变化的成功保存生成历史记录,返回 id、version、size、savedAt、displayName、source 和 current。versionId 使用历史记录的 id,不是内容哈希。

默认保留每份文稿最近 20 条历史(包含始终保留的上传原件),可在通用设置调整。恢复生成一条新记录,保留后续可回退的历史。存在有效编辑会话时返回 409 FILE_IN_USE,先保存并结束编辑会话再恢复。早期版本的用户或时间信息可能为空。

删除与恢复

删除后文稿进入回收站,默认保留 30 天;清理任务每小时运行。恢复文件不会恢复旧授权,需要重新签发。永久删除仅允许操作回收站文稿,删除后清理不再被任何文稿或历史引用的内容。回收站文稿与历史仍计入调用方存储用量。

预览与编辑授权

外部文稿的 edit 授权及续期仅接受所属业务后端的 API Key;管理员工作台只可预览。 业务端须先验证用户和文稿权限,详见调用方自有文稿。

POST/api/v1/files/{fileId}/grants

后端使用 Client ID 与 API Key 鉴权,请求体为 JSON,成功返回 201。必填字段如下:

字段 类型 / 必填 规则
mode string / 是 view 或 edit。只读授权不能保存。
userId string / 是 1–128 字符,来自当前用户的稳定 ID。
displayName string / 是 1–40 字符,参与者名称。
parentOrigin string / 是 业务父页面的完整 origin,须已获调用方授权。
expiresIn integer / 否 60–28800 秒。省略时采用控制台的默认授权时长,初始值为 240 分钟。
allowDownload boolean / 否 默认 false。控制嵌入页面是否可下载当前文件,与编辑权限独立。

响应的 id 是本次 grant ID;expiresAt 是会话到期时间,单位秒;ticketExpiresIn 为一次性票据兑换期限 60 秒;url 为前端应加载的完整链接。

查询与撤销

GET/api/v1/files/{fileId}/grants

返回仍在有效期内的授权数组,包含 id、mode、displayName、userId、expiresAt 和 revoked。其中 userId 为服务处理后的用户标识,业务侧应自行保存用户与 grant ID 的映射。

DELETE/api/v1/files/{fileId}/grants/{grantId}
cURL · 撤销一次访问
export NEXUS_GRANT_ID="替换为签发响应的 id"

curl --fail-with-body -X DELETE \
  "$NEXUS_URL/api/v1/files/$NEXUS_FILE_ID/grants/$NEXUS_GRANT_ID" \
  -H "X-Client-Id: $NEXUS_CLIENT_ID" \
  -H "Authorization: Bearer $NEXUS_API_KEY"

成功返回 204,没有响应体。撤销禁止该授权的后续读取与 WOPI 写入;已鉴权受理的回存仍可能完成,其他参与者的有效授权不受单独撤销影响。 嵌入页面正常每 5 秒检查授权并关闭失效编辑器;断网时页面获知撤销可能延迟,但服务端仍拒绝新的未授权请求。 业务退出、权限变更或主动关闭文稿时,可由业务后端撤销对应授权。

票据只能兑换一次,不能向多人分发同一 URL。当前标签页和业务父页面刷新可复用已兑换会话;新标签页、过期或被撤销后需重新签发。预览升级为编辑也需取得新授权。

续期当前授权

POST/api/v1/files/{fileId}/grants/{grantId}/renew

使用调用方后端 Key 鉴权;管理员工作台会话仅可续期托管文稿或外部预览授权,外部 edit 授权仅接受所属业务后端 Key。JSON 请求体为 {"expiresIn":900},范围 60–28800 秒,省略时采用通用设置。成功返回 {"id":"授权 ID","expiresAt":1791540000}。

只能延长仍有效且未撤销的授权,用户、文稿及读写权限保持不变。 新到期时间取原到期时间与“服务端当前时间 + expiresIn”的较大值,不会缩短,也不是在原期限上累加。过期或撤销后返回 401 SESSION_EXPIRED。业务后端应再次校验用户登录与文稿权限,并将续期时长限制在业务登录剩余时间内。 已暂停会话返回 409 SESSION_PAUSED,需重新签发;嵌入源不再被允许时返回 403 FRAME_ORIGIN_NOT_ALLOWED。

嵌入页面正常每 5 秒轮询,发现剩余不超过 120 秒时发送 expiring。 宿主须调用自己的业务后端实施续期及失败重试,iframe 不会代为调用业务接口;浏览器授权自身没有续期权限。 网络异常可能延迟提醒,业务后端也可按已知 expiresAt 提前续期。

页面发现更晚的到期时间且编辑器已加载时,获取新编辑凭据、发送更新指令,再发送 renewed。 该事件不是编辑器确认回执,也不表示保存成功;正常续期无需重载文稿。 浏览器离线期间,后端若已在有效期内续期,重连后可获取新期限继续操作;若已实际过期则必须重新签发。 grant 续期不会更新业务文件接口的 callbackToken 或签名 URL,见文件接口授权。

调用方自有文稿

文件继续保存在你的磁盘、数据库或对象存储中,Nexus 通过你的文件接口读取与回存,持久化接入元数据,并在回存前保存恢复副本。 未确认副本按内容独立保留 7 天,成功保存只清理对应内容;它不是调用方文件的完整历史或备份。 编辑引擎在协作期间会使用必要的临时缓存。副本下载与恢复流程见异常恢复。

编辑权限归属

Nexus 管理员管理服务,不能代替业务用户取得外部文稿的编辑权限。管理员工作台仅预览外部文稿; 编辑授权与续期必须由文件所属调用方后端使用 API Key 发起,工作台请求返回 403 BUSINESS_AUTH_REQUIRED。托管文稿的管理员编辑不受影响。

业务后端先核验当前用户、租户与文稿权限,再签发并记录 grantId 对应的编辑会话;关闭、撤权和到期时同步撤销。 不得把“存在其他活跃编辑者”作为另一个用户获得编辑权限的依据。 首次升级到此规则时,旧的外部编辑授权会失效,须从业务系统重新打开;文稿及恢复副本保留。

1. 授权文件接口地址

在API Key 设置的“文件接口地址”填写文件 API 的 origin,例如 https://api.example.com。它与业务前端的“允许嵌入地址”分别配置。readUrl 和 writeUrl 都必须属于已授权的接口地址。

2. 登记文件

POST/api/v1/files/external
cURL · 登记调用方文件
curl --fail-with-body "$NEXUS_URL/api/v1/files/external" \
  -H "X-Client-Id: $NEXUS_CLIENT_ID" \
  -H "Authorization: Bearer $NEXUS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "externalId": "business-file-123",
    "name": "季度报告.pptx",
    "version": "v1",
    "size": 123456,
    "readUrl": "https://api.example.com/files/123/content",
    "writeUrl": "https://api.example.com/files/123/content",
    "callbackToken": "replace-with-your-file-api-token"
  }'
字段 规则
externalId 必填,1–200 字符。同一调用方的同一 externalId 对应稳定的 Nexus ID。
name 必填,1–200 字符,文件名以 .pptx 结尾,不含路径。
version 必填,1–128 字符,可用字母、数字、下划线、点、冒号、短横线。须与读取接口 ETag 一致。
size 必填,正整数,须为文件的准确字节数,并符合服务大小限制。
readUrl 必填,可从 Nexus 后端访问的完整 HTTP(S) 文件地址。
writeUrl 可选。省略时该文件只能预览。
businessUrl 可选,业务文稿页面的完整 HTTP(S) 地址,origin 必须在调用方允许嵌入地址中。 工作台显示“前往业务系统编辑”;省略时显示返回业务系统的提示。不要在地址中携带长期凭据。
callbackToken 可选,16–2048 字符,调用方文件接口负责验证的 Bearer 凭据。与 Nexus API Key 用途不同。

登记时服务会实际读取并验证 PPTX、大小与版本。成功返回 201 和文稿元数据,然后使用返回的 id 按授权接口签发 iframe 链接。

3. 实现文件读取与保存

操作 Nexus 请求 文件接口响应
读取 GET readUrl
If-Match: "v1"
200、PPTX 原始字节、ETag: "v1"
保存 PUT writeUrl
If-Match: "v1"
Idempotency-Key: …
请求体为 PPTX 字节
200 或 204,并返回新版本,如 ETag: "v2"
版本冲突 请求版本与当前版本不同 409 或 412,不覆盖文件
HTTP · 文件保存请求示例
PUT /files/123/content HTTP/1.1
Authorization: Bearer <callbackToken>
If-Match: "v1"
Idempotency-Key: <本次保存的幂等标识>
Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation

<PPTX 二进制内容>

提供 callbackToken 时,读取与保存均携带 Bearer 头。也可使用有效期覆盖编辑会话的签名 URL;服务不跟随重定向。ETag 必须为强版本标识,例如 "v2",不使用 W/ 弱 ETag。

文件接口返回 401 或 403 时,Nexus 返回 502 EXTERNAL_AUTH_REJECTED,表示业务文件读写授权被拒绝, 不表示 Nexus 管理员登录失效。请从业务系统重新授权或由业务后端刷新失效的回调凭据,不要反复盲重试。 409/412 表示版本冲突;超时和其他失败返回 EXTERNAL_FILE_UNAVAILABLE,修复连接后可有限重试。

编辑上下文中的 saveError 为 {code, at} 或 null,at 为服务端 Unix 秒时间。 页面在保存回执核验及轮询中发现新错误时结束“保存中”,并向宿主发送带具体 code 的 error(stage 为 save)。 同一失败记录在当前页面去重;存在保存错误时,不会用协作者此前的版本确认本次失败保存。 新页面以打开时的错误时间为基线,不把历史错误重新报告为本次失败,恢复副本提示仍保留。 成功回写后清除此错误,只有成功确认的相同内容才会清理对应恢复副本。过期授权不能下载恢复副本; 重新取得 edit 且 allowDownload 的授权后,可下载副本,在业务端比较最新版本并合并恢复。

文件接口须原子地比较版本、保存内容并生成新 ETag,并按 Idempotency-Key 返回重复保存的同一结果。应先查询幂等记录,再检查 If-Match。回存确认后 Nexus 才更新版本;响应丢失时业务端可能已经提交, 重复请求应返回此前的成功结果,不能因旧 If-Match 再次报版本冲突。

grant 续期不会刷新 callbackToken 或签名 URL;需更新这些凭据时,用同一 externalId、当前版本及准确大小重新登记完整配置。 仅更新 callbackToken / readUrl 且版本和 writeUrl 不变时,不撤销现有授权。 版本或 writeUrl(含签名参数)变更会撤销旧授权,需重新签发;有有效编辑会话时禁止这些变更。 外部文件不支持下载“原上传文件”的历史副本。

多人协作与保存

同一调用方、同一个 Nexus 文件 ID 的参与者会进入同一文稿。为每位参与者分别签发授权,传入各自稳定的 userId 与名称,即可共同编辑、查看在线参与者与多人光标。

  • 保持文件 ID 一致。不要为每个用户重新上传一份文件;外部文件也应复用同一 externalId。
  • 权限分别签发。编辑者用 edit,只读参与者用 view。只读限制在 WOPI 服务端执行。
  • 等待保存确认。编辑器自动保存,也提供“保存”按钮;页面结合编辑器状态与服务端持久化结果确认后,才发送 saved 事件。
  • 外部文件保存成功才更新版本。你的回存接口必须确认新 ETag;版本冲突不会直接覆盖调用方文件。

每次发起保存前读取服务端当前保存时间,避免把此前其他参与者的保存误认为本次结果。 多人同时保存可能合并为一次写入,即使引擎只向其中一人返回回执,其他页面也会在共享文稿无待保存修改、服务端无保存错误且确认新写入后结束等待。 多个参与者收到相同 version 和 savedAt 属于正常情况;modified: false 本身不代表保存成功。

新回存失败会返回具体错误并保留未确认状态,不会用协作者此前的版本确认失败操作。 同一失败记录在当前页面去重,重试产生的新失败仍会报告;保存期间继续编辑的内容可能需要再次保存。

编辑器长连接断开时禁用保存,发送 error(stage: "connection")并保留未确认状态。 网络恢复后尝试重新连接,重新收到 ready 后可继续操作;已失效的授权须重新签发。 断网期间已经受理的回存仍可能完成,重连后应以服务端确认结果为准。

一位参与者断网或被单独撤销授权,不会撤销其他人的有效授权;其他人可以继续编辑并保存已同步的共享修改。 系统不保证离线输入被缓存或重放,未送达服务端的修改也不会进入恢复副本。 续期与异常恢复步骤见授权续期和异常恢复流程。

后端可通过文件详情的 version、savedAt 和 saveCount 核验保存结果,也可以通过下载接口取得最新结果。客户端发出“保存请求”不等于保存成功。

授权到期或撤销会阻止后续请求。失效前已经鉴权并开始回存的请求仍可完成并记录保存结果,避免外部接口已写入而元数据未更新;其他有效参与者也可能保存已经同步的修改。已下载的内容无法收回。

前端事件

嵌入页面通过 postMessage 向授权中的 parentOrigin 发送对象。每个事件包含 source: "presentation-service"、type、fileId 和 mode、grantId 和 expiresAt(秒)。此来源标识保留用于调用兼容,不是产品显示名称。

type 触发时机 附加字段
ready 编辑器完成文稿加载,重连后也可能再次发送 无
modified 待保存修改状态变化 modified:boolean
saving 开始保存流程,尚未确认持久化 无
saved 持久化结果通过核验,包括协作合并保存或重连后的确认 version、savedAt(秒)
expired 授权过期、撤销、文稿不可访问或编辑连接已失效,当前编辑器终止 message:提示文本
error 加载、保存、连接或编辑器错误 stage:open / save / connection;code、message 按错误类型提供,须兼容缺省
expiring 轮询发现授权剩余不超过 120 秒;按到期时间去重,凭据刷新后重新计时,断网可能延迟 expiresAt、remainingSeconds
renewed 页面已获取新编辑凭据并发送更新指令,不是编辑器确认回执或保存确认 expiresAt
capacity 刷新服务容量估计 connections、limit
paused / resumeRequested 空闲连接已暂停 / 用户请求继续;宿主核验权限后重新签发授权 无
closeRequested 用户请求关闭;宿主决定是否关闭页面并撤销授权 无
objectLink 对象联动状态、选择或翻页 values,见宿主联动协议
JavaScript · 校验并处理事件
const frame = document.querySelector('#nexus-ppt');
const nexusOrigin = 'https://slides.example.com'; // Nexus 服务的前端网址
const nexusFileId = '替换为当前文件的 Nexus ID';
const nexusGrantId = '替换为本次授权的 id';

window.addEventListener('message', async event => {
  if (event.origin !== nexusOrigin || event.source !== frame.contentWindow) return;
  const data = event.data;
  if (data?.source !== 'presentation-service' || data.fileId !== nexusFileId) return;
  if (data.grantId !== nexusGrantId) return;

  switch (data.type) {
    case 'ready':
      console.log('文稿已加载');
      break;
    case 'modified':
      console.log(data.modified ? '存在待保存修改' : '没有待保存修改');
      break;
    case 'saved':
      console.log('已保存', data.version, new Date(data.savedAt * 1000));
      break;
    case 'saving':
      console.log('保存中,等待持久化确认');
      break;
    case 'expiring':
      // 此接口由你的业务后端实现:核验登录、文稿权限和 grantId 归属,再调用续期 API。
      try {
        const renewal = await fetch('/api/presentations/123/renew', {
          method: 'POST', headers: {'Content-Type': 'application/json'},
          body: JSON.stringify({grantId: data.grantId})
        });
        if (!renewal.ok) {
          console.error('续期失败', renewal.status,
            '请按业务接口错误处理登录、权限或有限重试;原授权仍按服务端期限到期');
        }
      } catch (error) {
        console.error('续期请求未确认,恢复网络后由业务后端核验期限并重试', error);
      }
      break;
    case 'renewed':
      console.log('已发送编辑凭据更新', new Date(data.expiresAt * 1000));
      break;
    case 'expired':
      // 结束保存等待;已受理的回存可能仍在进行,由业务后端核验最终结果。
      frame.src = 'about:blank';
      console.log('请从业务后端重新获取授权');
      break;
    case 'error':
      // save / connection 错误应结束保存等待;重连后等待新的 ready。
      console.error(data.stage, data.code, data.message || '操作未完成,请检查状态');
      break;
  }
});

请在 iframe 加载前注册监听。事件来自 Nexus 服务地址,而非业务父页面或编辑引擎地址。示例中的控制台输出可替换为业务页面的状态提示;modified: false 本身不是一次保存确认。宿主主动保存与对象定位请参阅宿主联动协议。

此示例展示事件分发,不包含重试按钮和调度。实际宿主须提供续期失败提示及有限重试,不能等待下一次 expiring 代替重试。 对每个 grantId 合并重复续期请求;重新签发后更新当前 grantId,并忽略旧请求的迟到响应。 续期成功与保存确认分别处理;流程及错误分类见授权续期和异常恢复。

宿主保存与对象定位

宿主页面可以向授权 iframe 发送保存与对象定位指令。请在加载 iframe 前注册监听, 校验事件的 origin、source、fileId 和 grantId;重新签发后替换当前 grantId,忽略旧授权事件。 API Key 与 callbackToken 只保留在业务后端。

JavaScript · 宿主发送指令
const frame = document.querySelector('#nexus-ppt');
const nexusOrigin = 'https://slides.example.com';
// fileId 来自登记结果,grantId 来自本次授权的 id。
function command(type, values) {
  frame.contentWindow.postMessage({
    source: 'presentation-host', type, fileId, grantId, values
  }, nexusOrigin);
}

// 收到 ready 后,可保存当前可编辑文稿。
command('save');

保存开始返回 saving;回写确认后返回 saved,包含 version 和 savedAt。失败返回 error, stage: "save"、code 和 message。 保存期间的新修改仍需再次保存;不要把 modified: false 当成回写成功。 只读或尚未就绪的编辑器分别返回 EDITOR_READ_ONLY 或 EDITOR_NOT_READY,不会静默忽略保存请求。

保存过程中也可能收到连接错误(stage: "connection")或 expired。宿主应结束本次等待并显示“保存结果未确认”,核验业务版本或重连后确认,不能一直等待 saved,也不能把断连直接当作写入失败。 网络恢复后等待新的 ready 再发送操作。

对象配置与双向联动

对象功能异步加载。收到 objectLink 事件且 values.kind === "available" 后发送配置;不要仅凭文稿的 ready 就认为对象功能已就绪。收到 unavailable 时应禁用对象联动并提示重新打开。

JavaScript · 配置、定位与翻页
command('objectLink', {
  action: 'configure', enabled: true,
  objects: [{
    key: 'revenue-chart', label: '营收图表', number: 1, page: 2,
    x: 0.2, y: 0.3, width: 0.5, height: 0.4
  }]
});
// 等待 values.kind === 'ready' 后执行定位。
command('objectLink', {action: 'locate', key: 'revenue-chart'});
command('objectLink', {action: 'page', page: 1});

page 从 1 开始;x、y、width、height 按幻灯片尺寸归一化,与屏幕缩放无关。 key 在配置中应唯一,number 为正整数,最多配置 2000 个对象。 定位会切换幻灯片并高亮对应区域;它不会改动 PPTX 内容。 编辑器中的编号点击,以及几何范围唯一匹配的原生对象选中,都会回传业务 key。 对象移动、缩放或幻灯片结构改变后,业务方需要更新坐标配置;这些坐标不是永久对象 ID。

values.kind 含义
available / unavailable 对象功能可用 / 加载失败
ready 本次配置已接收
selected 选中对象,包含 key 和 page
page 当前幻灯片变化,包含 page
missing 目标不存在、所在页无效或联动未启用,包含 key

宿主续期与重连

收到 expiring 后,由业务后端核验用户权限、文稿及 grantId 归属并调用续期 API。 宿主须处理网络异常和非成功响应,显示失败并提供有限重试;不要等待同一到期时间的下一次 expiring 才重试。 对暂时网络故障可在授权仍有效时重试,401/403 应修复登录或权限,SESSION_EXPIRED / SESSION_PAUSED 应重新签发。 不要在前端修改 expiresAt 模拟续期。

renewed 表示页面已取得新编辑凭据并发送更新指令,不是编辑器的确认回执。 后端续期成功但浏览器断网时,该事件可能延迟到恢复连接后;续期成功也不证明任何修改已经保存。 断连时保留“保存结果未确认”,重连后等待新的 ready 再操作,并核验持久化结果。 若授权已实际过期或撤销,旧凭据不能发起新的保存,由业务后端重新签发;新页面不会自动重放离线修改。

授权失效前已鉴权受理的回存可以跨过到期时间完成;旧页面可能已失去查询权限,无法收到最终保存确认。 业务后端应查询最新文件版本及回调幂等记录,再决定是否重试或使用恢复副本。

保存异常与恢复流程

文件回调失败时保留编辑器,宿主按错误类别处理。不要用其他参与者此前的版本或 modified: false 消除本次未确认状态。若编辑器显示原生错误弹窗,关闭后再继续输入;弹窗期间的按键不能当作已录入的修改。

异常处理与重试条件
EXTERNAL_AUTH_REJECTED 业务文件接口拒绝读写授权(401/403)。由业务后端修复 callbackToken / 签名 URL 或重新授权;仅续期 grant 无效。确认文件接口恢复且编辑授权仍有效后再保存。
EXTERNAL_VERSION_CONFLICT 业务接口返回 409/412。保留恢复副本,与业务最新版本合并后条件写入,不能盲重试覆盖。
EXTERNAL_FILE_UNAVAILABLE / SAVE_UNCONFIRMED 接口故障、超时或结果未确认。先核验业务版本及幂等记录,排除已经写入但响应丢失;恢复连接后有限重试并等待新的 saved。
error(stage: connection)/ expired 结束保存等待,保留未确认提示;重连等 ready,或取得新授权。已受理的写入可能仍完成,不直接判定失败。

业务文件接口须先检查 Idempotency-Key 的已完成记录,再检查 If-Match, 让“已经写入但响应丢失”的重复请求返回原成功结果。真正冲突返回 409/412,不得绕过条件写入。

  1. 保留未确认提示,先由业务后端核验最新版本与回调记录;不要直接刷新丢弃编辑状态。
  2. 若存在恢复副本,使用仍有效的 edit 且 allowDownload: true 授权, 点击“下载恢复副本”或请求 GET /api/v1/embed/recovery/download(携带嵌入会话 Bearer token)。 授权过期先由业务后端重新授权;下载不会删除副本。
  3. 比较副本与业务最新版本并合并,协调结束其他有效编辑会话,再按最新版本条件写入。 如果发生冲突,重新比较;恢复副本不包含尚未送达服务端的离线输入。
  4. 业务文件在 Nexus 之外更新后,以原 externalId、最新 version 和 size 重新登记,再签发新授权打开。 版本或 writeUrl 变更在有有效编辑会话时返回 EXTERNAL_FILE_BUSY;允许变更后旧授权会撤销。

重新打开加载业务系统已保存版本,不会自动应用恢复副本。副本按内容独立保留 7 天,总预算 2 GiB; 空间不足时拒绝新的回存,不静默删除已有副本。成功保存只清理对应内容,下载提供最近尚未确认的副本。 新页面不会把历史保存错误重复报告为本次失败,但仍显示未确认恢复副本提示。

错误码

JSON 接口失败响应统一为以下格式;HTTP 状态码表示错误类别。下载成功返回二进制,删除成功返回空响应。

JSON · 错误响应
{"error": "ORIGIN_NOT_ALLOWED"}
HTTP 错误码 处理方式
401 AUTH_REQUIRED
INVALID_CREDENTIALS
检查 Client ID 与 Key、调用方是否停用;轮换后更新后端凭据。
401 TICKET_EXPIRED
SESSION_EXPIRED
INVALID_TOKEN
向业务后端重新取得文稿授权,不重用过期或已兑换票据。
403 ORIGIN_NOT_ALLOWED
FRAME_ORIGIN_NOT_ALLOWED
核对 parentOrigin 是否仍在该调用方的允许嵌入地址中;后者也可能在续期时返回。
403 INVALID_ORIGIN
BACKEND_ONLY
检查请求来源与鉴权方式;长期 Key 从业务后端使用。
403 FILE_ORIGIN_NOT_ALLOWED 在该 Key 的文件接口地址中授权 readUrl / writeUrl 所在 origin。
403 READ_ONLY
EXTERNAL_READ_ONLY
DOWNLOAD_DENIED
使用匹配业务权限的授权;外部文件编辑需要 writeUrl。
403BUSINESS_AUTH_REQUIRED 请从所属业务系统取得编辑授权;管理员工作台不能直接编辑或续期外部文稿。
502EXTERNAL_AUTH_REJECTED 业务文件接口返回 401/403。重新取得业务授权或刷新回调凭据;保留恢复副本,避免盲目重试。
404 FILE_NOT_FOUND
GRANT_NOT_FOUND
检查 ID 是否属于当前调用方,记录是否已删除。
409 EXTERNAL_VERSION_CONFLICT 读取调用方最新版本并协调冲突,再重新登记和授权。
409 EXTERNAL_FILE_BUSY 等待有效编辑会话结束,再更新外部文件版本或保存接口。
413 FILE_TOO_LARGE 缩小文件,或由管理员调整通用设置和代理请求大小限制。
422 INVALID_INPUT
INVALID_PPTX
INVALID_FILE_URL
检查字段、origin 格式、文件名和 PPTX 内容。
502 EXTERNAL_PROTOCOL_ERROR 检查实际字节数、size 及强 ETag 是否符合文件接口约定。
502 EXTERNAL_FILE_UNAVAILABLE
STORAGE_UNAVAILABLE
检查文件接口网络、凭据和存储连接;不要将失败显示为已保存。
503 EDITOR_UNAVAILABLE
STORAGE_UNAVAILABLE
检查编辑器或存储服务就绪状态,恢复后重试。
507 RECOVERY_STORAGE_FULL
RECOVERY_STORAGE_UNAVAILABLE
恢复区已满或无法持久化副本,本次不发起业务回存;保留编辑状态并修复存储后重试。
404 / 503 RECOVERY_NOT_FOUND / RECOVERY_STORAGE_UNAVAILABLE 下载时没有未确认副本,或恢复区暂不可读。核对业务版本和副本保留期限;存储异常须先修复。
400 EXTERNAL_ORIGINAL_UNAVAILABLE 外部文件没有托管原件副本,改为下载当前结果。
409 FILE_IN_USE 先结束有效编辑会话再恢复历史。
404 VERSION_NOT_FOUND 历史已清理或不属于该文稿。
409 FILE_NOT_TRASHED 只有回收站文稿可以永久删除。
409 FILE_IN_TRASH 恢复外部文稿记录后再登记。
409 FILE_QUOTA_EXCEEDED / STORAGE_QUOTA_EXCEEDED 清理文稿、历史或回收站,或调整调用方限额。
429 SESSION_QUOTA_EXCEEDED 结束其他会话或调整调用方会话限额。
429 REQUEST_RATE_LIMITED 遵循 Retry-After 秒数后重试。
503 EDITOR_CAPACITY_EXCEEDED 编辑引擎容量已满,稍后重试。
503 DATABASE_BUSY 数据库暂时繁忙,遵循 Retry-After 重试。
409 SESSION_PAUSED 会话已暂停,不能续期;宿主核验权限后重新签发授权。
409 EDITOR_NOT_OPEN 编辑器尚未打开,等待加载完成。

错误码表涵盖业务接入常见失败。编辑器保存错误还会在嵌入页面显示;文稿锁冲突时 WOPI 返回 409 并保持已有锁,调用方不需要自行实现 WOPI 锁协议。 SAVE_UNCONFIRMED 是前端保存事件的错误码,不是业务文件接口 HTTP 状态; 它表示结果尚未确认,需核验版本及幂等记录,不能直接认定没有写入。处理步骤见异常恢复流程。

部署与服务设置

服务由文件 API 与编辑引擎组成。生产部署为两者分别配置可访问的 HTTPS 地址,编辑引擎的反向代理须支持 WebSocket。服务间的 WOPI 走内部网络;外部业务始终使用 Nexus 签发的 /embed/ 链接。

dotenv · 部署地址示例
APP_ORIGIN=https://slides.example.com
CODE_PUBLIC_URL=https://office.example.com
CODE_SERVER_NAME=office.example.com
TLS_TERMINATION=true
BIND_ADDRESS=127.0.0.1
ADMIN_USERNAME=admin
ADMIN_PASSWORD=替换为管理员密码
ADMIN_CLIENT_ID=local

从项目的 .env.example 复制部署配置到 .env,替换地址与管理员凭据后运行:

Shell · 启动与检查
./deploy.sh
docker compose ps

curl --fail "$NEXUS_URL/health"
curl --fail "$NEXUS_URL/ready"

/health 检查服务与数据库;/ready 检查编辑引擎 discovery。未就绪时 /ready 返回 503。

指定构建镜像源

拉取基础镜像遇到 403 或网络错误时,可指定实际可访问的 Docker Hub 镜像仓库。 网络可直连 Docker Hub 时可使用 registry-1.docker.io;mirror.example.com 为占位示例,须替换为实际仓库:

Shell · 指定镜像仓库
./deploy.sh --registry registry-1.docker.io
./deploy.sh --registry mirror.example.com
./deploy.sh --registry mirror.example.com:5443/dockerhub --env-file /etc/nexus-ppt/production.env

地址不含 https:// 或凭据;私有仓库先 docker login。仓库须提供 library/python 与 collabora/code 的固定版本及摘要。该参数仅改变本次构建的基础镜像地址,不修改 Docker 全局配置、.env 或 Python 包下载源,下次部署需继续传入。 不传时仍使用 docker.io 并受 Docker 全局镜像代理影响;显式指定 docker.io 也不会绕过该代理。

在控制台维护运行配置

  • API Key:创建、轮换、启停调用方,维护允许嵌入地址和文件接口地址,保存后立即生效。
  • 文件存储:本地、S3 兼容存储、阿里云 OSS。测试连接、保存配置、迁移已有托管文件;不影响调用方自有文件的存储位置。
  • 通用设置:单文件大小 1–200 MB、默认授权 1–480 分钟、管理员会话 1–24 小时。默认分别为 30 MB、240 分钟、8 小时;历史默认 20 条、回收站 30 天、审计 90 天。

新文件默认授权时长采用当前配置,已经签发的授权不会随默认值变化。管理员会话时长仅对新登录生效。轮换 Key、停用或修改调用方地址会撤销已有文稿授权,重新启用后仍需重新签发。

限额、状态与审计

调用方设置提供文稿数量、存储 MB、同时打开的授权会话数、每分钟后端请求数;0 表示不限,默认每分钟 600 次。计数在服务端原子执行;恢复回收站文件同样受文稿数量限额约束。存储用量按该调用方的托管原件、当前内容和历史哈希去重,包含回收站。

运行状态显示引擎与存储连接、数据用量和磁盘余量。在线会话按最近 120 秒有心跳的授权计数,是应用层会话估计;编辑引擎实际连接限制仍由 CODE 执行。审计记录关键写操作、下载和失败请求,支持调用方、操作、结果及用户/文稿/请求 ID 查询,不记录 Key、票据或请求正文。

数据与备份

SQLite 保存服务配置、文稿元信息、历史及审计。使用项目根目录脚本备份、校验与恢复:

Shell · 备份与恢复
./ops.sh backup /srv/backups/nexus-ppt.zip
./ops.sh verify /srv/backups/nexus-ppt.zip
./ops.sh restore /srv/backups/nexus-ppt.zip

备份暂停服务并保存数据库、配置密钥、环境文件及全部托管内容,包括对象存储中的托管原件、历史和回收站;完成后恢复原有运行状态。恢复前自动生成安全备份,校验失败不会覆盖现有数据。

恢复将托管内容落到本地存储并撤销旧会话。可在控制台重新选择对象存储并迁移。外部调用方原文件,以及对象浏览器上传但未纳入文稿管理的独立对象,需由文件所属方另外备份。备份包含环境凭据,应妥善保存。

更新容器时保留数据卷。docker compose down -v 会删除服务数据,不应用作常规停止操作。生产代理的请求大小限制需与控制台上传上限匹配。单机 Compose 部署下,多台独立编辑引擎不能直接随机分配同一协作文稿。

项目使用固定版本的 Collabora CODE 编辑引擎,当前 home mode 限制为最多 20 个连接、10 份同时打开的文稿。更大规模部署需采用适合业务容量的编辑引擎版本,并用实际 PPTX 验证显示兼容性。

常见问题

iframe 空白或被浏览器阻止?

核对 parentOrigin 与当前业务页面 origin 是否完全相同,并在控制台授权;打开链接须在 60 秒内。业务页面自己的 CSP 也需要允许 Nexus 服务和编辑引擎的 frame-src。HTTPS 页面应嵌入 HTTPS 服务,避免混合内容被浏览器阻止。

两个人打开了不同的文稿,无法同步?

确认使用同一调用方、同一个 Nexus 文件 ID。分别签发用户授权即可,不要为每个参与者重新上传。用户身份来自各自的业务登录会话。

Key 轮换后调用失败?

将新 Key 更新到业务后端,按你的部署方式重新加载配置;旧 Key 和之前的文稿授权已撤销,需要为用户重新签发链接。

编辑器提示已保存,怎样取得结果?

托管文件通过下载接口取得当前 PPTX;调用方自有文件从自己的存储读取。业务页面以 saved 事件或后端文件元数据确认结果,不仅依据按钮点击或 modified 状态。

文件 API 可访问,但 Nexus 读取失败?

确认文件接口地址已授权,Nexus 容器可以访问该 URL;容器中的 localhost 指向容器自身。核对 callbackToken、If-Match、ETag 和准确字节数,接口直接返回 PPTX 而非跳转到其他地址。