目录

ApuSync APP 对接手册:设备配对、照片与文件直传

本文件同时定义云端发现与 NAS 数据接口,供 Flutter、iOS、Android、iPadOS、Windows、macOS 和 Linux APP 对接。配对后的默认体验是:用户输入 设备码+配对密钥,直接访问 NAS 主账号预先授权的目录;APP 不再填写 NAS 用户名、密码,也没有云端人工审批步骤。

运行时以 capabilities 返回的能力为准。新增 NAS 能力版本为 2.0,HTTP 路径保留 /v1 以兼容旧客户端;签名和 WebRTC 信令版本仍为 1。产品发布版本与协议版本是两个概念。

1. 两个服务,明确分工

服务 地址 接收的数据
云端 ApuSync https://sync.apuos.cn,内部 TCP 13260 设备身份、一次性配对、会话、签名 SDP/ICE
地址探测 STUN stun:sync.apuos.cn:13261,UDP STUN Binding 报文
NAS 局域网接口 http://<NAS-IP>:18080/api/v1/sync-client/v1,也可使用 NAS 配置的 HTTPS 文件、照片、索引、缩略图、上传任务
NAS 远程接口 APP 与 NAS 的 WebRTC DataChannel 与 NAS RPC 相同的业务参数和内容

云端不接收文件、照片、文件名、目录清单、文件哈希、照片元数据或 NAS 密码。 不得把下文的 NAS RPC 发到 sync 域名。云端无 TURN 或文件中继回退;若网络不能建立直连,APP 应明确提示直连失败并重试/切换网络,不得显示上传成功。

客户端自己的上传队列、系统相册读取权限、后台运行和网络重试由 APP 实现。服务端提供持久上传、完整性验证和提交回执,不代替 iOS/Android 的后台执行权限。

2. 设备身份、签名与时间单位

客户端首次运行生成 Ed25519 密钥对,私钥放在 Keychain/Keystore 等平台安全存储中。公钥为 32 字节小写 hex(64 字符);clientId = hex(SHA256(rawPublicKey))[0:32]。NAS 同样拥有独立的 Ed25519 身份;私钥不离开 NAS。

云端所有签名 POST 的 JSON 外层必须是:

{"signed":"<确切的 UTF-8 JSON 字符串>","signature":"<Ed25519 签名,128 字符小写 hex>"}

先序列化内部对象一次,再对该字符串的 UTF-8 字节签名,外层复用同一字符串。不要签名后重新排序字段、修改空格。内部对象拒绝未知字段。每次请求用随机 16 字节 nonce(32 字符小写 hex),时间偏差不超过 120 秒。云端 timestamp/expiresAt/bindingExpiresAt 均为 Unix 秒;NAS RPC 的日期数值和分享过期时间均为 Unix 毫秒,照片 capturedAt 为 ISO 日期字符串。EXIF 没有时区时返回 captureTimeZoneKnown:false,APP 不应擅自当 UTC。

Bearer、配对密钥、分享密钥和私钥不得放在 URL、查询参数、日志、崩溃上报或普通偏好设置里。生产连接校验 HTTPS 证书;云接口不允许查询参数。

3. NAS 主账号首次授权

这些 NAS 管理接口由主账号在 NAS 页面操作,APP 配对时不调用:

方法与 NAS 路径 请求 用途
GET /api/v1/apusync/direct NAS 登录 Bearer 在线状态、deviceCode、授权目录、已配对客户端
POST /api/v1/apusync/direct/settings {enabled,currentPassword,confirm:true} 启用/停用直连同步
POST /api/v1/apusync/direct/invitations {root,path,currentPassword,confirm:true} 选定授权目录并生成一次性配对密钥
POST /api/v1/apusync/direct/sessions/{sessionId}/revoke {currentPassword,confirm:true} 撤销该客户端,已有连接停止访问

邀请返回 deviceId,deviceCode,grantId,sessionId,pin,pairingKey,expiresAt,approvalRequired:false。pairingKey 与 pin 是同一个 12 位数字字符串,保留前导零。密钥最长 5 分钟有效,一次使用;一个密钥绑定一个客户端。每个新客户端另生成密钥。绑定有效期默认 365 天;短期连接会话仍至多 1 小时,APP 用原私钥恢复,无需重复输入密码。NAS 管理状态返回 bindingExpiresAt,旧授权的 loginMode 可能为 legacy-login,重新配对即可采用新流程。

4. 云端注册、配对、恢复

NAS 自注册(由 NAS 进程实现)

POST /v2/sync/nas/register,签名内部对象:

{"version":1,"action":"nas-session","deviceId":"<32hex>","publicKey":"<64hex>","timestamp":0,"nonce":"<32hex>"}

