Skip to content

受管桌面程序 ​

真实 PC 的本地临时 Companion 还要求 Host 支持私有 local-dev lease。该 lease 由 Host 记录进程 provenance 并负责失联后的自清理;旧客户端缺少该能力时安全拒绝临时 Companion,Browser Fake 和普通 network 调试仍可用。

companion 用于准备和启动随当前小程序审核版本发布的桌面辅助应用。V1 仅支持新 PC Host 的 windows-x64 与 macos-arm64;老 PC、Mobile、Web 和 Linux 不在支持范围内。

声明与构建 ​

项目必须在 package.json 显式声明高风险权限:

json
{
  "heybox": {
    "permissions": {
      "companion": { "enabled": true }
    },
    "companions": {
      "runner": {
        "displayName": "桌面辅助程序",
        "capabilities": ["stdio"],
        "targets": {
          "windows-x64": {
            "source": "companion/runner-windows-x64",
            "entrypoint": { "kind": "executable", "path": "runner.exe" },
            "elevation": "none",
            "args": []
          }
        }
      }
    }
  }
}

归档统一通过 package.json#heybox.companions 配置,source 相对该 package.json 所在目录解析。 目标平台写在 package.json#heybox.platforms;Vite 中只注册无参的 miniappManifest(),不要在 Vite 配置中复制 Companion 声明或平台声明。 构建和本地调试读取同一份声明。构建会检查 ZIP 结构和固定入口,复制到 dist/companions/<alias>/<target>.zip,并生成大小与 SHA-256;Web 文件和 Companion 文件进入同一上传 session 与同一审核版本。页面不能读取部署后的 manifest.json,也不能取得真实 CDN URL 或本机安装路径。

dist/companions/ 下全部归档的总大小上限为 500 MiB(500 × 1024 × 1024 bytes);该上限针对归档总量, 不是单个文件。超限时 hb-sdk build 以“Companion 归档总大小超过 500MiB 上限,请缩减归档后重新 build” 失败,hb-sdk remote deploy 以“Companion 归档总大小超过 500MiB 上限,请缩减归档后重新 deploy”失败, 需要先缩减桌面辅助应用产物再重新构建或部署。

hb-sdk dev 的真实 PC 联调可以直接使用这里声明的本地 ZIP,不要求先上传版本或获得线上平台批准。 项目必须已登录、绑定且声明权限及目标平台;每次打开会话需确认临时权限,准备与启动的可信手势仍分别保留。 dev server 只向 loopback 提供固定 Manifest/ZIP 路由,CLI 用短期票据绑定当前 dev 进程的 canonical snapshot。ZIP 或配置变化会校验新快照并重建 Runtime,临时进程不会热换二进制; 新产物必须重新准备并启动。校验失败时调试台显示阻塞原因,不会悄悄使用旧文件或线上产物。 单次开发服务最多保留 32 份快照、累计 ZIP 大小 1 GiB;达到上限后停止接纳新快照, 请先结束调试 Session,再重启开发服务,已有快照不会被自动逐出。 未配置本地 Companion 时使用 Browser Fake 做状态联调,真实 PC 不会回退到线上 candidate。

构建与 PC 安装都会检查真实二进制格式:Windows 为 AMD64 PE,macOS 为包含 arm64 的 Mach-O; 脚本即使改名或有执行权限也会被拒绝。授权弹窗展示产物来源、签名状态、目标平台、人类可读归档 大小、精确字节数与完整 SHA-256;本地 ZIP 明确标记“本地开发产物,未经过版本审核”。SHA-256 只用于 Host 可信授权展示,不进入小程序可读取的 CompanionInfo。 .app 从 XML 或 binary Info.plist 的 CFBundleExecutable 精确选择主入口。

Manifest 只接受固定入口和固定 args。V1 仅支持普通权限启动,elevation 只能省略或设为 none; required 会在构建时明确失败,管理员权限启动留待后续版本。公开 API 不接受 executable path、动态 args、 cwd、environment、shell 或任意命令;不要把 files.sandbox 中的普通文件当作可执行对象。 displayName 最多 120 个 Unicode code point;args 最多 64 项,单项最多 1024 UTF-8 bytes、总计最多 16384 UTF-8 bytes,并拒绝 NUL、CR 和 LF。

