# 闭环：从 ERP 到持续变好的应用

这一页以一个车间的生产进度看板为例（示例公司 Demo Company），把 Semantic（数据平台）的各部分和自进化串成一条线：**数据流**（ERP 只读推上来）→ **Ontology**（对象、Action）→ **数据源**（流接到哪个类型，写在定义里）→ **应用**（读对象集、实时订阅、用 Action 改）→ **访问**（谁能看、谁能改、分享）→ **留痕**（每次修改、应用日志）→ **自提升**（下一个版本）。车缝产量、库存、订单流程这类看板是同一套做法。

```
SAP / MES（只读）
   │  发布端：aidc semantic streams pipe + 适配器（客户箱上的 systemd 服务，不是 cron）
   ▼
Semantic ── 数据流 production-progress
   │         └─ Object Type 定义里的 datasources：哪一列对哪个属性
   ▼
Ontology ── 工单线体 / 线体、链接、Action「登记异常」「解决异常」「维护线体」
   ▼
Nexus 应用 · 智能体 ── 访问：成员可改、公开链接只读 ── 留痕：每次修改、打开、反馈、版本
   ▲                                                                     │
   └──────────────── 自进化 SDK：证据 → 提案 → 新语义版本 / 新应用版本 ◀──┘
```

## 1. 数据流：数据源只读地推上来

发布端装在客户箱上，读 SAP 的报工确认（AFRU）与生产进度报表，有变化才推一批到数据流 `production-progress`（见[数据流与数据源](connect.md)）。它只有一把只能往这条流发布的 Key，**永远不会写 SAP**。

## 2. Ontology：先定义「这是什么」

```bash
aidc semantic define ontology/          # production.issue_status（枚举）、production.line、production.order_line、三个 Action
aidc semantic publish --notes "生产进度首版"
```

`production.order_line` 的属性分两类：工单、线体、计划、累计合格、实际 JPH……来自 SAP（只读）；异常状态、异常说明、登记人、处理说明……只在语义层维护（`writeback`，即 editOnly）。Action 定义「登记异常」要哪些参数、改哪些属性、谁能做（见 [Semantic](semantic.md)）。

## 3. 数据源：流接到哪个类型，写在定义里

数据流的哪一列对哪个属性，是 Object Type 定义的一部分（backing datasource）。`production.order_line` 的定义里写：

```json
"datasources": [
  { "type": "stream", "stream": "production-progress", "mode": "mirror",
    "propertyMapping": { "orderLineKey": "key", "orderNo": "order", "lineCode": "line", "material": "material", "product": "product", "shift": "shift", "plan": "plan",
                         "ok": "ok", "gap": "gap", "jphActual": "jph_actual", "jphStd": "jph_std", "lastOp": "last_op", "day": "day" } }
]
```

属性 API name 是 camelCase，左边是属性、右边是流里的列（列名照 SAP 写）；主键 `orderLineKey` 对流的主键列 `key`（`<工单>|<线体>`）。线体由工单行汇成：`production.line` 接同一条流，只映射 `lineCode ← line`，`mode` 用 `upsert`。不改文件也行，命令改的同样是定义：

```bash
aidc semantic datasource set production.line --stream production-progress --map lineCode=line --mode upsert
aidc semantic datasource list                                      # 每个类型的数据源与同步进度
aidc semantic objects production.order_line --order-by gap:desc --page-size 5
```

定义落了之后，平台用流的当前全量同步一次；从这一刻起，SAP 每一次变化都进 Semantic：来自源头的属性更新，人改过的值不动。应用和智能体不再碰数据流，更不碰 SAP。智能体要改数据源，和改别的定义一样：在分支上改、开提案，人审核合并。

## 4. 应用：读对象集、实时订阅、用 Action 改

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