返回稳定 deviceCode(APU 加 12 位小写 hex)、短期 NAS bearer、expiresAt、mailboxUrl、stunUrls。新流程无需云端操作员批准,也不依赖 ApuConnect K。显式撤销或禁用的设备不能通过再次注册重新启用。旧 /v1/sync/nas/session 保留兼容。

APP 初次配对

  1. 用户从 NAS 页面获取设备码及一次性配对密钥。
  2. APP 以自己的私钥向 POST /v2/sync/pair 发送签名:
{"version":1,"action":"pair","deviceCode":"APU<12hex>","pin":"<12位配对密钥>","clientId":"<32hex>","publicKey":"<64hex>","timestamp":0,"nonce":"<32hex>"}
  1. 返回云端临时 bearer,expiresAt,sessionId,deviceId,nasPublicKey,invitation,mailboxUrl,stunUrls。保存 NAS 身份、公钥及 sessionId;验证 NAS 签名邀请,核对设备 ID、公钥派生、grantId、sessionId 和有效期。
  2. 用 Bearer 查询 GET /v1/sync/client/session 或接收 WSS approved 事件。NAS 会核验原始 APP 签名及配对密钥,自动签署授权。pending 是几秒的后台验证状态,不是等待人点击审批。
  3. 验证原始 approval 签名确实来自已绑定 NAS,内容包含自己的 clientId/publicKey、sessionId/grantId、未过期 expiresAt,之后才发送 SDP。
  4. 建立直连后收到 authenticated,其中 method:“paired-key”。APP 直接发 RPC,不发 login,不索要 NAS 密码。

恢复已配对设备

POST /v2/sync/client/resume,不带旧 Bearer,使用原客户端私钥:

{"version":1,"action":"pair-resume","deviceCode":"APU<12hex>","sessionId":"<原32hex>","clientId":"<原32hex>","publicKey":"<原64hex>","timestamp":0,"nonce":"<新的32hex>"}

它轮换云端 Bearer,旧客户端信箱关闭,信令计数重置;NAS 自动核验原绑定后重新签署短期授权。等待并验证新 approval,然后创建新的 RTCPeerConnection/DataChannel。旧连接不能借长期绑定继续使用。私钥遗失、绑定撤销、NAS 账号凭据变更或授权过期后,需要在 NAS 重新配对。

GET /v1/sync/capabilities 为公开能力查询;GET /v1/sync/client/session 为本会话恢复状态,不返回其他客户端清单。POST /v1/sync/client/disconnect 使用本客户端 Bearer 和签名 {version:1,action:“client-disconnect”,clientId,deviceId,sessionId,timestamp,nonce},用于明确断开/撤销,不是短暂切后台的心跳操作。签名信令和全部错误约定见 SYNC-PROTOCOL.md 与 SYNC-CLIENT-API.md。

5. 建立直连与统一 RPC

APP 创建可靠有序通道 apusync.control.v1,采用云返回的 STUN,只接受 application SDP 与 host/srflx ICE,不启用 TURN。WSS /v1/sync/mailbox 使用 Authorization: Bearer;浏览器可先发 {type:“authenticate”,bearer},不能把令牌拼 URL。每条 offer/answer/ICE 都需要原客户端签名,严格核对 NAS 签名、目标、序号和 DTLS fingerprint。完整 SDP 字段和限制见协议文档。

远程请求:

{"v":1,"type":"rpc","requestId":"op_001","operation":"files.stat","params":{"path":"照片/旅行.jpg"}}

成功/失败:

{"v":1,"type":"rpc-result","requestId":"op_001","status":200,"data":{"path":"照片/旅行.jpg","etag":"<64hex>"}}
{"v":1,"type":"rpc-result","requestId":"op_001","status":403,"error":"sync_scope_denied"}

请求最长 32768 字节,requestId 为 1–64 个字母、数字、_ 或 -。多个响应按 requestId 关联;客户端应串行提交同一个上传的片段。旧单文件二进制通道仍兼容,但新 APP 优先使用持久 uploads.*。

局域网 NAS REST 调试:GET /api/v1/sync-client/v1/capabilities;POST /api/v1/sync-client/v1/rpc,使用 NAS 登录 Bearer,body 只含 {operation,params},返回 {data:…} 或 {error:…}。这条 HTTP 调试方式的登录不改变默认的远程免登录配对流程。暂不将云端 Bearer 当作 NAS HTTP Bearer。

