接 @wps/wps_sdk 时,「能打开、能编辑」往往较早做完;真正容易返工的是关窗后的路径。业务上传、版本号、二次打开,都依赖一条本应用沙箱内的 filePath 。WPS 回传的 fileUri 在对方沙箱,直接当持久路径会在联调后期才暴露问题。本文按模块边界写一版可复用的解析链路:请求侧怎么开回传、结果侧怎么分支、落盘侧怎么收口。
阅读建议:已了解 registerApp 与基础 OpenFileRequest 打开流程。下文默认注册成功,专注 transfer → resolve → upload 三段。
为什么「打开成功」不等于「有 filePath」
WPSApi.sendRequest 的 Promise 在两种配置下返回时机不同:
- 未开回传 :WPS 拉起成功即可
OK,data通常为空。 - 开启回传 :要等用户关闭文档,
data才可能带上 URI/FD。
若 UI 在「仅拉起成功」那种返回上提示「已保存」,用户会认为业务库已更新,实际上还没有任何最终路径。建议在状态机里拆两个状态:opened 与 collected。产品文案也要分开:「已打开编辑器」与「已收回编辑结果」。
请求侧:只暴露一个布尔「要不要回传」
页面不要直接碰 enableTransferFile 与 wpsTransferType 两套语义。封装层统一写 wpsTransferType:
typescript
type OpenMode = {
editable: boolean;
waitTransfer: boolean;
transfer?: 'uri' | 'fd';
};
function buildOpenRequest(
ctx: common.UIAbilityContext,
path: string,
mode: OpenMode
) {
const req = new OpenFileRequest(ctx, path);
req.enableEdit = mode.editable;
if (mode.waitTransfer) {
req.wpsTransferType =
mode.transfer === 'fd' ? TransferType.FD : TransferType.URI;
}
return req;
}
默认用 URI 降低联调成本;对路径暴露更敏感的场景再切 FD。注册仍须先于打开:registerApp 未成功时其它 sendRequest 会 reject,进不了 result.code。
结果侧:统一 resolve,禁止页面直接读 fileUri
把「有没有最终路径」收成一个函数,页面只消费返回值:
typescript
function resolveFinalPath(
ctx: common.UIAbilityContext,
result: Result
): string | null {
if (result.code !== ResultCode.OK) {
throw new Error(result.msg ?? String(result.code));
}
if (!result.data) {
return null; // 打开成功但无回传
}
if (result.data.transferFd !== undefined && result.data.transferFd >= 0) {
return copyFdToAppSandbox(ctx, result.data) ?? null;
}
if (result.data.fileUri) {
return copyUriToAppSandbox(ctx, result.data.fileUri) ?? null;
}
return null;
}
URI 拷贝目录建议固定:filesDir/wps_callback/<timestamp>/,文件名取自 WPS 侧 path。FD 路径:分块读写、校验 transferFileSize、关闭两边 fd。拷贝完成后,若当前策略要求最小化临时文件,可异步 unlink WPS 侧 URI;是否删除由安全策略决定,不要和「有没有拿到 filePath」绑死。
业务侧:用新路径替换旧引用
最常见的线上 bug 是:打开时用了 sandboxPath,关窗后上传仍用同一个变量。正确做法是:
typescript
const openedPath = toSandbox(ctx, pickerUri);
const result = await WPSApi.sendRequest(
buildOpenRequest(ctx, openedPath, { editable: true, waitTransfer: true })
);
const finalPath = resolveFinalPath(ctx, result);
if (!finalPath) {
// 提示关窗,或标记为 opened-only
return;
}
await uploadAttachment(finalPath); // 禁止再用 openedPath
版本号、本地缓存 key、二次打开入口,全部改为 finalPath。单测覆盖:无 data、仅 URI、仅 FD、拷贝抛错、size 不一致。
联调矩阵与日志约定
| 用例 | 期望 |
|---|---|
| 不开回传 | OK 且 resolve 返回 null |
| URI + 关窗 | 非空 finalPath,可再次打开 |
| URI + 只切回 | null,不报失败 |
| FD + 大文件 | size 对齐,fd 已 close |
| 仍传 openedPath | 用例应失败(防回归) |
日志前缀统一 [WPS][transfer],输出 requestType/code/hasData/mode。Release 不打印完整路径中的敏感目录名时可做截断,但 Debug 必须能区分「无 data」与「拷贝失败」。
水印、extraOptions、修订参数与回传正交,可同请求配置。联调时仍建议:先只开 enableEdit + TransferType,确认 finalPath 稳定后再叠菜单策略。路径侧始终先把选择器文件 copy 进 filesDir,再交给 OpenFileRequest,否则打开层 ERROR 会掩盖回传问题。不落地相关约束若生效,云文档/分享等菜单可能被强制关闭,这与能否拿到 filePath 无直接冲突:回传仍按 URI/FD 走本地拷贝。拷贝成功后是否删除 WPS 临时文件,交给安全策略开关,不要写死在 resolveFinalPath 里。
| 维度 | URI | FD |
|---|---|---|
| 联调成本 | 低,路径可见 | 稍高,要处理分块与 close |
| 路径暴露 | 有临时路径字符串 | 以描述符为主 |
| 大文件 | 依赖文件系统 copy | 可控制缓冲与进度 |
| 校验 | 拷贝后可再 hash | 应用 transferFileSize |
默认工程走 URI;对路径字符串敏感或需要流式读时再切 FD。两种模式共用同一 resolveFinalPath 出口,页面无感。
错误分层、提示与评审检查
| 层 | 条件 | 用户提示示例 |
|---|---|---|
| 打开失败 | code !== OK |
展示映射后的短错误 |
| 仅打开 | OK && !data |
请关闭 WPS 完成保存 |
| 落盘失败 | 有 data 但 copy 抛错 | 本地保存失败,可重试 |
| 上传失败 | 已有 finalPath | 与 WPS 无关,走上传重试 |
不要把四层错误合成一句「保存失败」。PR 里建议固定勾选:是否只通过 resolveFinalPath 取业务路径;上传/落库是否仍引用打开前变量;FD 分支是否 closeSync;目标目录是否 mkdirSync;是否区分 opened / collected UI;单测是否覆盖空 data、URI、FD、size 不一致。
小结
关窗回传的工程重点不是「再记一个 API 名」,而是把打开路径 和业务路径 拆开:请求侧声明 wpsTransferType,结果侧统一 resolveFinalPath,业务侧只用最终 filePath。字段语义以官方对接文档为准;把落盘收进 transfer.ts 后,页面与上传队列都不必再理解 WPS 沙箱细节。后续若接断点上传或加密落盘,只扩展 transfer 模块出口,不必回头改每个打开按钮。工程稳定后,日常需求通常只是改上传接口或命名规则,不必再打穿整条打开链路。把打开路径与业务路径拆开,是关窗回传联调里最值回票价的一刀。
基于 WPS Open SDK 鸿蒙版对接实践整理,仅供开发者参考。
官方对接文档:365.kdocs.cn/l/clQl5cek2...
技术交流 QQ 群:628436767