Appearance
工作区卡片
工作区卡片把小程序的概览、进度或常用操作放到 PC 的游戏工作区。用户可以先看卡片,再按需打开小程序页面。
客户端支持
SDK 提供卡片构建、页面刷新和本地调试入口。工作区卡片需要支持该能力的 Windows/macOS 客户端及相应平台权限;浏览器调试页不会渲染卡片,请在真实 PC 客户端验收。
Controller 运行在 PC 提供的受限执行环境,独立于小程序页面。项目只需从 @heybox/hb-sdk/card-controller 导入构建器,无需安装额外的视图依赖。
阅读导航
第一次接入按顺序阅读;已经接入但没有显示卡片,可以直接跳到排障。
工作区卡片如何运行
| 部分 | 你要做什么 | 运行在哪里 |
|---|---|---|
| 项目声明 | 在 package.json#heybox 指定绑定游戏、权限和 Controller 文件 | 开发与构建阶段 |
| Controller | 返回卡片内容,处理刷新和卡片按钮 | PC 的受限执行环境,独立于小程序页面 |
| 小程序页面 | 读取卡片打开上下文,必要时请求刷新卡片 | 普通小程序页面,使用根 SDK |
声明卡片、批准权限、发布版本是三个独立步骤。 构建成功或小程序页面能打开,都不代表卡片已经可以展示。
快速接入
以下示例适用于已经启用 miniappManifest() 的工坊小程序项目。先完成快速开始,再合并配置,保留项目原有的其他 heybox 字段。
1. 声明入口与绑定游戏
json
{
"heybox": {
"platforms": ["windows", "macos"],
"bindAppIds": ["730"],
"permissions": {
"workspaceCards": { "enabled": true }
},
"workspaceCards": "./src/workspace-cards.ts"
}
}730 是示例游戏编号,请换成项目实际绑定的游戏。workspaceCards 指向项目内的一个 TypeScript 文件,不是卡片数组或构建后的 Manifest 对象。
2. 创建 Controller 文件
创建 src/workspace-cards.ts:
ts
import {
defineCard,
defineWorkspaceCardsController,
region,
text,
viewDocument,
} from '@heybox/hb-sdk/card-controller'
export default defineWorkspaceCardsController({
cards: {
summary: defineCard({
title: '游戏助手',
sizes: ['1x1'],
defaultSize: '1x1',
documents: {
'1x1': viewDocument([region('main', 'ready', [text('欢迎使用游戏助手')])]),
},
}),
},
})这张卡的 ID 是 summary,内容固定,所以可以省略 refresh。Controller 只从 @heybox/hb-sdk/card-controller 导入;普通页面才从 @heybox/hb-sdk 导入 SDK。
3. 构建并检查产物
bash
hb-sdk build构建不需要登录或绑定远端小程序。成功后检查 dist/manifest.json 中生成的 workspaceCards,以及 dist/_hb/workspace-cards/controller.mjs。摘要和文件回执由构建器生成,请勿手填或修改构建后的文件。
接下来按调试与发布在真实 PC 验收,再提交版本。dist/manifest.json 是发布合同,不是供业务页面从 CDN 读取的文件。
编写卡片与 Controller
卡片定义与卡片内容
defineCard() 声明卡片标题和尺寸;documents 描述每个尺寸的内容。卡片内容使用 viewDocument() 和布局构建器,不使用 HTML、Vue 组件或 DOM。
| 字段 | 写法与含义 |
|---|---|
cards 的对象键 | 卡片 ID,例如 summary;必须为小写 kebab-case,最多 64 个字符 |
title | 用户看到的标题,使用固定字符串 |
sizes | 支持的尺寸,只能是 1x1、2x1、2x2;必须包含 1x1 |
defaultSize | 默认尺寸,必须在 sizes 中 |
documents | 固定内容,可省略;省略时必须提供 refresh 产生内容 |
loadingDocuments / errorDocuments | 可选的等待/错误内容;提供时需覆盖全部尺寸,不能包含动作或动态图片 |
声明两个尺寸时,要写两份完整文档;PC 不会把小尺寸文档自动拉伸成大尺寸文档。所有卡片都使用同一 Controller,不需要声明 static/dynamic mode。
刷新与按钮怎样协作
| 入口 | 用途 | 返回 |
|---|---|---|
refresh(ctx) | 首次加载、周期刷新或页面请求刷新时计算内容 | { cards: { 卡片ID: { documents } } } |
defineAction({ run }) | 用户点击卡片按钮时执行操作 | 同样返回卡片内容 |
defineOpenMiniProgramAction() | 打开当前小程序页面 | 不提供 run,由 PC 处理打开行为 |
每张返回的卡片必须包含其全部声明尺寸。可以只返回本次要更新的卡片,但不能返回未声明的卡片或空状态。整批结果先校验;有一张不合规,这一批内容不会部分提交。
示例:计数器卡片
在 package.json#heybox.permissions 中同时声明 workspaceCards 和 storage 的 enabled: true。然后把 Controller 文件替换为:
ts
import {
button,
defineAction,
defineCard,
defineWorkspaceCardsController,
region,
text,
viewDocument,
type WorkspaceCardControllerResult,
} from '@heybox/hb-sdk/card-controller'
function render(count: number): WorkspaceCardControllerResult {
return {
cards: {
counter: {
documents: {
'1x1': viewDocument([
region('main', 'ready', [
text(`计数:${count}`),
button('加一', { action: increment(null) }),
]),
]),
},
},
},
}
}
const increment = defineAction<null>({
async run(ctx) {
if (!ctx.storage) throw new Error('当前环境未开放存储')
const { data } = await ctx.storage.getStorage({ key: 'counter' })
const next = (typeof data === 'number' ? data : 0) + 1
await ctx.storage.setStorage({ key: 'counter', data: next })
return render(next)
},
})
export default defineWorkspaceCardsController({
cards: {
counter: defineCard({
title: '共享计数器',
sizes: ['1x1'],
defaultSize: '1x1',
}),
},
actions: { increment },
async refresh(ctx) {
const stored = await ctx.storage?.getStorage({ key: 'counter' })
return render(typeof stored?.data === 'number' ? stored.data : 0)
},
})actions 的对象键生成动作 ID。increment(null) 把该动作绑定到按钮,不会在生成文档时执行 run。需要输入参数时,用 actionInput 声明输入形状,再把对应数据传给 handle;不要自行拼接 commandId。
注意:以上计数更新是先读后写,不保证并发加一。已完成的存储写入不会因为后续内容校验失败而回滚。
选择布局构建器
| 内容 | 常用构建器 |
|---|---|
| 文本与状态 | text、badge、icon |
| 横向/纵向/网格布局 | hstack、vstack、zstack、grid |
| 分组与分隔 | region、group、divider、spacer |
| 列表与数据展示 | list、meter、radialProgress、barChart |
| 切换与输入 | tabs、carousel、expandable、button、textField、toggle |
构建器与 PC 的 ViewDocument 合同共用,但卡片只允许受限子集。公开入口里有某个 builder,不代表工作区卡片允许使用它:当前禁止 renderer 和 frame;图片能力尚未接入,不要直接填远程图片 URL 或依赖 ctx.images。
精确参数与类型见 Card Controller API。Tabs、展开等本地交互由视图处理;需要执行业务操作时才绑定受管动作。
Controller 能读取哪些能力
| 上下文 | 说明 |
|---|---|
ctx.game.appId | 当前游戏工作区的编号 |
ctx.environment | 当前平台和语言 |
ctx.trigger | 触发原因与本次执行 ID |
ctx.operationId | 本次操作标识;有副作用的后端业务应据此或自身业务键做幂等 |
ctx.storage | 可选的设备存储,受 storage 声明约束 |
ctx.network | 可选的受控网络,受权限与 networkOrigins 共同约束 |
Controller 与同一小程序页面共享设备存储,按小程序隔离,不按登录账号隔离。只存设备偏好等非账号数据;不要存凭据、账号资料或账号派生内容。它不提供事务或 CAS。
使用网络必须同时满足:声明并获准 network、在 Controller 根对象中声明精确 HTTPS networkOrigins、当前 PC 提供 ctx.network。origin 不带路径,例如 https://api.example.com,不是 https://api.example.com/data。refresh 只允许 GET,动作中的网络合同允许 GET/POST;实际开放仍以 PC 权限策略为准。当前不支持 Controller 访问小黑盒官方域业务接口。
这些写法会被构建拒绝
- Controller 只能从
@heybox/hb-sdk/card-controller具名导入,不导入根 SDK、本地业务模块、Node、Electron 或其他依赖。 cards、actions、标题、尺寸、默认尺寸、networkOrigins和输入 schema 必须静态可读;不使用 spread、计算属性或运行时配置生成元数据。- 辅助逻辑放在同文件的
function中。顶层普通const只允许原始字面量;动作 handle 可使用const action = defineAction(...)。不要用顶层const documents = (...) => ...。 - 不使用
fetch、DOM、timer、动态 import、eval、Function或 WASM。异步能力只通过ctx提供的端口使用。 - 不在返回内容中提供刷新周期、cron 或
nextRefresh;刷新调度由 PC 负责。
| 限制 | 当前上限 |
|---|---|
| 卡片 | 每个小程序 3 张 |
| 动作 | 16 个 |
| 网络 origin | 8 个 |
| Controller 单文件 | 1 MiB |
| 每张返回的卡片状态 | 64 KiB |
卡片与小程序页面协作
普通前台页面使用根 SDK @heybox/hb-sdk,与 Controller 的作者入口分开。
从卡片打开小程序
Controller 使用 defineOpenMiniProgramAction() 生成打开动作,将 handle 放入 actions,再绑定到按钮。它只能打开当前小程序,不执行自定义 URL 或任意客户端命令。
例如,下面的 Controller 会展示“打开工具”按钮,并向页面传入 { section: 'overview' }:
ts
import {
actionInput,
button,
defineCard,
defineOpenMiniProgramAction,
defineWorkspaceCardsController,
region,
viewDocument,
} from '@heybox/hb-sdk/card-controller'
const openPage = defineOpenMiniProgramAction<{ section: string }>({
input: actionInput.object({ section: actionInput.string({ maxLength: 32 }) }),
})
export default defineWorkspaceCardsController({
cards: {
summary: defineCard({
title: '游戏助手',
sizes: ['1x1'],
defaultSize: '1x1',
documents: {
'1x1': viewDocument([
region('main', 'ready', [
button('打开工具', { action: openPage({ section: 'overview' }) }),
]),
]),
},
}),
},
actions: { 'open-page': openPage },
})页面通过 workspaceCards.getLaunchContext() 读取最近一次卡片启动信息:
| 字段 | 含义 |
|---|---|
source | 固定为 workspace-card |
launchId | 本次启动的唯一标识,用于去重 |
gameAppId | 触发打开的游戏编号 |
cardId / actionId | 触发打开的卡片与动作 |
params | 动作携带的可选 JSON 参数 |
launchedAt | 启动时间,毫秒时间戳 |
普通打开页面时上下文为空。页面未关闭时,下一次卡片点击仍可能带来新的启动上下文,所以需要订阅变化,不能只在首次加载读取一次。
ts
import { workspaceCards } from '@heybox/hb-sdk'
let lastLaunchId: string | undefined
const off = workspaceCards.onLaunchContextChange(context => {
if (!context || context.launchId === lastLaunchId) return
lastLaunchId = context.launchId
// 在这里切换到该游戏对应的业务内容。
console.log(context.gameAppId, context.cardId, context.params)
})
// 页面或组件销毁时调用 off();普通页面可在 pagehide 时清理。
window.addEventListener('pagehide', off, { once: true })订阅会立即回放当前值,不需要先额外调用 getLaunchContext()。账号变化、普通入口启动或会话结束后,上下文会清空。不要把这份来源信息当成用户身份或业务授权证明。
页面修改数据后请求刷新
ts
import { workspaceCards } from '@heybox/hb-sdk'
// 在业务数据保存成功后调用;cardIds 使用 Controller 声明的 ID。
const { scheduled } = await workspaceCards.requestRefresh({
gameAppId: '730',
cardIds: ['summary'],
})
console.log(`已调度 ${scheduled} 个活动实例`)省略参数时请求刷新当前小程序的活动实例。它不会创建游戏工作区、添加卡片或打开已关闭的实例。
scheduled 是进入调度的实例数,不是卡片张数,也不代表刷新完成。0 表示没有匹配的活动实例。需要查看最终效果时,应回到 PC 游戏工作区验收;不通过循环调用强迫立即刷新。
刷新合并、周期和失败退避由 PC 管理。卡片按钮的点击处理与视图内的 Tabs/展开交互不需要等待周期刷新。
当前 PC 默认每 30 分钟刷新一次内容,页面请求在 1 秒最小间隔内合并。已发布卡片的版本和权限每 30 秒复查,执行资格最多保留 60 秒;到期、无法续验、下架或撤权后立即停止 Controller 执行,清除旧文档和动作,并显示无内容、无动作的不可用占位。资格复查与内容刷新各自调度,固定内容卡可以省略 refresh。
Kit 1.19 起,稳定卡位由 Host 管理。同一逻辑卡片使用同一模板时复用原卡位;模板变化后,旧卡位仍由 Host 或用户管理,Extension 不会自动物理删除卡位。卡位保留不延长执行资格,也不会保留旧内容或旧动作。
兼容与错误处理
不支持该能力的 Host 可能使 requestRefresh() 返回 CAPABILITY_UNAVAILABLE。按错误处理捕获 HbMiniProgramSDKError,让普通页面继续可用。
详细签名见 workspaceCards API。
调试与发布
构建检查、本地 PC 调试与已发布版本展示是不同阶段,要分别验收。
本地调试
bash
hb-sdk dev- 确认项目声明了 Windows/macOS 中至少一个平台及目标游戏。
- 启动前完成 CLI 登录,并创建或绑定一个可管理的小程序;
hb-sdk dev校验通过后才会启动调试页。 - 从调试页的桌面端入口,在支持工作区卡片的 PC 测试客户端打开当前项目。
- 打开绑定游戏的工作区,检查卡片内容、每个尺寸和按钮行为。
- 修改 Controller 后检查新内容是否生效;退出本地调试后再验收已发布版本。
浏览器调试页只提供登录、绑定、启动与状态控制,不会在浏览器里展示或执行卡片。未登录或未绑定时,hb-sdk dev 会在启动前退出并给出修复指引;服务启动后的卡片构建失败只影响卡片连接,普通页面调试仍可继续。
当前本地卡片调试从项目声明与本地构建产物建立连接,不需要调用独立的后端“注册卡片调试会话”接口。CLI 与 PC 应使用相互匹配的开发版本;旧启动入口只能打开页面时,不能据此判断卡片已经接入。
本地调试权限不会修改线上平台批准状态,也不会临时批准 workspaceCards。卡片权限与网络、存储权限分别判断;正式验证时,以真实 PC 的能力和有效权限结果为准。
发布后展示
按CLI 与发布流程完成登录、绑定、构建、上传与提审:
bash
hb-sdk login
hb-sdk remote info
hb-sdk build
hb-sdk remote deploy --release-note "增加游戏工作区卡片"remote deploy 会运行项目的 scripts.build,前面的 hb-sdk build 用于先做离线自检。审核通过后按实际流程使用自动发布,或执行:
bash
hb-sdk remote release <version>展示卡片还需同时满足以下条件,不能只检查“审核通过”:
| 条件 | 如何核对 |
|---|---|
| 已发布版本包含卡片 | 看实际发布版本的 Manifest,而不是本地源码或待审版本 |
| 已批准工作区卡片权限 | 平台有效权限中的 workspaceCards 为 enabled;项目声明不能代替批准 |
| 游戏匹配 | 当前 PC 游戏属于发布版本的 bindAppIds |
| 平台匹配 | 发布版本支持当前 Windows/macOS 平台 |
| 构建产物匹配 | 使用本次构建上传的 Controller,不混用旧文件或手改生成的摘要 |
| PC 可以承载卡片 | 客户端支持该能力,工作区仍有可用容量 |
同一小程序在同一游戏工作区中,本地调试内容优先于已发布内容;结束本地调试后才回到已发布版本。定位线上显示问题前,先确认是否仍有本地调试连接。
常见问题与排障
| 现象 | 优先检查 | 下一步 |
|---|---|---|
hb-sdk build 失败 | Controller 文件路径、静态元数据、尺寸覆盖、受限 API | 按构建错误修正后重新构建,不跳过校验 |
| 浏览器能开页面,但没有卡片预览 | 浏览器调试页不渲染卡片 | 到真实 PC 的绑定游戏工作区验收 |
| PC 能开页面,但没有本地卡片 | 登录/绑定、桌面平台声明、卡片构建状态、CLI 与 PC 版本 | 检查调试页状态,再通过桌面入口打开 |
| 已发布小程序出现在游戏列表,但没有卡片 | 已发布版本的卡片配置和有效 workspaceCards 权限 | 平台权限启用后重新获取;仍缺失时交给服务端排查发布快照 |
| 游戏列表里也没有小程序 | 游戏绑定、平台、版本发布状态与工坊资格 | 核对当前发布版本及实际游戏编号 |
| 某张卡片或某个尺寸更新失败 | 返回卡片 ID、每个尺寸的文档、状态字节大小 | 一批内容校验失败不会部分提交;先用最小文档复现 |
| 按钮没有执行 | 动作在 actions 中注册、handle 参数与 schema、PC 开放状态 | 不自行拼接命令,核对实际错误 |
| 网络/存储端口不存在 | 声明、平台批准及 PC 能力 | 显式处理 ctx.network / ctx.storage 缺席 |
页面请求刷新返回 scheduled: 0 | 当前小程序是否有匹配的活动卡片实例 | 打开对应游戏工作区,核对游戏与卡片 ID |
| 修改代码但仍看到旧内容 | 发布版本是否变化,是否混用了本地与发布内容 | 分别检查本地重建状态和已发布版本 |
向维护者反馈时提供:小程序 ID、已发布版本号、实际游戏编号、PC 平台与版本、调试或发布场景、时间和错误码。不要附带登录凭据或完整请求 Cookie。
服务端可能在列表中保留小程序入口,同时省略无法通过权限或快照校验的卡片配置;接口返回成功不等于该小程序的卡片获准展示。
相关文档
- Card Controller API:构建器、Controller 和动作的精确签名。
- workspaceCards API:前台启动上下文与刷新接口。
- 权限声明、CLI 与本地 mock 调试、错误处理。
完成检查
- 项目声明了 Controller 文件、卡片权限、桌面平台和绑定游戏。
- 每张卡都支持
1x1,所有声明尺寸都有完整文档,按钮使用已声明动作。 - 构建通过,并在真实 PC 分别验收本地调试与已发布版本。
- 启动上下文按
launchId去重,订阅在页面或组件销毁时清理。 - 权限禁用、可选端口缺席、无活动实例、网络离线与不支持能力的情况均有处理。