远程所有路径都相对于已授权目录:path:“” 表示授权目录,root 可省略,若传必须等于授权 root。不能以绝对路径、..、符号链接或相册/人物 ID 越过目录。REST 则明确传 NAS root,path 相对于该 root。客户端禁止发送保留参数 scopePath,scopeExpiresAt,clientContext,publicationTicket;这些只由 NAS 可信进程注入。

每次文件操作继续走原有 NAS 能力检查和 Linux UID/GID/ACL 文件代理。配对不授予系统管理、任意根目录或其他账号权限。

6. 文件、下载、搜索和索引

下表业务参数均位于 params;root 遵循上一节。? 表示可选。

operation 参数 结果/规则
capabilities {} 支持操作、大小限制、stagingAvailable;先查询再展示按钮
auth.refresh {} {expiresAt,sessionRefreshed,nasTokenReturned:false};延长内部 NAS 会话,不延长云端授权
files.roots {} 远程只返回授权 root,不是全 NAS 磁盘清单
files.list {path?,offset?} entries、nextOffset;每页最多 32 项
files.mkdir {path?,name} 在当前授权目录下新建子目录
files.stat {path} name/path/directory/size/modified/etag/kind/mime,媒体元数据见下一节
files.read-chunk {path,offset?,ifModified?} base64 data、nextOffset、totalBytes、complete、lastModified;每块最多 16384 字节
files.delete {path,confirm:true} 移入回收站,返回 trashId;不永久删除
files.rmdir {path,confirm?} 空目录可删除,非空需 confirm:true;不能删除授权根目录
files.move {path或from,toParent,name?} 同 root 移动/改名,不静默覆盖目标
files.copy {path或from,toParent,name?} NAS 内复制,不经云端;100MiB/2048 节点受控上限,超限报错
files.replace {path:目标,from:替换源,overwrite:true,confirm:true,ifMatch} 将 NAS 已存在的源文件替换目标;原目标入回收站,源文件被移动
media.scan / photos.scan {path?,recursive?,cursor?} 每次受控扫描一批,返回 cursor、complete 和累计计数;持续调用至 complete
files.search / photos.search {query?,kind?,dateFrom?,dateTo?,place?,tags?,personId?,offset?,limit?} 已索引且有权访问的结果;索引可能不完整,见 indexMayBePartial
widgets.summary {} 当前授权范围的 photos/videos/indexedFiles/generatedAt

下载必须保存 lastModified 并在后续分块传回 ifModified;出现 sync_download_source_changed 时丢弃旧片段重新下载,不拼接两个版本。服务端不宣称不可变文件快照。零字节文件返回 complete:true、空 data。

搜索和智能相册使用 NAS 持久 SQLite 索引,不把全部文件名发到 APP 才筛选。首次对授权目录调用扫描,建议 recursive:true,用返回 cursor 循环;失败项目计入 errors,不等于全部照片成功解析。搜索 kind 使用 image/video/file;标签为字符串数组;日期过滤基于 capturedAt,缺失时以已知文件时间处理(以返回字段为准)。地点来自 EXIF GPS 或用户填写的 place,未配置地理编码时不虚构城市名。

7. 照片信息、缩略图与编辑

files.stat 对可解码图片返回 width/height、camera/make/lens、orientation、capturedAt、GPS、perceptualHash、projection 等已有字段;视频可通过 NAS ffprobe 返回 width/height、duration(秒)及已有编码信息。字段缺失表示源文件没有或 NAS 解码器不可用,不返回虚假值。indexState:“metadata-only” 与 decoderError 供 APP 显示格式限制。

operation 参数 结果/规则
files.thumbnail {path,size:“sm”或“m”} JPEG base64,sm 最大边 128,m 最大边 384;必要时继续缩小,使 JPEG ≤22KiB;返回实际 width/height/etag
metadata.set {path,metadata:{place?,capturedAt?,favorite?,projection?}} 持久保存到 NAS SQLite;projection 仅 null 或 equirectangular;不能伪造其他只读技术字段
tags.list {} 当前范围标签和计数
tags.set {path,tags:[“旅行”,“家庭”]} 替换该文件完整标签列表
tags.remove {path,tags:[“旅行”]} 移除指定标签,不影响其他标签
photos.edit {path,rotate?,crop?,name?,overwrite?,confirm?,ifMatch?} crop 为源图像像素 [x,y,width,height];rotate 为 0/90/180/270;默认另存,不修改原图

编辑输出限服务端实际支持的 JPEG/PNG;覆盖时必须显式 overwrite:true、confirm:true 及匹配的 ifMatch,旧版本进回收站。另存以 name 指定同目录新文件名。EXIF 方向、编辑后的元数据保留应由客户端依据实际返回确认,不能假定重编码保留全部原始 EXIF。