准备与启动 ​

ts
import { companion } from '@heybox/hb-sdk'

// 分别绑定到「准备」与「启动」按钮,不能在一个点击中串联。
function prepareFromUserAction() {
  return companion.prepare('runner', {
    onProgress(progress) {
      renderProgress(progress)
    },
  })
}

function launchFromUserAction() {
  return companion.launch('runner')
}

prepare() 和 launch() 必须分别由新的可信用户手势触发。每次真正开始新的准备操作前,Runtime 展示一次 Host 可信授权;已 ready 或加入同一在途准备时不会重复授权。每次真正创建新进程前,launch() 再展示独立 授权,不能复用 prepare 的手势或授权。launch() 不会隐式调用 prepare(),未准备时稳定失败。

准备操作属于 Host:页面 reload、最小化或进入后台不会取消。新页面可用 getPreparationStatus() 查询状态,并通过相同 alias 加入在途操作;显式取消使用 cancelPreparation(alias) 或 AbortSignal。页面 reload 只断开观察者,不会向 Host 发送取消;原子提交已经开始时,最终提交结果优先于取消意图。

prepare() 与 getInfo() 返回的 CompanionInfo.source 标明 Host 确认的产物来源:local-dev 是本地开发产物,preview 是审核前预览产物,released 是已发布版本产物;旧 Host 缺省时 source 为 undefined,表示来源未知。不要用 alias、displayName 或文件路径自行推断来源,也不要把 preview 产物当作已发布版本;当前 PC Host 不支持审核前预览时以 COMPANION_PREVIEW_UNSUPPORTED 失败。

准备状态是可恢复的 Host 状态,不等于当前页面 Promise 的状态:

getPreparationStatus() 状态含义推荐动作
not-prepared当前版本产物尚未准备等待新的可信用户操作调用 prepare()
preparingHost 正在下载、校验或解压展示 progress;需要继续观察时再次调用 prepare() 加入同一操作
ready产物已原子提交,可独立触发 launch()展示可启动状态;preparedAt 是 Host 提供的时间戳
failed最近一次准备失败根据 error.retryable 和 error.code 决定重试或停止

AbortSignal 取消的是当前调用并向 Host 发出取消意图,不是页面本地的同步回滚。若原子提交已开始,当前 Promise 可能因取消而失败,但稍后查询仍可能得到 ready;取消后需要用 getPreparationStatus() 对账,不要直接把 UI 固定为 not-prepared。

Session 与 stdio ​

同一小程序 identity 与 alias 同时最多有一个活动 Session。getActiveSession(alias) 会取得当前活动 Session 的控制权;新 Runtime generation 接管后,旧 controller 的写入、关闭 stdin、终止、附件和通知操作都会失败。 进程退出后 getActiveSession() 返回 undefined,业务结果应由小程序自行持久化。

CompanionSession.state 只有 running 和 exited。onExit() 在 Session 已退出后注册时会立即收到保存的退出结果, waitForExit() 对同一 Session 始终等待同一次退出;组件销毁时仍应调用所有 on*() 返回的取消订阅函数。 失去 controller 不等于进程退出,也不会让 waitForExit() 提前完成。收到 onOwnershipLost() 后应停止写入;确有继续 控制的需要时重新调用 getActiveSession(alias),不要对旧 controller 操作做无界重试。

stdio 仅在 Manifest 声明 stdio 时存在,输入输出都是原始 Uint8Array。SDK 不做 UTF-8、换行、NDJSON、 RPC 或业务状态解析。输出采用 at-least-once 投递,收到 chunk 后 SDK 会确认 sequence;断连与重取控制权可能 导致重复,因此业务 framing 必须携带可去重标识,并能处理拆包、粘包和重放。

