打地基:让 Action 有东西可改

第 2 课 · 共 7 课 约 7 分钟

写两个 Value Type 和两个对象类型、一条链接,dry-run 检查、define 落库、publish 出版本;拼错的字段会被当场拒收。

本课目标

读完这一课,你将能够

  • 写出两个 Value Type、两个对象类型和一条链接的定义
  • 用 dry-run 检查、define 落库、publish 出版本
  • 说出定义写错时平台怎么拦,以及智能体作者要走哪条路

Action 改的是对象,先有对象类型

Action 不凭空改数据,它改的是对象。所以第一步不是写 Action,而是让你的 Semantic 里有它要改的东西。这一课打好整个座位申请的地基:两个 Value Type,两个对象类型,一条链接。地基一次打好,后面的 Action 一个个往上加。

建一个目录 seat-lab,里面再建 starter 和 ontology 两个子目录(后面几课的命令都在 seat-lab 里运行;第 6、7 课生成的应用也会放在这里,和定义文件分开各占一个子目录)。定义是 JSON 文件,kind 用官方的名字:valueType、objectType、linkType、actionType。

第一份定义:客户公司

把下面的文件存成 starter/01-customer.json。它有两个定义:客户阶段是一个带约束的类型(只能是这四个值之一,写入时检查),客户公司是一个对象类型。

[
  {
    "kind": "valueType",
    "apiName": "customerStage",
    "title": "客户阶段",
    "schema": {
      "baseType": "string",
      "constraints": [{ "type": "enum", "options": ["试点", "付费", "暂停", "内部"] }],
      "version": "1.0.0"
    }
  },
  {
    "kind": "objectType",
    "apiName": "customer",
    "title": "客户公司",
    "description": "一家客户公司。教程里的起点:先有它,座位申请才有可改的东西。",
    "schema": {
      "pluralDisplayName": "客户公司",
      "visibility": "PROMINENT",
      "titleColumn": "name",
      "columns": [
        { "name": "customerId", "type": "string", "primaryKey": true, "title": "公司 ID" },
        { "name": "name", "type": "string", "title": "名称", "notNull": true },
        { "name": "stage", "type": "string", "title": "阶段", "valueType": "customerStage" },
        { "name": "seats", "type": "integer", "title": "座位数", "unit": "个" },
        { "name": "members", "type": "integer", "title": "成员数", "unit": "人" }
      ]
    }
  }
]
  • apiName:Object Type 和 Value Type 用 camelCase,Action 用 kebab-case。一旦有人在用,就别改它。
  • primaryKey:一个对象类型只有一个主键属性;titleColumn 是这个对象在界面和日志里显示的名字。
  • valueType:属性引用 Value Type,就带上了它的约束。stage 填「不存在」会被拒,Action 的参数引用它也一样。

座位申请:另一个对象类型和一条链接

在 ontology 目录里再存三个文件:01-seat-request-status.json(申请的状态)、02-seat-request.json(申请本身),以及 03-links.json(「一家公司有多张申请」这条链接)。

