# 语义 SDK（Semantic）

**Semantic 是 AIDC 的数据平台**（Data Connection + Ontology + Automate + Security）。企业数据的一切都在这里：数据从哪来、存成什么对象、谁能看、谁改了什么、变了之后自动做什么、花了多少钱。ERP / MES / OA 这些数据源永远只读；应用和智能体读对象集、用 **Action** 改（参数校验、谁能做、留痕）。

```
ERP / MES / OA ──(发布端，只读)──▶ 数据流 ──(Object Type 定义里的 datasources)──▶ Semantic（对象 · 链接 · Action）──▶ 应用 · 智能体 · 自动化 · SQL
```

```js
import { semantic } from "/developer/sdk/v1/aidc.js";
```

先读这两页：[Object：从表到对象](objects.md)（对象是什么、由哪几部分组成、怎样从零构建一个对象、和直接查表比有什么不同）；[数据管道](pipeline.md)（从源系统到对象的每一段、按需计算与数据健康、把定时看板迁进 Semantic）。

## Semantic 是数据平台

| 要做什么 | 用什么 | 标准名 |
| --- | --- | --- |
| 读对象、沿链接走、聚合 | `semantic.ontology().objects(t).where(…).fetchPage() / aggregate() / pivotTo()` | Ontology SDK（OSDK） |
| 实时 | `objectSet.subscribe({ onChange, onOutOfDate })` | Object set subscription |
| 改数据 | `client.action(a).applyAction(…)`（只有这一条写路径） | Actions |
| 数据从哪来 | `semantic.streams` / `semantic.stream()` + Object Type 定义里的 `datasources` | Data Connection · Streams · backing datasource |
| SQL | `semantic.sql()` / `semantic.database()` | Ontology SQL |
| 谁能看 | `semantic.filesystem`（Private / Group / Public / Open to Internet）· `semantic.admin`（当前账号、成员） | Filesystem ResourceRoles · Admin |
| 变了做什么 | `semantic.automate`（数据变化 / 定时 → Action、模型分析、通知） | Automate |
| 留痕与用量 | Action Log、`editsHistory`、`semantic.observability`、`semantic.usage`，平台对象类型 `aidcModelUsage` / `aidcAppEvent` / `aidcAutomationRun` | Action log · Observability · Resource Management |
| 建本体 | 分支与提案（智能体作者；谁审核合并看组织的审批策略） | Global Branching · Ontology proposals · approval policy |

原来的连接、数据、权限、工作流、日志、账单六个 SDK 就是上面这些部分，1.20.0 起都在 `semantic` 里。旧名照样能用、和新家是同一份实现：`connect.stream === semantic.stream`、`auth.me === semantic.admin.getCurrentUser`、`workflow.run === semantic.automate.run`、`log.feedback === semantic.observability.feedback`、`billing.summary === semantic.usage.summary`，`connect.agent` 搬到了 `model.agent`；应用清单 `sdk` 里写旧名也照收（归一成 `semantic`）。

## 数据从哪来：数据流与数据源

数据在源头一变，源头旁边的**发布端**（`aidc semantic streams pipe` + 适配器，只读）就把变化推上来，平台按序落账（[数据流](connect.md)）。**数据流接到哪个 Object Type、哪一列对哪个属性，写在 Object Type 的定义里**（即 Ontology Manager 的 Datasources 页里选的 backing datasource）——改数据源就是改本体：

```json
{ "kind": "objectType", "apiName": "production.order_line", "title": "工单线体进度",
  "schema": {
    "columns": [ { "name": "orderLineKey", "type": "string", "primaryKey": true }, { "name": "orderNo", "type": "string" }, { "name": "lineCode", "type": "string" }, … ],
    "datasources": [
      { "type": "stream", "stream": "production-progress", "propertyMapping": { "orderLineKey": "key", "orderNo": "aufnr", "lineCode": "line", "ok": "gmnga" }, "mode": "mirror" }
    ] } }
```

- **规则**（定义时就校验）：一个 Object Type 恰好一个主键属性（复合键在数据源里拼成一列，如 `<工单>|<线体>`）；属性 API name 用 camelCase，流里的列名照源系统写、在 `propertyMapping` 里对上；流要在本组织存在；映射到的列要在流里；主键属性必须映射；只在语义层维护的属性（`writeback`，即 editOnly）和派生属性不映射；同一条流在一个类型里只出现一次。还是数据源的流删不掉（先从定义里去掉）。
- **mode**：`mirror` = 流里没了的行在对象上标记「源头已消失」；`upsert` = 只增改（多行汇成一个对象、保留历史）。
- **谁能改**：开发者直接定义（`aidc semantic define`，或下面的快捷命令）；智能体在分支上改、开提案，按组织的审批策略审核合并（提案的 merge checks 多一项 `datasources`）。
- **定义落了之后**：平台按定义建好同步，用流的当前全量同步一次；之后流每落账一批就增量同步。定义里去掉数据源（或归档类型），同步就停，已有对象保留。
- **没写 = 沿用**：重新定义时没写 `datasources`，沿用现有的数据源（返回一条警告）；要去掉全部数据源，明确写 `"datasources": []`。

```bash
aidc semantic streams create production-progress --title "生产进度" --key key --field key --field aufnr --field line --field gmnga:number
aidc semantic streams key production-progress --label "SAP 旁边的发布端"      # aidc-pk-…，只显示一次
aidc semantic datasource set production.order_line --stream production-progress --map orderLineKey=key --map orderNo=aufnr --map lineCode=line --map ok=gmnga
aidc semantic datasource list                                                    # 每个类型的数据源与同步进度
aidc semantic datasource sync                                                    # 平台数据源立即同步（= /semantic 的「立即同步」）
```

```js
await semantic.streams.create({ name: "orders", title: "订单", key: "order_no", fields: [{ name: "order_no" }, { name: "qty", type: "number" }] });
const pub = semantic.stream("orders").publisher();   // 发布端：只发与上次相比的差
await pub.rows(await readErpOrders());
```

**另一种数据源：用 SQL 读业务库（Data Connection）**。ERP / MES 不装发布端时，平台直接用只读账号读：每张表一个 **TableImport**（一条只读 SELECT），每次执行写成一个 **dataset** 的事务，只把变了的行写成对象。dataset 在 Object Type 的定义里是 `{ "type": "dataset", "dataset": "<TableImport 的 apiName>", "propertyMapping": {…} }`（SNAPSHOT 的导入 = 对象就是这些行，没了的标「源头已消失」；APPEND = 只增改）。客户内网的库经装在客户网络里的 agent proxy 连（只出站、只转发字节）。读的时候过期才同步（`freshness.maxAgeSeconds`），没人读就不跑。写法与命令见[闭环](loop.md)。

**建 sync 之前先探索源**（本组织开发者）：source 页的 **Explore** 列出源里的表与视图（可按表名搜索），点开一张表看列、主键、外键与被引用的表，直连的 source 还能取几行样本；勾选要的表，一次建好 batch sync（每张一条 `SELECT *`，之后在 sync 页改增量、挑列）。只读；样本行不落库，经 agent proxy 的 source 只看表与结构。命令行：

```bash
aidc semantic connectivity explore <connectionRid> --search order              # 表名里含 order 的表与视图
aidc semantic connectivity explore <connectionRid> --table dbo.SalesOrder        # 列、主键、外键与被引用
aidc semantic connectivity explore <connectionRid> --table dbo.SalesOrder --preview --rows 5   # 样本（只在直连的 source 上）
```

**从底层重算：加工（Transforms）**。指标不要写在导入 SQL 里每次回源系统整份算：原始数据经 TableImport 进 dataset 只读一次，指标由 **Transform** 从 dataset 算成新的 dataset（一条 DuckDB 的 SELECT，输入按别名当表用），再作 Object Type 的数据源（`"dataset": "<Transform 的 apiName>"`）。输入一提交新事务就自动构建；同一个 Transform 同时只跑一个，跑的时候又来了新数据就跑完再跑一次。读的时候过期，执行的是它上游的同步，跑完一路算到对象。

- **非增量**（不写 `incremental`）：每次整份重算。适合小表、要看前后行的计算（排名、前一天的值）。
- **`append`**：只算新加的行、追加到产出。适合逐行的清洗、过滤、补字段（新产出只取决于新输入行）。
- **`mergeAndReplace`**：新加的行先算出受影响的键（每个输入写 `key` 表达式），只把这些键在输入里的全部行重算一遍，再和上一份产出按键合并。适合按键汇总（按表计 × 班日、按订单、按日期 × 车间）。产出了受影响以外的键会失败——说明 `key` 写错了。
- **`snapshotInputs`**：参考表（清单、价格表），整份读，它们的变化不触发、也不使之前的结果失效。
- **什么时候整份**：第一次构建、`semanticVersion` 变了（改了口径要回溯历史就加一）、非 snapshot 输入被整份替换过（SNAPSHOT 事务）。输入与定义都没变时构建直接成功、不跑（`--force` 照跑）。`requireIncremental: true` 时不能增量就失败（防止大表意外整份算）。
- **时间**：dataset 里不带时区的时间在 SQL 里是北京时间（例：班日 = `CAST(read_at - INTERVAL 450 MINUTE AS DATE)`，7:30 起算）；`current_date` 也是北京时间。
- **边界**：只执行一条 SELECT，只能读这次的输入；一次构建读的文件 ≤ 384 MB、产出 ≤ 500 万行、300 秒内算完。一个 dataset 只有一个产出者（TableImport 或 Transform）；输入不能绕回自己的产出。

```json
{ "apiName": "meter-shift-stats", "displayName": "表计班日统计",
  "inputs": {
    "readings": { "dataset": "meter-readings", "key": ["meter_code", "CAST(read_at - INTERVAL 450 MINUTE AS DATE)"] },
    "meters": { "dataset": "meters" } },
  "sql": "SELECT r.meter_code, CAST(r.read_at - INTERVAL 450 MINUTE AS DATE) AS shift_date, min(r.read_at) AS first_time, max(r.read_at) AS last_time, arg_max(r.value, r.read_at) AS last_value, count(*) AS read_count FROM readings r SEMI JOIN meters m ON m.meter_code = r.meter_code GROUP BY ALL",
  "incremental": { "semanticVersion": 1, "snapshotInputs": ["meters"], "output": "mergeAndReplace", "key": ["meter_code", "shift_date"] } }
```

```bash
aidc semantic transforms put --ontology cell-demo --file meter-shift-stats.json --dry-run   # 先预演：输入、环、名字
aidc semantic transforms put --ontology cell-demo --file meter-shift-stats.json
aidc semantic transforms build <transformRid> --wait        # 构建一次（之后输入一有新数据就自动构建）
aidc semantic transforms show <transformRid>                 # 定义与上次构建：增量还是整份、受影响的键、读写多少行
```

