HarmonyOS WPS Open SDK 实践:从联调踩坑收敛到统一打开链路

0 阅读5分钟

刚接 @wps/wps_sdk 时,我把打开写在按钮回调里,注册写在 Ability 另一处,结果冷启动连点全是 reject;修完注册后又踩沙箱路径 ERROR;再修完发现编辑按钮其实一直只读。后来把链路收成「注册断言 → 沙箱拷贝 → 按模式打开 → 可选回传装饰」,联调才稳定。本文按工程视角写痛点与收敛代码,方便对照真机日志。

为什么痛点总在联调才爆

文档里的 API 并不长,麻烦在于语义分层:

  • rejectresult.code 不是同一通道:未注册走前者,鉴权失败常见 1013,路径问题常见 ERROR。
  • 拉起成功关窗回传 不是同一状态:没配 wpsTransferTypeOK && !data 合法。
  • 只读默认 很安静:忘写 enableEdit = true 时没有「大声失败」,只是菜单不对。

所以「一次写满水印 + 功能开关 + 回传」往往把四种失败叠在一起。建议分层:最小打开通了,再叠策略。

注册层:把 1013 与重复调用收掉

import { common } from '@kit.AbilityKit';
import {
  WPSApi,
  RegisterAppRequest,
  Result,
  ResultCode,
  SdkConstants,
} from '@wps/wps_sdk';

export let wpsReady = false;

