做鸿蒙办公审批时,附件编辑链路最容易散:有人只写了 OpenFileRequest,忘记先 registerApp;有人加了水印却没关分享;有人配置了回传,测试却只「切回应用」从不关窗,然后报 SDK 没回调。本文按工程视角把 WPS Open SDK 收成三个模块:注册、策略打开、回传落盘。叙述偏短,代码尽量完整,并插入三张关键片段截图,方便联调时对照工程屏幕。读者需已完成 @wps/wps_sdk HAR 集成,下文不再重复依赖声明。我这几天把审批、合同、公文三类附件都在同一套封装里跑通了:差异只在水印文案和是否开修订,调用时序完全一致,后面接手的同事不容易把注册逻辑又抄到页面按钮里。
接口时序(先画清再写代码)
scss
Application.onCreate
→ await ensureRegistered(ctx)
Index 按钮
→ copyPickerToSandbox
→ openWithOfficePolicy (WaterMark + extraOptions + TransferType.URI)
→ 用户关闭 WPS 窗口
→ copyCallbackUri → uploadQueue
sendRequest 在未注册时会 reject ;注册后打开失败走 result.code(常见 ERROR=-2、鉴权 1013)。日志建议固定打 requestType/code/msg。appKey/appSecret 与 bundleName 绑定,调试包和上架包名不同就要分别申请,否则在预发环境正常、生产突然 1013 的坑会反复出现。
政企审批对「关窗后的文件」语义要求很死:UI 不能在 data 为空时提示「已同步」,否则业务同学会以为服务器已有最新稿。把「打开成功」和「回传成功」拆成两个状态,能省很多扯皮。
模块 1:注册与 Result 断言
把鉴权与「是否注入激活序列号」收敛到 ensureRegistered。对接文档推荐注册成功后用 setWpsFileToken 全局注入;不要每次打开再塞 OpenFileRequest.wpsToken。
typescript
import { common } from '@kit.AbilityKit';
import {
WPSApi, RegisterAppRequest, Result, ResultCode,
} from '@wps/wps_sdk';
export let wpsReady = false;
export function logResult(tag: string, r: Result) {
console.info(`[${tag}] type=${r.requestType} code=${r.code} msg=${r.msg ?? ''}`);
}
export function assertOk(r: Result, stage: string) {
if (r.code === ResultCode.ERROR_CODE_AUTH_FAILURE) {
throw new Error(`${stage} 1013: ${r.msg ?? ''}`);
}
if (r.code === ResultCode.ERROR) {
throw new Error(`${stage} ERROR: ${r.msg ?? 'check path/params'}`);
}
if (r.code !== ResultCode.OK) {
throw new Error(`${stage} code=${r.code}`);
}
}
export async function ensureRegistered(ctx: common.UIAbilityContext) {
if (wpsReady) return;
const r = await WPSApi.sendRequest(
new RegisterAppRequest(ctx, APP_KEY, APP_SECRET)
);
logResult('register', r);
assertOk(r, 'register');
if (ACTIVATION_SN) {
WPSApi.setWpsFileToken(ACTIVATION_SN);
}
wpsReady = true;
}
激活序列号只在有申请材料时注入。冷启动放在 EntryAbility.onCreate;首页在 wpsReady 前禁用打开按钮,避免冷启动连点触发 reject。可为 assertOk 写单测:Mock 1013、ERROR、OK,减少手测依赖。凭据走 flavor rawfile 或 CI 注入,Release 日志不打 secret。
模块 2:策略打开(可编辑 + 水印 + 关外发 + 回传)
从 DocumentPicker 拿到的路径先拷到沙箱,再构造策略请求。下图是参数赋值的骨架,便于对照本机编辑器:
typescript
import fs from '@ohos.file.fs';
import {
OpenFileRequest, OpenFileExtraOptions, WaterMark,
TransferType, WPSApi,
} from '@wps/wps_sdk';
export function toSandbox(ctx: common.UIAbilityContext, src: string): string {
const dir = `${ctx.filesDir}/wps_in`;
fs.mkdirSync(dir, true);
const dest = `${dir}/${Date.now()}_${src.split('/').pop() ?? 'file.docx'}`;
fs.copyFileSync(src, dest);
return dest;
}
export function buildPolicyRequest(
ctx: common.UIAbilityContext,
path: string,
watermarkText: string
): OpenFileRequest {
const req = new OpenFileRequest(ctx, path);
req.enableEdit = true;
req.enableLocalization = false;
req.wpsTransferType = TransferType.URI;
const wm = new WaterMark();
wm.Enable = true;
wm.WaterMaskText = watermarkText;
wm.Angle = -30;
wm.FontColor = '#19000000';
wm.FontSize = 16;
req.wpsWaterMarkParams = wm;
const opt = new OpenFileExtraOptions();
opt.enableShare = false;
opt.enableCloud = false;
opt.enablePrint = false;
opt.enableExport = false;
opt.enableScreenShot = false;
opt.enableOpenWithExternalApp = false;
req.extraOptions = opt;
return req;
}
export async function openWithOfficePolicy(
ctx: common.UIAbilityContext,
sandboxPath: string,
watermarkText: string
) {
await ensureRegistered(ctx);
const req = buildPolicyRequest(ctx, sandboxPath, watermarkText);
const r = await WPSApi.sendRequest(req);
logResult('open', r);
assertOk(r, 'open');
return r;
}
enableEdit 默认只读,办公编辑必须显式 true。不落地模式下部分能力会被强制关闭,联调时以真机菜单为准;需要放开打印时先评估 enableLocalization = true,再调 extraOptions。联调建议分层叠加:可编辑 → 不落地 → 功能开关 → 水印 → 回传,避免一次堆满参数后 ERROR 无法归因。
修订场景可同请求配置:
typescript
import { Revision } from '@wps/wps_sdk';
const rev = new Revision();
rev.UserName = CURRENT_USER_NAME;
rev.EnterReviseMode = true;
rev.ShowRevisionPanel = true;
req.wpsRevisionParams = rev;
水印文案建议拼「工号|部门|密级」,审计截图时能直接对到人。FontColor 带透明度,避免压字看不清正文。
模块 3:关窗后拿 filePath
typescript
import fileuri from '@ohos.file.fileuri';
export function copyWpsUri(ctx: common.UIAbilityContext, wpsUri: string): string {
const f = fs.openSync(wpsUri, fs.OpenMode.READ_ONLY);
const dir = `${ctx.filesDir}/wps_callback`;
fs.mkdirSync(dir, true);
const dest = `${dir}/${Date.now()}.docx`;
fs.copyFileSync(f.fd, dest);
fs.closeSync(f);
return fileuri.getUriFromPath(dest);
}
export async function editAttachmentAndCollect(
ctx: common.UIAbilityContext,
pickerPath: string
): Promise<string | undefined> {
const sandbox = toSandbox(ctx, pickerPath);
const stamp = `${USER_ID}|${DEPT}|审批附件`;
const r = await openWithOfficePolicy(ctx, sandbox, stamp);
if (!r.data?.fileUri) {
return undefined;
}
return copyWpsUri(ctx, r.data.fileUri);
}
切回应用 ≠ 关窗。OK && !data 是中间态,不是失败。FD 模式可把 wpsTransferType 换成 TransferType.FD,读 transferFd 并校验 transferFileSize。上传应排队:网络成功后再改业务附件版本号,避免关窗瞬间 UI 先报成功。
页面侧拼装与联调
typescript
// EntryAbility.onCreate
await ensureRegistered(this.context);
async function onTapEdit() {
try {
if (!wpsReady) return;
const picked = await selectOneDocument();
const appUri = await editAttachmentAndCollect(hostContext, picked);
if (appUri) {
await uploadQueue.push(appUri);
}
} catch (e) {
promptAction.showToast({ message: String(e) });
}
}
清单:reject(未注册)、1013(凭据)、ERROR(路径)、只能预览(enableEdit)、外发菜单(不落地/extraOptions)、无 data(未关窗)、换 HAR 后 clean、多 flavor 校验 bundleName。三模块测通后,业务增量通常只在水印文案与上传接口。弱网或政企内网下建议单独回归 registerApp 超时;系统或 WPS 客户端升级后,再抽测一小一大两个附件的 URI 回传。单元测试可以对 assertOk 与 copyWpsUri 用 Mock Result,覆盖空 data、仅 URI、拷贝失败等分支,减少真机连点成本。
若后续要做断点续传,只需替换上传层;注册与策略打开模块可以保持不动,回归范围也会小很多。字段含义仍以官方对接文档为准,文档更新后对照 OpenFileRequest/ResultData 表扫一遍即可。
小结
稳定接入靠时序,不靠在页面里堆 if。把 注册 → 策略打开 → 关窗 copy → 上传 收成模块,审批附件编辑就能在鸿蒙端稳定复用。更多字段以官方对接文档为准。
基于 WPS Open SDK 鸿蒙版实践整理。
官方对接文档:365.kdocs.cn/l/clQl5cek2...
QQ 群:628436767