360° 识别依赖源文件 GPano/spherical metadata 或用户显式标注;2:1 宽高比本身不能证明是全景。APP 用 projection 决定球面展示;视频播放通过 files.read-chunk/NAS 既有媒体流接口实现,本接口不传播放器 UI,不把转码任务放云端。支持的解码格式以 NAS 已安装引擎为准。

8. 独立相册、人物、相似照片

operation 参数 结果/规则
albums.create {name} {id,name,virtual:true,revision}
albums.list {} 当前目录范围相册
albums.rename {id,name} 重命名相册
albums.delete {id} 只删虚拟相册,不删照片
albums.add / albums.remove {albumId,paths:[…]} 最多 100 路径;同一照片可在多个相册,不移动原件
albums.items {albumId,offset?,limit?} 当前仍有权限的成员
albums.smart.create {name,rules:{query?,kind?,tags?,personId?,dateFrom?,dateTo?,place?}} 创建规则相册
albums.smart.list {} 相册及规则
albums.smart.update {id,rules:{…}} 更新规则
albums.smart.query {albumId,offset?,limit?} 根据 NAS 索引动态查询,不能手工增删规则相册成员
faces.index {path} 本机 YuNet 检测+SFace 特征,返回人脸框、id、候选身份;人工确认,不自动认定身份
faces.list {path} 该照片已索引人脸、确认/拒绝状态
people.list {} 当前范围已确认人物和照片路径
people.rename {personId,personName} 重命名可见人物
faces.confirm {faceId,personId} 或 {faceId,personName} 确认到已有或新人物
faces.reject {faceId} 拒绝本条身份,保留审计状态
media.similar {offset?,limit?} dHash64 汉明距离≤6 的候选组,最多扫描 5000 张索引图;不自动删除

人脸特征向量只保存在 NAS,不出现在普通 APP 响应或云端。确认前 NAS 检查文件 etag;原照片已改变时需重新索引。索引同版本可复用,替换后的旧人脸失效。SFace 候选为相似度提示,需要用户确认。

人脸模型路径为 /opt/apuos-models/photos/yunet.onnx、sface.onnx,需要可信 root 所有、不可被普通用户改写的固定模型文件和 NAS Python OpenCV 引擎。缺少模型返回 face_models_not_installed,不产生模拟人脸。接口遵循现有 AI 授权和资源控制策略,不另行修改许可模式。

9. 分享与成员

operation 参数 结果/规则
shares.create {paths:[…],members:[clientId…],permission:“read”或“read-write”,expiresAt?} id、仅创建时返回 shareKey、expiresAt;最长 30 天且不超过本次会话授权
shares.list {} 当前客户端创建的分享,不返回 shareKey
shares.members {id,members:[…]} 创建者更新成员;成员最多 64 个已知 clientId
shares.resolve {id,shareKey} 创建者或列出的客户端、相同 NAS 账号和目录授权、未过期密钥才能读取分享条目
shares.revoke {id} 创建者撤销,后续 resolve 拒绝

本版是已配对设备之间的受限分享。分享不能给陌生匿名访问者开放 NAS,也不能授予超过原账号/目录 ACL 的权限。read/read-write 记录分享意图,实际文件读写始终受 NAS 既有授权约束;APP 不应把它表示成独立跨账号写权限。密钥不拼 URL。邀请另一个用户访问其他账号目录,需要 NAS 原有账号/共享权限先授权;本接口不绕过它。

10. 持久上传与自动照片备份

uploads.create:

{"requestId":"<APP生成并持久保存的32hex>","path":"手机照片/2026-10","name":"IMG_0001.jpg","size":123456,"sha256":"<整文件64hex>","overwrite":false}

相同 requestId 和完全相同绑定重试返回同一个上传。更换目标、内容或覆盖策略后复用 requestId 返回 409。最大 64GiB,零字节也支持;实际还受 NAS 空间、配额和暂存卷限制。stagingAvailable:false 必须向用户报告存储不可用,不可把只建立清单当备份成功。

operation 参数/用途
uploads.list {offset?,limit?};默认 16,最大 32,任务持久化
uploads.get {id};重新读取 received、prefixDigest、status、receipt
uploads.segment {id,offset,data,chunkSha256};data 是最多 16384 字节的 base64,offset 为整文件字节偏移
uploads.action {id,action:“pause”或“resume”或“cancel”}
uploads.commit {id};校验整文件并发布,可能先返回 verifying,轮询后再提交
uploads.forget {id,confirm:true};仅清理已完成/取消任务及暂存,不删除已发布文件