## 实时：订阅对象集

```js
const client = semantic.ontology();
const issues = client.objects("production.order_line").where({ status: "异常" });
const sub = issues.subscribe({
  onChange({ object, state }) { state === "REMOVED" ? drop(object.__primaryKey) : upsert(object); },
  onOutOfDate() { reload(); },            // 整个对象集要重读：刚订阅时来一次、断得太久、依赖的类型变了
  onError({ subscriptionClosed, error }) { if (subscriptionClosed) showOffline(error); },
}, { properties: ["lineCode", "gap", "status"] });
// sub.unsubscribe()
```

写法同 OSDK 的 `objectSet.subscribe`：对象进来或变了 → `ADDED_OR_UPDATED`（带属性），离开对象集（被过滤掉、删了、源头消失）→ `REMOVED`（只带主键）。同步、Action、别人的修改都会推过来；断线自动带着序号续传，不丢不重；页面隐藏超过 1 分钟断开、切回续上；应用里的连接时长计入应用的计算分钟。CLI：`aidc semantic subscribe <类型> [--where '{…}']`。SDK 走 `/api/v1/ontologySubscriptions/ontologies/{ns}/streamSubscriptions`（SSE，消息一样）；官方 OSDK 直连 `/api/v2/ontologySubscriptions/ontologies/{ns}/streamSubscriptions`（WebSocket，子协议 `Bearer-<token>`）。

## 访问与账号

```js
const me = await semantic.admin.getCurrentUser();      // 我是谁、什么角色、能读哪些类型、能执行哪些 Action
if (!me.signedIn) semantic.admin.signIn();
await semantic.filesystem.setResourceAccess(rid, "public");                        // Private / Group / Public / Open to Internet
await semantic.filesystem.shareResource(rid, { group: "<组 id>", role: "viewer" });  // 部门就是组
await semantic.filesystem.shareApplication({ audience: "company" });                // 应用的分享
```

开放程度四档、Owner / Editor / Viewer 三种角色、只有 Owner 改分享；资源是 Ontology、Object Type、数字员工、文件。详见[访问与账号](auth.md)。

## 组织、用户与组

组织架构是平台自带的，不在本体里另建：**组织**（一家公司，RID 写 `cell-…`）→ **用户**（主组织恰好一个；在别家持 License 是那家的访客）→ **组**（用户和 / 或其他组的集合，可以嵌套）。
部门就是组，上下级 = 组的嵌套；岗位、部门名这类信息是**用户属性**（`department`、`jobTitle`，键 → 多值）。资源分享、Marking 成员、对象安全策略的 `group` 条件都可以写组；智能体是资源，不是组织成员。

```bash
aidc semantic admin groups create --name 财务 --description 财务部的人           # 组织管理员（本组织开发者）
aidc semantic admin groups add-members <财务组 id> <账号 id> <应付组 id>         # 成员是账号或组（嵌套，不能成环）
aidc semantic admin groups add-members <财务组 id> <外部审计账号 id> --expires 2026-12-31   # 临时成员
aidc semantic admin groups members <财务组 id> --transitive                     # 连嵌套组里的人
aidc semantic admin users set-attributes <账号 id> department=财务 jobTitle=会计   # 身份源属性（主组织的组织管理员）
aidc semantic filesystem share <智能体 rid> --group <财务组 id>                  # 私有智能体只给财务组用
```

组的 `organizations` 决定哪些组织看得见它；组名在主组织内唯一；`multipass:` 开头的属性保留给平台。接口照 AdminV2：`/api/v1/admin/groups/**`、`/api/v1/admin/users/**`、`/api/v1/admin/organizations/{rid}`（设计与对照：aidc-cloud `docs/organizations.md`）。

## 数据安全：Markings 与安全策略

财务、薪资、采购价、技术配方这类数据也进 Semantic，用两种控制管住。上面的开放程度和角色决定「能不能打开这个资源」，下面这些再往里管到每一行、每一列：

| 控制 | 挂在哪 | 怎么判 |
| --- | --- | --- |
| **Markings**（强制控制） | Ontology、Object Type、数据流、TableImport；或者写在行上（`marking` 类型的属性） | 挂着的 Marking 全都满足才看得见（同一个 DISJUNCTIVE 类别里满足任一即可）。不看角色，Owner、开发者也一样；只限制、不授予；沿层级（Object Type ← Ontology）和数据依赖（数据流 → 用它的类型的对象）传播 |
| **对象安全策略**（行） | Object Type 定义里的 `objectSecurityPolicy` | 按对象的属性和读的人（组、账号、用户属性、行上的 Marking）判断每个对象看不看得见 |
| **属性安全策略**（列） | Object Type 定义里的 `propertySecurityGroups` | 一组属性要另外满足的条件或 Marking；不通过的人读到 null，也不能拿它筛选、排序、求和 |

先建 Marking，再挂到资源上或写进定义：

```bash
aidc semantic admin marking-categories create --name 数据分级                  # 类别：本组织开发者建
aidc semantic admin markings create --name 薪资 --category <类别 id> --member <人事组 id>
aidc semantic admin markings grant <薪资 id> USE <HR 负责人账号 id>             # USE = 可以把它挂到资源上
aidc semantic filesystem mark <配方类型的 rid> <技术配方 id>                    # 整个类型挂 Marking（要 USE、是类型的 Owner）
aidc semantic filesystem markings <rid>                                         # 直接挂的 + 继承来的
```

Marking 和类别建了不能删。角色：ADMINISTER 管成员与角色，USE 挂上，DECLASSIFY 去掉或停止继承；角色和成员互相独立（管 Marking 的人不一定看得见它保护的数据）。

行和列写在定义里（Demo Company 的员工薪酬；Marking 一律用 id 引用，`aidc semantic admin markings list` 查）：

```json
{
  "kind": "objectType", "apiName": "employeeCompensation", "title": "员工薪酬",
  "schema": {
    "columns": [
      { "name": "employeeId", "type": "string", "primaryKey": true },
      { "name": "accountId", "type": "string" },
      { "name": "baseSalary", "type": "decimal" },
      { "name": "rowMarkings", "type": "array", "arraySubType": "marking", "notNull": true }
    ],
    "objectSecurityPolicy": {
      "name": "compensation-rows",
      "granularPolicy": { "type": "and", "conditions": [
        { "type": "markingProperty", "property": "rowMarkings" },
        { "type": "or", "conditions": [
          { "type": "group", "name": "人事" },
          { "type": "comparison", "comparison": { "operator": "EQUAL",
            "left": { "type": "userProperty", "userProperty": { "type": "userId", "userId": {} } },
            "right": { "type": "property", "property": "accountId" } } }
        ] }
      ] }
    },
    "propertySecurityGroups": [{ "name": "pay", "properties": ["baseSalary"], "appliedMarkings": { "<薪资 id>": "MANDATORY" } }],
    "dataSecurity": { "markingConstraint": { "markingIds": ["<高管薪酬 id>"] } }
  }
}
```

意思是：HR 部门或本人看得见这一行，行上带的 Marking（比如高管的行带「高管薪酬」）也要满足；金额另外要「薪资」。其他三类数据的常见配法：

| 数据 | 配法 |
| --- | --- |
| 财务 | Marking「财务」挂在 ERP 财务数据流上，用这条流的类型自动带上 |
| 采购价 | 采购订单行对采购员可见，`unitPrice` 放进属性安全策略，要「采购价」 |
| 技术配方 | 配方类型整个挂「技术配方」，成员只给研发负责人 |

规则：

- **读**：所有读路径都按读的人判定——Ontology API、对象集订阅、聚合、编辑历史、旧数据 API、/semantic 页面、自动化。Agent Key 是智能体身份，不是任何 Marking 的成员；Ontology SQL（Semantic 数据库）不含受保护的类型。
- **Action**：可以新建自己看不见的对象；改一个属性要看得见它现在的值；删要看得见整个对象；链接两端都要看得见；开了 Action Log 的，执行人要满足被改类型的全部 Marking。
- **自动化**：条件和效果按拥有者（最后改条件或效果的人）的权限算；邮件通知按每个收件人的权限过滤。
- **只在读的时候判定**：函数的结果、Action 写进去的值、导出的文件不带原数据的安全。
- **改安全策略**：它是本体定义的一部分。改的人要满足改前改后涉及的全部 Marking，新加 Marking 要 USE，去掉或停止继承要 DECLASSIFY + USE。智能体在分支上改、开提案，由满足这些条件的审核人批准、合并（审批策略是 yolo 时也一样：Marking 是硬闸）；`--dry-run` 自测时权限不够照样给出结果，「要谁来批」写在 warnings 里。分支预览（`--branch`）按 main 与分支里更严的那份判定。
- **上限**：每个策略最多 10 个比较；权重是常量 1、集合 1,000、Marking 条件 3,000，总和小于 10,000（属性安全策略连同对象安全策略一起算）；不能对组或 Marking 成员取反；策略用到的属性为空的行谁都看不见；`marking` 属性必须 `notNull`，值要在 `dataSecurity.markingConstraint` 列出的 Marking 里。

试策略（照 Ontology Manager 的 Test security policies；只给开发者，结果不带属性值）：

```bash
aidc semantic security test employeeCompensation --user <账号 id> --pk e1,e2
aidc semantic security test employeeCompensation --user <账号 id> --object '{"employeeId":"x","rowMarkings":[]}' --policy 新策略.json
```

SDK：`semantic.admin.createMarking / addMarkingMembers / addMarkingRoleAssignments…`、`semantic.filesystem.addMarkings / removeMarkings / resourceMarkings`、`client.testSecurity(类型, {…})`；读对象时带 `$loadPropertySecurityMetadata: true`（照 OSDK），受属性安全策略保护的值会带上它的安全标记。

## 文件与图片：Media sets（非结构化数据）

图片、文档、音视频、邮件放进**媒体集**（Media sets），字节在你公司自己的 S3 存储里，Semantic 只存元数据；对象用**媒体引用属性**指向其中的文件，附件用 **Attachment 属性**。设计与规则：aidc-cloud `docs/semantic-media.md`。

- **媒体集**有 schema（`IMAGERY`、`DOCUMENT`、`AUDIO`、`VIDEO`、`SPREADSHEET`、`EMAIL`、`DICOM`、`MULTIMODAL`），按文件头认格式，不符合的拒收（`invalid_media_item_schema`）；PDF 文档媒体集可以加 `TXT / DOCX / PPTX`（additional input formats）。
- **同路径覆盖**：同一路径再上传，旧的变成历史版本，直接引用仍可读；删除是软删。**Transactional** 媒体集的写入要在事务里，提交后才可见。
- **保留策略**：上传后 N 天、或被覆盖 / 删除后 N 天永久删除；缩短立即生效。
- **虚拟媒体集**：登记公司存储里某个文件夹下已有的文件（例如知识库），不拷贝；点 Sync 登记新文件。
- **智能体读文件**：`semantic.mediaSets.extractText(媒体集, 媒体项)` 取 PDF / TXT 全文；`metadata()` 拿页数、尺寸、时长。

