鸿蒙 WPS Open SDK:用 Facade 归一 Result 错误码

5 阅读5分钟

做 HarmonyOS 文档二开时,页面里直接写 if (result.code === 0) 很快会失控:注册失败、打开参数错误、关窗回传失败都挤在同一分支,Toast 文案也难以维护。更稳妥的做法是在 SDK 边界加一层 Facade / Adapter,把 @wps/wps_sdkResult 与抛异常,转成业务侧可 switch 的领域错误。本文给出一套可复用的归一化思路与 TypeScript 示例,事实对齐官方对接实践,代码按工程封装习惯重写。

为什么需要归一化

WPSApi.sendRequest 在应用已注册的前提下,多数失败仍会 resolve Result,只有「尚未注册成功」等场景会 throw。若 UI 层只写 try/catch,会漏掉带 code 的失败;若只看 code,又会漏掉 throw。Facade 的职责就是:对外只暴露「成功数据」或「领域错误枚举」,把 requestType / code / msg / data 的细节留在适配层。

调用链仍建议保持清晰:registerAppOK →(按形态)可选 setWpsFileToken → 选择器文件进沙箱 → OpenFileRequestsendRequest → 解析关闭回传。Facade 不改变时序,只改变错误出口形状。

领域错误模型

下面用一组字符串联合类型表示领域错误,便于埋点与 UI 映射:

领域码来源特征UI / 运维建议
AUTH_FAILUREcode === 1013 / ERROR_CODE_AUTH_FAILURE引导检查凭据与包名,阻断后续打开
NOT_REGISTEREDsendRequest throw 或本地门闩未就绪提示稍后重试,触发重新注册
PARAM_OR_OPENResultCode.ERROR-2查路径、参数、是否默认只读被误判
TRANSFER_BIZ关闭回传阶段正整数 code按回传失败处理,保留原始 code
UNKNOWN其它非 OK上报 code+msg,避免静默

成功时把 Result.data(如有)交给业务;失败时绝不把原始 Result 直接丢进页面组件。

Facade 实现草图

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

type WpsDomainError =
  | { kind: 'AUTH_FAILURE'; msg?: string }
  | { kind: 'NOT_REGISTERED'; msg?: string }
  | { kind: 'PARAM_OR_OPEN'; code: number; msg?: string }
  | { kind: 'TRANSFER_BIZ'; code: number; msg?: string }
  | { kind: 'UNKNOWN'; code?: number; msg?: string };

type WpsOutcome =
  | { ok: true; data?: ResultData }
  | { ok: false; error: WpsDomainError };

let ready = false;

export function mapResult(stage: 'register' | 'open' | 'transfer', r: Result): WpsOutcome {
  if (r.code === ResultCode.OK) {
    return { ok: true, data: r.data };
  }
  if (r.code === ResultCode.ERROR_CODE_AUTH_FAILURE) {
    return { ok: false, error: { kind: 'AUTH_FAILURE', msg: r.msg } };
  }
  if (r.code === ResultCode.ERROR) {
    return { ok: false, error: { kind: 'PARAM_OR_OPEN', code: -2, msg: r.msg } };
  }
  if (stage === 'transfer' && typeof r.code === 'number' && r.code > 0) {
    return { ok: false, error: { kind: 'TRANSFER_BIZ', code: r.code, msg: r.msg } };
  }
  return { ok: false, error: { kind: 'UNKNOWN', code: r.code, msg: r.msg } };
}

export function registerFacade(activationSn?: string): void {
  WPSApi.registerApp(APP_KEY, APP_SECRET, {
    onCallback: (result: Result) => {
      const out = mapResult('register', result);
      if (!out.ok) {
        ready = false;
        console.error('register domain', out.error);
        return;
      }
      // 非个人形态 HAR 时,可在 OK 后注入激活序列号
      if (activationSn && !SdkConstants.isPersonalSdk()) {
        WPSApi.setWpsFileToken(activationSn);
      }
      ready = true;
    }
  });
}

说明:mapResultstage 解释正整数,避免把回传业务码误判成打开参数错误。注册回调里单独处理 AUTH_FAILURESdkConstants.isPersonalSdk() 用于决定是否注入序列号,页面层无需感知 HAR 形态细节。

打开与回传走同一出口

