@coda-console/protocol 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,7 @@ Coda 与插件之间的约定,Coda 和插件都引用它,独立演进:
4
4
 
5
5
  - **插件清单的类型**(`schemas/plugin/`):插件目录 `package.json` 里 `contributes` 的每个键是一种类型,各有一份 JSON Schema(draft 2020-12,`$id` 为 `https://coda.pub/schemas/plugin/v1/<文件>`)。`plugin.json` 是整份清单,`setting.json` 是一项设置的声明。插件写的合乎它用到的类型就对;写给别的插件扩展点的键由那个插件声明,扩展点可以带自己的 schema(`extensionPoints.<名>.schema`,声明它的插件目录里的一个文件),写给它的东西合乎那份 schema 才对。
6
6
  - **通用校验器**(`validate.mjs`):用 Ajv 对清单做 schema 校验。JSON Schema 说不了的只用五个通用关键字(`x-unique-by`、`x-max-contains`、`x-ref`、`x-placeholders`、`x-file`,照 Kubernetes 的 `x-kubernetes-*`),每个在哪里写都是同一个意思,写法见文件开头。校验器不随功能变长:类型加字段就是改 schema。
7
+ - **流程方言**(`workflow/`):项目和插件的流程(`.coda/workflows/*.bpmn`,BPMN 2.0)在 Coda 的命名空间 `https://coda.pub/schema/bpmn/1.0` 里能写什么。`moddle.json` 是 `coda:` 属性与元素(bpmn-moddle 的描述格式),`actions.json` 是 `coda:action` 的各个动作和它们接受的参数,`dialect.json` 是流程种类、Coda 自己发的事件、步骤失败的错误码和表达式能读的运行变量。`workflow.mjs` 用它们读一份流程,方言里没有的都拒绝;每个值能写什么、流程的结构对不对,现在仍由 Coda 读流程时检查。
7
8
  - **Agent 接入协议**(`docs/agent-protocol.md`):Coda 怎样用 ACP 接 Agent,以及 Coda 加的扩展(会话指令、插话、`_coda/wait`、执行失败、工具调用的审核)。
8
9
 
9
10
  ## 用法