ts
const session = await companion.getActiveSession('runner')
if (session?.stdio) {
  const stopOutput = session.stdio.onStdout(bytes => decodeBusinessFrame(bytes))
  const stopOwnership = session.onOwnershipLost(() => setControlsEnabled(false))
  await session.stdio.write(encodeBusinessFrame({ type: 'status' }))
  // 组件销毁时调用 stopOutput() 和 stopOwnership();业务退出协议可先写入,再 closeStdin()。
}

平台不提供语义跨平台不稳定的 request-close。业务可先通过自身 stdio 协议请求正常退出,或关闭 stdin; terminate() 是强制结束完整受管进程树。页面隐藏或 reload 不结束 Session,用户真正关闭小程序窗口时由 Host 处理完整进程树生命周期。

可选能力 ​

  • attachments:session.attachments.open(id) 返回只读 FileHandle;opaque id 来自桌面程序自身业务协议,页面无法指定物理路径。
  • notifications:页面解析业务协议后,可请求 Host 展示固定类别通知;页面完全断开时不承诺业务通知。
  • stdio:仅提供原始字节双向流,不提供文本编码或 RPC。

只有 Manifest 声明且 Host 实现的能力才出现在 Session。不要根据操作系统或属性存在性猜测支持情况。

调试与验收 ​

调试台不展示浏览器、手机与 PC Companion 的汇总就绪状态、本地 alias、平台或产物摘要;真实入口不可用时只在对应入口内说明原因。 真实本地调试持续验证 CLI 会话在线;连续失联约 30 秒、票据到期或快照变化会销毁旧 Runtime,关闭临时 stdio 并终止临时进程树。恢复后重新确认、准备与启动。调试台持续提示本地可用不代表线上批准。companion 等需要平台批准的权限当前均未开放,开发者申请入口将在近期开放;入口开放并取得批准前,线上与预览不可用。

Browser Dev Host 只提供确定性的 Fake 场景,用于开发准备进度、Session 状态、stdio framing、错误 UI 与恢复逻辑。 Fake 不选择、不下载、不解压、不启动本机程序,也不经过操作系统安全检查或真实进程组。 因此 Browser Fake、Mobile 或 Web 的结果不能作为桌面发布证据;发布前必须分别在目标 Windows/macOS 新 PC Host 完成归档摘要、准备取消、启动、stdio、退出与进程树清理验收。

失败处理 ​

  • USER_GESTURE_REQUIRED:当前 prepare 或 launch 缺少新的可信用户手势。
  • USER_CANCELLED:用户拒绝或关闭本次 Host 授权,应正常结束流程。
  • COMPANION_NOT_PREPARED:先由新的用户操作显式调用 prepare()。
  • COMPANION_INTEGRITY_FAILED / COMPANION_ARCHIVE_INVALID:停止重试并提交新审核版本,不能绕过摘要或系统安全检查。
  • COMPANION_SESSION_OWNERSHIP_LOST:当前 Runtime 已失去 controller;需要时重新调用 getActiveSession()。
  • COMPANION_DESCRIPTOR_UNAVAILABLE:授权或描述符暂时不可用,可在有界重试策略下重试。
  • COMPANION_ELEVATION_DENIED:为未来管理员权限启动保留的兼容错误码;V1 构建不接受 elevation: 'required'。
  • METHOD_FORBIDDEN / COMPANION_UNSUPPORTED:当前 Host 或平台未实现,不要循环重试或用任意执行替代。
  • COMPANION_PREVIEW_UNSUPPORTED:当前 PC Host 不支持审核前预览 Companion,需要升级客户端后重试。

完整类型与方法见 API Reference。

完成检查 ​

  • 权限、目标 ZIP、固定入口和固定参数都随同一版本提交审核。
  • prepare 与 launch 使用两次独立可信用户操作和 Host 授权。
  • stdio 业务协议处理了原始字节 framing、at-least-once 去重和断连恢复。
  • 未向页面暴露或传入 URL、物理路径、动态 args、环境变量、shell 或 PID。
  • Browser Fake 只用于开发,目标平台均已完成新 PC 真机验收。