const client = semantic.ontology();
const lines = client.objects("production.order_line");
render((await lines.fetchPage({ $orderBy: { gap: "desc" } })).data);
lines.subscribe({ onChange: ({ object, state }) => patch(object, state), onOutOfDate: reload });   // 实时
const me = await semantic.admin.getCurrentUser();                                                   // 我能做什么
await client.action("production.flag_issue").applyAction({ __object: row.__primaryKey, issue: "缺料", severity: "异常" });
semantic.observability.feedback({ topic: "看板", message: "希望按班次筛选" });
```

清单登记用到的 SDK 与语义资源（部署时会核对 import，用了没登记的直接拒收）：

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

```bash
aidc app deploy app/        # → Developer 预览（test），版本号 = 清单 version
aidc app publish production-live --notes "语义层 + 异常登记"   # 测过的版本 → Nexus
aidc app registry           # 登记表：这个应用用了哪些 SDK、读写哪些类型 / Action
```

## 5. 访问：给谁用

- 本公司成员打开 `/nexus/cell-demo/apps/production-live` 就能看、能执行「登记异常」（Action 的 roles 含 member）。
- 班组长在别的公司账号下？`aidc share production-live --user 他的邮箱 --role editor --days 30`。
- 给客户或领导一个只读大屏链接：`aidc share production-live --public`（真实数据，发之前确认；随时 `aidc share revoke`）。
- 类型、Ontology 本身谁能看：Private / Group / Public / Open to Internet，见[访问与账号](auth.md)。

## 6. 留痕：用得怎么样

```bash
aidc semantic observability summary production-live --days 7
aidc semantic edits-history production.order_line --pk <主键>
```

打开几次、谁在用、登记了多少次异常、哪些反馈还没处理、每个版本号的状态（已上线 / 已被替换…）、语义版本——都在第一条里。第二条看一个对象的每次修改（谁、何时、改前改后）；开了 `actionLog` 的 Action 每次提交还会写一个 Action Log 对象。

## 7. 自进化：下一个版本

```bash
aidc evolve suggest production-live --file 生产日报口径.md --conversation 班组长群.txt
aidc evolve proposals --status open
aidc evolve accept <id> && aidc evolve apply <id>      # 语义类：改说明 / 同义词 / 加属性 → 自动发 v2
```

语义类提案由开发者直接应用；智能体作者要在分支上改、开 Ontology 提案，由人审核合并。应用类提案（「加按班次筛选」）由开发者或智能体改代码，`aidc app deploy` → 测试 → `aidc app publish`，最后 `aidc evolve apply <id> --version 1.2.0`；提案关联的反馈自动标记为已处理，反馈人下次打开就能看到改进。

## 8. 自动化：把各部门的应用串起来

每个部门的应用发布后，就是公司能力目录里的一组能力（清单 `exports` 显式导出的，和从登记的类型 / Action 自动提取的）。跨部门的事——比如报价要用营业的询价、设计的 BOM、采购的行情、制造的工艺、财务的核价口径——写成一个工作流应用：

```bash
aidc semantic automate capabilities                        # 看有哪些能力可以用
aidc app deploy quote-workflow --dry-run                   # 部署前对照能力目录校验
aidc semantic automate run quote-workflow --param rfq_no=RFQ-2609-002 --preview
```

按 Automate 的写法：`trigger.change` 是条件（对象新建 / 修改时，登记询价 → 自动出报价草稿），步骤是效果；画布上看得到每一步用了谁的能力、产出了什么。它写回的结果照样进 Semantic、有留痕、进应用日志——闭环里多了一条「部门之间」的线。见[自动化](workflow.md)。

## 数据放在哪里

- Semantic 的对象、变化记录、日志、分享、提案都在 AIDC 中枢（Supabase Postgres），按公司隔离；只存看板与协作需要的业务字段，身份证、银行卡、密码之类的字段在定义层就被拒收。
- ERP / MES 只被发布端读；发布 Key 只能往一条流发。
- 还是某个类型数据源的流删不掉，先从定义里去掉数据源（同步就停，对象保留）；删掉对象类型的定义会归档它（对象保留，直到开发者清理）。

## 9. 不写 cron：Loop 从 Semantic 读、由 Automate 触发（标准写法）

一个 Loop 拆成三件事，各有各的标准构件，不再是一段跑在客户箱上的脚本：

| Loop 里的一步 | 标准写法 | 在哪 |
| --- | --- | --- |
| **什么时候做**（定时、数据一变） | **Automation 的条件**：对象集条件 Objects added / modified / removed、Run on all objects、时间条件 | Semantic · Automate |
| **算**（读数据、汇总、判断） | **函数**（Function）：`aidc semantic functions publish` 发布，Automation 用 **Function 效果**执行（在平台的隔离运行时里跑，读 Semantic） | Semantic · Functions |
| **发**（发钉钉 / 企业微信、回写 ERP、调外部系统） | **Action** + **webhook**：Automation 用 **Action 效果**执行一个 Action；这个 Action 配一个 side effect webhook（规则之后调外部系统），webhook 挂在 Data Connection 的 REST Connection 上（凭证由平台加密保管，不在脚本里）。发给 AIDC 账号的用邮件通知效果 | Semantic · Action + Data Connection |

客户箱上的定时任务（每 5 分钟查一遍 ERP、和快照比、有新行就发钉钉）就这样换成 Semantic 的几件东西：**TableImport**（ERP 只被读一次）、**Object Type**（数据的业务名字）、**Automation**（数据一变 / 到点就做事）、**函数**与 **Action**（算与发）。名字用 Data Connection 与 Automate 的标准名。

```
ERP（只读账号）──▶ 平台的 worker 用 SQL 直接读（客户内网的库：经装在客户网络里的 agent proxy 开的隧道，agent 只出站、只转发字节）
                 ──▶ Dataset（每次执行一个事务）──▶ 只把变了的行写成 Object Type 的对象（Semantic）
                 ──▶ Automation（Objects added / modified / removed · 到点）
                 ──▶ Function 效果（算） · Action 效果（发：side effect webhook → 钉钉 / 企业微信 / ERP）· 邮件