@@ -23,6 +24,15 @@ valueProblem('composer-modes.json#/$defs/parameters', parameters, '插件给出
23
24
  pointProblem(panelsSchema, manifest.contributes.panels, 'contributes.panels', 'plugin');
24
25
  ```
25
26
 
27
+ ```js
28
+ import { workflowProblem, readWorkflow, CODA_MODDLE, CODA_ACTIONS } from '@coda-console/protocol/workflow';
29
+
30
+ // 一份流程(名字不带 .bpmn,用在说明里):方言里都有就返回 null,否则是一句说明。
31
+ const problem = await workflowProblem(readFileSync('.coda/workflows/deploy.bpmn', 'utf8'), 'deploy');
32
+ // 要接着读它的:{ root } 是解析好的 definitions,或 { problem }。
33
+ const { root } = await readWorkflow(source, 'deploy');
34
+ ```
35
+
26
36
  扩展点的 schema 和这里的并列:可以 `$ref` 协议的 schema(`common.json#/$defs/line40`),可以用五个关键字;它自己写的 `$id` 不算数,指针只指它校验的那份值里的位置。
27
37
 
28
38
  `TYPES` 是 Coda 自己的类型(`contributes` 的键),`SCHEMAS` 是全部 schema(按文件名)。
@@ -33,7 +43,7 @@ pointProblem(panelsSchema, manifest.contributes.panels, 'contributes.panels', 'p
33
43
 
34
44
  ## 谁在用
35
45
 
36
- - **Coda**:读插件时用它校验清单,再整理成自己的结构;只另看插件身份(ID、设置名在 ID 之下),以及装到一台机器上才知道的(来源、已装插件的冲突)。
46
+ - **Coda**:读插件时用它校验清单,再整理成自己的结构;只另看插件身份(ID、设置名在 ID 之下),以及装到一台机器上才知道的(来源、已装插件的冲突)。读流程时先用 `readWorkflow` 按方言解析,再查每个值。
37
47
  - **coda-plugins 的契约检查包**(`@coda-console/plugin-check`):在写插件的地方用同一个校验器判清单。
38
48
 
39
49
  ## 版本
@@ -7,6 +7,24 @@ Coda 就知道这个 Agent 在一轮之外还在做什么,如实显示、不
7
7
 
8
8
  **改协议**:本文在 `coda-console/coda-protocol`,与插件清单的类型(`schemas/`)同一个仓库、同一个版本(npm 包 `@coda-console/protocol`)。Coda 与公开仓库 `coda-console/coda-plugins` 的契约检查包都依赖这个包:改协议就在这里开 PR,发布新版本后两边各自升依赖。检查要换判定方式时(新的请求怎么发、怎样算合格),另给 coda-plugins 开 PR 改检查本身。文中提到的 Coda 文档与源码在 Coda 的仓库里。
9
9
 
10
+ ## 生命周期归属
11
+
12
+ 插件在 `contributes.agents[]` 声明 `lifecycle`,默认 `managed`;外部自主运行的 Bot 声明 `external`。
13
+ 这是生命周期的归属,不是任务时长。普通 Agent 的长期委派仍按原有调用关系汇报。
14
+
15
+ - `managed`:Coda 按轮次建立回复关系,属于调用的后台活动继续补交结果。
16
+ - `external`:Coda 连接适配器,观测原生对话与活动;连接或本地轮次结束不代表外部 Bot 或其调度停止。
17
+ 发给它的会话消息是一次委派,附原请求编号和回传地址;没有自动回复或后续跟随,不根据原生发言的时间猜测回复对象。
18
+ 需要交回结果时调用 Coda `send`,`childId` 为回传地址,`replyTo` 为原请求编号,`prompt` 为结果,`final` 默认 `true`。
19
+ 完成后该委派不再接收新回传;约定的进展或需要发起方处理的问题用 `final: false`。回传唤醒发起会话,不创建反向调用。
20
+ 原委派授权覆盖指定接收方到发起方的回传,Coda 校验这两方与原请求;另发新请求仍走通常授权。
21
+ 外部 Agent 使用独立会话,不能被 `spawn` 接管为受管子会话,也不接受 `takeOver` 抢占回复轮次。
22
+
23
+ 常驻观测连接及外部运行记录使用 `_coda/wait` 的 `kind: "observation"`,保持适配器在线,但不作为等待结果的后台任务。
24
+ `waiting` 表示正在观测,界面用呼吸状态;有新原生活动要交互时仍以 `handling` 开始、回到 `waiting` 结束自主轮次。
25
+ 外部生命周期的原生活动起因记作 `external`,不归到碰巧进行中的 Coda 调用,也不向以前的调用方自动回传。
26
+ `session/cancel` 只表达适配器明确支持的局部停止;插件必须说明它实际停止的对象。
27
+
10
28
  ## ACP 部分
11
29
 
12
30
  Coda 是 ACP 的客户端。每个会话的 Agent 进程由一个会话托管进程(Coda 的 docs/session-host.md)持有,经标准输入输出说 JSON-RPC;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coda-console/protocol",
3
- "version": "1.2.0",
4
- "description": "Coda 的协议:插件清单的类型(JSON Schema)、通用校验器与 Agent 接入协议",
3
+ "version": "1.4.0",
4
+ "description": "Coda 的协议:插件清单的类型(JSON Schema)、通用校验器、流程方言与 Agent 接入协议",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -11,10 +11,18 @@
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
14
- "files": ["schemas", "validate.mjs", "docs"],
14
+ "files": [
15
+ "schemas",
16
+ "validate.mjs",
17
+ "workflow",
18
+ "workflow.mjs",
19
+ "docs"
20
+ ],
15
21
  "exports": {
16
22
  "./validate": "./validate.mjs",
23
+ "./workflow": "./workflow.mjs",
17
24
  "./schemas/*": "./schemas/*",
25
+ "./workflow/*": "./workflow/*",
18
26
  "./docs/*": "./docs/*",
19
27
  "./package.json": "./package.json"
20
28
  },
@@ -22,7 +30,8 @@
22
30
  "node": ">=24"
23
31
  },
24
32
  "dependencies": {
25
- "ajv": "8.20.0"
33
+ "ajv": "8.20.0",
34
+ "bpmn-moddle": "10.3.1"
26
35
  },
27
36
  "scripts": {
28
37
  "test": "node --test 'test/**/*.test.mjs'"
@@ -17,6 +17,11 @@
17
17
  "additionalProperties": false
18
18
  },
19
19
  "command": {"$ref": "common.json#/$defs/command"},
20
+ "lifecycle": {
21
+ "description": "生命周期归属。默认 managed:Coda 按轮次接管委派与回话;external:外部自主运行,Coda 观测活动,消息只委派参数,结果须按请求明确回传。",
22
+ "type": "string",
23
+ "enum": ["managed", "external"]
24
+ },
20
25
  "modes": {
21
26
  "description": "审批档位的说法:title 不超过 10 字,unset 不超过 20 字,note 不超过 100 字,auto、full 各不超过 200 字",
22
27
  "type": "object",
@@ -0,0 +1,49 @@
1
+ {
2
+ "description": "coda:action:serviceTask 上写它,就是 Coda 在后端里做的一步,不写脚本。参数写成 <coda:param name value>,只认这里列出的",
3
+ "actions": {
4
+ "publish-stage": {"params": ["stage", "detail"],
5
+ "description": "发布运行主体的会话此刻在流程里的阶段(stage)和放在状态旁的说明(detail),两者都空就是清掉;只用于由会话事件启动的运行"},
6
+ "remove-worktree": {"params": [],
7
+ "description": "整个移除运行主体的会话的工作树,分支保留提交;result 是 removed、absent、in-use、busy 或 failed"},
8
+ "stop-process": {"params": [],
9
+ "description": "停掉运行主体的会话托管的 Agent 进程,下次发消息时透明恢复;result 是 stopped、absent 或 busy"},
10
+ "hand-to-user": {"params": [],
11
+ "description": "把运行主体的会话的 Agent 已被告知、还没答复的流程任务都转给用户"},
12
+ "compact-session": {"params": [],
13
+ "description": "请运行主体的会话的 Agent 压缩上下文(_coda/session/compact,见 docs/agent-protocol.md),从不失败:没压缩就写明原因"},
14
+ "hold-group": {"params": ["group", "capacity", "borrow", "exclusive"],
15
+ "description": "为这一步所在的那一段排进并发组 group,离开这一段或运行结束才放开;borrow 为 only 只借不排、no 从不借,exclusive 为 true 占满整组"},
16
+ "run-workflows": {"params": ["kind", "commit", "inputs"],
17
+ "description": "在 commit(默认本次运行的)上启动项目里所选种类(kind,逗号分隔)的全部流程,各给它声明了的 inputs(JSON 对象),等它们都结束;有一个失败就失败"},
18
+ "workflow-kinds": {"params": ["commit"],
19
+ "description": "项目在这个提交上有没有各种类可调用的流程(项目自己的和启用插件的),各是布尔值"},
20
+ "cold-store": {"params": ["anyAfter", "archivedAfter"],
21
+ "description": "运行所属项目的会话里,已结束、超过 anyAfter(根会话已归档的超过 archivedAfter)没有更新的工具调用,详情移到冷文件"},
22
+ "trim-builds": {"params": ["idleAfter"],
23
+ "description": "运行所属项目里,会话没有归档、空闲满 idleAfter 的工作树清掉构建产物"},
24
+ "sweep-uploads": {"params": ["olderThan"],
25
+ "description": "删掉上传后超过 olderThan、既没有消息也没有草稿引用的附件;清的是整台机器的"},
26
+ "reclaim-secrets": {"params": ["archivedFor"],
27
+ "description": "运行所属项目里,根会话归档满 archivedFor 的会话,删掉它们的密钥目录"},
28
+ "reclaim-commands": {"params": ["archivedFor"],
29
+ "description": "同 reclaim-secrets,删的是批准执行命令的输出目录"},
30
+ "desktop-type": {"params": ["host", "input", "value", "field"],
31
+ "description": "把一份密文(value:路径或句柄)键入它绑定的网站(或 host)网页上的输入框;result 是 typed、refused、busy 或 expired"},
32
+ "desktop-auth": {"params": ["value"],
33
+ "description": "把会话在内存里保管的密码(value:句柄)键入系统的身份验证并提交"},
34
+ "run-command": {"params": ["command", "cwd", "approval"],
35
+ "description": "用户在本运行人工任务 approval 的卡片上批准过这条命令后,以用户的身份在会话的工作副本(或 cwd)里执行它;result 是 exited、stopped 或 unknown"},
36
+ "tell-overdue": {"params": [],
37
+ "description": "告知运行主体的会话里过了期限的等待项,像结果一样叫醒 Agent;根会话的同时告诉用户;同一项的同一个期限只说一次"},
38
+ "publish-event": {"params": ["name", "subject", "payload", "commit", "project", "session"],
39
+ "description": "发布这个插件声明的一个事件:交给为 subject(<种类>:<标识>)在跑、等着它的运行,也可以启动订阅它的流程;payload 是它们读到的 github.event"},
40
+ "list-projects": {"params": [],
41
+ "description": "本机的项目(每项 id、name、path、origin),和运行所属项目的 ID"},
42
+ "add-project": {"params": ["repo"],
43
+ "description": "从仓库地址添加项目:本机已有就是它,没有就克隆;新项目没有 Tag 时带上运行所属项目的 Tag"},
44
+ "tell-session": {"params": ["text", "key", "session"],
45
+ "description": "告诉一个会话(不写就是运行所属的)一句话,像结果一样叫醒它的 Agent;同一个 key(或同样的话)只说一次,已归档就不说"},
46
+ "session-state": {"params": ["session"],
47
+ "description": "一个会话(不写就是运行所属的)现在的样子:它或它的根会话是否已归档"}
48
+ }
49
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "description": "Coda 的 BPMN 方言里共用的名字:流程种类、Coda 自己发的事件、步骤失败时抛出的错误码、表达式按名字读的运行变量",
3
+ "kinds": {
4
+ "deploy": "部署后端与服务;只跑合并进默认分支的提交",
5
+ "preview": "发布预览版客户端;只跑合并进默认分支的提交",
6
+ "release": "发布正式版客户端;只跑合并进默认分支的提交",
7
+ "check": "检查一个提交,PR 的流程在评审前调用;任何提交都能跑",
8
+ "premerge": "合并前检查:批准合并后,在要合并的提交上只跑一次;任何提交都能跑"
9
+ },
10
+ "deploying": ["deploy", "preview", "release"],
11
+ "events": {
12
+ "session.opened": "会话开了:新建,或带着它的 Agent 从归档里拿回来。开始事件订阅它的流程跟随这个会话(生命周期流程)",
13
+ "session.facts": "只有 Coda 看得到的会话事实变了:轮次、等用户的请求、后台工作、它在等什么、定时唤醒、进程、归档。生命周期流程在哪里等都收得到",
14
+ "workflow.dispatch": "手动发起(workflow_run、客户端的按钮):由事件启动的流程也订阅它,才能手动发起",
15
+ "workflow.cancel": "要它停下:在等它的运行被告知,走到自己的结束,以取消结束"
16
+ },
17
+ "userMessage": "^user\\.[a-z][a-z0-9-]{0,39}$",
18
+ "stepFailed": "STEP_FAILED",
19
+ "variables": ["inputs", "run", "github", "vars"]
20
+ }
@@ -0,0 +1,236 @@
1
+ {
2
+ "name": "Coda",
3
+ "prefix": "coda",
4
+ "uri": "https://coda.pub/schema/bpmn/1.0",
5
+ "description": "Coda 的 BPMN 方言:挂在 BPMN 元素上的 coda: 属性,与写在 extensionElements 里的 coda: 元素(bpmn-moddle 的描述格式)。认不出的 coda: 内容一律不算数",
6
+ "xml": {"tagAlias": "lowerCase"},
7
+ "associations": [],
8
+ "types": [
9
+ {
10
+ "name": "ProcessProperties",
11
+ "isAbstract": true,
12
+ "extends": ["bpmn:Process"],
13
+ "description": "流程本身:concurrency 并发、key 未结束运行的业务键、deploy 只跑默认分支的提交、title 运行在卡片上的名字(同 GitHub 的 run-name)、kind 种类(见 dialect.json 的 kinds)、dispatch 能否手动发起、inline Agent 用 workflow_run 发起时等它结束再回答、subagents 为 false 时会话 Agent 的原生子 Agent 不能发起、agents 生命周期流程跟随哪些 Agent 的会话(ID,逗号或空格分隔,不写是全部)",
14
+ "properties": [
15
+ {"name": "concurrency", "isAttr": true, "type": "String"},
16
+ {"name": "key", "isAttr": true, "type": "String"},
17
+ {"name": "deploy", "isAttr": true, "type": "Boolean"},
18
+ {"name": "title", "isAttr": true, "type": "String"},
19
+ {"name": "kind", "isAttr": true, "type": "String"},
20
+ {"name": "dispatch", "isAttr": true, "type": "Boolean"},
21
+ {"name": "inline", "isAttr": true, "type": "Boolean"},
22
+ {"name": "subagents", "isAttr": true, "type": "Boolean"},
23
+ {"name": "agents", "isAttr": true, "type": "String"}
24
+ ]
25
+ },
26
+ {
27
+ "name": "ScriptTaskProperties",
28
+ "isAbstract": true,
29
+ "extends": ["bpmn:ScriptTask", "bpmn:ServiceTask"],
30
+ "description": "脚本步骤(scriptTask)的:audience、group 运行时占的并发组、timeout 与 cancelTimeout(分钟)、workingDirectory、checkout、retry 网络失败时再跑几次(1 到 5)。写在 serviceTask 上只为提示脚本步骤要写成 scriptTask",
31
+ "properties": [
32
+ {"name": "audience", "isAttr": true, "type": "String"},
33
+ {"name": "group", "isAttr": true, "type": "String"},
34
+ {"name": "timeout", "isAttr": true, "type": "Real"},
35
+ {"name": "cancelTimeout", "isAttr": true, "type": "Real"},
36
+ {"name": "workingDirectory", "isAttr": true, "type": "String"},
37
+ {"name": "checkout", "isAttr": true, "type": "String"},
38
+ {"name": "retry", "isAttr": true, "type": "String"}
39
+ ]
40
+ },
41
+ {
42
+ "name": "ServiceTaskProperties",
43
+ "isAbstract": true,
44
+ "extends": ["bpmn:ServiceTask"],
45
+ "description": "Coda 自己做的一步:action 是 actions.json 里的一个动作,参数写成 coda:param",
46
+ "properties": [
47
+ {"name": "action", "isAttr": true, "type": "String"}
48
+ ]
49
+ },
50
+ {
51
+ "name": "RunsOnProperties",
52
+ "isAbstract": true,
53
+ "extends": ["bpmn:ScriptTask", "bpmn:SubProcess"],
54
+ "description": "脚本步骤要在哪里跑(同 GitHub 的 runs-on):标签如 linux,x64,由这台机器上的项目绑到执行环境;写在子流程上对其中每个脚本步骤生效",
55
+ "properties": [
56
+ {"name": "runsOn", "isAttr": true, "type": "String"}
57
+ ]
58
+ },
59
+ {
60
+ "name": "LoopProperties",
61
+ "isAbstract": true,
62
+ "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
63
+ "description": "多实例(同 Camunda 8):inputCollection 是进入时读的集合表达式,每个元素一份,元素在每份里叫 inputElement",
64
+ "properties": [
65
+ {"name": "inputCollection", "isAttr": true, "type": "String"},
66
+ {"name": "inputElement", "isAttr": true, "type": "String"}
67
+ ]
68
+ },
69
+ {
70
+ "name": "FlowNodeProperties",
71
+ "isAbstract": true,
72
+ "extends": ["bpmn:FlowNode"],
73
+ "description": "status:走到这个节点时运行卡片上的一行;optional:阶段(最外层的子流程)进入了才显示;expect:子流程进入后预计多久(ISO 8601 时长或表达式),过了它,运行所在会话等的东西就到期",
74
+ "properties": [
75
+ {"name": "status", "isAttr": true, "type": "String"},
76
+ {"name": "optional", "isAttr": true, "type": "Boolean"},
77
+ {"name": "expect", "isAttr": true, "type": "String"}
78
+ ]
79
+ },
80
+ {
81
+ "name": "StartEventProperties",
82
+ "isAbstract": true,
83
+ "extends": ["bpmn:StartEvent"],
84
+ "description": "计时器开始事件的:reportTo 运行向哪个会话汇报(会话引用,或读本插件设置的 =vars.…),reportOn 是 failure(默认)或 always,scope 是 project(默认,每个有它的项目各跑)或 machine(整台机器一次)",
85
+ "properties": [
86
+ {"name": "reportTo", "isAttr": true, "type": "String"},
87
+ {"name": "reportOn", "isAttr": true, "type": "String"},
88
+ {"name": "scope", "isAttr": true, "type": "String"}
89
+ ]
90
+ },
91
+ {
92
+ "name": "EndEventProperties",
93
+ "isAbstract": true,
94
+ "extends": ["bpmn:EndEvent"],
95
+ "description": "notify:这一轮要结果回传(true、false 或表达式),只写在最外层的结束事件上,送达仍等整个运行结束",
96
+ "properties": [
97
+ {"name": "notify", "isAttr": true, "type": "String"}
98
+ ]
99
+ },
100
+ {
101
+ "name": "UserTaskProperties",
102
+ "isAbstract": true,
103
+ "extends": ["bpmn:UserTask"],
104
+ "description": "人工任务的:assignee 交给谁,handback 为 false 时卡片不给「交回 Agent」,三个按钮的名字",
105
+ "properties": [
106
+ {"name": "assignee", "isAttr": true, "type": "String"},
107
+ {"name": "handback", "isAttr": true, "type": "Boolean"},
108
+ {"name": "completeLabel", "isAttr": true, "type": "String"},
109
+ {"name": "rejectLabel", "isAttr": true, "type": "String"},
110
+ {"name": "noteLabel", "isAttr": true, "type": "String"}
111
+ ]
112
+ },
113
+ {
114
+ "name": "Input",
115
+ "superClass": ["Element"],
116
+ "description": "运行的一个输入:label 是表单上的名字,defaultFrom 是从会话发起、没给值时从哪里取(在 default 之前)",
117
+ "properties": [
118
+ {"name": "name", "isAttr": true, "type": "String"},
119
+ {"name": "type", "isAttr": true, "type": "String"},
120
+ {"name": "default", "isAttr": true, "type": "String"},
121
+ {"name": "required", "isAttr": true, "type": "Boolean"},
122
+ {"name": "description", "isAttr": true, "type": "String"},
123
+ {"name": "label", "isAttr": true, "type": "String"},
124
+ {"name": "defaultFrom", "isAttr": true, "type": "String"}
125
+ ]
126
+ },
127
+ {
128
+ "name": "Script",
129
+ "superClass": ["Element"],
130
+ "description": "脚本步骤的脚本正文",
131
+ "properties": [
132
+ {"name": "body", "isBody": true, "type": "String"}
133
+ ]
134
+ },
135
+ {
136
+ "name": "Inspect",
137
+ "superClass": ["Element"],
138
+ "description": "进节点执行记录的一个业务值:只记明确点名的,从不记整个环境",
139
+ "properties": [
140
+ {"name": "label", "isAttr": true, "type": "String"},
141
+ {"name": "type", "isAttr": true, "type": "String"},
142
+ {"name": "value", "isAttr": true, "type": "String"},
143
+ {"name": "phase", "isAttr": true, "type": "String"}
144
+ ]
145
+ },
146
+ {
147
+ "name": "Env",
148
+ "superClass": ["Element"],
149
+ "description": "步骤的一个环境变量:value 可以是表达式",
150
+ "properties": [
151
+ {"name": "name", "isAttr": true, "type": "String"},
152
+ {"name": "value", "isAttr": true, "type": "String"}
153
+ ]
154
+ },
155
+ {
156
+ "name": "Output",
157
+ "superClass": ["Element"],
158
+ "description": "步骤的一个输出(脚本写进 $GITHUB_OUTPUT,只写 name);写在结束事件上是运行交给调用方的值(value,结束时读)",
159
+ "properties": [
160
+ {"name": "name", "isAttr": true, "type": "String"},
161
+ {"name": "value", "isAttr": true, "type": "String"}
162
+ ]
163
+ },
164
+ {
165
+ "name": "Choice",
166
+ "superClass": ["Element"],
167
+ "description": "人工任务给的一个答复:id 是任务输出里的 choice;可以套着更多答复,按住时给出;fields 是它检查并发送的输入(不写是全部,空串是没有)",
168
+ "properties": [
169
+ {"name": "id", "isAttr": true, "type": "String"},
170
+ {"name": "name", "isAttr": true, "type": "String"},
171
+ {"name": "detail", "isAttr": true, "type": "String"},
172
+ {"name": "primary", "isAttr": true, "type": "Boolean"},
173
+ {"name": "fields", "isAttr": true, "type": "String"},
174
+ {"name": "choices", "type": "Choice", "isMany": true},
175
+ {"name": "options", "type": "Option", "isMany": true}
176
+ ]
177
+ },
178
+ {
179
+ "name": "Option",
180
+ "superClass": ["Element"],
181
+ "description": "一个答复上可以独立勾选的附加项:default(是否默认勾上)与 shown(是否给出)都在任务打开时读;没给出的总是 false",
182
+ "properties": [
183
+ {"name": "id", "isAttr": true, "type": "String"},
184
+ {"name": "name", "isAttr": true, "type": "String"},
185
+ {"name": "detail", "isAttr": true, "type": "String"},
186
+ {"name": "shown", "isAttr": true, "type": "String"},
187
+ {"name": "default", "isAttr": true, "type": "String"}
188
+ ]
189
+ },
190
+ {
191
+ "name": "Field",
192
+ "superClass": ["Element"],
193
+ "description": "人工任务卡片上显示和询问的一个字段",
194
+ "properties": [
195
+ {"name": "type", "isAttr": true, "type": "String"},
196
+ {"name": "id", "isAttr": true, "type": "String"},
197
+ {"name": "label", "isAttr": true, "type": "String"},
198
+ {"name": "text", "isAttr": true, "type": "String"},
199
+ {"name": "src", "isAttr": true, "type": "String"},
200
+ {"name": "href", "isAttr": true, "type": "String"},
201
+ {"name": "plugin", "isAttr": true, "type": "String"},
202
+ {"name": "view", "isAttr": true, "type": "String"},
203
+ {"name": "args", "isAttr": true, "type": "String"},
204
+ {"name": "presentation", "isAttr": true, "type": "String"},
205
+ {"name": "default", "isAttr": true, "type": "String"},
206
+ {"name": "autofill", "isAttr": true, "type": "String"},
207
+ {"name": "keep", "isAttr": true, "type": "String"},
208
+ {"name": "host", "isAttr": true, "type": "String"},
209
+ {"name": "dir", "isAttr": true, "type": "String"},
210
+ {"name": "required", "isAttr": true, "type": "Boolean"},
211
+ {"name": "multiline", "isAttr": true, "type": "Boolean"},
212
+ {"name": "masked", "isAttr": true, "type": "Boolean"},
213
+ {"name": "maxLength", "isAttr": true, "type": "Integer"}
214
+ ]
215
+ },
216
+ {
217
+ "name": "Fields",
218
+ "superClass": ["Element"],
219
+ "description": "卡片上的一组字段:value 是装着它们的 JSON",
220
+ "properties": [
221
+ {"name": "value", "isAttr": true, "type": "String"},
222
+ {"name": "host", "isAttr": true, "type": "String"},
223
+ {"name": "dir", "isAttr": true, "type": "String"}
224
+ ]
225
+ },
226
+ {
227
+ "name": "Param",
228
+ "superClass": ["Element"],
229
+ "description": "coda:action 或 callActivity 的一个参数:value 可以是表达式",
230
+ "properties": [
231
+ {"name": "name", "isAttr": true, "type": "String"},
232
+ {"name": "value", "isAttr": true, "type": "String"}
233
+ ]
234
+ }
235
+ ]
236
+ }
package/workflow.mjs ADDED
@@ -0,0 +1,59 @@
1
+ // Coda's dialect of BPMN 2.0 (workflow/): what a workflow of a project or a plugin may write in Coda's namespace, as a
2
+ // bpmn-moddle descriptor (moddle.json); the steps it may ask of Coda itself, each with the parameters it takes
3
+ // (actions.json); and the names workflows share (dialect.json): their kinds, the events Coda sends, the error code of a
4
+ // failed step, the run's variables an expression reads by name. Coda reads a workflow with them, and coda-plugins'
5
+ // contract check does where a plugin is written. readWorkflow refuses what the dialect does not have; what each value may
6
+ // be is still Coda's to check as it reads the workflow.
7
+ import { BpmnModdle } from 'bpmn-moddle';
8
+ import moddle from './workflow/moddle.json' with { type: 'json' };
9
+ import actions from './workflow/actions.json' with { type: 'json' };
10
+ import dialect from './workflow/dialect.json' with { type: 'json' };
11
+
12
+ export const CODA_MODDLE = moddle;
13
+ export const CODA_NAMESPACE = moddle.uri;
14
+ // Each action by name, with the parameters it takes (`coda:param`).
15
+ export const CODA_ACTIONS = Object.fromEntries(Object.entries(actions.actions).map(([name, action]) => [name, action.params]));
16
+ export const WORKFLOW_KINDS = Object.keys(dialect.kinds);
17
+ // The kinds that deploy or release: a workflow of one runs only commits merged into the default branch.
18
+ export const DEPLOYING_KINDS = dialect.deploying;
19
+ // The events Coda sends (dialect.json says what each is).
20
+ export const CODA_EVENTS = Object.keys(dialect.events);
21
+ export const SESSION_OPENED = 'session.opened', SESSION_FACTS = 'session.facts', DISPATCH_EVENT = 'workflow.dispatch', CANCEL_EVENT = 'workflow.cancel';
22
+ // A message the user sends from a run's page (`user.<what>`), while the run waits on it.
23
+ export const USER_MESSAGE = new RegExp(dialect.userMessage);
24
+ export const STEP_FAILED = dialect.stepFailed;
25
+ export const RUN_VARIABLES = dialect.variables;
26
+
27
+ const SIZE_LIMIT = 1_000_000;
28
+ const children = element => Object.values(element).flatMap(v => (Array.isArray(v) ? v : [v])).filter(v => v?.$type && v.$parent === element);
29
+ // Everything in the definition under `element`, depth first, by a stack of its own, pushed one at a time: a definition may
30
+ // nest deeper than the call stack goes, and hold more in one place than a call takes arguments.
31
+ const all = element => {
32
+ const found = [], stack = [], push = kids => { for (let i = kids.length - 1; i >= 0; i--) stack.push(kids[i]); };
33
+ push(children(element));
34
+ while (stack.length) { const next = stack.pop(); found.push(next); push(children(next)); }
35
+ return found;
36
+ };
37
+
38
+ // A workflow's definition (`file`, its name without `.bpmn`, for what is said) read with Coda's dialect: `{ root }`, the
39
+ // parsed definitions, or `{ problem }`, what of it the dialect does not have — not BPMN, a coda: attribute or element it
40
+ // does not know, an action it does not have or a parameter that action does not take.
41
+ export async function readWorkflow(source, file = 'workflow') {
42
+ if (typeof source !== 'string' || source.length > SIZE_LIMIT) return { problem: `${file}.bpmn 过大` };
43
+ let parsed;
44
+ try { parsed = await new BpmnModdle({ coda: CODA_MODDLE }).fromXML(source); }
45
+ catch (e) { return { problem: `${file}.bpmn 不是有效的 BPMN 2.0:${e.message}` }; }
46
+ const { rootElement: root, warnings } = parsed;
47
+ if (warnings.length) return { problem: `${file}.bpmn 有无法识别的内容:${warnings.slice(0, 3).map(w => w.message).join(';')}` };
48
+ for (const task of all(root).filter(e => e.$type === 'bpmn:ServiceTask' && e.action !== undefined)) {
49
+ if (!Object.hasOwn(CODA_ACTIONS, task.action)) return { problem: `${task.id}:coda:action 只能是 ${Object.keys(CODA_ACTIONS).join('、')}` };
50
+ const params = CODA_ACTIONS[task.action];
51
+ const given = (task.extensionElements?.values ?? []).filter(v => v.$type === 'coda:Param');
52
+ if (given.some(p => !params.includes(p.name))) return { problem: `${task.id}:coda:action ${task.action} 只接受参数 ${params.join('、')}` };
53
+ }
54
+ return { root };
55
+ }
56
+ // What of a workflow's definition the dialect does not have, or null.
57
+ export async function workflowProblem(source, file) {
58
+ return (await readWorkflow(source, file)).problem ?? null;
59
+ }