PKU harness

生成式界面

助手用 JSON 描述一段界面,App 用原生组件把它显示出来,可以显示在对话里(`ui.render`),也可以显示在看板上。表单提交的内容会作为你的下一条消息发回给助手。

有些回答用文字表达并不方便。例如,助手需要你填写一个金额,或者需要你从列表里选一项,或者手上的数据更适合用卡片展示。这时,助手可以直接回复一段界面。

具体做法是:助手写出一份 JSON 格式的界面描述,我们叫它 spec。App 从一套固定的组件目录(catalog)里取出对应的原生组件,把这段界面显示出来。这套机制基于 json-render。

组件的属性都是语义化的,例如色调(tone)和尺寸(size),不能直接写颜色值。因此,助手搭出来的界面和 App 其他部分的风格一致。助手搭建看板小组件时用的也是这套组件目录,所以它只需要学会一种界面描述方式。

spec 的结构

// specSchema in @pku-harness/sdk (genui.ts)
type UISpec = {
  root: string;                                  // 根元素的 id
  elements: Record<string, {
    type: string;                                // 目录中的组件名
    props: Record<string, unknown>;              // 列出的属性都要给出,不用时为 null
    children?: string[];                         // 子元素 id
    on?: Record<string, unknown>;                // 事件,例如 { press: { action, params } }
    visible?: unknown;
  }>;
  state?: Record<string, unknown>;               // 初始状态,例如 { form: { amount: "50" } }
};

spec 里可以保存一份状态(state)。组件的属性可以绑定到状态上,有三种绑定方式。

绑定含义
{ "$state": "/card/balance" }从状态中读取一个值(JSON pointer)
{ "$bindState": "/form/amount" }把输入控件和状态双向绑定
{ "$template": "今天共 ${/today/count} 节课" }插入状态值的字符串

组件目录

目录里共有 17 个组件。表中「可以有子元素」的组件用 children 包含其他组件;「绑定 value」表示这个输入控件的值应当用 $bindState 绑定到状态。

组件属性说明
Columngap纵向排列,可以有子元素
Rowgap, align, wrap横向排列,可以有子元素
Cardtitle, subtitle分组面板,可以有子元素。不要嵌套卡片。
Headingtext, size
Texttext, tone, size
Statlabel, value, prefix, suffix, decimals, delta, deltaTone, caption, icon, size关键数字,数值变化时有动画
Badgelabel, tone
ListItemtitle, subtitle, value, icon事件 press
Divider—
Progresslabel, value(0–1), tone
TextFieldlabel, placeholder, value, keyboard绑定 value
Togglelabel, value绑定 value
Choicelabel, options, value最多 12 个选项;绑定 value
Sparklinevalues, tone
Ringvalue(0–1), label, caption, tone, size
DateTimelabel, value, mode原生选择器;值为本地时间 "YYYY-MM-DD HH:mm"
Buttonlabel, variant事件 press

组件被点击时,会触发 on.press 里定义的动作。动作有两种。

动作参数效果
submit{ label }把 /form 下的全部内容作为用户的下一条消息发回给助手
callTool{ tool, input }直接调用某个插件工具

callTool 调用工具时,权限检查和助手调用时相同。

这一页的内容可能落后于实际代码,但运行时的组件目录始终是最新的。ui.render 工具的描述里包含了 services.genui.describeCatalog() 的输出,所以助手看到的永远是当前的目录。services.genui.checkSpec(spec) 可以用来校验一份 spec。

示例:充值表单

下面的 spec 显示一张「充值校园卡」卡片,里面有一个金额选择和一个确认按钮。你点「确认」后,所选金额会作为你的下一条消息发回给助手。

{
  "root": "card",
  "elements": {
    "card": { "type": "Card", "props": { "title": "充值校园卡", "subtitle": "选择金额" }, "children": ["amount", "ok"] },
    "amount": {
      "type": "Choice",
      "props": {
        "label": null,
        "options": [{ "label": "¥50", "value": "50", "hint": null }, { "label": "¥100", "value": "100", "hint": "常用" }],
        "value": { "$bindState": "/form/amount" }
      }
    },
    "ok": { "type": "Button", "props": { "label": "确认", "variant": "primary" }, "on": { "press": { "action": "submit", "params": { "label": "充值" } } } }
  },
  "state": { "form": { "amount": "100" } }
}

写给插件作者

远程插件不能自己显示界面。它们只返回数据,由助手来生成界面。

内置插件的页面可以用 host.components.GenUI 显示一份 spec:<GenUI spec={spec} onSubmit={(form, label) => …} />。

生成式界面适合对话中一次性的场景。如果用户会经常打开某个界面,请把它做成原生页面或小组件(contributes.screens / widgets)。

本页目录