```js
const photos = await semantic.mediaSets.create({ name: "巡检照片", mediaSchema: "IMAGERY" });
const { mediaItemRid } = await semantic.mediaSets.upload(photos.rid, bytes, { mediaItemPath: "line-a/pump.png" });   // ≤ 4.4 MB
await semantic.mediaSets.uploadLarge(photos.rid, file, { mediaItemPath: "video/site.mp4" });                       // 大文件直传 S3（≤ 50 GB）
const text = await semantic.mediaSets.extractText(docsRid, pdfRid);                                                   // PDF 全文
```

**在对象上用**：Object Type 的媒体引用属性要配 media source（写在定义的 `datasources` 里，第一个是 Upload destination）；Action 参数可以是临时上传的媒体或附件，Action 提交成功后才持久化（附件一小时内要用上，一个附件一生最多关联 10 个对象）。

```json
{ "columns": [{ "name": "inspectionId", "type": "string", "primaryKey": true }, { "name": "photo", "type": "mediaReference" }, { "name": "report", "type": "attachment" }],
  "datasources": [{ "type": "media", "mediaSourceRids": [{ "type": "mediaSetRid", "mediaSetRid": "ri.mio.aidc.media-set.…" }], "properties": ["photo"] }] }
```

```js
const client = semantic.ontology();
const photo = await client.uploadMedia(fileBytes, "site-north.png");            // 临时媒体 → MediaReference
const report = await client.uploadAttachment(pdfBytes, "巡检单.pdf");           // 附件 → { rid, filename, sizeBytes, mediaType }
await client.action("record-inspection").applyAction({ inspectionId: "I-1", photo, report: report.rid });
const meta = await client.media("inspection", "I-1", "photo").fetchMetadata();  // { path, sizeBytes, mediaType }
const bytes = await (await client.media("inspection", "I-1", "photo").fetchContents()).arrayBuffer();
```

访问：媒体集沿 Ontology 继承（可再分享给人、部门，或设为 Public）；**把媒体集配成属性的 media source = 访问控制交给本体**——看得见对象和这个属性的人就能读它的媒体。Agent Key 只读（受限 Key 见下）。读内容时服务端 302 到预签名 S3 地址（10 分钟有效），`fetch` 会自动跟随；整页 HTML、SVG 这类会执行的内容只在 Object View 的沙箱 iframe 里显示，直接在地址栏打开会去 Object View 的全屏视图（`?embedded=true`）。

**大文件往属性上传**（照 Upload Media Content，直传形态）：`client.uploadMediaContent(类型, 属性, 文件, { mediaItemPath, contentType })` 开会话、把字节直传公司的 S3、完成后拿 MediaReference，一小时内交给 Action——字节不经过平台，多大都行。

```js
const html = await client.uploadMediaContent("loopReport", "html", file, { mediaItemPath: "daily-output.html", contentType: "text/html" });
await client.action("publish-loop-report").applyAction({ slug: "daily-output", title: "日产量日报", sha256, html });
```

**机器发布（受限 Agent Key）**：客户箱上的脚本定期发布整页报表，用一把**受限 Agent Key**（application restrictions：operations = `api:use-ontologies-read` / `api:use-ontologies-write`，resources = 列出的对象类型与 Action）。它只能读列出的类型、往它们的媒体属性直传、执行列出的 Action，别的接口一律 403，聊天网关也不认它。Key 由本组织管理员在控制台签发（`restrictions` 字段）。箱上用 Python 标准库脚本 `publish_loop_report.py`（在 `developer/semantic-agent/`）：内容没变就一个请求都不发，变了只有 3 个很小的 JSON 请求 + 1 次直传 S3，发布后不回读。

```bash
AIDC_AGENT_KEY=aidc-sk-… AIDC_SEMANTIC_ONTOLOGY=cell-demo \
python3 publish_loop_report.py --file out/daily-output.html --slug daily-output --title "日产量日报" --data-date 2026-10-01
# → {"ok": true, "decision": "published", "pageUrl": "https://www.ai-dc.ai/semantic/cell-demo/objects/loopReport/daily-output", …}
```

```bash
aidc semantic media-sets create --name 巡检照片 --schema IMAGERY
aidc semantic media-sets upload <媒体集 RID> pump.png --path line-a/pump.png
aidc semantic media-sets items <媒体集 RID> --path line-a/pump.png     # 版本历史
aidc semantic media-sets text <媒体集 RID> <媒体项 RID>                 # PDF 全文
aidc semantic upload-media loopReport html daily-output.html --content-type text/html --json   # 往属性直传 → MediaReference
```

## Space：每个人与智能体自己的空间

Space 在 `www.ai-dc.ai/semantic/<组织>/space`。本组织的每个人、每个智能体都有自己的 Space（Your files）。人与智能体在 Space 里是一样的：都能放东西、都能被分享到。

| 放什么 | 是什么 |
| --- | --- |
| 页 | Markdown。保存时带上次读到的版本，过期的保存被拒（不悄悄覆盖别人的修改） |
| 文件 | 字节直传到公司自己的 S3，单个最大 5 GB（智能体 64 MB） |
| 站点 | 一份自包含的 HTML（≤ 1.5 MB）自动部署成网页，有托管地址；换内容地址不变 |
| Project | 共享的容器。分享 Project，里面的东西一起给到（只加不减） |

- **分享**：给人、智能体、组；角色 Owner / Editor / Viewer；开放程度 Private / Group / Public / Open to Internet（站点设成 Open to Internet，不登录也能打开）。只有 Owner 能分享、放进回收站、永久删除。
- **智能体读的不超过它的负责人（owner）**：owner 看不到的，智能体也读不到。分享给有 owner 的智能体时加 `--also-owner`（SDK：`alsoOwner: true`），同时分享给它的 owner。
- **智能体建的东西**：智能体是 Owner；它有已核验的 owner 时，owner 也是 Owner。智能体不能把东西开到 Public 或 Open to Internet：要人来设。
- **删除**：先放进回收站（站点随即停止服务、可以恢复），再永久删除（字节、站点、分享一起删）。
- **读内容**：页 = Markdown 原文；文件 = 302 到预签名地址；站点 = 浏览器去托管页，Key 直接拿 HTML 原文。

```bash
aidc space deploy weekly-report.html --title "周报" --json        # 部署站点 → siteUrl
aidc space share <id> --tier internet                              # 不登录也能打开
aidc space share <id> --user zhangsan --agent procurement --also-owner --role viewer   # 分享给人、智能体（及它的 owner）
aidc space page "周会纪要" --file notes.md                          # 建页；aidc space edit <id> --file notes.md 改页
aidc space upload contract.pdf photo.png                            # 上传文件（.html 自动部署成站点）
aidc space ls --tab shared                                          # 分享给我的；--tab favorites / yours，--view pages / sites / images / trash
aidc space get <id> --out back.html                                 # 下载：页、文件、站点的 HTML
aidc space people                                                   # 分享的候选：本组织的人、智能体（AI）、组
aidc space empty-trash --dry-run                                    # 回收站有多少项、多少字节；去掉 --dry-run 就清空（永久删除，释放存储）
```

```js
const site = await semantic.space.deploySite({ displayName: "周报", html });
await semantic.space.share(site.id, { to: [{ type: "agent", id: "procurement" }], alsoOwner: true, tier: "group" });
const shared = await semantic.space.list({ tab: "shared" });            // 分享给我的
const { markdown, version } = await semantic.space.readPage(pageId);
await semantic.space.update(pageId, { markdown: markdown + "\n- 新的一行", baseVersion: version });
```

接口在 `/api/v1/space/**`：`items`（列表、建、取、改、放进回收站）、`items/{id}/content`、`items/{id}/share`（取 / 分享）、`items/{id}/favorite`、`restore`、`permanentlyDelete`、`trash/empty`（清空回收站，支持 dry-run）、`uploads`（直传会话）与 `uploads/{id}/complete`、`recent`、`principals`。Space 里的东西算进 owner 的存储（回收站里的照样算，清空才释放），见 `aidc me`。认网页登录、开发者 Key 与 Agent Key（受限的 Agent Key 要有 `api:use-filesystem-read` / `api:use-filesystem-write`）。

## 自动化（Automate）

数据一变（或到了时间）就做事：写在工作流应用的清单 `workflow` 里，按 Automate 的写法——**条件**（`trigger.change` = 对象新建 / 修改，`when` 过滤；`trigger.schedule` = 时间；手动）加**效果**（Action、模型分析、通知）。

```js
const { run } = await semantic.automate.run({ rfq_no: "RFQ-001" }, { slug: "quote-workflow", mode: "preview" });
semantic.automate.watch(run.id, { onRun: draw, onDone: finish }, { slug: "quote-workflow" });
```

触发读的是 Semantic 的变化账（和对象集订阅同一条），不轮询、不写 cron。详见[自动化](workflow.md)。

组织级的 Automation（`aidc semantic automations upsert --file`）用 Automate 的条件与效果：对象集条件（Objects added / modified / removed）、时间条件、**Run on all objects**（到点对对象集里现在的全部对象跑效果——「逾期」「超过一天没处理」这种因为时间过去才进集合的提醒用它）。邮件通知的收件人可以取自触发对象的属性（`{ "type": "propertyBacked", "property": "assigneeAccountId" }`），`"executionMode": { "type": "perObject" }` 每个对象一封：

```json
{
  "apiName": "dev-task-overdue", "displayName": "任务逾期", "status": "active",
  "condition": { "type": "runOnAllObjects", "every": "1d", "at": "09:30", "weekdays": [1, 2, 3, 4, 5],
    "objectSet": { "type": "filter", "objectSet": { "type": "base", "objectType": "devTask" }, "where": { "type": "and", "value": [
      { "type": "in", "field": "status", "value": ["待接受", "进行中", "阻塞"] },
      { "type": "relativeDateRange", "field": "dueDate", "relativeEndTime": { "value": 0, "timeUnit": "DAYS" }, "timeZoneId": "Asia/Shanghai" } ] } } },
  "effects": [{ "type": "notification", "channel": "email", "executionMode": { "type": "perObject" },
    "recipients": [{ "type": "propertyBacked", "property": "assigneeAccountId" }],
    "title": "任务逾期：{{title}}", "template": "{{#objects}}任务「{{title}}」截止 {{dueDate}}，现在是「{{status}}」。{{/objects}}" }]
}
```