export function assertOk(r: Result, stage: string): void {
  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 ?? 'path/params'}`);
  }
  if (r.code !== ResultCode.OK) {
    throw new Error(`${stage} code=${r.code}`);
  }
}

export async function ensureRegistered(ctx: common.UIAbilityContext): Promise<void> {
  if (wpsReady) return;
  const r = await WPSApi.sendRequest(
    new RegisterAppRequest(ctx, APP_KEY, APP_SECRET)
  );
  console.info(`[register] code=${r.code} msg=${r.msg ?? ''}`);
  assertOk(r, 'register');
  if (!SdkConstants.isPersonalSdk() && ACTIVATION_SN) {
    WPSApi.setWpsFileToken(ACTIVATION_SN);
  }
  wpsReady = true;
}

要点:wpsReady 短路避免重复注册;1013 单独抛,方便和路径 ERROR 区分;序列号用 setWpsFileToken 全局注入,不在每次 OpenFileRequest 上塞 wpsToken。凭据走 flavor rawfile 或 CI 注入,Release 不打 secret。换 HAR 后务必 clean,旧 so 残留会让错误码「看起来随机」。

打开层:沙箱 + enableEdit 一个入口

import fs from '@ohos.file.fs';
import { OpenFileRequest, WPSApi } from '@wps/wps_sdk';

export function copyToFilesDir(ctx: common.UIAbilityContext, src: string): string {
  const dir = `${ctx.filesDir}/wps_lite`;
  fs.mkdirSync(dir, true);
  const dest = `${dir}/${Date.now()}_${src.split('/').pop() ?? 'file.docx'}`;
  fs.copyFileSync(src, dest);
  return dest;
}

export async function openByMode(
  ctx: common.UIAbilityContext,
  picked: string,
  mode: 'preview' | 'edit'
) {
  await ensureRegistered(ctx);
  if (!wpsReady) return;
  const sandbox = copyToFilesDir(ctx, picked);
  const req = new OpenFileRequest(ctx, sandbox);
  req.enableEdit = mode === 'edit';
  const r = await WPSApi.sendRequest(req);
  console.info(`[${mode}] code=${r.code} msg=${r.msg ?? ''}`);
  assertOk(r, mode);
  return r;
}
modeenableEdit用途
previewfalse / 不设查看附件
edittrue修改附件

联调先测 preview 确认注册与路径,再测 edit。拷贝失败与打开失败拆 toast,避免同事一直怀疑 enableEdit。同一单据多次编辑用新时间戳文件名,减少覆盖中间态。

回传层:后置装饰,别和 assertOk 耦死

import { TransferType, WaterMark, OpenFileExtraOptions } from '@wps/wps_sdk';

export function decoratePolicy(req: OpenFileRequest, stamp?: string): void {
  req.wpsTransferType = TransferType.URI;
  if (stamp) {
    const wm = new WaterMark();
    wm.Enable = true;
    wm.WaterMarkText = stamp;
    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;
  req.extraOptions = opt;
}

export async function pickOpenAndMaybeTransfer(
  ctx: common.UIAbilityContext,
  mode: 'preview' | 'edit',
  withTransfer: boolean,
  stamp?: string
) {
  await ensureRegistered(ctx);
  const picked = await selectOneDocument();
  const sandbox = copyToFilesDir(ctx, picked);
  const req = new OpenFileRequest(ctx, sandbox);
  req.enableEdit = mode === 'edit';
  if (withTransfer) {
    decoratePolicy(req, stamp);
  }
  const r = await WPSApi.sendRequest(req);
  assertOk(r, 'pickOpen');
  // withTransfer 时:关窗后才可能有 r.data.fileUri;切回应用不等于关窗
  return r;
}

上传成功不要写进 assertOk。关窗回传的路径在 WPS 侧沙箱,业务要再 copy 到本应用目录。FD 模式用 transferFd 分块读并校验 transferFileSize。不落地模式下部分开关会被强制关闭,以真机菜单为准。

页面拼装与踩坑对照

// EntryAbility.onCreate
await ensureRegistered(this.context);

async function onOpen(mode: 'preview' | 'edit') {
  try {
    if (!wpsReady) return;
    await openByMode(hostCtx, pickerUri, mode);
  } catch (e) {
    promptAction.showToast({ message: String(e) });
  }
}
现象先查
reject未注册 / wpsReady
1013包名与凭据 / HAR
ERROR沙箱路径
只能看不能改enableEdit
以为没保存轻量未开回传时正常

清单:冷启动连点、多 flavor bundleName、换 HAR 后 clean、小 docx/xlsx 各测 preview/edit、弱网抽测注册超时。系统或 WPS 客户端升级后再跑一轮最小闭环。可为 assertOk 写 Mock 1013/ERROR/OK 单测,减少真机连点。

工程里我还会再抽一层「打开入口」,避免页面散落字面量路径与重复 try/catch。示意如下:把 DocumentPicker、拷贝、mode、可选 decorate 串起来,业务页只传 preview|edit 与水印文案。这样换 HAR、改 flavor 凭据时,回归范围集中在 wps/ 目录而不是几十个按钮回调。Debug 包可开详细日志看完整 msg;Release 只保留 code 与精简错误映射,避免 secret 与绝对路径进崩溃上报。

export async function pickAndOpen(
  ctx: common.UIAbilityContext,
  mode: 'preview' | 'edit',
  stamp?: string
) {
  await ensureRegistered(ctx);
  if (!wpsReady) return;
  const picked = await selectOneDocument();
  const sandbox = copyToFilesDir(ctx, picked);
  const req = new OpenFileRequest(ctx, sandbox);
  req.enableEdit = mode === 'edit';
  if (stamp) {
    decoratePolicy(req, stamp);
  }
  const r = await WPSApi.sendRequest(req);
  assertOk(r, 'pickAndOpen');
  return r;
}

需要回传时再在 decoratePolicy 里打开 TransferType.URI,并用单独的 copyCallback 落盘;不要把上传成功耦合进 assertOk。同一单据多次编辑请用新时间戳文件名。机型差异主要体现在生命周期动画速度,测试用例应明确「关闭窗口」触发回传。联调检查清单通常写成四条:冷启动后是否只调用一次 registerApp 且无 1013;选择器路径是否已进入 filesDir;预览与编辑两按钮是否共用入口且仅差 enableEdit;日志是否能按 requestType 过滤。四条都绿再开 wpsTransferType。以上叠完后,日常联调成本会明显下降,新人也不容易把注册逻辑又抄回页面。

小结

痛点收敛的本质是语义分层:注册挡住 reject 与 1013,沙箱挡住 ERROR,enableEdit 分清模式,回传配置解释空 data。入口收进 wps/ 模块后,业务页只负责触发与提示,后续加水印或上传也不会把鉴权摊回页面。字段以官方对接文档为准;文档更新后对照 OpenFileRequest / Result 扫一遍即可。把入口与断言收进模块后,业务页只负责触发与提示,后续加上传或水印也不会再把鉴权逻辑摊回页面。


基于 WPS Open SDK 鸿蒙版实践整理。
官方对接文档:365.kdocs.cn/l/clQl5cek2…
QQ 群:628436767