export async function openDocFacade(
  context: common.UIAbilityContext,
  sandboxPath: string,
  opts: { editable?: boolean; transfer?: boolean } = {}
): Promise<WpsOutcome> {
  if (!ready) {
    return { ok: false, error: { kind: 'NOT_REGISTERED', msg: 'gate closed' } };
  }
  const request = new OpenFileRequest(context, sandboxPath);
  request.enableEdit = opts.editable === true; // 默认只读
  if (opts.transfer) {
    request.enableTransferFile = true;
  }
  try {
    const result = await WPSApi.sendRequest(request);
    const stage = opts.transfer ? 'transfer' : 'open';
    const out = mapResult(stage, result);
    if (!out.ok) {
      console.error('open/transfer domain', out.error, result.requestType);
    }
    return out;
  } catch (e) {
    return {
      ok: false,
      error: { kind: 'NOT_REGISTERED', msg: (e as Error).message },
    };
  }
}

说明:未就绪时直接返回领域错误,避免无意义 throw。enableEdit 显式默认只读,与对接语义一致。开启回传时用 stage='transfer' 解释正整数;未开启时按 open 阶段映射 -2 等。

页面侧只消费 WpsOutcome

async function onOpenClicked(ctx: common.UIAbilityContext, path: string) {
  const out = await openDocFacade(ctx, path, { editable: true, transfer: true });
  if (out.ok) {
    // 回传成功时再读 out.data?.fileUri / transferFd,并拷贝到沙箱
    return;
  }
  switch (out.error.kind) {
    case 'AUTH_FAILURE':
      // 引导检查 appKey/appSecret 与 bundleName
      break;
    case 'NOT_REGISTERED':
      registerFacade();
      break;
    case 'PARAM_OR_OPEN':
      // 查沙箱路径、参数组合
      break;
    case 'TRANSFER_BIZ':
      // 上报业务 code,提示回传失败
      break;
    default:
      break;
  }
}

与原始错误码表的对应关系

对接侧常量不变,Facade 只做翻译:

SDK code常量Facade kind
0OK成功分支
-1NONE通常视为未就绪 / UNKNOWN
-2ERRORPARAM_OR_OPEN
1013ERROR_CODE_AUTH_FAILUREAUTH_FAILURE
其它正整数(回传)TRANSFER_BIZ
throwNOT_REGISTERED

Result.msg 建议原样进入日志,UI 文案用领域码映射中文,避免把底层英文/内部文案直接甩给用户。Result.data 仅在 ok: true 时下发,防止失败态误读空 fileUri

工程实践要点

  • 注册必须先于打开;Facade 用 ready 门闩双保险。
  • 选择器 URI 先拷贝进 filesDir 再打开,减少 -2
  • 默认只读;可编辑场景显式 enableEdit = true
  • 监控按 kind 聚合,再下钻原始 code
  • Release 不打印 secret;msg 注意脱敏。

再补几条容易在评审里被问到的边界:

ResultCode.NONE-1:表示默认/未赋值,不要当成功,也不要直接映射成 PARAM_OR_OPEN。若回调尚未触发就读到 -1,说明时序仍停在注册前,应显示加载态而不是错误 Toast。

成功但无文件:未开启关闭回传时,code === OKdata 为空是预期行为。Facade 的 ok: true 仍成立,页面若强依赖 fileUri,应在调用参数里要求 transfer: true,并在成功分支校验 data

单元测试怎么切:为 mapResult 写表驱动测试即可覆盖 0 / -2 / 1013 / 正整数 / 缺省 codeopenDocFacadeready=falsecatch 两条 NOT_REGISTERED 路径各测一次。不要在单测里真拉起 WPS;把 WPSApi.sendRequest 换成可注入的函数类型即可。

埋点维度kind 作告警主键,stage 作下钻维度,原始 code 作标签。这样回传业务码上涨时,不会误触发「凭据大面积失败」的值班规则。UI 文案按 kind 配置中文,避免把底层 msg 原文甩给终端用户。

与页面状态机配合:建议页面只有 Idle / Registering / Ready / Opening / Transferring / Failed(kind) 有限状态。Facade 返回后只做状态迁移,不在按钮回调里再写一套 if (code === …)。冷启动在 Ability 阶段调用 registerFacade,首页打开按钮绑定 Ready 门闩,可减少竞态。

小结

在鸿蒙 WPS 二开里,错误处理的质量往往决定联调效率。把 Result 与 throw 收进 Facade,页面只面对有限领域错误,1013、-2、回传正整数就不会再挤在同一个 else。上述适配层可直接嵌进现有 TypeScript 工程:先落地 mapResult 与门闩,再逐步把散落的 result.code === 0 替换掉。当你需要扩展水印、修订或不落地等能力时,继续把失败收口到同一套 WpsDomainError,比在每个页面复制错误码表更稳。


更多参数见官方对接文档:365.kdocs.cn/l/clQl5cek2…
技术交流 QQ 群:628436767