做成应用:界面给人点,Skills 给智能体用

第 6 课 · 共 7 课 约 8 分钟

用 Nexus 应用把 Action 接给人和智能体:清单登记、先校验后执行的界面、谁能点、发布、复制给智能体。

本课目标

读完这一课,你将能够

  • 用 aidc app init --template semantic 起一个应用,并在清单里登记它能碰的对象和 Action
  • 写「先校验、再确认执行」的界面,拦人的规则一条也不留在页面里
  • 说清谁能点、怎样发布,以及智能体怎样用同一个 Action

一个应用 = Skills + APIs + 可选的界面

应用是给人和智能体用同一批 Action 的地方:智能体照 Skills 调 APIs,人在需要看、比较、确认的时候才打开界面。界面能做的事,智能体大多也能做,因为两边调的是同一批 Action;差别在身份:界面里开发者按开发者执行,aidc app call 一律按成员执行。先起一个(在第 2 课的 seat-lab 目录里运行,应用会生成在 seat-lab/seat-desk):

aidc app init seat-desk --template semantic --title "座位申请台"
aidc app check seat-desk       # 本地校验:清单、包、Skills
aidc app deploy seat-desk      # → test 通道:/developer/cell-acme/apps/seat-desk
aidc app publish seat-desk     # → Nexus:/nexus/cell-acme/apps/seat-desk

模板给你 plugin.json(清单)、index.html、app.js、style.css 和一条 Skill。app.js 已经是一个能跑的页面:读一个对象类型,每行一个 Action 按钮。想最快看到效果,只改三处:app.js 顶上的 TYPE 改成 "seatRequest",ACTION 改成 "reject-seat-request",清单里登记这两个名字。部署后,每张申请旁边就有了「拒绝」按钮,点下去弹出批复意见的输入框。

清单里登记:这个应用能碰什么

应用是沙箱里的页面,能碰什么全看清单。座位申请台的登记是这样的:

{
  "types": [
    "customer",
    "seatRequest",
    "log.submit-seat-request",
    "log.approve-seat-request",
    "log.reject-seat-request",
    "log.auto-approve-seat-request"
  ],
  "actions": [
    "submit-seat-request",
    "approve-seat-request",
    "reject-seat-request"
  ],
  "write": []
}
  • types:能读的对象类型,包括四个 Action Log 类型(每个 Action 一个 log.<Action>),界面底部的 Action Log 表读的就是它们。
  • actions:能执行的 Action。没登记的,就算 Action 存在也调不了:「应用清单没有登记 Action …」。自动批准没登记在这里,界面碰不到它。
  • write:留空。应用不直接写对象,改数据只走 Action;这一行空着,就是把「直接写」这条路关上。

同一个清单里还有 auth(谁能打开:本公司成员,公开链接的访客登录后只读)和 limits(资源上限:不存在不设上限的应用)。

界面:先校验,你点确认再执行

模板的按钮是一步到位。座位申请台改成两步:先「校验」(只校验,不写入,把逐项结果摆给人看),人点「确认执行」才真的执行。核心就是这两个函数:

/** 校验(不写入)。只读访客的校验也会被拒绝(403):只有能执行的人才能校验。 */
async function check(action, params) {
  $("alert").hidden = true;
  try {
    const r = await client.action(action).applyAction(params, { $validateOnly: true });
    showResult({ action, params, validateOnly: true, validation: r.validation });
  } catch (err) {
    showResult({ action, params, validateOnly: true, validation: err?.details?.validation, message: err?.details?.validation ? null : err.message });
  }
}

async function execute() {
  if (!pending) return;
  const { action, params } = pending;
  $("confirm").disabled = true;
  try {
    const r = await client.action(action).applyAction(params, { $returnEdits: true });
    showResult({ action, params, validateOnly: false, validation: r.validation, edits: r.edits, operationId: r.operationId });
    ui.toast("已执行", { variant: "success", description: "申请、公司和 Action Log 都已更新" });
    semantic.observability.track("action", { action });
    await load();
  } catch (err) {
    showResult({ action, params, validateOnly: false, validation: err?.details?.validation, message: err?.details?.validation ? null : err.message });
  }
}

页面里没有拦人的业务规则:「要比现在的多」「只能批待审批的」全在 Action 里(表单旁那句「50 座以内会被自动批准」只是提示,不参与判断)。页面只负责把 validation 里的每一项画成 ✓ 和 ✗,把 edits 画成「这次改了什么」。谁能点什么,也不用页面自己判断,问平台:

let me = null; // 服务端确认的身份:角色,以及这个角色能执行哪些 Action
const can = (action) => Boolean(me && semantic.admin.can(me, { action }));
client.objects("seatRequest").subscribe({ onChange: refresh, onOutOfDate: refresh, onError: ({ subscriptionClosed, error }) => subscriptionClosed && fail(error) });