**效果**有三种：**Action**（`"type": "action"`：以 automation 的拥有者——最后一次改条件或效果、保存它的人——的身份执行一个 Action Type，按他现在的权限，和他自己在界面里执行一样：参数校验、提交条件、Action Log、留痕都照常）、**Function**（`"type": "function"`：执行一个已发布的函数版本，在平台的隔离运行时里跑；运行里只记结果摘要，函数返回的 Ontology 编辑不生效——要改对象用 Action）、**邮件通知**。发钉钉 / 企业微信、回写 ERP 这类「调外部系统」写成 Action 的 side effect webhook，再用 Action 效果执行它。

```json
{
  "apiName": "close-overdue", "displayName": "逾期工单自动关", "status": "active",
  "condition": { "type": "objectsAdded", "objectSet": { "type": "filter", "objectSet": { "type": "base", "objectType": "ticket" },
    "where": { "type": "eq", "field": "status", "value": "overdue" } }, "evaluation": { "everyMinutes": 10 } },
  "effects": [
    { "type": "action", "actionType": "close-ticket", "parameters": { "ticket": { "$effectInput": "object" }, "note": "自动关闭" },
      "executionSettings": { "retryConfig": { "backoffStrategy": { "type": "constantBackoff", "attemptLimit": 3, "durationMillis": 60000 } } } },
    { "type": "function", "function": "summarizeTicket", "version": "1.2.0", "autoUpgrade": true, "parameters": { "ticketId": { "$effectInput": "primaryKey" } } }
  ],
  "fallbackEffects": [ { "type": "action", "actionType": "log-failure",
    "parameters": { "message": { "$effectInput": "failureMessage" }, "event": { "$effectInput": "eventId" }, "automation": { "$effectInput": "automationRid" } } } ]
}
```

- **效果输入**：参数写 `{ "$effectInput": "…" }` 就代入触发对象——`object`（一个对象：每个对象执行一次）、`objects`（这一批对象：对象集 / 对象列表参数）、`primaryKey`、`property:<属性>`（触发时的值）、`automationRid`、`eventId`；`failureMessage` 只在失败效果里有值。只有暴露对象的条件（对象集条件、Run on all objects）能用对象类输入；`objects` 不能和单对象输入混用。
- **执行方式**：不写就按输入推断（单对象输入 → 每个对象一次；`objects` → 这批对象一次；没用效果输入 → 每次触发一次）；`"executionMode": { "type": "perBatchOfObjects" }` + `"executionSettings": { "batchSize": 100 }` 按批（批大小是上限，不凑满）。
- **顺序、重试、失败效果**：Action / Function 效果按列出的顺序执行，一个对象在某个效果上失败了，就不进入后面的效果；`executionSettings.retryConfig.backoffStrategy` 配重试（`constantBackoff { attemptLimit, durationMillis }` / `exponentialBackoff { attemptLimit, stepDurationMillis, maxDurationMillis }`，可加 `jitter`：`{ "type": "jitterFactor", "jitterFactor": 0.25 }` 或 `{ "type": "jitterDurationMillis", "jitterDurationMillis": 5000 }`）；重试用完仍失败，对失败的那批对象执行 `fallbackEffects`。效果至少执行一次：Action 与函数要能承受重复执行。
- **保存时就校验**：Action / 函数与版本要在、参数名对、必填参数有值、效果输入和参数类型对得上、拥有者能执行这个 Action。
- **函数**：`aidc semantic functions publish <源码.js> --api-name … --version … --output '{…}'` 发布（见下面「动力部分」）；`autoUpgrade: true` 自动用同一大版本里更新的稳定版本。
- **已停用**：钉钉 / 企业微信通知与 `agentScript`（在客户箱上跑 Loop 脚本）——新的 automation 不再接受（`automate_legacy_effect`），已经在跑的照常运行；迁移表见[闭环](loop.md)。

## 应用（Applications，Workshop 模块）

看板、报表、工作台不用再写成一份 HTML 整页上传：在 Semantic 里建一个 **Application**（Workshop 模块），只存定义——页面、分区、组件、变量——数据每次打开时按看的人的权限从 Ontology 现算，没人打开就不计算、不同步。组件照 Workshop：Metric Card、Object Table、Chart: XY（柱 / 横向柱 / 折线，可分段）、Chart: Pie、Pivot Table、Filter List、Markdown、Data Freshness、Object Set Title；变量是对象集（REST API v2 的 ObjectSet，可套 Filter List 的筛选）、对象集聚合、静态值。

```bash
# 保存一个新版本（没有就新建）；先 dryRun 校验：类型、属性、变量引用全部按你的权限检查
curl -X POST "$AIDC/api/v1/workshop/modules?ontology=cell-demo&dryRun=true" -H "authorization: Bearer $AIDC_API_KEY" -H "content-type: application/json" -d @cloud-costs.json
# 用应用：取一页的数据（智能体也用这个；每个组件一份结果）
curl -X POST "$AIDC/api/v1/workshop/modules/ri.aidc.workshop.cell-demo.module.cloud-costs/evaluate" -H "authorization: Bearer $AIDC_API_KEY" -H "content-type: application/json" -d '{"page":"overview","filters":{"vendorFilter":{"vendor":"Vercel"}}}'
```

```json
{
  "apiName": "cloud-costs", "displayName": "云费用看板",
  "definition": {
    "header": { "title": "云费用看板" },
    "variables": [
      { "id": "vendorFilter", "type": "objectSetFilter", "objectType": "cloudCost" },
      { "id": "costs", "type": "objectSet", "objectSet": { "type": "base", "objectType": "cloudCost" }, "filters": ["vendorFilter"] },
      { "id": "totalUsd", "type": "numeric", "aggregation": { "objectSet": "costs", "metric": { "type": "sum", "field": "costUsd" } } }
    ],
    "pages": [{ "id": "overview", "title": "总览", "sections": [
      { "widgets": [{ "id": "filters", "type": "filterList", "objectSet": "costs", "output": "vendorFilter", "filters": [{ "property": "vendor", "kind": "select" }] }] },
      { "widgets": [{ "id": "kpi", "type": "metricCard", "metrics": [{ "label": "合计（美元）", "variable": "totalUsd" }] }] },
      { "layout": "columns", "sections": [
        { "widgets": [{ "id": "byVendor", "type": "chartXY", "chart": "bar", "objectSet": "costs", "groupBy": { "type": "exact", "field": "vendor" }, "metric": { "type": "sum", "field": "costUsd" } }] },
        { "widgets": [{ "id": "byMonth", "type": "pivotTable", "objectSet": "costs", "rows": [{ "type": "exact", "field": "month" }], "metrics": [{ "label": "美元", "metric": { "type": "sum", "field": "costUsd" } }], "totals": true }] }
      ] }
    ] }]
  }
}
```

- **日历区间**（本月、本年、上月）用 Date / Timestamp 变量（`{ "id": "monthStart", "type": "timestamp", "transformation": { "startOf": "month", "timeZoneId": "Asia/Shanghai" } }`），在过滤里写 `{ "type": "gte", "field": "createdAt", "value": "{{monthStart}}" }`；`relativeDateRange` 是相对今天前后几天，不对齐日历。
- **只算当前页**（Workshop 的 lazy loading）；读由数据源同步的类型前，过期才按需同步（页面上的「立即同步」最多等 20 秒）。
- **版本与发布**：每次保存是一个新版本；用户看到的是发布的那个版本。智能体可以保存版本；谁能发布看组织的审批策略（和本体提案同一条规矩）：review（缺省）下，作者是智能体的版本要人在 Semantic 的应用页上点「发布」；yolo 下，开发者 Key（含智能体在用的）也能发布（`aidc semantic applications publish <rid> <版本>`）。每次发布都记进审计日志。
- **权限**：打开应用看组织在这个 Ontology 上的角色（成员只看已发布的，开发者看草稿与任一版本）；应用里的数据另外按类型判定——读不到的类型，组件显示无权限，不会漏数据。
- 页面：`https://www.ai-dc.ai/semantic/<组织>/applications`。筛选、翻页、切换页面都在 URL 里，可以直接分享。

## 留痕与用量

- **改数据的留痕**：开了 `actionLog` 的 Action 每次成功提交写一个 Action Log 对象；`client.editsHistory(类型, { primaryKey })` 看一个对象的每次修改。
- **应用日志**：`semantic.observability.track / feedback / error` 记使用、反馈、错误；`summary / events` 看（[应用日志](log.md)）。
- **用量与账单**：`semantic.usage.summary / records / check / prices / estimate`（[用量与账单](billing.md)）。
- **AIDC 平台对象类型**：平台替你的组织记下的应用、模型用量、应用日志、自动化运行，可以作为只读的 Object Type 进你的 Ontology——`aidcApplication`、`aidcModelUsage`（按天 × 应用 / Key × 模型）、`aidcAppEvent`、`aidcAutomationRun`。本组织开发者打开 /semantic 时，智能体（数据库 Adis）会开一个提案，审核合并后才出现（组织的审批策略是 yolo 时，检查通过就自动合并；不需要就关掉提案，同一批定义不会再提）；之后按需同步（合并后、点「立即同步」、超过 1 小时有人看时）。用量、日志、自动化运行只给本组织开发者（可以在 Share 里开给别人），应用列表和 Nexus 里看到的一样。有了它们，看账单、查反馈就是查对象集，超支提醒就是一个自动化：

```js
const byDay = await client.objects("aidcModelUsage").aggregate({ $select: { "costUsd:sum": "desc" }, $groupBy: { day: "exact" } });
const openFeedback = await client.objects("aidcAppEvent").where({ kind: "feedback", status: "open" }).fetchPage();
```

## Ontology

