複合指令
複合指令是一支已儲存的程式,能在同一個交易內跨多張自訂資料表寫入。你只需建置一次——具型別的 inputs、有序的步驟、宣告的 outputs——之後每次呼叫都是全部提交或全部回滾,呼叫端不必為了「訂單只寫了一半」再自己寫補償邏輯。
建置的門檻刻意高於呼叫。建立指令必須指定標籤,定義中觸及的每一張資料表都要掛在該標籤下,而且你必須有管理權。執行時則只需要對這些資料表的讀取權,加上各步驟本身所需的權限——正因如此,你才能把一個範圍明確、留有稽核紀錄的寫入動作,交給沒有能力自行建置它的人。
Dependency refresh 有兩種政策。一般 column/rule/IaC/purge-repair mutation 只在 mutation 的 commit 內刷新 selected、先前 valid 且仍可編譯的 command;DBAPIError 以外的 failure 會降級或略過,mutation 優先,command 保持 stale。Multi-table purge/repair probe 依 rewritten table 自己的 scope 分組;這種分組是完整的,因為 command contract 採 exact-scope-closed:每個 closure member 都必須通過 command 的精確 scope filter,跨 scope authoring 則以 A referenced table does not exist in this scope 失敗。每個 participating scope 都會收到完整的 rewritten-table set。透過 no-dependent fast path 併發建立的新 command,仍可能依 mutation 前形狀編譯並落成 stale。Trigger write 會先驗證 candidate graph,並要求所有 selected refresh 成功;trigger、history、IaC state 與 refreshed definitions 原子提交,失敗則以 command_dependency_refresh_failed 回滾 trigger mutation。兩種政策都不修復原本已 stale 的 command。
呼叫指令有兩條通道。/execute 是交易式寫入通道:它會保留 idempotency key、遇到 require_approval 規則時把整份計畫暫存並回應 409 approval_required,並留下可回頭查閱的執行稽核紀錄。/query 則是給 mode: "query" 指令的唯讀通道——不上鎖、以 cursor 分頁、沒有重播 key,也不留執行紀錄。走錯通道會得到 409,不會默默執行。
除了三個範圍路由之外,還有一條位於 /agent/commands/… 的 agent 通道。它沒有範圍區段:由服務 token 驗證呼叫端,再以伺服器注入的 header 綁定唯一的 principal,指令的範圍與 acting room 都由此推導。這條路由刻意不出現在 OpenAPI,也不是瀏覽器整合介面。
Agent execute 還要求伺服器注入 X-TeamSync-Agent-Channel。選填的 X-TeamSync-Agent-Group-Scope 是 64 個小寫十六進位字元的 digest,而且只有 channel 是 line_group 或 line_room 時才必填;其他 channel 一律不得出現。具型別 header 缺漏或格式錯誤時,request validation 會回 422;格式正確但出現條件組合錯誤時,則回 400 {"detail":{"error":"invalid_agent_group_scope"}}。這些 header 會參與授權、idempotency 與 delivery identity,模型絕不可自行產生。
在人工 scoped REST 上,expected_contract_digest 是選填的 request 欄位。動態生成的 write-command 工具則把它保留為伺服器私有值,不會放進模型可見的 schema;使用者在後續以明確自然語言同意後,伺服器 adapter 才會注入從建置後 command definition 釘選的 digest。Query 模式工具不使用此欄位。
/private/module/custom_tables/{scopeWithId}/commands在單一範圍內存放一支具名的有序步驟程式,於同一個交易內跨多張資料表寫入。
當一個商業動作必須同時異動多張資料表、而且「只成功一半」會弄壞資料時使用,例如建立訂單同時扣庫存、建立預約同時佔用時段。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands - 部門
/private/module/custom_tables/department/{department_id}/commands - 公司
/private/module/custom_tables/company/{company_id}/commands
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
請求結構
CommandCreateRequest| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agent_enabled | boolean | 否 | Whether the chatroom agent may see and invoke this command. When False the command stays fully usable over REST but is never loaded as an agent tool. |
definition | CommandDefinition-Input | 是 | Typed inputs and ordered multi-table write steps. |
description | string | 否 | Human-readable purpose and usage notes. |
name | string | 是 | Command display name; unique within its scope. |
tag_id | string | 是 | Required tag from the same custom-table scope. Every referenced table must currently be assigned to this tag. |
回應結構
CommandResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agent_enabled | boolean | 否 | Whether the chatroom agent may see and invoke this command. |
chatroom_id | string | null | 否 | Chatroom scope id |
company_id | string | null | 否 | Company scope id |
created_at | string (date-time) | 是 | 未指定 |
created_by | string | 是 | 未指定 |
definition | CommandDefinition-Output | 是 | Full command definition DSL body stored in CustomTableCommand.definition. |
department_id | string | null | 否 | Department scope id |
description | string | 否 | 未指定 |
id | string | 是 | Command UUID |
is_deleted | boolean | 否 | Soft-delete marker |
lifecycle_redacted | boolean | 否 | True when callback and lifecycle-trigger configuration is omitted because the caller can execute but cannot manage every target table. |
name | string | 是 | 未指定 |
tag_id | string | null | 否 | 未指定 |
updated_at | string (date-time) | 是 | 未指定 |
範例
建立「下訂單並扣庫存」指令
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111"
},
"body": {
"name": "建立訂單並扣庫存",
"description": "一次寫入訂單並扣減對應品項庫存;庫存不足時整筆回滾。",
"agent_enabled": true,
"tag_id": "33333333-3333-4333-8333-333333333333",
"definition": {
"version": 2,
"inputs": [
{
"name": "customer",
"type": "string",
"required": true,
"max_length": 100,
"description": "客戶名稱"
},
{
"name": "item",
"type": "string",
"required": true,
"max_length": 100,
"description": "品項名稱"
},
{
"name": "quantity",
"type": "integer",
"required": true,
"description": "出貨數量"
}
],
"steps": [
{
"kind": "select",
"name": "stock",
"from": {
"table": "55555555-5555-4555-8555-555555555555",
"as": "s"
},
"select": {
"id": "$row.s.id",
"on_hand": "$row.s.庫存"
},
"where": {
"$and": [
{
"$eq": [
"$row.s.品項",
"$input.item"
]
}
]
},
"max_rows": 1
},
{
"kind": "let",
"name": "on_hand",
"type": "integer",
"value": {
"$single": {
"relation": "$rel.stock",
"expr": "$item.on_hand"
}
}
},
{
"kind": "assert",
"name": "stock_ok",
"condition": {
"$gte": [
"$var.on_hand",
"$input.quantity"
]
},
"message": "庫存不足,無法建立訂單"
},
{
"kind": "insert",
"name": "order",
"table": "44444444-4444-4444-8444-444444444444",
"values": {
"客戶": "$input.customer",
"品項": "$input.item",
"數量": "$input.quantity",
"狀態": "draft",
"建立者": "$ctx.user_id"
},
"returning": {
"id": "$new.id"
}
},
{
"kind": "update",
"name": "consume",
"table": "55555555-5555-4555-8555-555555555555",
"for_each": "$rel.stock",
"set": {
"庫存": {
"$sub": [
"$item.on_hand",
"$input.quantity"
]
}
},
"match": {
"id": "$item.id"
},
"max_rows": 1,
"returning": {
"id": "$new.id"
}
}
],
"outputs": {
"order_id": {
"value": {
"$single": {
"relation": "$rel.order",
"expr": "$item.id"
}
}
},
"remaining": {
"value": {
"$sub": [
"$var.on_hand",
"$input.quantity"
]
}
}
}
}
}
}{
"id": "22222222-2222-4222-8222-222222222222",
"name": "建立訂單並扣庫存",
"description": "一次寫入訂單並扣減對應品項庫存;庫存不足時整筆回滾。",
"agent_enabled": true,
"definition": {
"version": 2,
"inputs": [
{
"name": "customer",
"type": "string",
"required": true,
"max_length": 100,
"description": "客戶名稱"
},
{
"name": "item",
"type": "string",
"required": true,
"max_length": 100,
"description": "品項名稱"
},
{
"name": "quantity",
"type": "integer",
"required": true,
"description": "出貨數量"
}
],
"steps": [
{
"kind": "select",
"name": "stock",
"from": {
"table": "55555555-5555-4555-8555-555555555555",
"as": "s"
},
"select": {
"id": "$row.s.id",
"on_hand": "$row.s.庫存"
},
"where": {
"$and": [
{
"$eq": [
"$row.s.品項",
"$input.item"
]
}
]
},
"max_rows": 1
},
{
"kind": "let",
"name": "on_hand",
"type": "integer",
"value": {
"$single": {
"relation": "$rel.stock",
"expr": "$item.on_hand"
}
}
},
{
"kind": "assert",
"name": "stock_ok",
"condition": {
"$gte": [
"$var.on_hand",
"$input.quantity"
]
},
"message": "庫存不足,無法建立訂單"
},
{
"kind": "insert",
"name": "order",
"table": "44444444-4444-4444-8444-444444444444",
"values": {
"客戶": "$input.customer",
"品項": "$input.item",
"數量": "$input.quantity",
"狀態": "draft",
"建立者": "$ctx.user_id"
},
"returning": {
"id": "$new.id"
}
},
{
"kind": "update",
"name": "consume",
"table": "55555555-5555-4555-8555-555555555555",
"for_each": "$rel.stock",
"set": {
"庫存": {
"$sub": [
"$item.on_hand",
"$input.quantity"
]
}
},
"match": {
"id": "$item.id"
},
"max_rows": 1,
"returning": {
"id": "$new.id"
}
}
],
"outputs": {
"order_id": {
"value": {
"$single": {
"relation": "$rel.order",
"expr": "$item.id"
}
}
},
"remaining": {
"value": {
"$sub": [
"$var.on_hand",
"$input.quantity"
]
}
}
}
},
"tag_id": "33333333-3333-4333-8333-333333333333",
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"department_id": null,
"company_id": null,
"created_by": "77777777-7777-4777-8777-777777777777",
"created_at": "2026-07-21T02:10:00",
"updated_at": "2026-07-21T02:10:00",
"is_deleted": false,
"lifecycle_redacted": false
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | definition 未通過建置驗證;errors 會列出所有問題,例如 callback URL 指向不被允許的主機。 | |
| 403 | 請求使用 UserAPIKey 而非 JWT 驗證。 | |
| 404 | 步驟引用了此範圍內不存在的資料表,或呼叫者無權管理的資料表;兩種情況回傳相同內容。 | |
| 404 | tag_id 不是此範圍內的標籤。 | |
| 409 | 此範圍內已有同名的活指令。自 2026-07-28 起,刪除的指令會立即釋放名字,所以只有活的持有者會撞名。 | |
| 409 | 兩筆編寫寫入爭用同一個指令範圍,InnoDB 中止了這一筆。交易已回滾,沒有任何指令被建立——直接重送請求即可。 | |
| 422 | 引用的資料表目前未掛在 tag_id 上;table_ids 會列出不符合的資料表。 | |
| 422 | 編譯後的定義完全沒有引用任何自訂資料表——只有 let、只有 callback,或根本沒有 step。在這道閘門之前,同樣的 body 會回 201 並存下一支讓所有權限檢查都視為無害的指令。 | |
/private/module/custom_tables/{scopeWithId}/commands回傳此範圍內、呼叫者對每一張被引用資料表都有讀取權的指令,依建立時間新到舊排序。
適合用來建立指令選單,或在把按鈕接上某支指令前,確認特定使用者實際看得到哪些複合動作。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands - 部門
/private/module/custom_tables/department/{department_id}/commands - 公司
/private/module/custom_tables/company/{company_id}/commands
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
skip | query | int >= 0 | 否 | 略過前幾筆可見指令,預設為 0。 |
limit | query | int 1..200 | 否 | 最多回傳幾筆指令,可填 1 到 200,預設為 50。 |
回應結構
CommandListResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
items | CommandResponse[] | 否 | 未指定 |
total | integer | 是 | 未指定 |
範例
列出聊天室中第一頁指令
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111"
},
"query": {
"skip": "0",
"limit": "20"
}
}{
"items": [
{
"id": "22222222-2222-4222-8222-222222222222",
"name": "建立訂單並扣庫存",
"description": "一次寫入訂單並扣減對應品項庫存;庫存不足時整筆回滾。",
"agent_enabled": true,
"definition": {
"version": 2,
"inputs": [
{
"name": "customer",
"type": "string",
"required": true,
"max_length": 100,
"description": "客戶名稱"
},
{
"name": "item",
"type": "string",
"required": true,
"max_length": 100,
"description": "品項名稱"
},
{
"name": "quantity",
"type": "integer",
"required": true,
"description": "出貨數量"
}
],
"steps": [
{
"kind": "select",
"name": "stock",
"from": {
"table": "55555555-5555-4555-8555-555555555555",
"as": "s"
},
"select": {
"id": "$row.s.id",
"on_hand": "$row.s.庫存"
},
"where": {
"$and": [
{
"$eq": [
"$row.s.品項",
"$input.item"
]
}
]
},
"max_rows": 1
},
{
"kind": "let",
"name": "on_hand",
"type": "integer",
"value": {
"$single": {
"relation": "$rel.stock",
"expr": "$item.on_hand"
}
}
},
{
"kind": "assert",
"name": "stock_ok",
"condition": {
"$gte": [
"$var.on_hand",
"$input.quantity"
]
},
"message": "庫存不足,無法建立訂單"
},
{
"kind": "insert",
"name": "order",
"table": "44444444-4444-4444-8444-444444444444",
"values": {
"客戶": "$input.customer",
"品項": "$input.item",
"數量": "$input.quantity",
"狀態": "draft",
"建立者": "$ctx.user_id"
},
"returning": {
"id": "$new.id"
}
},
{
"kind": "update",
"name": "consume",
"table": "55555555-5555-4555-8555-555555555555",
"for_each": "$rel.stock",
"set": {
"庫存": {
"$sub": [
"$item.on_hand",
"$input.quantity"
]
}
},
"match": {
"id": "$item.id"
},
"max_rows": 1,
"returning": {
"id": "$new.id"
}
}
],
"outputs": {
"order_id": {
"value": {
"$single": {
"relation": "$rel.order",
"expr": "$item.id"
}
}
},
"remaining": {
"value": {
"$sub": [
"$var.on_hand",
"$input.quantity"
]
}
}
}
},
"tag_id": "33333333-3333-4333-8333-333333333333",
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"department_id": null,
"company_id": null,
"created_by": "77777777-7777-4777-8777-777777777777",
"created_at": "2026-07-21T02:10:00",
"updated_at": "2026-07-21T02:10:00",
"is_deleted": false,
"lifecycle_redacted": false
}
],
"total": 1
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 403 | 請求使用 UserAPIKey 而非 JWT 驗證。 | |
| 403 | 聊天室範圍的呼叫者既不是該聊天室成員,角色也不足以代行成員權限。 | |
| 404 | 路徑上的公司或部門屬於其他租戶。這裡回 404 而非 403,避免此 id 變成存在性探測工具。 | |
/private/module/custom_tables/{scopeWithId}/commands/{command_id}回傳一支已儲存的指令,含完整 definition、標籤與範圍 id。
在製作呼叫表單前先讀取,才能知道每個宣告的 input 名稱、型別,以及是否必填。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands/{command_id} - 部門
/private/module/custom_tables/department/{department_id}/commands/{command_id} - 公司
/private/module/custom_tables/company/{company_id}/commands/{command_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要讀取的指令 UUID。 |
回應結構
CommandResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agent_enabled | boolean | 否 | Whether the chatroom agent may see and invoke this command. |
chatroom_id | string | null | 否 | Chatroom scope id |
company_id | string | null | 否 | Company scope id |
created_at | string (date-time) | 是 | 未指定 |
created_by | string | 是 | 未指定 |
definition | CommandDefinition-Output | 是 | Full command definition DSL body stored in CustomTableCommand.definition. |
department_id | string | null | 否 | Department scope id |
description | string | 否 | 未指定 |
id | string | 是 | Command UUID |
is_deleted | boolean | 否 | Soft-delete marker |
lifecycle_redacted | boolean | 否 | True when callback and lifecycle-trigger configuration is omitted because the caller can execute but cannot manage every target table. |
name | string | 是 | 未指定 |
tag_id | string | null | 否 | 未指定 |
updated_at | string (date-time) | 是 | 未指定 |
範例
在呼叫前讀取指令定義
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"command_id": "22222222-2222-4222-8222-222222222222"
}
}{
"id": "22222222-2222-4222-8222-222222222222",
"name": "建立訂單並扣庫存",
"description": "一次寫入訂單並扣減對應品項庫存;庫存不足時整筆回滾。",
"agent_enabled": true,
"definition": {
"version": 2,
"inputs": [
{
"name": "customer",
"type": "string",
"required": true,
"max_length": 100,
"description": "客戶名稱"
},
{
"name": "item",
"type": "string",
"required": true,
"max_length": 100,
"description": "品項名稱"
},
{
"name": "quantity",
"type": "integer",
"required": true,
"description": "出貨數量"
}
],
"steps": [
{
"kind": "select",
"name": "stock",
"from": {
"table": "55555555-5555-4555-8555-555555555555",
"as": "s"
},
"select": {
"id": "$row.s.id",
"on_hand": "$row.s.庫存"
},
"where": {
"$and": [
{
"$eq": [
"$row.s.品項",
"$input.item"
]
}
]
},
"max_rows": 1
},
{
"kind": "let",
"name": "on_hand",
"type": "integer",
"value": {
"$single": {
"relation": "$rel.stock",
"expr": "$item.on_hand"
}
}
},
{
"kind": "assert",
"name": "stock_ok",
"condition": {
"$gte": [
"$var.on_hand",
"$input.quantity"
]
},
"message": "庫存不足,無法建立訂單"
},
{
"kind": "insert",
"name": "order",
"table": "44444444-4444-4444-8444-444444444444",
"values": {
"客戶": "$input.customer",
"品項": "$input.item",
"數量": "$input.quantity",
"狀態": "draft",
"建立者": "$ctx.user_id"
},
"returning": {
"id": "$new.id"
}
},
{
"kind": "update",
"name": "consume",
"table": "55555555-5555-4555-8555-555555555555",
"for_each": "$rel.stock",
"set": {
"庫存": {
"$sub": [
"$item.on_hand",
"$input.quantity"
]
}
},
"match": {
"id": "$item.id"
},
"max_rows": 1,
"returning": {
"id": "$new.id"
}
}
],
"outputs": {
"order_id": {
"value": {
"$single": {
"relation": "$rel.order",
"expr": "$item.id"
}
}
},
"remaining": {
"value": {
"$sub": [
"$var.on_hand",
"$input.quantity"
]
}
}
}
},
"tag_id": "33333333-3333-4333-8333-333333333333",
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"department_id": null,
"company_id": null,
"created_by": "77777777-7777-4777-8777-777777777777",
"created_at": "2026-07-21T02:10:00",
"updated_at": "2026-07-21T02:10:00",
"is_deleted": false,
"lifecycle_redacted": false
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 403 | 請求使用 UserAPIKey 而非 JWT 驗證。 | |
| 404 | 指令不存在於此範圍、已被軟刪除,或引用了呼叫者無法讀取的資料表。 | |
/private/module/custom_tables/{scopeWithId}/commands/{command_id}整支取代指令的名稱、說明、標籤與 definition,並依即時 schema 重新驗證。
當指令背後的商業規則改變,或它寫入的資料表新增了必須填寫的欄位時使用。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands/{command_id} - 部門
/private/module/custom_tables/department/{department_id}/commands/{command_id} - 公司
/private/module/custom_tables/company/{company_id}/commands/{command_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要取代的指令 UUID。 |
請求結構
CommandUpdateRequest| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agent_enabled | boolean | 否 | Whether the chatroom agent may see and invoke this command. When False the command stays fully usable over REST but is never loaded as an agent tool. |
definition | CommandDefinition-Input | 是 | Typed inputs and ordered multi-table write steps. |
description | string | 否 | Human-readable purpose and usage notes. |
name | string | 是 | Command display name; unique within its scope. |
tag_id | string | 是 | Required tag from the same custom-table scope. Every referenced table must currently be assigned to this tag. |
回應結構
CommandResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agent_enabled | boolean | 否 | Whether the chatroom agent may see and invoke this command. |
chatroom_id | string | null | 否 | Chatroom scope id |
company_id | string | null | 否 | Company scope id |
created_at | string (date-time) | 是 | 未指定 |
created_by | string | 是 | 未指定 |
definition | CommandDefinition-Output | 是 | Full command definition DSL body stored in CustomTableCommand.definition. |
department_id | string | null | 否 | Department scope id |
description | string | 否 | 未指定 |
id | string | 是 | Command UUID |
is_deleted | boolean | 否 | Soft-delete marker |
lifecycle_redacted | boolean | 否 | True when callback and lifecycle-trigger configuration is omitted because the caller can execute but cannot manage every target table. |
name | string | 是 | 未指定 |
tag_id | string | null | 否 | 未指定 |
updated_at | string (date-time) | 是 | 未指定 |
範例
更新指令定義與說明
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"command_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"name": "建立訂單並扣庫存",
"description": "一次寫入訂單並扣減對應品項庫存;庫存不足時整筆回滾。改為一次最多扣 50 件。",
"agent_enabled": false,
"tag_id": "33333333-3333-4333-8333-333333333333",
"definition": {
"version": 2,
"inputs": [
{
"name": "customer",
"type": "string",
"required": true,
"max_length": 100,
"description": "客戶名稱"
},
{
"name": "item",
"type": "string",
"required": true,
"max_length": 100,
"description": "品項名稱"
},
{
"name": "quantity",
"type": "integer",
"required": true,
"description": "出貨數量"
}
],
"steps": [
{
"kind": "select",
"name": "stock",
"from": {
"table": "55555555-5555-4555-8555-555555555555",
"as": "s"
},
"select": {
"id": "$row.s.id",
"on_hand": "$row.s.庫存"
},
"where": {
"$and": [
{
"$eq": [
"$row.s.品項",
"$input.item"
]
}
]
},
"max_rows": 1
},
{
"kind": "let",
"name": "on_hand",
"type": "integer",
"value": {
"$single": {
"relation": "$rel.stock",
"expr": "$item.on_hand"
}
}
},
{
"kind": "assert",
"name": "stock_ok",
"condition": {
"$gte": [
"$var.on_hand",
"$input.quantity"
]
},
"message": "庫存不足,無法建立訂單"
},
{
"kind": "insert",
"name": "order",
"table": "44444444-4444-4444-8444-444444444444",
"values": {
"客戶": "$input.customer",
"品項": "$input.item",
"數量": "$input.quantity",
"狀態": "draft",
"建立者": "$ctx.user_id"
},
"returning": {
"id": "$new.id"
}
},
{
"kind": "update",
"name": "consume",
"table": "55555555-5555-4555-8555-555555555555",
"for_each": "$rel.stock",
"set": {
"庫存": {
"$sub": [
"$item.on_hand",
"$input.quantity"
]
}
},
"match": {
"id": "$item.id"
},
"max_rows": 1,
"returning": {
"id": "$new.id"
}
}
],
"outputs": {
"order_id": {
"value": {
"$single": {
"relation": "$rel.order",
"expr": "$item.id"
}
}
},
"remaining": {
"value": {
"$sub": [
"$var.on_hand",
"$input.quantity"
]
}
}
}
}
}
}{
"id": "22222222-2222-4222-8222-222222222222",
"name": "建立訂單並扣庫存",
"description": "一次寫入訂單並扣減對應品項庫存;庫存不足時整筆回滾。改為一次最多扣 50 件。",
"agent_enabled": false,
"definition": {
"version": 2,
"inputs": [
{
"name": "customer",
"type": "string",
"required": true,
"max_length": 100,
"description": "客戶名稱"
},
{
"name": "item",
"type": "string",
"required": true,
"max_length": 100,
"description": "品項名稱"
},
{
"name": "quantity",
"type": "integer",
"required": true,
"description": "出貨數量"
}
],
"steps": [
{
"kind": "select",
"name": "stock",
"from": {
"table": "55555555-5555-4555-8555-555555555555",
"as": "s"
},
"select": {
"id": "$row.s.id",
"on_hand": "$row.s.庫存"
},
"where": {
"$and": [
{
"$eq": [
"$row.s.品項",
"$input.item"
]
}
]
},
"max_rows": 1
},
{
"kind": "let",
"name": "on_hand",
"type": "integer",
"value": {
"$single": {
"relation": "$rel.stock",
"expr": "$item.on_hand"
}
}
},
{
"kind": "assert",
"name": "stock_ok",
"condition": {
"$gte": [
"$var.on_hand",
"$input.quantity"
]
},
"message": "庫存不足,無法建立訂單"
},
{
"kind": "insert",
"name": "order",
"table": "44444444-4444-4444-8444-444444444444",
"values": {
"客戶": "$input.customer",
"品項": "$input.item",
"數量": "$input.quantity",
"狀態": "draft",
"建立者": "$ctx.user_id"
},
"returning": {
"id": "$new.id"
}
},
{
"kind": "update",
"name": "consume",
"table": "55555555-5555-4555-8555-555555555555",
"for_each": "$rel.stock",
"set": {
"庫存": {
"$sub": [
"$item.on_hand",
"$input.quantity"
]
}
},
"match": {
"id": "$item.id"
},
"max_rows": 1,
"returning": {
"id": "$new.id"
}
}
],
"outputs": {
"order_id": {
"value": {
"$single": {
"relation": "$rel.order",
"expr": "$item.id"
}
}
},
"remaining": {
"value": {
"$sub": [
"$var.on_hand",
"$input.quantity"
]
}
}
}
},
"tag_id": "33333333-3333-4333-8333-333333333333",
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"department_id": null,
"company_id": null,
"created_by": "77777777-7777-4777-8777-777777777777",
"created_at": "2026-07-21T02:10:00",
"updated_at": "2026-07-21T06:40:00",
"is_deleted": false,
"lifecycle_redacted": false
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 指令不存在於此範圍,或呼叫者無法管理它目前引用的每一張資料表。 | |
| 409 | 這支指令仍有執行被暫存待覆核;staged_change_ids 會列出這些暫存變更。 | |
| 409 | 此範圍內已有其他活指令使用這個新名稱。刪除的指令不再佔用名字。 | |
| 409 | 有資料表觸發器會呼叫這支指令,因此不能在 write 與 query 之間切換執行模式。 | |
| 409 | 有資料表觸發器會呼叫這支指令,因此新定義不得新增對需覆核資料表的引用。 | |
| 409 | 兩筆編寫寫入爭用同一個指令範圍,InnoDB 中止了這一筆。交易已回滾,已儲存的定義維持不變——直接重送請求即可。 | |
| 422 | 新定義中引用的資料表未掛在 tag_id 上。 | |
| 422 | 新的定義編譯後資料表集合是空的。這也是既有零資料表指令無法原地修好的原因——除非新定義引用了資料表,否則 PUT 對新舊定義一視同仁地拒絕。 | |
/private/module/custom_tables/{scopeWithId}/commands/{command_id}將指令軟刪除,並回傳已標記 is_deleted 的資料。
確認沒有觸發器引用、也沒有執行仍在等待覆核後,用它下架一支複合動作。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands/{command_id} - 部門
/private/module/custom_tables/department/{department_id}/commands/{command_id} - 公司
/private/module/custom_tables/company/{company_id}/commands/{command_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要刪除的指令 UUID。 |
回應結構
CommandResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
agent_enabled | boolean | 否 | Whether the chatroom agent may see and invoke this command. |
chatroom_id | string | null | 否 | Chatroom scope id |
company_id | string | null | 否 | Company scope id |
created_at | string (date-time) | 是 | 未指定 |
created_by | string | 是 | 未指定 |
definition | CommandDefinition-Output | 是 | Full command definition DSL body stored in CustomTableCommand.definition. |
department_id | string | null | 否 | Department scope id |
description | string | 否 | 未指定 |
id | string | 是 | Command UUID |
is_deleted | boolean | 否 | Soft-delete marker |
lifecycle_redacted | boolean | 否 | True when callback and lifecycle-trigger configuration is omitted because the caller can execute but cannot manage every target table. |
name | string | 是 | 未指定 |
tag_id | string | null | 否 | 未指定 |
updated_at | string (date-time) | 是 | 未指定 |
範例
刪除未被引用的指令
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"command_id": "22222222-2222-4222-8222-222222222222"
}
}{
"id": "22222222-2222-4222-8222-222222222222",
"name": "建立訂單並扣庫存",
"description": "一次寫入訂單並扣減對應品項庫存;庫存不足時整筆回滾。",
"agent_enabled": true,
"definition": {
"version": 2,
"inputs": [
{
"name": "customer",
"type": "string",
"required": true,
"max_length": 100,
"description": "客戶名稱"
},
{
"name": "item",
"type": "string",
"required": true,
"max_length": 100,
"description": "品項名稱"
},
{
"name": "quantity",
"type": "integer",
"required": true,
"description": "出貨數量"
}
],
"steps": [
{
"kind": "select",
"name": "stock",
"from": {
"table": "55555555-5555-4555-8555-555555555555",
"as": "s"
},
"select": {
"id": "$row.s.id",
"on_hand": "$row.s.庫存"
},
"where": {
"$and": [
{
"$eq": [
"$row.s.品項",
"$input.item"
]
}
]
},
"max_rows": 1
},
{
"kind": "let",
"name": "on_hand",
"type": "integer",
"value": {
"$single": {
"relation": "$rel.stock",
"expr": "$item.on_hand"
}
}
},
{
"kind": "assert",
"name": "stock_ok",
"condition": {
"$gte": [
"$var.on_hand",
"$input.quantity"
]
},
"message": "庫存不足,無法建立訂單"
},
{
"kind": "insert",
"name": "order",
"table": "44444444-4444-4444-8444-444444444444",
"values": {
"客戶": "$input.customer",
"品項": "$input.item",
"數量": "$input.quantity",
"狀態": "draft",
"建立者": "$ctx.user_id"
},
"returning": {
"id": "$new.id"
}
},
{
"kind": "update",
"name": "consume",
"table": "55555555-5555-4555-8555-555555555555",
"for_each": "$rel.stock",
"set": {
"庫存": {
"$sub": [
"$item.on_hand",
"$input.quantity"
]
}
},
"match": {
"id": "$item.id"
},
"max_rows": 1,
"returning": {
"id": "$new.id"
}
}
],
"outputs": {
"order_id": {
"value": {
"$single": {
"relation": "$rel.order",
"expr": "$item.id"
}
}
},
"remaining": {
"value": {
"$sub": [
"$var.on_hand",
"$input.quantity"
]
}
}
}
},
"tag_id": "33333333-3333-4333-8333-333333333333",
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"department_id": null,
"company_id": null,
"created_by": "77777777-7777-4777-8777-777777777777",
"created_at": "2026-07-21T02:10:00",
"updated_at": "2026-07-21T07:15:00",
"is_deleted": true,
"lifecycle_redacted": false
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 指令不存在於此範圍,或呼叫者既無法管理它引用的每一張資料表、也不具備指令所屬範圍的權威(聊天室建立者/管理員、部門管理員或公司管理員)。這個範圍權威後備正是資料表被刪或被 purge 之後指令仍刪得掉的原因;兩項檢查都不過的人拿到的就是這顆不可枚舉的 404。 | |
| 409 | 這支指令仍有執行被暫存待覆核。 | |
| 409 | 仍有資料表觸發器帶著指向這支指令的 invoke_command 動作;references 會列出每張資料表與觸發器。 | |
| 409 | 兩筆編寫寫入爭用同一個指令範圍,InnoDB 中止了這一筆。交易已回滾,指令仍然存在——直接重送請求即可。 | |
/private/module/custom_tables/{scopeWithId}/commands/{command_id}/execute以綁定的 inputs 執行 write 模式指令,所有步驟一起提交,或一起回滾。
真正要做出商業動作時呼叫——下訂單、訂時段、結案——尤其是「只成功一半」會弄髒資料的情況。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands/{command_id}/execute - 部門
/private/module/custom_tables/department/{department_id}/commands/{command_id}/execute - 公司
/private/module/custom_tables/company/{company_id}/commands/{command_id}/execute
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要執行的指令 UUID。 |
db | query | int | 否 | Redis 邏輯資料庫編號,因共用 client 相依而外洩到 schema 上。它是內部旋鈕,不屬於指令契約,請不要填。 |
請求結構
CommandExecuteRequest| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
expected_contract_digest | string | null | 否 | Optional lowercase 64-hex SHA-256 of the exact current stored command definition. A trusted manual caller may send it when executing against an exact reviewed definition snapshot; omission preserves normal manual execution behavior. Generated agent write-command tools keep the digest server-private and inject their pinned value only after later unambiguous natural-language confirmation. A mismatch fails before input validation, idempotency reservation, audit creation, row locking, or mutation. |
idempotency_key | string | null | 否 | Actor-bucketed idempotency key; failed executions do not block retry. |
inputs | object | 是 | Values for the command's declared inputs; server context is excluded. |
回應結構
CommandExecutionResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
authorization_snapshot | object | null | 否 | Immutable command scope, tag, and referenced-table audit facts. |
command_id | string | null | 否 | 未指定 |
created_at | string (date-time) | null | 否 | 未指定 |
duration_ms | integer | null | 否 | 未指定 |
error | string | null | 否 | 未指定 |
executed_at | string (date-time) | null | 否 | Wall-clock stamp frozen at the ORIGINAL execution; an idempotent replay returns the same stamp, so callers can distinguish a replayed earlier result from a fresh write. |
execution_id | string | 是 | 未指定 |
idempotency_key | string | null | 否 | 未指定 |
input_snapshot | object | null | 否 | 未指定 |
invoker_client_id | string | null | 否 | 未指定 |
invoker_id | string | null | 否 | 未指定 |
outputs | object | null | 否 | Declared version-2 scalar or bounded relation outputs. Internal temporary relations and step rows are never exposed. |
response_body | object | null | 否 | 未指定 |
result_refs | object[] | null | 否 | 未指定 |
staged_change_id | string | null | 否 | 未指定 |
status | "succeeded" | "staged" | "failed" | 是 | 未指定 |
steps | (CommandExecutionV1StepResponse | CommandExecutionV2StepResponse)[] | 否 | 未指定 |
truncated | boolean | null | 否 | True when the persisted response snapshot was trimmed to the audit byte cap: declared outputs/step rows are omitted from this body. Set on an idempotent replay of a large execution so the caller can distinguish a trimmed snapshot from a genuinely empty result. |
範例
一次完成下訂單與扣庫存
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"command_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"inputs": {
"customer": "長榮工業",
"item": "螺絲 M4",
"quantity": 12
},
"idempotency_key": "order-2026-07-21-0042"
}
}{
"execution_id": "66666666-6666-4666-8666-666666666666",
"status": "succeeded",
"steps": [
{
"name": "stock",
"kind": "select",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
},
"on_hand": {
"type": "integer",
"nullable": true
}
}
},
{
"name": "on_hand",
"kind": "let",
"status": "succeeded",
"row_count": 0,
"schema": {}
},
{
"name": "stock_ok",
"kind": "assert",
"status": "succeeded",
"row_count": 0,
"schema": {}
},
{
"name": "order",
"kind": "insert",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
}
}
},
{
"name": "consume",
"kind": "update",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
}
}
}
],
"outputs": {
"order_id": "88888888-8888-4888-8888-888888888888",
"remaining": 8
}
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | 這次執行展開後會超過 1000 筆 row action 上限;actual 是觸發上限的實際數量。 | |
| 400 | identity 型別的 input 指向的使用者或社群媒體 client 不屬於此範圍。 | |
| 400 | 版本 1 的 update 或 delete 步驟把選填 input 綁在 match 條件裡,而呼叫者沒有送這個值。現在整次執行失敗,而不是把那個 key 丟掉:丟掉會放寬選列條件,打到建置者從未鎖定的資料列。 | |
| 400 | 被省略的選填 input 出現在 $case 裡——某個分支的 then,或 else。所有分支都必須綁得出值,因為分支要更晚才會對著鎖定的前影像挑選,所以「這個分支綁不出來」只能是錯誤,不能靠猜。 | |
| 403 | 版本 2 程式從呼叫者無法讀取的資料表 select 或 join。過去 row ACL 會把它變成永不匹配的條件,該步驟回傳空 relation,於是「先查再寫」的去重步驟查不到東西,就寫進了一筆重複資料。 | |
| 403 | 程式有 insert 步驟,而該資料表解析出的 can_insert 是 false。這道檢查與讀取閘門在 `_prepare_program_access` 的同一輪完成——在任何 action core 能 flush 之前——所以不會留下半筆資料。 | |
| 403 | 程式有 update 或 delete 步驟,而該資料表解析出的 can_edit 是 "none"。與 insert 同一輪;filtered 或 own 授權在這裡會通過,改在稍後 ACL 範圍內的 match 查詢之後逐列把關。 | |
| 403 | 程式要寫入的某張 channel 治理資料表,替你解析不出 scope。REST 不綁 acting room,floor 改為只依執行者逐表解析,因此訊息是「不帶房間」的版本——你名下沒有任何房間在那張表上宣告 scope_values,或候選房間不只一個而無法解析。`table_id` 指出被拒的資料表,`rule_id` 指出讓它進入治理的 channel 規則。 | |
| 403 | 寫入 floor 拒絕的是資料列本身:insert 或 update 的受管連結落在你的房間宣告的 scope 之外。同一個 code 也涵蓋前影像檢查(`this record is outside your channel’s scope`,update 打到你的 scope 看不見的列),以及 policy 形式(`this write does not satisfy this table’s channel policy`)。請以 `detail.error` 判別,不要看訊息字串。 | |
| 403 | B10 歸因。insert 或 update 的連結值指向一筆確實存在、也通過你的 row ACL 的目標,只有目標資料表的 channel scope 把它藏起來——所以這條通道直接說出 floor 的 code,而不是收斂成統一的 404。在這個分支存在之前,同樣的 v2 寫入會掉進一般性的 500 `command_execution_failed`。當 floor 的判定是那樣時,`scp_scope_undeclared` 與 `scp_rule_dangling` 也會從這裡冒出來;這個形狀沒有 `rule_id`。 | |
| 403 | 連結目標所屬的資料表對你解析出的 `can_read` 是 "none"。這是資料表層級的拒絕,與下面逐列的 404 不同,而且只針對「新增的」目標 id——保留與移除的 id 不受檢查。 | |
| 404 | 指令不存在於此範圍、已被軟刪除,或引用了呼叫者無法讀取的資料表。 | |
| 404 | 連結值指向的目標不存在、已軟刪除,或被 row ACL 對你藏起來。三種情況共用同一個 body,所以這條路由不會開出 id 列舉的破口。注意:這個 404 與同一路由另一個代表「Command not found」的 404 撞在一起——請讀 detail 字串,不要只看狀態碼。 | |
| 409 | require_approval 規則觸發,整份計畫被暫存而非提交。寫入是被扣住,不是遺失。 | |
| 409 | 選填的 expected_contract_digest 不符合 command 的即時定義。檢查發生在 command input 驗證、idempotency 保留、建立稽核、row lock 或異動之前,因此沒有資料被改動,也不可原樣重試這份已過期的釘選 request。 | |
| 409 | 同一位執行者以相同 idempotency key 送出不同的 inputs。 | |
| 409 | 另一個持有相同 key 的請求仍在執行中,等它結束後再重試。 | |
| 409 | 這支指令的 mode 是 "query",必須改用 /query 呼叫。 | |
| 409 | 兩次執行爭用同一批資料列,InnoDB 中止了這一次。交易已回滾,沒有任何部分寫入落地,retryable 為 true——請用「同一把」idempotency key 重試:失敗路徑會釋放該 key 的保留,因此重用是合法的。 | |
| 422 | 宣告的 rule 或 assert 步驟失敗;rule 是規則名稱,message 是建置者自訂的文字。 | |
| 503 | Idempotency 儲存無法連線,為避免重複寫入而直接失敗。 | |
/private/module/custom_tables/{scopeWithId}/commands/{command_id}/query執行唯讀指令,回傳其宣告 relation output 的一頁資料,以及所有 scalar outputs。
用於複合讀取——join、彙總、篩選投影——把邏輯寫在伺服器一次,而不是讓前端自己拼。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands/{command_id}/query - 部門
/private/module/custom_tables/department/{department_id}/commands/{command_id}/query - 公司
/private/module/custom_tables/company/{company_id}/commands/{command_id}/query
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要執行的 query 模式指令 UUID。 |
請求結構
CommandQueryRequest| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
inputs | object | 是 | Values for the command's declared inputs; server context is excluded. |
page | CommandQueryPage | null | 否 | Optional pagination window; defaults to the first page of 100 rows. |
回應結構
CommandQueryResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
next_cursor | string | null | 否 | Opaque cursor for the next page, or null when exhausted. |
outputs | object | 否 | Declared scalar outputs (relation outputs are paginated via rows). |
rows | object[] | 否 | Projected rows of the paginated relation output for this page. |
total_available | integer | null | 否 | Total rows available in the paginated relation, when known. |
範例
讀取篩選後訂單投影的第一頁
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"command_id": "99999999-9999-4999-8999-999999999999"
},
"body": {
"inputs": {
"min_quantity": 10
},
"page": {
"output": "result",
"limit": 2
}
}
}{
"rows": [
{
"id": "88888888-8888-4888-8888-888888888888",
"客戶": "長榮工業",
"數量": 12
},
{
"id": "88888888-8888-4888-8888-222222222222",
"客戶": "大同機械",
"數量": 10
}
],
"outputs": {},
"next_cursor": "eyJvIjoyLCJkIjoiOWYyYzFhIn0",
"total_available": 7
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | cursor 已無法對應這支指令的 dependency contract、inputs 或 output 名稱,通常是換頁之間 schema 或 inputs 變了。 | |
| 400 | page.output 指的不是這支指令的 relation output。 | |
| 403 | 程式的 select 來源或 join 目標解析出的 can_read 是 none。在這道閘門之前,同樣的呼叫會回 200 加一個空 relation,agent 則回報「查無符合資料」。 | |
| 404 | 指令不存在於此範圍、已被軟刪除,或引用了呼叫者無法讀取的資料表。 | |
| 409 | 這是一支寫入指令,請改用 /execute 呼叫。 | |
| 422 | 這一頁序列化後超過 64 KiB 的輸出上限;請調小 page.limit 或縮減 output 的 select 欄位。 | |
/private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions回傳單一指令的稽核紀錄——inputs、outputs、狀態、耗時,以及執行當下擷取的權限快照,新到舊排序。
用來回答「那次到底跑成功了沒、改了什麼」,而不必回頭翻每一張被影響的資料表。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands/{command_id}/executions - 部門
/private/module/custom_tables/department/{department_id}/commands/{command_id}/executions - 公司
/private/module/custom_tables/company/{company_id}/commands/{command_id}/executions
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要查詢執行紀錄的指令 UUID。 |
skip | query | int >= 0 | 否 | 略過前幾筆可見執行紀錄,預設為 0。 |
limit | query | int 1..200 | 否 | 最多回傳幾筆執行紀錄,可填 1 到 200,預設為 50。 |
回應結構
CommandExecutionListResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
items | CommandExecutionResponse[] | 否 | 未指定 |
total | integer | 是 | 未指定 |
範例
列出某支指令的近期執行紀錄
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"command_id": "22222222-2222-4222-8222-222222222222"
},
"query": {
"skip": "0",
"limit": "20"
}
}{
"items": [
{
"execution_id": "66666666-6666-4666-8666-666666666666",
"status": "succeeded",
"steps": [
{
"name": "stock",
"kind": "select",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
},
"on_hand": {
"type": "integer",
"nullable": true
}
}
},
{
"name": "on_hand",
"kind": "let",
"status": "succeeded",
"row_count": 0,
"schema": {}
},
{
"name": "stock_ok",
"kind": "assert",
"status": "succeeded",
"row_count": 0,
"schema": {}
},
{
"name": "order",
"kind": "insert",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
}
}
},
{
"name": "consume",
"kind": "update",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
}
}
}
],
"outputs": {
"order_id": "88888888-8888-4888-8888-888888888888",
"remaining": 8
},
"command_id": "22222222-2222-4222-8222-222222222222",
"invoker_id": "77777777-7777-4777-8777-777777777777",
"idempotency_key": "order-2026-07-21-0042",
"duration_ms": 84,
"created_at": "2026-07-21T03:02:11",
"input_snapshot": {
"customer": "長榮工業",
"item": "螺絲 M4",
"quantity": 12
},
"result_refs": [
{
"name": "order",
"table_id": "44444444-4444-4444-8444-444444444444",
"record_ids": [
"88888888-8888-4888-8888-888888888888"
]
}
],
"authorization_snapshot": {
"version": 1,
"scope": {
"kind": "chatroom",
"id": "11111111-1111-4111-8111-111111111111"
},
"tag_id": "33333333-3333-4333-8333-333333333333",
"table_ids": [
"44444444-4444-4444-8444-444444444444",
"55555555-5555-4555-8555-555555555555"
],
"command_updated_at": "2026-07-21T02:10:00",
"audience": "internal",
"origin": "manual",
"ai": false,
"company_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"acting_chatroom_id": "11111111-1111-4111-8111-111111111111",
"department_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd"
}
}
],
"total": 1
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 403 | 請求使用 UserAPIKey 而非 JWT 驗證。 | |
| 404 | 指令不存在於此範圍、已被軟刪除,或引用了呼叫者無法讀取的資料表。 | |
/private/module/custom_tables/{scopeWithId}/commands/{command_id}/executions/{execution_id}回傳一筆執行稽核紀錄,含 input 快照、寫入結果參照、儲存的回應內容與權限快照。
收到 409 approval_required 後輪詢它,觀察 staged 紀錄在覆核結束後變成 succeeded 或 failed。
- 聊天室
/private/module/custom_tables/chatroom/{chatroom_id}/commands/{command_id}/executions/{execution_id} - 部門
/private/module/custom_tables/department/{department_id}/commands/{command_id}/executions/{execution_id} - 公司
/private/module/custom_tables/company/{company_id}/commands/{command_id}/executions/{execution_id}
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 聊天室 | 可使用 | require_jwt_user + ChatRoomJoinedRequired |
| 部門 | 可使用 | require_jwt_user + DepartmentTenantRequired |
| 公司 | 可使用 | require_jwt_user + CompanyTenantRequired |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 執行紀錄所屬的指令 UUID。 |
execution_id | path | uuid | 是 | 要讀取的執行紀錄 UUID。 |
回應結構
CommandExecutionResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
authorization_snapshot | object | null | 否 | Immutable command scope, tag, and referenced-table audit facts. |
command_id | string | null | 否 | 未指定 |
created_at | string (date-time) | null | 否 | 未指定 |
duration_ms | integer | null | 否 | 未指定 |
error | string | null | 否 | 未指定 |
executed_at | string (date-time) | null | 否 | Wall-clock stamp frozen at the ORIGINAL execution; an idempotent replay returns the same stamp, so callers can distinguish a replayed earlier result from a fresh write. |
execution_id | string | 是 | 未指定 |
idempotency_key | string | null | 否 | 未指定 |
input_snapshot | object | null | 否 | 未指定 |
invoker_client_id | string | null | 否 | 未指定 |
invoker_id | string | null | 否 | 未指定 |
outputs | object | null | 否 | Declared version-2 scalar or bounded relation outputs. Internal temporary relations and step rows are never exposed. |
response_body | object | null | 否 | 未指定 |
result_refs | object[] | null | 否 | 未指定 |
staged_change_id | string | null | 否 | 未指定 |
status | "succeeded" | "staged" | "failed" | 是 | 未指定 |
steps | (CommandExecutionV1StepResponse | CommandExecutionV2StepResponse)[] | 否 | 未指定 |
truncated | boolean | null | 否 | True when the persisted response snapshot was trimmed to the audit byte cap: declared outputs/step rows are omitted from this body. Set on an idempotent replay of a large execution so the caller can distinguish a trimmed snapshot from a genuinely empty result. |
範例
檢視等待覆核中的暫存執行
{
"pathParams": {
"chatroom_id": "11111111-1111-4111-8111-111111111111",
"command_id": "22222222-2222-4222-8222-222222222222",
"execution_id": "66666666-6666-4666-8666-666666666666"
}
}{
"execution_id": "66666666-6666-4666-8666-666666666666",
"status": "staged",
"steps": [],
"command_id": "22222222-2222-4222-8222-222222222222",
"invoker_id": "77777777-7777-4777-8777-777777777777",
"idempotency_key": "order-2026-07-21-0043",
"staged_change_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"duration_ms": 119,
"created_at": "2026-07-21T05:44:02",
"input_snapshot": {
"customer": "大同機械",
"item": "螺絲 M4",
"quantity": 400
},
"response_body": {
"error": "approval_required",
"process_id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
"rule_id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
"rule_label": "高金額訂單需主管覆核",
"staged_change_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
},
"authorization_snapshot": {
"version": 1,
"scope": {
"kind": "chatroom",
"id": "11111111-1111-4111-8111-111111111111"
},
"tag_id": "33333333-3333-4333-8333-333333333333",
"table_ids": [
"44444444-4444-4444-8444-444444444444",
"55555555-5555-4555-8555-555555555555"
],
"command_updated_at": "2026-07-21T02:10:00",
"audience": "internal",
"origin": "manual",
"ai": false,
"company_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"acting_chatroom_id": "11111111-1111-4111-8111-111111111111",
"department_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd"
}
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 404 | 指令不存在於此範圍,或呼叫者無法讀取它引用的每一張資料表。 | |
| 404 | 該指令下沒有這個 id 的執行紀錄,或呼叫者看不到它。 | |
/private/module/custom_tables/agent/commands/{command_id}/execute在可信的服務通道上執行指令;身分來自伺服器注入的 header,而不是路徑上的範圍區段。
當 agent runtime 的模型工具需要代表特定「使用者+聊天室」或社群媒體 client 執行複合寫入時使用。
- 完整路由
/private/module/custom_tables/agent/commands/{command_id}/execute
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 完整路由 | 可使用 | resolve_command_service_principal (X-API-Key service token) |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要執行的指令 UUID;其範圍由綁定的 principal 推導。 |
db | query | int | 否 | Redis 邏輯資料庫編號,因共用 client 相依而外洩到 schema 上,請不要填。 |
請求結構
CommandExecuteRequest| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
expected_contract_digest | string | null | 否 | Optional lowercase 64-hex SHA-256 of the exact current stored command definition. A trusted manual caller may send it when executing against an exact reviewed definition snapshot; omission preserves normal manual execution behavior. Generated agent write-command tools keep the digest server-private and inject their pinned value only after later unambiguous natural-language confirmation. A mismatch fails before input validation, idempotency reservation, audit creation, row locking, or mutation. |
idempotency_key | string | null | 否 | Actor-bucketed idempotency key; failed executions do not block retry. |
inputs | object | 是 | Values for the command's declared inputs; server context is excluded. |
回應結構
CommandExecutionResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
authorization_snapshot | object | null | 否 | Immutable command scope, tag, and referenced-table audit facts. |
command_id | string | null | 否 | 未指定 |
created_at | string (date-time) | null | 否 | 未指定 |
duration_ms | integer | null | 否 | 未指定 |
error | string | null | 否 | 未指定 |
executed_at | string (date-time) | null | 否 | Wall-clock stamp frozen at the ORIGINAL execution; an idempotent replay returns the same stamp, so callers can distinguish a replayed earlier result from a fresh write. |
execution_id | string | 是 | 未指定 |
idempotency_key | string | null | 否 | 未指定 |
input_snapshot | object | null | 否 | 未指定 |
invoker_client_id | string | null | 否 | 未指定 |
invoker_id | string | null | 否 | 未指定 |
outputs | object | null | 否 | Declared version-2 scalar or bounded relation outputs. Internal temporary relations and step rows are never exposed. |
response_body | object | null | 否 | 未指定 |
result_refs | object[] | null | 否 | 未指定 |
staged_change_id | string | null | 否 | 未指定 |
status | "succeeded" | "staged" | "failed" | 是 | 未指定 |
steps | (CommandExecutionV1StepResponse | CommandExecutionV2StepResponse)[] | 否 | 未指定 |
truncated | boolean | null | 否 | True when the persisted response snapshot was trimmed to the audit byte cap: declared outputs/step rows are omitted from this body. Set on an idempotent replay of a large execution so the caller can distinguish a trimmed snapshot from a genuinely empty result. |
範例
從 agent runtime 執行訂單指令
{
"pathParams": {
"command_id": "22222222-2222-4222-8222-222222222222"
},
"body": {
"inputs": {
"customer": "長榮工業",
"item": "螺絲 M4",
"quantity": 12
},
"idempotency_key": "agent-run-9f2c1a"
}
}{
"execution_id": "66666666-6666-4666-8666-666666666666",
"status": "succeeded",
"steps": [
{
"name": "stock",
"kind": "select",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
},
"on_hand": {
"type": "integer",
"nullable": true
}
}
},
{
"name": "on_hand",
"kind": "let",
"status": "succeeded",
"row_count": 0,
"schema": {}
},
{
"name": "stock_ok",
"kind": "assert",
"status": "succeeded",
"row_count": 0,
"schema": {}
},
{
"name": "order",
"kind": "insert",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
}
}
},
{
"name": "consume",
"kind": "update",
"status": "succeeded",
"row_count": 1,
"schema": {
"id": {
"type": "string",
"nullable": false
}
}
}
],
"outputs": {
"order_id": "88888888-8888-4888-8888-888888888888",
"remaining": 8
}
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | header 既沒有完整的 user + chatroom 組合,也沒有單獨的社群媒體 client id,或兩種形式混用。 | |
| 400 | 具型別的 channel headers 組合無效。X-TeamSync-Agent-Group-Scope 只在 line_group 與 line_room 時必填,其他 channel 一律不得出現。 | |
| 401 | X-API-Key 與伺服器設定的指令服務 token 不符。 | |
| 403 | 指定的身分不存在、已停用,或不屬於它宣稱的聊天室或租戶。 | |
| 403 | 程式讀取了 acting room 未授予讀取權的資料表。這條通道的部門資料表權限釘在 acting room 上,執行者其他聊天室給的授權在這裡不算數。 | |
| 403 | insert 步驟打到 acting room 未授予 insert 權的資料表。與讀取 403 是同一道前置閘門,所以不會寫入任何資料。 | |
| 403 | update 或 delete 步驟打到的資料表,在 acting room 下 `can_edit` 解析為 "none"。filtered 或 own 授權會通過這裡,改在稍後逐列把關。 | |
| 403 | acting room 在程式要寫入的某張 channel 治理資料表上沒有宣告 scope_values。這條通道釘住房間,所以訊息會把房間點名——不綁房間的 REST 對應版本,同一個 code 會說 `no chatroom of yours declares a scope on this table`。 | |
| 403 | 寫入 floor 拒絕了這筆資料列:acting room 宣告的 scope 涵蓋不到該步驟要寫的紀錄或它要設的連結。這是終局性的政策拒絕——從同一個房間重試不可能成功,agent protocol 也要求把訊息轉達給使用者後停手,不要繞路。 | |
| 403 | 連結值指向的目標確實存在、也通過 ACL,只有目標資料表的 channel scope 對 acting room 把它藏起來。與 REST 通道相同的 B10 歸因;在它之前,這個情況會回 500 `command_execution_failed`。 | |
| 403 | 連結目標所屬資料表在 acting room 下解析出的 `can_read` 是 "none"。只針對新增的目標 id。 | |
| 404 | 從綁定 principal 的聊天室、部門或公司都看不到這個 id 的指令。 | |
| 404 | 指令存在、範圍也看得到,但 agent_enabled 是 false。回應內容與「id 不存在」完全相同,所以 agent 通道不會洩漏任何存在性訊號。 | |
| 404 | 連結目標不存在、已軟刪除,或被 row ACL 藏起來——三種情況共用同一個 body。與上面的 Command not found 404 不同,請讀 detail 字串。 | |
| 409 | 同一位 principal 以相同 idempotency key 送出不同的 inputs。 | |
| 409 | require_approval 規則觸發,計畫已被暫存待覆核。 | |
| 409 | Write 模式 command 的定義,在伺服器完成不寫入的準備後、依使用者後續自然語言同意執行前發生變更。Adapter 呼叫在保留、稽核、加鎖或異動前就被拒絕;請丟棄過期的準備狀態並再次詢問使用者。 | |
| 409 | 兩次執行爭用同一批資料列,InnoDB 中止了這一次。交易已回滾,沒有任何部分寫入落地,retryable 為 true——請用「同一把」idempotency key 重試:失敗路徑會釋放該 key 的保留,因此重用是合法的。 | |
| 422 | 必要的具型別 header 缺漏或格式錯誤。X-TeamSync-Agent-Channel 必須是 ChatChannelType 值;X-TeamSync-Agent-Group-Scope 一旦出現,就必須恰好是 64 個小寫十六進位字元。這些 shape 錯誤會先由 request validation 拒絕,之後才輪到 400 的組合檢查。 | |
| 503 | 伺服器未設定 CUSTOM_TABLE_COMMAND_SERVICE_TOKEN。 | |
/private/module/custom_tables/agent/commands/{command_id}/query在可信的服務通道上執行唯讀指令,回傳一頁投影後的分頁資料。
用於只讀取的 agent 工具——查詢、彙總、篩選清單——讓模型完全碰不到寫入通道。
- 完整路由
/private/module/custom_tables/agent/commands/{command_id}/query
範圍
| 範圍 | 是否提供 | 驗證與權限 |
|---|---|---|
| 完整路由 | 可使用 | resolve_command_service_principal (X-API-Key service token) |
參數
| 欄位 | 位置 | 型別 | 必填 | 說明 |
|---|---|---|---|---|
command_id | path | uuid | 是 | 要執行的 query 模式指令 UUID;其範圍由綁定的 principal 推導。 |
請求結構
CommandQueryRequest| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
inputs | object | 是 | Values for the command's declared inputs; server context is excluded. |
page | CommandQueryPage | null | 否 | Optional pagination window; defaults to the first page of 100 rows. |
回應結構
CommandQueryResponse| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
next_cursor | string | null | 否 | Opaque cursor for the next page, or null when exhausted. |
outputs | object | 否 | Declared scalar outputs (relation outputs are paginated via rows). |
rows | object[] | 否 | Projected rows of the paginated relation output for this page. |
total_available | integer | null | 否 | Total rows available in the paginated relation, when known. |
範例
從 agent runtime 取得投影的下一頁
{
"pathParams": {
"command_id": "99999999-9999-4999-8999-999999999999"
},
"body": {
"inputs": {
"min_quantity": 10
},
"page": {
"output": "result",
"limit": 2,
"cursor": "eyJvIjoyLCJkIjoiOWYyYzFhIn0"
}
}
}{
"rows": [
{
"id": "88888888-8888-4888-8888-333333333333",
"客戶": "中鋼結構",
"數量": 10
}
],
"outputs": {},
"next_cursor": null,
"total_available": 7
}錯誤
| 狀態 | 發生條件 | 錯誤本文 |
|---|---|---|
| 400 | header 既沒有完整的 user + chatroom 組合,也沒有單獨的社群媒體 client id,或兩種形式混用。 | |
| 401 | X-API-Key 與伺服器設定的指令服務 token 不符。 | |
| 403 | 指定的身分不存在、已停用,或不屬於它宣稱的聊天室或租戶。 | |
| 403 | select 來源或 join 目標在 acting room 的授權下解析出 can_read 為 none,因此呼叫大聲失敗,而不是回一頁空資料讓模型讀成「什麼都沒有」。 | |
| 404 | 從綁定 principal 的聊天室、部門或公司都看不到這個 id 的指令。 | |
| 409 | 這是一支寫入指令,必須改走 agent 的 execute 路由。 | |