每个持久块 2097152 字节,最后一块可更短。每块所有片段使用相同 chunkSha256,即整个持久块的 SHA-256,不是当前 16KiB 片段的哈希。received 代表落盘的持久偏移;fragmentReceived 只是当前内存接收位置,fragmentDurable:false 时进程重启会回退到 received。断线重新查询,不从本机记忆的最后 ACK 盲目续写。

LAN 原生 APP 可用 PUT /api/v1/sync-client/v1/uploads/{id}/chunks/{offset},头 X-Chunk-SHA256 和 NAS Bearer,body 原始 2MiB 块;远程优先用 segment。prefixDigest = SHA256(previousDigestBytes || chunkSHA256Bytes || uint64BE(chunkLength)),初始值/完整链例子见 NAS 原有持久上传文档。

只有 status:committed 且 receipt 的最终路径、size、sha256 与预期一致,才算备份完成。 verifying、ready、网络 delivered、100% 进度都不是成功。commit_uncertain 需要查询最终文件并核验,不自动重复覆盖。APP 保存本地资产 ID、任务 ID、requestId、文件指纹、服务器回执和失败原因,才能完成跨重启照片备份队列。

覆盖编辑后的原文件:先 files.stat 取得 etag,再创建新任务,传 overwrite:true,ifMatch:<etag>。校验完成后 NAS 将旧版本移入回收站并发布新版本;etag 不匹配返回冲突,不接受无条件覆盖。普通自动备份建议新文件名,不自动删除 NAS 文件;双向删除传播不在本版协议中。

11. 客户端最小实现顺序与错误处理

  1. 持久保存 Ed25519 身份,完成设备码+密钥配对、签名验证和自动授权等待。
  2. 创建直连,收到 authenticated 后读取 capabilities 与 files.roots/list。
  3. 本机计算 SHA-256,保存 requestId,分片上传,校验提交回执。
  4. 添加暂停、恢复、进程重启查询、空间不足和版本冲突提示。
  5. 调用 photos.scan 建 NAS 索引,再做缩略图、搜索、标签、虚拟相册。
  6. 按实际模型能力启用人脸索引和人工确认,再加入分享成员和智能相册。
状态/错误 APP 行为
400/invalid_request、reserved_parameter 修正字段;不要自动重发相同错误
401/unauthorized、sync_authentication_required 云端用原密钥 resume;若已撤销或绑定无效,提示重新配对
403/permission_denied、sync_scope_denied 保留失败原因;不得改 root 或切换到云中继绕过
409/etag、binding、checksum 冲突 重新读取文件或任务,提示冲突,不能静默覆盖
413/image_pixel_limit、body limit 降低客户端图片或请求大小;保留原文件
429/capacity、dispatch_busy 有上限的指数退避,不并发轰炸 NAS
503/transfer_storage_unavailable 提示 NAS 暂存卷/空间/引擎不可用,保留队列后重试
face_models_not_installed/decoder unavailable 隐藏相关操作或显示未安装依赖,不显示伪成功
直连失败 明确显示网络限制;保持待传队列,无隐式 TURN/云文件中继

语言、APP 外观、相册系统权限和桌面小组件 UI 属于客户端;NAS 已有证书、两步验证和管理员设置仍使用各自受保护的接口,不通过这个目录授权暴露系统管理权。账号停用、权限修订、主账号凭据变化、配对撤销和会话过期都必须使已有文件操作失效。

12. 联调与实现索引

云端实现 backend/src/sync.rs、sync_client.rs;NAS 实现 sync_direct/{mod,cloud,rtc,transfer}.rs、sync_client_routes.rs、sync_client/uploads.rs、storage/sync_library.rs 及原文件代理/接收器。机器可读操作目录在 NAS docs/backup/app-sync-operations.json。本文件仅使用占位示例,不包含真实设备凭据。

联调记录应分别写清:本机自动化、Linux 文件代理 UID/ACL、实际 Mac→NAS WebRTC、真实人脸模型、外网不同 NAT 和 APP 原生后台权限。某项未验收不能由“构建通过”替代;具体发布与部署记录见 CHANGELOG。

文档来源与适用范围

本页与本机 APP 对接手册保持一致,覆盖云端 v2 配对和 NAS 2.0 能力;运行时仍以 capabilities 为准。旧版接口的兼容细节见 云端信令兼容参考、WebRTC 兼容参考、断点上传详解。

源文档:apuos-server/docs/APUSYNC-APP-API.md;源提交:260a48606155c34bc6018d6078a83f8900affdbe;源文档 SHA-256:78e07a443fa9b447d8337ba1b0bf3054e8b399b1d71bc298c591ad1aa9871fe2。本页不包含真实设备凭据。