```

不另开备份库：ERP 只被只读的 SQL 读，结果落进 Semantic 的 Dataset 与对象。

**① 连接与 TableImport（一次，开发者）**：客户内网里的库先装 agent proxy、建一条经它的出口策略；Connection 写 JDBC 地址和只读账号，口令写一次，平台加密保存、不再回显。查询只能是一条 SELECT，带窗口（例：近 45 天），只选 Loop 用得到的列。

```bash
aidc semantic connectivity agent register --ontology cell-demo --api-name site-box --display-name "现场机器"   # 凭证只给一次 + 三步安装
aidc semantic connectivity egress create --ontology cell-demo --file erp-via-agent.json        # agentProxy：ERP 的主机:端口，经 site-box
aidc semantic connectivity connection --ontology cell-demo --file stock-connection.json         # jdbc url + 只读账号 + 出口策略
aidc semantic connectivity import <connectionRid> --file inbound-lines.json      # SNAPSHOT 窗口；target = Object Type 与列 → 属性
aidc semantic connectivity execute <connectionRid> <tableImportRid> --wait       # 第一次全量，之后只写变化
```

`stock-connection.json` 的样子（字段与 API 请求体一致）：

```json
{
  "apiName": "stock-erp", "displayName": "库存 ERP",
  "configuration": { "type": "jdbc", "url": "jdbc:sqlserver://erp.internal.example.com:1433;databaseName=db_Stock", "driverClass": "com.microsoft.sqlserver.jdbc.SQLServerDriver",
    "credentials": { "username": "readonly", "password": { "type": "asPlaintextValue", "value": "<只读口令>" } } },
  "worker": { "type": "foundryWorker", "networkEgressPolicyRids": ["<出口策略 RID>"] }
}
```

文件里的口令只在建的这一次请求里用到，平台加密保存后不再回显：用完删掉文件；以后换口令用 `printf '%s' "$PASSWORD" | aidc semantic connectivity secret <connectionRid> Password`（从标准输入读，不进命令行）。

已经在用旧版 agent（客户箱上被叫醒、箱上比差）的连接，用迁移向导换过来：`aidc semantic connectivity migrate <connectionRid> --policies <策略 RID> --yes`（箱上登记的口令由 agent 交给平台加密保存；30 天内可以 `revert-migration` 退回）。

**② 读：自动连接**。读由 TableImport 供数的类型时，比 `freshness.maxAgeSeconds` 旧就按需同步一次（先给当前值；`?fresh=true` / `fresh=True` 等结果）。没人读 = 不同步 = 不花钱。

**③ 触发与发：Automation + Action 效果**。「这个合同的线类物料一入库就私发质检」：先有一个发消息的 Action（例如 `send-inspection-notice`：参数是入库行，配一个 side effect webhook 调钉钉的 REST Connection），Automation 的效果执行它：

```json
{
  "apiName": "po-0001-inbound", "displayName": "ACME-0001 线类入库 → 私发质检员", "status": "active",
  "condition": { "type": "objectsAdded", "evaluation": { "everyMinutes": 10, "workHours": "07:00-21:00" },
    "objectSet": { "type": "filter", "objectSet": { "type": "base", "objectType": "stock.inboundLine" },
      "where": { "type": "and", "value": [ { "type": "startsWith", "field": "orderNo", "value": "ACME-0001" }, { "type": "containsAnyTerm", "field": "materialName", "value": "涤纶线 弹力线 高弹" } ] } } },
  "effects": [ { "type": "action", "actionType": "send-inspection-notice",
    "parameters": { "lines": { "$effectInput": "objects" }, "title": "辅料到货入库，请做辅料质检" },
    "executionSettings": { "retryConfig": { "backoffStrategy": { "type": "exponentialBackoff", "attemptLimit": 3, "stepDurationMillis": 60000, "maxDurationMillis": 600000 } } } } ],
  "fallbackEffects": [ { "type": "action", "actionType": "log-failure",
    "parameters": { "message": { "$effectInput": "failureMessage" }, "event": { "$effectInput": "eventId" } } } ],
  "replaces": [ { "box": "demo", "profile": "purchasing", "jobId": "a1b2c3d4e5f6", "name": "ACME-0001 到货入库", "runsPerDay": 144 } ]
}
```

```bash
aidc semantic automations upsert --ontology cell-demo --file po-0001-inbound.json --dry-run   # 编译对象集、校验 Action 与参数
aidc semantic automations upsert --ontology cell-demo --file po-0001-inbound.json            # 从「现在」开始，不回放历史
aidc semantic automations show <automationRid>                                                # 最近 20 次运行：每次执行的结果、Action 的操作 id
```

- **以谁的身份**：Action 与函数以 automation 的**拥有者**（最后一次改条件或效果、保存它的人）的身份执行，按他现在的权限——和他自己在界面里执行一样：没有权限、提交条件不过就失败，失败原因写在运行里；他的账号停用了，效果就不再执行（由在用的开发者重新保存，就接过所有权）。
- **效果输入**：参数写 `{ "$effectInput": "…" }` 就代入触发对象——`object`（一个对象，每个对象执行一次）、`objects`（这一批对象：对象集 / 对象列表参数）、`primaryKey`、`property:<属性>`（触发时的值）、`automationRid`、`eventId`；失败效果里还有 `failureMessage`。一批对象太多就分批：`"executionMode": { "type": "perBatchOfObjects" }` + `"executionSettings": { "batchSize": 100 }`。
- **重试与失败效果**：每个效果可以配重试（`constantBackoff` / `exponentialBackoff`，可加抖动 `jitterFactor` / `jitterDurationMillis`）；重试用完仍失败，对失败的那批对象执行 `fallbackEffects`（例如记一条失败、通知负责人）。效果按列出的顺序执行，一个对象在某个效果上失败了，就不进入后面的效果。效果至少执行一次：Action 与函数要能承受重复执行（幂等）。

**④ 还要写代码的 Loop（日报、汇总、判断）**：写成函数，在平台上执行，不在客户箱上跑。函数是一个 ES module，默认导出 `(params, ctx) => 结果`，`ctx` 读对象（`loadObjects`、`aggregate`、`getObject`）：

```js
// count-open.js
export default async (params, ctx) => {
  const lines = { type: "filter", objectSet: { type: "base", objectType: "stock.inboundLine" }, where: { type: "eq", field: "status", value: params.status } };
  const r = await ctx.aggregate(lines, [{ type: "count", name: "n" }]);
  return Number(r.data[0]?.metrics[0]?.value ?? 0);
};
```

```bash
aidc semantic functions publish count-open.js --api-name countOpenLines --version 1.0.0 \
  --output '{"type":"integer"}' --parameters '{"status":{"dataType":{"type":"string"}}}'
