JOJO 网络草稿与运行 API
本批提供网络草稿的创建、保存、重开和删除。请求使用登录用户 JWT Bearer Token;读取需项目 read(显式授权,或同团队已加入成员的隐式只读),保存与删除需项目 write(显式授权)。没有写权限时保存/删除返回 403 write_forbidden,前端据列表接口的 canWrite 提前进入只读状态。不接受 API Key、Agent Token 或 OAuth Token。本页说明协议与前端行为,文档更新不代表当前线上版本或验收已通过。
项目「JOJO 网络」页面沿用 ProjectLayout / WnLayoutShell 四区:SideHeader 提供新建与搜索,SideBody 显示网络菜单,MainHeader 放名称、保存状态、画布/Flow 切换与保存操作,MainBody 放全高画布或 Flow 列表及只读 IPO 抽屉(标题、描述、输入、输出;正文到工作流页面编辑,可「返回网络」重开原来的点)。控件继承全局 Naive UI 明暗主题,自定义画布使用主题变量;窄屏提供不依赖桌面侧栏的网络选择与新建入口。抽屉宽度在 320–480 间自适应;工作区 ≥ 680px 时抽屉与画布并排(画布至少 360,让出抽屉宽度,所选项不在可见范围时以当前缩放平移过去),更窄时为全宽浮层。首次读取期间侧栏显示加载态;未保存的新网络立即出现在侧栏菜单与窄屏选择器。真实网络新建的工作流描述与正文为空。
读取、保存、复制、checkpoint 提交与未保存确认期间锁定冲突操作;保存结果未知时,鼠标与键盘均不能绕过冻结修改图或文档,恢复沿用同一请求重试。关闭按钮、遮罩或 Esc 取消保存确认均保留当前草稿及抽屉,取消后可再次切换或添加网络。本地入口的身份夹具仅用于隔离验收,不改变上述生产鉴权要求。四区适配保持本页 API、revision、独立复制、协同和 checkpoint 协议,不新增 Run 或发布;本轮浏览器与上线验收仍须单独执行。
获取网络列表
GET /projects/:projectId/networks响应:
{"networks":[{"id":"11111111-1111-4111-8111-111111111111","name":"客服网络","revision":1}],"canWrite":true}canWrite=false 表示当前用户对项目只有查看权限(例如团队成员查看队友创建的项目):可以读取网络,不能新建、保存或删除。
重新打开网络
GET /projects/:projectId/networks/:networkId返回 {id,name,revision,graph,documents,io_diagnostics,entry_compatibility}。graph 只存节点、边、入口和关联文档 ID;documents 含本网 owned 文档的 id,title,content,content_hash,content_sync_seq,network_id,version_hash,io_contract,包括已从图移除但未删除的文档。
content 是只读 canonical 投影;version_hash 由服务端从该正文计算,运行请求原样回传。它复用平台 FNV-1a 32位版本口径(8位小写hex,空内容为 empty),用于版本握手,不能作为权限或安全完整性凭据。原 content_hash / content_sync_seq 保留供存储、复制协议使用;旧数据库 hash 列过期时不影响 version_hash。无法得到合法投影的文档返回 version_hash:null 及诊断,不伪造可运行版本。
io_contract 读取正文中的全部 Input 声明(稳定ID、公共名字、类型、required、schema、description、consumed)及严格 Output 字段、各结束点的实际字段集合。consumed 表示实际引用;未使用声明仍可查看,但不升级为运行必填。Output 来自文档尾部声明,代码块 return 不替代 Output。连接按公共字段名精确匹配,支持类型、nullable、嵌套结构及 Exit 的可选省略检查。
entry_compatibility 按每个启用入口的可达路径判断能否启动;io_diagnostics 定位入口、节点或边的文档与字段。其他入口不可达的坏文档不阻止所选入口运行。图仍可保存供修复。
文档版本清单与单个文档
GET /projects/:projectId/networks/:networkId/documents/versions
GET /projects/:projectId/networks/:networkId/documents/:documentId均需项目 read。版本清单返回 {documents:[{id,version_hash,content_sync_seq}]},不返回正文;单个文档返回与上面的 documents 项相同的结构。文档不属于本网或已删除时返回404 network_document_not_found。
项目 WebSocket 的 workflow_document_checkpoint 应用事件只通知 project_id/document_id/content_hash/content_sync_seq。客户端只回读受影响文档,保留未保存图与选中项;重连及页面恢复可见时补同步版本清单。此推送覆盖 gateway 正文写入;网络批量保存 RPC 由本地回读及补同步收敛,不保证跨客户端即时推送。
创建或保存网络
PUT /projects/:projectId/networks/:networkId新建网络使用新 UUID 和 expectedRevision=0,更新使用最近读取的 revision。requestId 是一次保存的 UUID;响应不确定时使用完全相同的 ID 和请求体重试。
{
"requestId":"33333333-3333-4333-8333-333333333333",
"expectedRevision":0,
"name":"客服网络",
"graph":{
"nodes":[],
"edges":[],
"entries":[{
"id":"entry-1","pointId":"external-1","position":{"x":0,"y":0},
"source":"api","target":"","enabled":false,
"flowId":"22222222-2222-4222-8222-222222222222"
}]
},
"documents":[{
"id":"22222222-2222-4222-8222-222222222222",
"title":"入口 Flow",
"content":{
"type":"doc",
"content":[
{"type":"title","attrs":{"id":"title","name":"title"},"content":[{"type":"text","text":"入口 Flow"}]},
{"type":"description","attrs":{"id":"description","name":"description"}},
{"type":"inputsOrTrigger","attrs":{"id":"inputs","name":"inputs","type":null}},
{"type":"paragraph","attrs":{"id":"body","name":"body"}}
]
}
}]
}新文档对象必须在 content 与复制源字段之间二选一。复制同项目 Flow 用以下形状,服务端读取并验证源 checkpoint,创建新独立文档:
{
"id":"44444444-4444-4444-8444-444444444444",
"title":"副本 Flow",
"sourceDocumentId":"55555555-5555-4555-8555-555555555555",
"sourceSyncSeq":12,
"sourceHash":"源文档最近读取的 content_hash"
}已保存的 Flow 只通过 flowId 引用,不重复发送正文到网络保存。修改其正文使用原文档 checkpoint/协同编辑通道。
新建 Flow 的输入采用现有编辑器层级 inputsOrTrigger(type=input) → inputs → input,保存后在工作流页面由原协同编辑器修改。Flow 列表按文档 UUID 展示一次;进入文件页可保留网络选择,返回时重开同一网络。取消离开确认保留当前文档抽屉;同一次 Esc 不会穿透。同步后的服务端明确拒绝会显示原编辑器错误界面并保留待保存文档。
本地验收入口补齐内部高度传递和文件页编辑容器;协同编辑器退出后过滤旧实例的迟到光标更新,文件页可继续返回同一网络。新修复的完整浏览器验收仍须按仓库本地验收步骤执行。
项目壳侧栏的长网络菜单独立滚动,正文按剩余高度分配;移动端通过独立选择/新建入口使用网络。此适配不改变保存、协作或首次保存前 input 配置的范围。
图最大 200 节点、500 边、50 入口,至少一个入口;Flow ID 必须为 UUID。入口来源为 api、subscription 或 romp;enabled 入口必须连接顶层节点。节点类型 jojo,data 为 {label,description,flowId,container?};容器内节点带 parentNode。边类型 jojo,包含合法 source/target 及 input/output handles,data 为 {flowId,kind?}。不保存临时选中、运行状态或 Flow 正文。
成功响应:
{"id":"11111111-1111-4111-8111-111111111111","revision":1,"requestId":"33333333-3333-4333-8333-333333333333"}2xx 表示网络、文档、名称目录和回执已经一起提交。目录或任一写入失败不会返回部分成功。相同请求重放不会重复创建文档或增加 revision;相同请求 ID 换体返回冲突。
删除网络
DELETE /projects/:projectId/networks/:networkId?expectedRevision=3&deleteDocuments=true| 参数 | 位置 | 必填 | 说明 |
|---|---|---|---|
| expectedRevision | query | 是 | 最近读取的网络 revision(正整数);不一致返回 409 revision_conflict |
| deleteDocuments | query | 是 | true:本网工作流一起移入回收站(可恢复);false:工作流转为普通工作流,放进文件列表里以网络名命名的新文件夹 |
成功响应:
{"id":"11111111-1111-4111-8111-111111111111","deletedDocuments":2,"retainedDocuments":0,"folderKey":null}保留模式下 folderKey 是新文件夹的 key(folder-xxxxxxxx)。网络图删除后无法恢复;被移入回收站或转为普通文件的工作流不再属于任何网络,按普通工作流恢复、改名、移动或永久删除。网络、受管目录与工作流归属在同一事务内提交。
已删除网络的保存回执仍保留:首次创建响应丢失后用同一 requestId 重试会返回原结果,不会把网络重新建出来。之后读取该网络返回 404,客户端应丢弃本地草稿,不要换新 requestId 以 expectedRevision=0 重新提交。
删除网络中的工作流
DELETE /projects/:projectId/networks/:networkId/documents/:documentId只允许删除最近一次保存的网络图(节点、边、入口)没有引用的本网工作流;仍在使用时返回 409 network_document_in_use,需要先在网络中移除对应的点、边或入口并保存。成功后工作流移入回收站,同时脱离网络(恢复后是普通工作流)。
成功响应:
{"id":"22222222-2222-4222-8222-222222222222","networkId":"11111111-1111-4111-8111-111111111111"}如果其他页面里还有引用该工作流的未保存网络草稿,再保存时会收到 409 copy_required,需要移除对应节点或重新复制工作流。
错误
| 状态 | error | 处理 |
|---|---|---|
| 400 | invalid_resource_id / invalid_request / invalid_graph / invalid_flow | 检查 UUID、名称、图和 Flow 格式;删除网络时检查两个 query 参数 |
| 400 | flow_dependencies_unsupported | 首批不接公共子 Flow、工作流工具或 Skill 依赖 |
| 401 | unauthorized | 重新登录 |
| 403 | forbidden | 没有项目查看权限 |
| 403 | write_forbidden | 只有查看权限,不能保存或删除(需项目编辑权限) |
| 404 | project_not_found / network_not_found | 资源不存在或不可访问 |
| 404 | network_document_not_found | 工作流不属于该网络、已删除或不存在 |
| 409 | network_document_in_use | 工作流仍被已保存的网络图引用,先移除并保存网络 |
| 409 | network_run_in_progress | 删除网络时网络正在运行,先停止运行再删除 |
| 409 | revision_conflict | 重读最新网络,不能覆盖别人的更新(删除网络时同理) |
| 409 | idempotency_conflict | 同一请求 ID 的内容不一致 |
| 409 | source_conflict | 源 checkpoint 已变化,重新选择复制源 |
| 409 | copy_required / managed_folder_conflict | 核验独立文档归属及受管目录 |
| 413 | request_too_large | 缩小请求,最大 4 MiB |
| 503 | network_storage_unavailable / project_access_unavailable | 保留草稿;未知保存结果使用原请求重试 |
没有 secret 调试、发布、版本或 Worker API;网络运行见下文「运行网络」。
运行网络(2026-10-12,迁移 20261012190000_project_network_runs)
按已保存的网络图手动触发一次整网运行。后端统一调度:关抽屉、刷新页面不中断,前端订阅项目推送后读详情接回进度(网络运行与步骤登记以 app_event 推送,table_name 为 project_network_runs / project_network_run_steps、data.row_id 为网络运行 id;子 runs 走既有 change_event),运行中另有 10 秒兜底读取。启动、停止需项目 execute 权限(无则 403 execute_forbidden),读列表、详情需 read。
运行语义:从所选入口出发,入口工作流的输出是本次运行的「网络上下文」,再投递给入口目标节点;节点输出非空时并行运行它的全部出边;出边输出非空时投递给目标节点,reply 边(连回入口点的边)的输出作为出口输出逐条追加。每个节点按到达顺序逐条串行处理(同一条消息的出边跑完才处理下一条),不同节点、不同边并行。工作流输出为 {} 或所有字段都是 null/'' 时这一路到此为止,不算错误。
每个元素运行时按其 Input 名取值:上游投递的输出对象同名字段优先,其次网络上下文同名字段,都没有就不传(由工作流自身必填校验报错)。Input 名为 network_outputs 时注入本次运行到目前为止的出口输出数组。每步是一次普通工作流运行(计一次运行费),会出现在项目运行列表,session_id = network-run:<运行 id>,causation_id = 上游步骤的运行 id。
启动
POST /projects/:projectId/networks/:networkId/runs{"entryId":"entry-1","inputs":{"q":"你好"},"expectedRevision":3,"expectedDocumentHashes":{"22222222-2222-4222-8222-222222222222":"a1234567"}}成功 201:{"id":"<网络运行 id>"}。启动前校验:
| 状态 | error | 说明 |
|---|---|---|
| 400 | invalid_request | body 只能含 entryId / inputs(对象)/ expectedRevision(正整数)/ 必需 expectedDocumentHashes(文档UUID→服务端 version_hash);所选入口所有可达文档均须齐备,不接受本网之外的ID |
| 400 | network_run_entry_invalid | 入口不存在或未启用 |
| 400 | network_run_container_unsupported | 从该入口可达的元素里有容器或子节点,附 elements: [{kind,id,label}] |
| 400 | network_io_contract_invalid | 可达文档的真实接口或连接不匹配,附 diagnostics,包含入口/元素/flowId/字段/来源/原因码;包括缺少显式Output、损坏声明或不可达绑定 |
| 400 | workflow_reference_invalid | canonical 起跑正文中存在非法变量引用,附 flowId 与 details |
| 400 | input_validation_failed | 入口工作流的输入不合规,附 details: [{field,message}] |
| 409 | revision_conflict | expectedRevision 不是已保存的最新版本,先保存/重读 |
| 409 | document_version_conflict | 可达文档正文已变化,附 documents:[{id,expectedHash,actualHash}];刷新接口后重新确认输入 |
| 409 | network_run_in_progress | 同一网络已有运行中的记录 |
服务端先取得所选入口全部可达文档的耐久 checkpoint(最多 4 篇并发;每篇先完成协同同步屏障,再读一次已保存状态:Yjs 状态逐字节未变就直接沿用、不加文档版本号,有变化才提交新版本),全部完成后统一核对版本、活动引用、接口连接和入口实际输入,全部通过后才受理网络运行。每个子步骤使用这次固定的正文/hash/seq,仍检查当时的执行权限、余额与工具清单;HTTP 请求不能自行提供冻结正文。这里只固定本次文档集合,不承诺跨文档同一瞬间事务或进程恢复。
最近运行
GET /projects/:projectId/networks/:networkId/runs{"runs":[{"id":"…","status":"completed","entryId":"entry-1","createdAt":"…","finishedAt":"…"}]}最近 20 次,按创建时间倒序。status 为 running / completed / failed / stopped。读取时若有心跳超过 30 秒的 running 记录,服务端先把它判定为中断(failed / network_run_interrupted)再返回;否则只读、不写库。运行详情同理。
运行详情
GET /projects/:projectId/networks/:networkId/runs/:runId{
"run": {"id":"…","networkId":"…","networkRevision":3,"entryId":"entry-1","status":"running",
"inputs":{"q":"你好"},"outputs":[{"say":"…"}],"error":null,
"createdAt":"…","finishedAt":null,"triggeredByUserId":"…"},
"steps": [{"seq":1,"elementKind":"entry","elementId":"entry-1","runId":"…","status":"success","startedAt":"…","finishedAt":"…"}]
}steps 按 seq 升序,runId 指向该步工作流自己的运行记录(过程到 GET /runs/:runId 与时间线查看,不复制);status 原样取自工作流运行,finishedAt 仅在其终态时给出。找不到返回 404 network_run_not_found。
run.error(failed 时):
| code | 含义 |
|---|---|
| network_run_step_failed | 某步工作流运行出错或被取消(elementKind/elementId/message) |
| network_run_start_failed | 某步运行没能创建(如余额不足,message 以原因码开头,例如 insufficient_credits: …) |
| network_run_ask_unsupported | 某步工作流以提问(ask)结束,网络运行不支持 |
| network_run_output_required | 某步没有返回声明的最终Output |
| network_run_step_limit / network_run_element_limit / network_run_timeout | 总步数超 200 / 单元素运行超 20 次 / 总时长超 15 分钟,已停止全部在跑步骤 |
| network_run_interrupted | 后端进程中断(心跳超过 30s 未更新) |
任一步失败时该路停止、其他分支继续跑完,已得到的出口输出保留。stopped 时 error 为 null。
停止
POST /projects/:projectId/networks/:networkId/runs/:runId/stop返回 {"status":"running"} 表示已请求停止(终态以详情为准);已结束的运行直接返回当前状态。停止对所有在跑的工作流运行发送取消信号,跨实例同样有效。
已知限制
- 运行中保存网络或编辑文档只影响下次运行;本次调度使用起跑图及全部可达文档的固定版本。前端运行期间锁定编辑,其他标签页或 API 仍可保存。
- 编排在后端 API 进程内;进程重启时运行以
network_run_interrupted明确失败。 - 删除网络时运行记录一并删除;网络运行中不能删除网络(409
network_run_in_progress)。
网络运行的状态与输入保护(2026-10-12)
过期判定同时约束项目与网络,详情和停止在修改运行状态前核验网络归属。停止标志在终态事务中生效,正常完成不能覆盖已经受理的停止。启动前先读取停止标志;停止期间正在创建的步骤完成登记、取消派发后才结束网络运行。取消连续失败返回 failed 与 network_run_cancel_failed,步骤可能仍在运行,需要从步骤详情重试停止。
步骤详情增加可空的 outputIndex。它是该步骤在 run.outputs 中的下标,从 0 开始;没有出口输出时为 null。同一步骤重复提交相同出口结果不会追加重复输出,不同结果会被拒绝。出口写入串行提交,network_outputs 顺序与记录一致。
系统在入口覆盖 network_outputs 为 []。后续步骤按起跑时固定正文的真实 Input 公共名字映射并校验输入;上游值优先,缺字段才回退到入口Output。只有声明并使用的 Input 参与运行校验。network_outputs 是已有的出口结果列表,不是新增共享上下文对象。运行失去 inline lease 时不传播未可靠提交的输出。