第三段是订阅:申请一变(别人提交、自动批准、你批准),页面就重新读,不用轮询。

谁能点,怎么发布,智能体怎么用

开发者成员 / 只读访客
提交申请可以(校验、执行)成员可以;只读访客连校验都不行(403)
批准 / 拒绝可以不行:roles 只有 developer
看申请、公司、Action Log可以可以(清单登记过的类型)

aidc app deploy 把应用放进 test 通道,先在 Developer 里试;aidc app publish 才发到 Nexus,让公司的人打开。发布要 developer 账号。Demo 里的座位申请台就是这样发布的,你可以对照着看:

智能体用同一批 Action,靠 Skill 和 API。Skill 是一份写给智能体的做法:什么时候用、按什么步骤调什么、哪些事不能做。写入一律先 --preview:读、算都真的跑,写入只返回计划,让人看过再正式执行。

---
name: handle-seat-requests
description: "处理客户公司的座位申请:看有哪些待审批的申请、替某家公司提交新的申请、批准或拒绝。用户说「有哪些待审批的座位申请」「帮 Demo Customer C 申请把座位数提到 20」「批准 Demo Customer A 的申请」时使用。写入之前一律先预演,把计划给人看,人确认后再正式执行。"
---

# 处理座位申请

座位申请是 Semantic 里的对象(`seatRequest`),改它只走 Action:`submit-seat-request`(提交)、`approve-seat-request`(批准)、`reject-seat-request`(拒绝)。规则都在 Action 里,你不用也不能绕开。

## 输入

- 公司:用名字或公司 ID。不确定是哪一家,先列出公司让人选,不要猜。
- 申请的座位数:要比现有的多。人没说数字就问。

## 步骤

1. 确认已登录:`aidc whoami`。没有就请人自己在终端运行 `aidc login`(不要索要密码)。
2. 看待审批的申请(命令里的 `cell-demo` 是这个应用所在公司的代码,你自己部署的应用换成你的公司代码):`aidc app call cell-demo/seat-desk seatRequest --input '{"where":{"status":"submitted"}}' --json`,只念返回的内容。
3. 提交申请:先预演 `aidc app call cell-demo/seat-desk submit-seat-request --param customer=<公司 ID> --param seats=<座位数> --param reason=<理由> --preview --json`。预演不通过,把失败信息原样告诉人;通过了,把计划给人看,人说可以再去掉 `--preview` 正式执行。
4. 批准或拒绝:批准要 `request`(申请编号)和 `customer`(公司 ID)两个参数;拒绝要 `request` 和 `note`(理由必填)。这两个动作的 `roles` 只有 developer,而 `aidc app call` 一律按成员执行,会得到「只有 developer 能执行」:不要重试,请人在界面里批准,或者用开发者 Key 的 `aidc semantic apply`。
5. 座位数在 50 以内的申请会被自动批准,不用你再批。

## 输出

- 每张申请一行:公司、现有座位 → 申请座位、状态、谁申请的。
- 执行之后说清改了什么(返回值里的 `edits`),并给出操作编号。

## 不要

- 替人批准或拒绝:批准是业务决定,只有人说了才做。
- 因为预演不通过就换个参数硬凑:失败信息就是答案(比如「申请的座位数要比现在的多」)。
- 编造座位数、成员数或申请状态:只用命令返回的内容。
aidc app apis cell-acme/seat-desk        # 有哪些 API:6 个类型的查询(含四个 log.*)和三个 Action
aidc app call cell-acme/seat-desk submit-seat-request \
  --param customer=acme-east --param seats=35 --preview --json

要点

  • 应用 = Skills + APIs + 可选的界面;界面能做的,智能体大多也能做,因为调的是同一批 Action,差别在身份:aidc app call 一律按成员执行。
  • 清单 semantic 登记能读的类型和能执行的 Action,write 留空:没登记的调不了,改数据只走 Action。
  • 界面先 $validateOnly、人确认再执行;规则不写在页面里,能不能点问平台(can)。
  • test 通道试,publish 才上 Nexus;智能体用 Skill 和 aidc app call --preview,写入先预演。

练一练

做出你自己的座位申请台

在你自己的公司里做;test 通道只有开发者看得到,放心试。

用 semantic 模板起 seat-desk,改 TYPE、ACTION 和清单,check、deploy,打开 test 地址,在一张待审批的申请上点一次「拒绝」。

小测

选一个答案,马上看解析。

Q1应用清单里 semantic.write 留空,意味着什么?

Q2为什么界面在执行之前先跑一次 $validateOnly?

Q3智能体要提交一张座位申请,正确的做法是?

延伸阅读