aidc semantic functions list
```

Automation 用 Function 效果执行它：`{ "type": "function", "function": "countOpenLines", "version": "1.0.0", "autoUpgrade": true, "parameters": { "status": "open" } }`（`autoUpgrade` = 自动用同一大版本里更新的稳定版本）。函数的结果在运行里只记摘要（版本、类型、大小）；要把结果发出去，后面跟一个 Action 效果，或者把「算 + 发」写成函数支撑的 Action。返回 Ontology 编辑的函数在 Function 效果里**编辑不生效**——要改对象，用 Action。看板不再整页上传：做成 Workshop 模块，打开时按权限从 Semantic 现算。

**迁移表**：钉钉 / 企业微信通知与 `agentScript`（在客户箱上跑 Loop 脚本）这两种效果已经停用——新的 automation 不再接受（返回 `automate_legacy_effect`），已经在跑的照常运行，替换时可以原样保留，迁走之后去掉：

| 原来的效果（已停用） | 换成 |
| --- | --- |
| `notification`，`channel: "dingtalk"` / `"wecom"`（经客户箱上数字员工的凭证发） | `action` 效果：执行一个「发消息」的 Action，这个 Action 配 side effect webhook（webhook 挂在钉钉 / 企业微信的 REST Connection 上）；收件人、标题是 Action 的参数（静态值或 `property:<属性>`）。发给 AIDC 账号的：`notification` + `channel: "email"` |
| `agentScript`（Loop 脚本在客户箱上跑，读 Semantic） | 函数：`aidc semantic functions publish`，`function` 效果执行；要把结果发出去，后面跟一个 `action` 效果 |
| `agentScript` 的 `deliver`（脚本输出发钉钉） | `action` 效果 + side effect webhook |
| `quietHours`（静默时段顺延） | 时间条件写清楚什么时候发（`at`、`weekdays`），或在 Action 的提交条件里判断 |

**迁移一个 cron 的定式**：写 Automation（带 `replaces`）→ `--dry-run` 看对象集与校验 → 建好后与原 cron 并行一轮、对上 → `hermes cron pause <jobId>`（暂停不删，保留两周）。Automation 的详情页（`/semantic/<组织>/automate/automations/<apiName>`）「替代的 cron」一栏列出它替掉了哪些 cron、原来每天跑多少次。