语义层的建模语言、名字和 API 形状全部用标准的 Ontology 构件（设计：`docs/semantic-ontology.md`）。上面的写法（`describe`、`objects`、`action().apply`）继续可用；新写法是 `semantic.ontology()`，与 OSDK 同形。示例取自 Demo Company 的示例本体（[打开示例](https://www.ai-dc.ai/nexus/s/cQjivrjhrGGGbOMurJ-4HR3OSiAW0wln)，要先登录 AIDC 账号；命名空间 `cell-demo`，全部是虚构数据；定义与数据在 `developer/apps/ontology-demo-data/`，应用在 `developer/apps/ontology-demo/`）。

### 语义部分（名词）

| 构件 | 是什么 | Demo 里的例子 |
| --- | --- | --- |
| **Object Type** | 一类对象：主键、title、属性 | `customer` 客户公司、`agent` 智能体、`application` 应用、`modelUsage` 模型用量、`cloudCost` 云费用 |
| **Property** | 属性；base type 照官方（string、integer、long、double、decimal、boolean、date、timestamp、array、struct、geopoint、vector、timeseries…） | `customer.location`（geopoint）、`customer.contact`（struct）、`modelUsage.dailyTokens`（timeseries） |
| **Derived Property** | 沿链接读时计算（最多 3 跳；count、sum、avg、min、max、collectList、collectSet） | `customer.agentCount` = 智能体个数；`customer.modelCostUsd` = 模型费用合计 |
| **Link Type** | 两个 Object Type 的关系，两端各有名字；1:N 用外键，N:M 另存每一条链接 | `customerAgents`：`customer.agents` ↔ `agent.customer`；`applicationAgents`（N:M） |
| **Interface** | 多个 Object Type 共有的形状，可以继承 | `Billable`（继承 `Monthly`）：`modelUsage` 和 `cloudCost` 都实现它，可以一起按月汇总成本 |
| **Shared Property** | 跨类型复用的属性定义 | `costUsd`、`month` |
| **Value Type** | 带约束的类型（enum、range、length、regex…），写入时校验 | `cellId`（`cell-` 开头）、`yearMonth`、`customerStage`（试点 / 付费 / 暂停） |

### 动力部分（动词、函数）

| 构件 | 是什么 | Demo 里的例子 |
| --- | --- | --- |
| **Action Type** | 受控的写操作：参数 + 规则（createObject、modifyObject、createOrModifyObject、deleteObject、createLink、deleteLink）+ 提交条件（submission criteria） | `adjust-seats`：座位数不能少于成员数；`assign-agent`：给应用接入一个智能体（建一条 N:M 链接） |
| **Action Log** | 开了 `actionLog` 的 Action，每次成功执行都写一个 `log.<action>` 对象 | `log.adjust-seats`：谁在什么时候把哪家的座位数改成多少 |
| **Object Set** | 对象的集合：过滤、并 / 交 / 差、沿链接走（searchAround）、按接口取、派生属性 | 付费客户的全部智能体 |
| **Function** | 代码写的逻辑，在隔离运行时里执行；语义化版本，发布后不能改 | `aidc semantic functions publish count.js --api-name countPaidCustomers --version 1.0.0 --output '{"type":"integer"}'`（ES module，默认导出 `(params, ctx) => 结果`，`ctx` 读对象）；查询经 `/api/v2/ontologies/{ns}/queries/countPaidCustomers/execute` 执行，也能做自动化的 Function 效果 |

### 定义

定义还是 JSON，`kind` 用标准名（`objectType`、`linkType`、`actionType`、`interfaceType`、`sharedPropertyType`、`valueType`；旧的 `object`、`link`、`action`、`enum` 照旧可用）。

```json
{
  "kind": "linkType",
  "apiName": "customerAgents",
  "title": "公司的智能体",
  "schema": {
    "from": "customer", "to": "agent", "cardinality": "ONE_TO_MANY",
    "apiNameAtoB": "agents", "apiNameBtoA": "customer",
    "foreignKey": { "side": "to", "property": "customerId" }
  }
}
```

```json
{
  "kind": "actionType",
  "apiName": "adjust-seats",
  "title": "调整座位数",
  "schema": {
    "parameters": [
      { "name": "customer", "type": "object", "objectType": "customer", "required": true },
      { "name": "seats", "type": "integer", "required": true, "min": 1, "max": 10000 }
    ],
    "rules": [{ "type": "modifyObject", "objectType": "customer", "object": "$customer", "values": { "seats": "$seats" } }],
    "submissionCriteria": [{
      "condition": { "type": "comparison", "left": { "param": "seats" }, "operator": "gte", "right": { "param": "customer", "property": "members" } },
      "failureMessage": "座位数不能少于成员数"
    }],
    "actionLog": true
  }
}
```

名字照官方大小写：Object Type 与 Interface 用 PascalCase（大写字母开头、只含字母和数字，如 `Employee`、`GreigeInStore`；Object Type 名在本体里不分大小写唯一，`ontology`、`object`、`link` 这类保留字不能用），Property、Link Type 两端、Value Type、Shared Property 用 camelCase，Action Type 用 kebab-case。早先小写开头的 Object Type 名（如 `customer`、`production.order_line`）照常能用，定义时给警告；新的类型请用 PascalCase。

提交条件里比较的值可以是参数（`{ "param": … }`，对象参数可带 `property`）、常量（`{ "literal": … }`），或执行人（`{ "user": "id" | "name" | "role" | "authorKind" }`）。`authorKind` 是执行人是人还是智能体（`human` / `agent`；Agent Key 和声明了智能体身份的开发者 Key 都是 `agent`）。「只有人能批」写成 `{ "type": "comparison", "left": { "user": "authorKind" }, "operator": "is", "right": { "literal": "human" } }`——用肯定的写法，不要写成「不是 agent」。用了 `authorKind` 的 Action 暂时只能经 v1 接口、CLI 或 Semantic 界面执行，v2 接口（OSDK）会回 403。

从零做一个会执行的 Action（定义、只校验、执行、提交条件、命令行 / SDK / REST、做成应用、交给自动化）：Academy 课程[《动手：做一个会执行的 Action》](https://www.ai-dc.ai/academy/semantic-action)，配套样板见[样板应用](samples.md)的「座位申请」。新的 Object Type 只能有一个主键属性（旧的复合主键类型照旧可用，定义时给警告）。

定义里写了不认识的字段（多半是拼错了，比如把 `submissionCriteria` 写成 `submissionCriterias`）会直接被拒，错误信息给出路径——以前这类字段会被悄悄丢掉，Action 就少了提交条件。

### Action 调外部系统：webhook（writeback 与 side effect）

webhook 挂在 Data Connection 的**数据源**上，凭证在数据源里（平台加密保存），不经过调用方。Action 可以把 webhook 配成两种：

- **writeback**（一个）：校验通过之后、规则之前调用；外部系统拒绝（或超时）就整个 Action 不生效，语义层一条都不改，原因原样返回给执行人（`webhook_failed`）。输出在规则里用 `$writeback.<输出名>`。
- **side effect**（`sideEffects`，最多 10 个）：改动提交之后调用，可以多个、不保证顺序；失败不回滚改动、也不报给执行人——结果记在这次 Action 的执行记录里（失败另记一条错误事件）。适合发消息、通知、写到别的系统。

```json
{
  "kind": "actionType",
  "apiName": "stop-ec2-instance",
  "title": "关机",
  "schema": {
    "parameters": [{ "name": "instance", "type": "object", "objectType": "ec2Instance", "required": true }, { "name": "reason", "type": "string" }],
    "submissionCriteria": [{ "condition": { "type": "comparison", "left": { "param": "instance", "property": "state" }, "operator": "is", "right": { "literal": "running" } }, "failureMessage": "只有运行中的实例能关机" }],
    "webhooks": {
      "writeback": { "webhook": "aws/ec2-stop-instances", "inputs": { "region": "$instance.region", "instanceId": "$instance.instanceId", "expectedLaunchTime": "$instance.launchTime" } },
      "sideEffects": [{ "webhook": "im-robot/send-text", "inputs": { "content": "$reason" } }]
    },
    "rules": [{ "type": "modifyObject", "objectType": "ec2Instance", "object": "$instance", "values": { "lastAction": "stop", "lastActionAt": "$now", "lastActionResult": "$writeback.currentState" } }],
    "roles": ["developer"],
    "actionLog": true
  }
}
```

- `webhook` 写成 `<数据源>/<名字>`（如 `aws/ec2-stop-instances`、`im-robot/send-text`），或 webhook 的 RID。输入的写法与规则相同：`$参数`、`$对象参数.属性`、字面量；side effect 的输入还可以用 `$writeback.<输出名>`。定义时校验：webhook 存在、必填输入都给了、没有多余的输入、`$writeback.<输出>` 是它的输出、这个组织能用那个数据源。
- 规则只写只在语义层维护的属性（`writeback: true`）；状态这类从数据源来的属性等数据源同步回来——规则改它会一直盖住之后同步来的值（定义时会提醒）。
- 只校验（`$validateOnly`）与预演不调用外部系统（预演给出会怎么调）；有 writeback 的 Action 一次只能提交一个请求（不收 applyBatch）。
- 平台提供的数据源 `aws` 只给 AIDC 自己的组织（`aws/ec2-stop-instances`、`aws/ec2-start-instances`、`aws/ec2-set-tag`）。样板：`developer/apps/aws-servers`。

#### 自己的系统：REST API 数据源上的 webhook

自己的系统（企业 IM 的机器人、工单系统、ERP 的接口……）先建一个 **REST API 数据源**（Connection，`configuration.type` 写 `rest`：一个或多个 domain，每个 domain 的认证 Basic / Bearer Token / API Key（请求头或查询参数），带认证的 domain 必须是 HTTPS；附加秘密给模板用），再在它上面建 webhook：

```json
{
  "connection": "im-robot",
  "apiName": "send-text",
  "displayName": "群里发一条文本",
  "inputs": { "content": { "type": "string" } },
  "calls": [{
    "method": "POST",
    "path": "/robot/send",
    "body": { "type": "json", "value": { "msgtype": "text", "text": { "content": "{{content}}" } } },
    "extract": { "errcode": "/errcode", "errmsg": "/errmsg" }
  }],
  "outputs": { "errcode": { "type": "integer" }, "errmsg": { "type": "string" } },
  "limits": { "timeoutMs": 10000, "rateLimit": { "executions": 20, "per": "minute" } }
}
```

- **输入**：`boolean`、`integer`、`long`、`double`、`string`（`allowedValues` 限定取值）、`date`、`timestamp`、`list`（`elementType`）、`record`（`fields` 要求的键 / `valueType` 其余键）、`optional`（`wrappedType`，可以不给）；不是 `optional` 的都必填。
- **请求**（`calls`，最多 10 个，按顺序执行）：`method`、`domain`（数据源有多个 domain 时写 host）、相对路径 `path`、`query`、`headers`、`body`——`json`（写成 JSON）、`formUrlEncoded`、`text`（`contentType` 可选 `text/plain`、`application/json`、`application/xml`、`text/xml`、`text/html`）。数据源的认证自动加上，请求里不再写。最多一个会改外部系统的请求（GET / HEAD / OPTIONS 以外的方法；换令牌这类不改数据的 POST 标 `"isHttpMethodSafe": true`）。
- **模板**：`{{名字}}` 原样插入文本，`{{json 名字}}` 插入 JSON，`{{名字.键.0}}` 取记录的键 / 列表的下标；`json` 请求体里整个字符串恰好是一个 `{{名字}}` 时换成那个值本身（数字仍是数字）。`{{secrets.<名字>}}` 取数据源的附加秘密——只在发送那一刻解开，预演与任何回显里都打码。
- **串起来**：`extract` 用 JSON pointer 从响应里取值（`"/"` 整个响应、`"/a/b"` 按键、`"/items/0"` 按下标），取出来的变量给后面的请求用（`{{accessToken}}`）——比如先 `GET /gettoken` 换令牌，再带着令牌发消息。
- **输出**：`outputs` 的 `from` 指一个抽取变量（不写 = 同名的抽取变量，再没有就取最后一个响应里同名的顶层字段）。很多 IM 接口出错时仍回 HTTP 200、把错误放在 `errcode` 里：把它配成输出，执行记录里就看得到。
- **上限**：`timeoutMs`（缺省 20 秒，最多 180 秒）、`rateLimit`（每秒 / 分钟 / 小时 / 天几次，缺省每分钟 30 次）、`concurrency`（同时几个，缺省 10）、`retryableStatusCodes`（遇到就再试，最多 2 次）。
- **出口**：平台从公网直连；解析到内网 / 本机地址的 domain 会被拒绝。
- 建（按 `apiName` 幂等，定义变了版本 +1）、读、删（还有 Action 在用时拒绝）、手动试：

```bash
aidc semantic connectivity webhook put --ontology cell-demo --file send-text.json [--dry-run]
aidc semantic connectivity webhooks --ontology cell-demo [--connection im-robot]
aidc semantic connectivity webhook test <webhookRid> --inputs '{"content":"测试"}' --dry-run   # 只渲染请求（秘密打码），不发
aidc semantic connectivity webhook test <webhookRid> --inputs '{"content":"测试"}'             # 真发一次：输出、每个请求的状态 / 耗时 / 大小
```

API：`GET|POST /api/v1/connectivity/webhooks?ontology=<组织>`、`GET|PUT|DELETE /api/v1/connectivity/webhooks/{webhookRid}`、`POST /api/v1/connectivity/webhooks/{webhookRid}/execute`（`{ "inputs": {…}, "dryRun": true }`）。webhook 的权限跟着数据源走：看、建、改、删、手动试都只给本组织开发者（数据源、同步、agent、出口策略的配置同样只给开发者看——连到哪里、怎么认证、查什么只该让管线开发者知道；其他人经 Object Type 拿数据）；经 Action 调用只看 Action 的权限。

### Action 的通知：执行后发邮件

Action 的 Notification side effect：Action 可以配 `notifications`——所有改动提交之后，给收件人每人发一封邮件。派任务、转派、提交验收、验收通过这类「要让某个人知道」的 Action 用它。

```json
{
  "kind": "actionType",
  "apiName": "create-dev-task",
  "title": "派任务",
  "schema": {
    "parameters": [{ "name": "title", "type": "string", "required": true }, { "name": "assignee", "type": "object", "objectType": "employee", "required": true }],
    "rules": [{ "type": "createObject", "objectType": "devTask", "values": { "taskId": "$uuid", "title": "$title", "assigneeId": "$assignee", "assigneeAccountId": "$assignee.accountId", "assignedAt": "$now", "status": "待接受" } }],
    "notifications": [{
      "recipients": "$assignee.accountId",
      "subject": "新任务：{{{title}}}",
      "body": "{{{actionTriggerer}}} 派给你一个任务：{{{title}}}",
      "link": { "text": "打开任务", "target": { "type": "newObject", "objectType": "devTask" } }
    }],
    "actionLog": true
  }
}
```

- **收件人**：`$参数`、`$对象参数.属性`（对象上存的 AIDC 账号 ID）、`$user`（执行人），或固定的账号 ID；可以写成数组。收件人必须是这个组织的人（本组织账号、持本组织 License）。
- **内容**：`subject`（≤ 250 字）、`body`（≤ 1,000 字，超长截断）；`{{{参数}}}`、`{{{对象参数.属性}}}`（对象参数写它的标题）、`{{{actionTriggerer}}}`（执行人）。内容按改动之前渲染。`link` 指对象参数、这次新建的对象（`newObject`），或一个 URL（`https://…` / 站内 `/…`）。
- **权限**：缺省有一个收件人不合格（不是本组织的人、账号停用、看不到这次引用的数据）整个 Action 就不执行；`"notificationSettings": { "renderingSettings": "anyNotificationRenderingCanFail" }` 改成只发给合格的人。
- 预演（`$validateOnly` + preview）返回会发给谁、标题与正文，不发。每封都记在外发账里（`aidc notify` 看得到）；邮件通道没配时记为「未配置」。

到期提醒（逾期、超过一天没接受）用上面「自动化（Automate）」里的 Run on all objects：见 `developer/apps/dev-work/automations/`。

### 读与写（OSDK 写法）

```js
const client = semantic.ontology();                        // 应用里自动取应用所在公司；CLI / 智能体用 Key 所属公司

const paying = client.objects("customer").where({ stage: "付费", seats: { $gt: 30 } });
const page = await paying.fetchPage({ $orderBy: { seats: "desc" }, $pageSize: 20 });
// page.data[i]：{ __primaryKey, __apiName, __title, name, seats, location: { type: "Point", coordinates: [...] }, … }

const agents = await paying.pivotTo("agents").fetchPage();  // 沿链接走（searchAround）
const one = await client.objects("customer").fetchOne("cell-demo-a");

// 聚合（结果形状同 OSDK）
const byMonth = await client.interface("Billable").aggregate({ $select: { "costUsd:sum": "desc" }, $groupBy: { month: "exact" } });

// 派生属性
const withCost = client.objects("customer").withProperties({ apps: (b) => b.pivotTo("applications").aggregate("$count") });

// Action：先只校验，再执行
await client.action("adjust-seats").applyAction({ customer: "cell-demo-a", seats: 60 }, { $validateOnly: true });
const r = await client.action("adjust-seats").applyAction({ customer: "cell-demo-a", seats: 60 }, { $returnEdits: true });
// r.validation.result = VALID / INVALID（参数逐项、提交条件逐条）；r.edits：新建 / 修改 / 删除了哪些对象和链接
```

where 写法：`$eq $ne $gt $gte $lt $lte $isNull $in $contains $startsWith $containsAnyTerm $containsAllTerms $containsAllTermsInOrder $matchesRegex $within $and $or $not`；聚合：`$count`、`属性:sum|avg|min|max|exactDistinct|approximateDistinct`，分组 `exact`、`$fixedWidth`、`$ranges`、`$duration`。

#### Python（客户箱上的智能体与 Loop 脚本）

客户箱上的智能体（数字员工）和它们的 Loop 脚本用 `aidc_semantic.py`（`developer/semantic-agent/`，只用 Python 标准库），写法和上面一一对应：

```python
from aidc_semantic import ontology, sql, connect, publish_loop_report

client = ontology()
stock = client.objects("stock.finishedInStore").where({"storeNo": "G02", "stockQty": {"$gt": 0}})
page = stock.fetch_page(page_size=50, order_by={"storeInDate": "desc"}, select=["orderNo", "stockQty"])
for o in stock.iterate(select=["orderNo", "stockQty"]): ...            # 一页一页读完
stock.aggregate({"$select": {"stockQty:sum": "desc"}, "$groupBy": {"materialTypeName": {"$exactWithLimit": 100}}})
stock.count()
client.action("adjust-seats").apply({"customer": "cell-demo-a", "seats": 60}, validate_only=True)

rows = sql('SELECT "orderNo", sum("stockQty") AS kg FROM "stock.finishedInStore" GROUP BY 1')   # Ontology SQL
conn = connect(source="hsdyeingerp")              # 原来直连 ERP 的脚本：只换连接，T-SQL 照写（改写到原表镜像；改写器 aidc_semantic_tsql.py 随箱上安装）
publish_loop_report(html, slug="daily-output", title="日产量日报")    # 整页看板发布进 Semantic（受限 Key）
```

- 凭证按顺序找，环境变量没有就读 `$HERMES_HOME/.env`（智能体自己的 profile；不去 `~/.env`、`./.env` 找）：`AIDC_SEMANTIC_TOKEN`（Automate 运行时自带，只认环境变量）→ `AIDC_API_KEY`（显式给的开发者 Key 或只读 Agent Key，本机调试用）→ 平台给箱上系统用户发的只读 Key（`/etc/aidc/semantic/<用户>.key`，与 `aidc-semantic` 命令同一个文件；组织也从这里来）。智能体不用配，也不该自己去找 Key：能读什么由它的服务用户在组织里的授予决定（下一节）。发布看板另用受限 Key `AIDC_AGENT_KEY`。组织 `AIDC_SEMANTIC_ONTOLOGY`。
- 智能体的 Key 在 Semantic 上代表这个智能体的**服务用户**：不能登录、默认没有任何角色，组织管理员把它加进一个组、给组授予 Ontology（或某些 Object Type）上的 Viewer 后才读得到；组成员可以带到期时间（临时开通）。人持有的 Key（账号认领的、桌面兑换的）按这个人自己的权限判。没有角色时接口回 403，信息里写着是哪个服务用户（`svc.<组织>.<智能体>`）。
- 只读 Key 的 scope：`api:use-ontologies-read` 管读对象的全部接口（查 / 聚合 / 计数、对象集、链接、媒体与附件、元数据），只读列出的类型（`*` = 全部公司级类型）；Ontology SQL 另要 `api:use-sql-queries-execute`，且只给 `*` 的 Key。
- Key 只发给 `https://www.ai-dc.ai`（调试可用 localhost），不跟随重定向。
- 源查询的目录路径 `AIDC_SEMANTIC_CATALOG` 同样先读环境变量，再读 `$HERMES_HOME/.env`。未配置时使用共享目录 `/opt/aidc-semantic/state/erp-catalog.json`，SDK 副本不会到自己的安装目录另找一份；其他目录请显式配置。配置路径改变后重新加载目录，指定文件读取失败时报错，不退回旧缓存。
- 多个 `$groupBy` 字段时，总组数受第一个字段的上限约束——要的组多，每个字段的上限都写够。
- `fresh=True`（`fetch_page` / `aggregate`）：由 ERP 同步来的类型过期时先等同步完（最多 20 秒）。

### 本体由智能体构建

智能体不能直接改 main 上的本体。它在分支上改，开提案，再按组织的审批策略审核、合并：

1. `aidc semantic branch create add-department`：开分支（基线 = 当前语义版本）。
2. `aidc semantic branch modify add-department ontology/`：把定义放到分支上（一整份清单，`--dry-run` 试跑，`--expected-version` 防止并发覆盖）。
3. `aidc semantic objects agent --branch add-department`：在分支上预览（读接口都收 `?branch=`）；`branch validate` 看 merge checks，`branch conflicts` 看和 main 冲突的地方，`branch rebase` 跟上 main。
4. `aidc semantic branch propose add-department --title … --trigger "为什么要改"`：开提案。平台自动附上按资源的改动、校验结果、影响面（30 天的写入与活跃用户、依赖的应用）、破坏性改动。
5. 逐项批准（破坏性改动要输入资源名确认），全部批准、merge checks 通过后合并：落到 main，发一个语义版本。审批策略是 review 时，人在 `/developer/<公司>/ontology/proposals/<id>` 上做；是 yolo 时，合格的作者开提案就自动批准、合并，别的提案也可以用 `aidc semantic proposal approve <id>` / `proposal merge <id>` 做。

作者由凭证决定：Agent Key 一律是智能体；开发者 Key 在环境变量 `AIDC_AGENT_ID` 存在时，CLI 自动带 `x-aidc-author: agent:<id>`，作者也记为智能体。智能体作者直接定义 main 会得到 409 `branch_required`；智能体作者也不能直接写数据（新建、修改、删除、导入会得到 403），改数据走 Action。

### 审批策略：review 与 yolo

每个组织有一个审批策略，管提案的批准、合并与应用的发布。`aidc semantic approval-policy` 查看，`aidc semantic approval-policy set review|yolo [--dry-run]` 修改（只给本组织开发者本人：网页登录，或没有声明智能体身份的开发者 Key；智能体不能改）。

| | review（缺省） | yolo |
| --- | --- | --- |
| 谁能批准、合并 | 本组织开发者的网页登录 | 网页登录，加上能改这个本体的 Key：开发者 Key（含智能体在用的），或代表的主体在 Ontology 上是 Editor / Owner 的 Agent Key |
| 作者自己的批准 | 不算数 | 作者能改这个本体就算数（Contributor approval） |
| 检查通过后 | 等人点「合并」 | 自动合并 |
| 发布智能体写的应用版本 | 人在网页上点 | 开发者 Key 也能发布 |

两种策略下都不变：安全策略的改动要满足相关 Marking 的人批准（作者满足不了，这一项留给别的审核人）；merge checks 不过合不了；任何一项被驳回都合不了。每一次批准、驳回、合并、发布、改策略都记进审计日志（谁、经网页还是 Key、结果）。只读（Viewer）的智能体照样能开提案，批准要交给能改这个本体的审核人。改回 review 时，智能体给的批准作废、回到待审核。

### 和 ERP、OA 的关系

ERP、MES、OA 缺省只读：发布端把数据推进数据流，按 Object Type 定义里的 `datasources` 同步进来。人和智能体的改动只走 Action，落在语义层（对象的 edits），数据源下一次同步不会冲掉它们。要真的改源系统（关一台服务器、回写一张单据），给 Action 配 writeback webhook（见上一节）：源系统先接受，语义层才记一笔。

## Semantic 平台（www.ai-dc.ai/semantic）

登录 [ai-dc.ai/semantic/app](/semantic/app) 就进到自己组织的 Semantic；不属于任何组织的账号落到 Public。界面分成下面这些应用，只调这一页写的同一套 API：

| 页面 | 地址 | 做什么 |
| --- | --- | --- |
| 概览 | `/semantic/<cell>` | Object Type 一览（对象数、属性、链接、Public 标记）、最近的变化、待审核的提案、数据库状态 |
| Ontology 图 | `/semantic/<cell>/graph` | 画布：Object Type 是节点、Link Type 是边（多对多虚线）；拖动、缩放、点开看属性与关系 |
| Object Types | `/semantic/<cell>/object-types[/<apiName>]` | Ontology Manager：属性（base type、SQL 列、主键 / 标题 / Value Type / 派生 / 只在语义层）、链接、用到它的 Action、数据源、Share |
| Object Explorer | `/semantic/<cell>/objects/<apiName>[/<主键>]` | 表格：搜索、筛选、排序、按属性看分布；Object View：属性、每条链接上的对象、编辑历史、能执行的 Action |
| Actions | `/semantic/<cell>/actions[/<apiName>]` | 参数表单：先「只校验」（VALIDATE_ONLY）再提交；Action Log |
| SQL Console | `/semantic/<cell>/sql` | Ontology SQL（见下一节）；开发者在「连接」里轮换口令拿只读直连串 |
| Proposals | `/semantic/<cell>/proposals` | 智能体开的 Ontology proposal：逐项批准（可一次批准全部非破坏性的）、合并 |
| Access | `/semantic/<cell>/access` | 开放程度（Private / Group / Public / Open to Internet）、分享给谁、角色，见 [访问与账号](/developer/docs/auth) |
| Public | `/semantic/public` | 所有登录的 AIDC 账号都看得见的 Ontology、Object Type、数字员工、文件，以及「分享给我」 |

网页会话调 `/api/v1/ontologies/**` 时，角色由统一的访问判定决定（Private / Group / Public）：本组织开发者 = developer（与开发者 Key 同权）；本组织成员（Ontology 对组织可见时）= member；别的组织的人按授予——Editor = editor、Viewer = viewer（只读）；Ontology 与所有 Object Type 都没有角色 = 403。

## Semantic 数据库（Ontology SQL）

每个组织一个 Postgres schema（`ont_<组织>`），第一次用时按需建好，定义变了下一次查询时自动重建。按 Ontology SQL 的规则：

- **表**：每个 Object Type 一个视图，表名 = API name、列名 = 属性 API name（列类型：string → text、integer / long → bigint、double → double precision、decimal → numeric、date、timestamp → timestamptz，其余 jsonb）。每个多对多 Link Type 一个视图，两端的列名 `<objectTypeApiName>_<relationApiName>`（值是走这条链接到达的对象的主键）；每个 Interface 一个视图（实现它的类型 UNION ALL，另有 `__objectType`、`__primaryKey`）。一对多直接 join。派生属性是读时算的，不进 SQL。
- **数据**：视图直接读当前值（数据源 ⊕ 语义层改动），不复制；删除的、源头已消失的不出现。
- **上限**：只能一条 SELECT（可以 WITH / VALUES / TABLE 开头），最多 10,000 行、20 秒；位置参数 `$1、$2…`；`dryRun` = EXPLAIN。写入永远走 Action。
- **两个只读角色**：`reader`（本组织看得见的全部类型：本组织成员、开发者 Key、Agent Key、在 Ontology 上有 Viewer 以上授予的人）与 `public`（只有授予了 Everyone 的类型：只看得见 Public 的外部账号）。应用票据不走 SQL——在应用里用 `semantic.ontology()`。
- **直连**：本组织开发者可以轮换口令，拿到 reader 的连接串，用 psql、BI 工具、Notebook 只读直连（只读事务、20 秒超时、最多 10 个连接、只看得见本组织的视图）。口令由服务端派生、不落库，库里只有 SCRAM 校验值；再轮换一次旧口令立即失效。

```js
const r = await semantic.sql(`
  SELECT o.name, count(a.*) AS agents
  FROM organization o LEFT JOIN agent a ON a."organizationId" = o."organizationId"
  GROUP BY 1 ORDER BY 2 DESC`);
const cost = await semantic.sql('SELECT month, sum("costUsd") FROM "modelUsage" WHERE "organizationId" = $1 GROUP BY 1', { parameters: ["cell-aidc"] });
const db = await semantic.database();                       // 表与列、我用哪个角色
const conn = await semantic.rotateDatabaseCredentials();    // 开发者：psql "<conn.uri>"
```

```bash
aidc semantic sql 'SELECT stage, count(*) FROM organization GROUP BY 1' [--csv]
aidc semantic database --rotate
```

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/api/v1/sqlQueries/executeOntology` | 执行（照官方 ExecuteOntologySqlQueryRequest：`query`、`parameters`、`rowLimit`、`dryRun`、`ontologyIdentifier`）；响应是 JSON（官方是 Arrow） |
| GET | `/api/v1/ontologies/{ns}/database` | 数据库信息；开发者另有直连信息（不含口令） |
| POST | `/api/v1/ontologies/{ns}/database/sync` | 按当前定义重建视图（开发者） |
| POST | `/api/v1/ontologies/{ns}/database/credentials` | 轮换直连口令，新连接串只返回这一次（开发者） |

## 旧写法（1.2.x：describe / objects / action / define / publish）

1.2.x 起的写法照样能用（和上面同一份数据、同一套 Action 引擎），新代码请用上面的 `semantic.ontology()`。

### 三个概念

| 概念 | 是什么 | 例子（生产进度看板） |
| --- | --- | --- |
| **对象类型** | 一类业务对象：主键 + 属性。属性要么来自数据源（同步进来，只读），要么只在语义层维护（`writeback`：备注、负责人、异常状态…） | `production.order_line`（工单线体进度）：工单、线体、计划、累计合格 ← SAP；异常状态、异常说明、登记人 ← 语义层 |
| **链接** | 对象之间的关系。最常见的是一个属性引用另一个类型的主键（`references`） | 工单线体 → 线体（`lineCode`） |
| **Action** | 受控的写操作：参数（类型 / 必填 / 长度 / 枚举）+ 改哪些属性 + 谁能做 + 日志里的一句话 | `production.flag_issue` 登记异常：参数「异常说明」「级别」；写 `issue`、`status`、`owner = 执行人` |

改数据只有两条路：**成员用 Action**（校验、权限、留痕）；**开发者用旧数据层直接改**（管理用）。两条路的改动都落在语义层（对象的 edits），数据源下一次同步只更新它自己那一层，不会冲掉人改过的值。

### 读：说明书与对象

```js
const space = await semantic.describe();
// space.types：看得见的对象类型（属性、单位、同义词、链接、对象数）
// space.actions：Action 与「我能不能执行」（allowed）
// space.markdown：给大模型读的整份说明书——智能体先读它再干活
// space.release：当前语义版本（v3…）

const hit = semantic.search(space, "合格数");        // 按业务说法定位：[{ kind: "property", type: "production.order_line", name: "ok" }]

const issues = await semantic.objects("production.order_line")
  .where({ status: { in: ["关注", "异常"] }, day: "2026-09-26" })
  .sort("-gap")
  .list();                                           // { objects, total, seq }

const order = await semantic.objects("production.order_line").get("100000012345|L01-05");
const line = await semantic.objects("production.order_line").linked(order.pk, "production.order_line.lineCode");
const byLine = await semantic.objects("production.order_line").aggregate({ groupBy: ["lineCode"], metrics: { ok: ["ok", "sum"] } });
```

每个对象：`props`（当前值 = 数据源的值被语义层改动覆盖后）、`rev`（每改一次 +1）、`edited`（语义层改过的属性）、`overridden`（被改动盖住、数据源现在是另一个值的属性 → 数据源的值）、`sourceGone`（数据源里已经没有这一行）。

条件写法：`{ 属性: 值 }` 相等；`{ 属性: { gt, gte, lt, lte, ne, in, nin, contains, startsWith, null } }`；多个属性之间是「且」；`$or: [{…}, {…}]` 任意一组成立。

### 实时

```js
const view = semantic.objects("production.order_line").where({ day: today }).sort("-ok").live({
  onUpdate(objects, change) { render(objects); },   // 先全量，之后数据一变就回调（同步、Action、别人的修改都算）
  onStatus({ connection }) { badge(connection); },
});
// view.close()
```

底层是旧数据层的 `watch`（SSE，断线按序号续传，不丢不重）。

### 写：执行 Action

```js
const r = await semantic.action("production.flag_issue").apply(
  { issue: "缺料：顶蓬面料未到", severity: "异常" },
  { object: "100000012345|L01-05", ifRev: order.rev },
);
// r.object：改后的对象；r.event.summary：「L01-05 登记异常：缺料…」（进操作记录）
```

- 参数按定义校验（不合法 422，信息里写明哪一项）；`semantic.checkParams(actionInfo, params)` 可以在表单提交前本地预检。
- 谁能执行：应用清单 `semantic.actions` 登记了它，且访客角色在 Action 的 `roles` 里（缺省 developer / member / editor）。只读访客（公开链接、只读分享）永远不能写；一律登录起没有匿名访客。
- `ifRev`：对象当前 rev 不等于它就 409 `rev_mismatch`（别人刚改过，刷新再改）。
- `dryRun: true`：只校验、返回计划，不落库。

### 定义与发布（开发者：CLI / 智能体）

定义就是 JSON。四种词条：`enum`、`object`、`link`、`action`。

```json
{
  "kind": "object",
  "apiName": "production.order_line",
  "title": "工单线体进度",
  "description": "每个工单在每条线体上的当日进度（SAP 生产进度报表）",
  "schema": {
    "titleColumn": "lineCode",
    "synonyms": ["工单行", "生产任务"],
    "columns": [
      { "name": "orderLineKey", "type": "text", "primaryKey": true, "title": "工单|线体" },
      { "name": "orderNo", "type": "text", "title": "工单" },
      { "name": "lineCode", "type": "text", "title": "线体", "references": { "entity": "production.line", "column": "lineCode" } },
      { "name": "ok", "type": "integer", "title": "累计合格", "unit": "件", "synonyms": ["合格数", "良品数"] },
      { "name": "status", "type": "text", "enumRef": "production.issue_status", "writeback": true, "title": "异常状态" },
      { "name": "issue", "type": "text", "writeback": true, "title": "异常说明" }
    ]
  }
}
```

```json
{
  "kind": "action",
  "apiName": "production.flag_issue",
  "title": "登记异常",
  "schema": {
    "objectType": "production.order_line",
    "operation": "modify",
    "parameters": [
      { "name": "issue", "type": "text", "title": "异常说明", "required": true, "maxLength": 200 },
      { "name": "severity", "type": "text", "title": "级别", "enumRef": "production.issue_status" }
    ],
    "edits": { "issue": "$issue", "status": "$severity", "owner": "$userName", "flaggedAt": "$now" },
    "roles": ["developer", "member", "editor"],
    "summary": "{lineCode} 登记异常：{issue}"
  }
}
```

名字：一个 Object Type 恰好一个主键属性；Object Type 名用 PascalCase（小写开头的旧名照收、给警告）；属性与 Action 参数的 API name 是 camelCase（小写字母开头、只含字母和数字）。早先定义的 snake_case 属性 / 参数与复合主键原样保留（定义时给警告），新加的一律按这条规则，不合规的整批拒收。

取值表达式：`$参数名`、`$now`（当前时间）、`$user`（执行人账号）、`$userName`（执行人名字）、`$uuid`（新主键，create 用）；其他是字面量（以 `$` 开头的字面量写成 `$$…`）。`operation`：`modify` 改一个对象、`create` 新建（edits 要给出主键）、`delete` 删除（语义层墓碑）。

```bash
aidc semantic define ontology/            # 目录里的 *.json 整批提交：一起校验、按 枚举 → 对象 → 链接 → Action 落（有则改）
aidc semantic define ontology/ --dry-run  # 只检查：整批引用是否闭合（批内互相引用算数）、相对上一个语义版本有哪些破坏性变更
aidc semantic publish --notes "加了异常登记"   # 发一个语义版本 vN（只增；定义没变不占号）
aidc semantic describe --markdown         # 给智能体读的说明书
aidc semantic act production.flag_issue --object "100000012345|L01-05" --param issue=缺料 --param severity=异常
```

- **引用闭合**：Action 改的属性、引用的参数、外键目标、枚举都必须存在，否则定义直接 422。一个目录是一批（`semantic.defineAll(定义数组)` / `POST …/semantic/{命名空间}/types`）：在「整批改完之后」的全体定义上校验，批内互相引用算数，不闭合一条都不写。
- **破坏性变更**（删类型 / 删属性 / 改类型 / 改主键 / Action 删参数或新增必填参数…）会在定义时返回、在发布时记档（`changeKind: breaking`）。发布前先确认调用它的应用都改好了。
- **语义版本**就是这家公司 Ontology 的版本号，与应用版本号一起出现在应用日志里；自进化 SDK 采纳的语义改进会自动发新版本。

## 权限一览

| 谁 | 读 | 执行 Action | 直接写（旧数据层） | 定义 / 发布 |
| --- | --- | --- | --- | --- |
| 开发者 Key（CLI / 智能体） | 本公司全部 | 全部 | 全部 | ✓ |
| 应用里的 developer 访客 | 清单 `semantic.types` 登记的（含部门级） | 清单登记 + roles 允许 | 清单 `semantic.write` 登记的 | — |
| 应用里的 member / editor | 清单登记、且对公司全员可见的 | 清单登记 + roles 允许 | — | — |
| viewer（只读分享 / 公开链接） | 同上 | — | — | — |

应用清单里这样登记：

```json
{
  "sdk": ["semantic", "ui"],
  "semantic": {
    "types": ["production.order_line", "production.line"],
    "actions": ["production.flag_issue", "production.resolve_issue"],
    "write": []
  }
}
```

`"*"` 表示看得见的全部（数据浏览器这类通用应用）。

上表之外，[数据安全](#数据安全markings-与安全策略)（Markings、对象 / 属性安全策略）对所有人生效，包括开发者 Key：挂了 Marking 的数据，不是成员就读不到。

## API

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/api/v1/developer/semantic/{命名空间}` | 说明书（按凭证裁剪） |
| GET / POST | `/api/v1/developer/semantic/{命名空间}/types` | 全部定义（开发者）/ 一次定义一批（`{definitions: […]}`，整批校验） |
| GET / PUT / DELETE | `/api/v1/developer/semantic/{命名空间}/types/{apiName}` | 读 / 定义 / 归档 |
| POST | `/api/v1/developer/semantic/{命名空间}/actions/{apiName}` | 执行 Action |
| GET / POST | `/api/v1/developer/semantic/{命名空间}/releases` | 语义版本列表 / 发布 |

旧的数据层（`/api/v1/developer/data/…`：查询、聚合、订阅、直接写）照样能用；新代码用下面的 Ontology API 与对象集订阅。命名空间 = 公司 cellId（`cell-…`）；应用里自动取应用所在公司。

Ontology API 是 Ontology REST API v2 的形状，前缀是 `/api/v1/ontologies/{命名空间}`，响应体放在信封的 `data` 里、字段照官方：

| 方法 | 路径 | 官方操作 |
| --- | --- | --- |
| GET | `/ontologies`、`/ontologies/{ns}`、`/{ns}/fullMetadata` | List / Get Ontology、Full Metadata |
| GET | `/{ns}/objectTypes[/{type}[/fullMetadata \| /outgoingLinkTypes[/{link}]]]` | Object Type、Outgoing Link Types |
| GET | `/{ns}/actionTypes`、`/interfaceTypes`、`/sharedPropertyTypes`、`/valueTypes`、`/queryTypes`（各带 `/{apiName}`） | 各类型的 List / Get |
| GET | `/{ns}/objects/{type}`、`/{type}/{pk}`、`/{type}/{pk}/links/{link}[/{pk2}]` | List Objects、Get Object、Linked Objects |
| POST | `/{ns}/objects/{type}/search`、`/aggregate`、`/count` | Search、Aggregate、Count |
| GET / POST | `/{ns}/objects/{type}/{pk}/timeseries/{property}/firstPoint`、`lastPoint`、`streamPoints` | Time Series |
| POST | `/{ns}/objectSets/loadObjects`、`/aggregate`、`/createTemporary`；GET `/{ns}/objectSets[/{rid}]` | Object Set |
| POST | `/{ns}/objectTypes/{type}/editsHistory` | Edits History |
| POST | `/{ns}/actions/{action}/apply`、`/applyBatch`（最多 20 个，一个事务） | Apply Action |
| GET / POST | `/{ns}/functions`（`?dryRun=true` 只校验） | AIDC：已发布的函数（最新版本与全部版本）/ 发布一个版本（开发者；智能体作者只能发预发布版本） |
| POST | `/api/v1/ontologySubscriptions/ontologies/{ns}/streamSubscriptions`（SSE） | Object set subscription（SDK 用） |
| GET | `/api/v2/ontologySubscriptions/ontologies/{ns}/streamSubscriptions`（WebSocket） | Object set subscription（官方 OSDK 直连） |
| POST | `/{ns}/datasources/platform/sync` | 同步 AIDC 平台数据（平台对象类型；开发者） |
| GET / POST | `/{ns}/branches[/{name}[/modify \| validate \| conflicts \| discard \| rebase \| lock \| fullMetadata \| proposals]]` | Global Branching |
| GET / POST | `/{ns}/proposals[/{id}[/tasks/{apiName}/review \| merge \| close]]` | Ontology proposal（review、merge：审批策略 review 只收网页登录，yolo 也收能改这个本体的 Key） |
| GET / PUT | `/{ns}/approval-policy` | 组织的审批策略（PUT 只给本组织开发者本人，`x-aidc-dry-run: true` 只看会变什么） |
| POST | `/{ns}/objectTypes/{type}/security/test` | AIDC：Test security policies（Ontology Manager 的同名功能，官方没有公开 API） |

Markings 的管理用 AdminV2 / FilesystemV2 的接口：`/api/v1/admin/markingCategories[/{id}]`、`/api/v1/admin/markings[/{id}[/markingMembers[/add \| /remove] \| /roleAssignments[/add \| /remove]]]`、`/api/v1/admin/markings/getBatch`、`/api/v1/admin/users/{userId}/getMarkings`、`/api/v1/filesystem/resources/{rid}/markings \| addMarkings \| removeMarkings`。

完整字段见 [OpenAPI](/developer/openapi.json)。