{
  "kind": "valueType",
  "apiName": "seatRequestStatus",
  "title": "座位申请状态",
  "schema": {
    "baseType": "string",
    "constraints": [{ "type": "enum", "options": ["submitted", "approved", "rejected"] }],
    "version": "1.0.0"
  }
}
{
  "kind": "objectType",
  "apiName": "seatRequest",
  "title": "座位申请",
  "description": "客户公司申请把座位数提到新的数目的一张单。由 Action 新建、批准或拒绝,不来自任何数据源。",
  "schema": {
    "pluralDisplayName": "座位申请",
    "icon": { "blueprint": { "name": "inbox", "color": "#E5620C" } },
    "visibility": "PROMINENT",
    "synonyms": ["加座申请", "申请"],
    "titleColumn": "customerName",
    "columns": [
      { "name": "requestId", "type": "string", "primaryKey": true, "title": "申请编号" },
      { "name": "customerId", "type": "string", "title": "公司", "notNull": true },
      { "name": "customerName", "type": "string", "title": "公司名称" },
      { "name": "currentSeats", "type": "integer", "title": "申请时的座位数", "unit": "个" },
      { "name": "seats", "type": "integer", "title": "申请的座位数", "unit": "个" },
      { "name": "reason", "type": "string", "title": "理由" },
      { "name": "status", "type": "string", "title": "状态", "valueType": "seatRequestStatus" },
      { "name": "requestedBy", "type": "string", "title": "申请人(账号 ID)" },
      { "name": "requestedAt", "type": "timestamp", "title": "申请时间" },
      { "name": "decidedBy", "type": "string", "title": "决定人(账号 ID)" },
      { "name": "decidedAt", "type": "timestamp", "title": "决定时间" },
      { "name": "decisionNote", "type": "string", "title": "批复意见" }
    ]
  }
}
{
  "kind": "linkType",
  "apiName": "customerSeatRequests",
  "title": "公司的座位申请",
  "schema": {
    "from": "customer",
    "to": "seatRequest",
    "cardinality": "ONE_TO_MANY",
    "apiNameAtoB": "seatRequests",
    "apiNameBtoA": "customer",
    "displayNameAtoB": "座位申请",
    "displayNameBtoA": "所属公司",
    "foreignKey": { "side": "to", "property": "customerId" }
  }
}

申请里存了公司名和申请时的座位数,而不是每次去查公司:申请是一张单据,要记住的是提出那一刻的情况。customerId 是外键,链接 customerSeatRequests 靠它把两端接起来;这些属性都由 Action 写入,不来自任何数据源。

dry-run、define、publish

  1. dry-run:只检查

    整批定义一起校验,批内互相引用算数;不通过一条都不写。

  2. define:落库

    有则改,没有就建。改动如果是破坏性的(删属性、改类型…),会在返回里列出来。

  3. publish:出版本

    发一个语义版本 vN,只增不减;定义没变就不占版本号。

aidc semantic define starter --dry-run --json   # 检查
aidc semantic define starter                    # 落库
aidc semantic publish --notes "客户"
{
  "dryRun": true,
  "defined": [
    { "apiName": "customerStage", "kind": "valueType", "created": true, "breaking": [] },
    { "apiName": "customer", "kind": "object", "created": true, "breaking": [] }
  ],
  "warnings": []
}

上面是 starter,里面现在只有 01-customer.json(02-register-customer.json 下一课再放)。ontology 也照这个顺序做,里面现在只有前面那三个文件(04 到 07 是四个 Action,后面几课再放):

aidc semantic define ontology --dry-run && aidc semantic define ontology
aidc semantic publish --notes "座位申请的对象类型"
aidc semantic ontology --json     # 全貌:两个对象类型,一条链接

要点

  • 先有对象类型,Action 才有东西可改;地基一次打好,Action 一个个往上加。
  • 定义是 JSON:kind 用官方名字,Object Type 用 camelCase,Action 用 kebab-case。
  • dry-run 只检查,define 落库,publish 出版本;整批一起校验,要么都成,要么都不写。
  • 拼错的字段会被拒收并指出路径;智能体作者要走分支和提案,不能直接写 main。

练一练

把地基放进你的公司

在你自己的公司里做;定义可以反复 define,不会写坏数据。

存好四个定义文件,按上面的顺序做:先 starter,再 ontology(ontology 里的链接引用 starter 里的 customer,顺序不能反)。第一遍 --dry-run 里,defined 每一项的 created 是不是 true?全部落库之后再各跑一次 --dry-run:这次呢?

小测

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

Q1define --dry-run 会做什么?

Q2为什么座位申请里要存 currentSeats(申请时的座位数)?

Q3发布之后再 define 一份没有任何变化的定义,再 publish,会怎样?

延伸阅读