nx-kv 0.17.0 → 0.19.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/CHANGELOG.md +108 -0
- package/assets/kv-get/references/wait-workflow.md +4 -0
- package/package.json +1 -1
- package/src/core/workflow/edges.js +31 -0
- package/src/core/workflow/event-names.js +10 -0
- package/src/core/workflow/layout.js +119 -0
- package/src/core/workflow/lifecycle.js +58 -0
- package/src/core/workflow/plugin-registry.js +47 -0
- package/src/core/workflow/project.js +108 -0
- package/src/core/workflow/readiness.js +98 -0
- package/src/core/workflow/snapshot.example.json +46 -0
- package/src/core/workflow/types.js +169 -0
- package/src/modules/docs/brief.js +84 -0
- package/src/modules/localtask/bus/index.js +41 -0
- package/src/modules/localtask/dag.js +6 -0
- package/src/modules/localtask/engine.js +49 -0
- package/src/modules/localtask/index.js +43 -2
- package/src/modules/localtask/mutator/index.js +153 -0
- package/src/modules/localtask/mutator/validate.js +62 -0
- package/src/modules/localtask/scheduler/index.js +55 -0
- package/src/modules/localtask/scheduler/state-drive.js +30 -0
- package/src/modules/localtask/service.js +140 -15
- package/src/modules/localtask/store/file-adapter.js +51 -0
- package/src/modules/localtask/store/index.js +100 -0
- package/src/modules/localtask/store/memory-adapter.js +26 -0
- package/src/modules/localtask/store/topic-adapter.js +43 -0
- package/src/modules/localtask/writes.js +120 -0
- package/src/modules/repos/boards.js +63 -28
- package/src/modules/repos/index.js +3 -3
- package/src/modules/repos/service.js +47 -6
- package/src/modules/repos/transfer.js +17 -2
- package/src/modules/tasktool/add.js +11 -1
- package/src/modules/tasktool/facade/get.js +46 -0
- package/src/modules/tasktool/facade/index.js +4 -0
- package/src/modules/tasktool/facade/submit.js +49 -0
- package/src/modules/tasktool/facade/wait.js +3 -0
- package/src/modules/tasktool/getloop.js +2 -84
- package/src/modules/tasktool/plugins/base.js +92 -0
- package/src/modules/tasktool/plugins/blockgoal.js +23 -0
- package/src/modules/tasktool/plugins/container.js +47 -0
- package/src/modules/tasktool/plugins/discover.js +45 -0
- package/src/modules/tasktool/plugins/index.js +20 -0
- package/src/modules/tasktool/plugins/runtime.js +69 -0
- package/src/modules/tasktool/plugins/taskgoal-store.js +100 -0
- package/src/modules/tasktool/plugins/taskgoal.js +17 -0
- package/src/modules/tasktool/plugins/timegoal-store.js +748 -0
- package/src/modules/tasktool/plugins/timegoal.js +51 -0
- package/src/modules/tasktool/plugins/waitgoal-config.js +84 -0
- package/src/modules/tasktool/plugins/waitgoal.js +75 -0
- package/src/modules/tasktool/taskgoal.js +2 -100
- package/src/modules/tasktool/timegoal.js +2 -748
- package/src/modules/tasktool/wait.js +25 -0
- package/src/modules/templates/index.js +21 -0
- package/src/modules/templates/service.js +62 -9
- package/src/shared/frontmatter.js +28 -1
- package/src/web/frontend/App.jsx +15 -46
- package/src/web/frontend/components/layout/breadcrumb.jsx +26 -0
- package/src/web/frontend/components/layout/crumbs.js +34 -0
- package/src/web/frontend/components/md/md-panes.jsx +26 -8
- package/src/web/frontend/components/md/shortcut-sheet.jsx +1 -0
- package/src/web/frontend/components/md/tool-bar.jsx +11 -0
- package/src/web/frontend/components/repos/desc-modal.jsx +250 -0
- package/src/web/frontend/components/sidebar/index.jsx +4 -3
- package/src/web/frontend/modules/md/md-parse.js +122 -3
- package/src/web/frontend/modules/md/style.css +54 -5
- package/src/web/frontend/modules/md/use-templates.js +7 -3
- package/src/web/frontend/modules/prompts/placeholders.js +98 -0
- package/src/web/frontend/modules/prompts/prompt-editor.jsx +169 -0
- package/src/web/frontend/modules/prompts/style.css +63 -0
- package/src/web/frontend/modules/prompts/template-slots.jsx +87 -0
- package/src/web/frontend/page/dir/landing.jsx +11 -0
- package/src/web/frontend/page/dir/task.jsx +203 -38
- package/src/web/frontend/page/group/index.jsx +8 -2
- package/src/web/frontend/page/progress/index.jsx +19 -3
- package/src/web/frontend/page/repos/index.jsx +128 -101
- package/src/web/frontend/page/settings/index.jsx +14 -133
- package/src/web/frontend/page/settings/prompts-section.jsx +249 -0
- package/src/web/frontend/page/settings/style.css +39 -0
- package/src/web/frontend/page/templates/index.jsx +27 -0
- package/src/web/frontend/panels.jsx +19 -10
- package/src/web/frontend/style-v2.css +20 -1
- package/src/web/frontend/style.css +145 -35
- package/src/web/frontend/workspace/areas.js +55 -0
- package/src/web/frontend/workspace.js +272 -7
- package/src/web/public/assets/Introduction-C4qC8N7g.js +1 -0
- package/src/web/public/assets/attach-BZrdOH46.css +1 -0
- package/src/web/public/assets/attach-DcyGhCUW.js +1 -0
- package/src/web/public/assets/cloud-D0SRl665.js +3 -0
- package/src/web/public/assets/dir-B4egV6kV.js +3 -0
- package/src/web/public/assets/group-Cbd1PVuv.js +1 -0
- package/src/web/public/assets/index-DidFyMnw.js +50 -0
- package/src/web/public/assets/index-fhYYxphP.css +1 -0
- package/src/web/public/assets/md-history-C29-dF29.js +5 -0
- package/src/web/public/assets/member-picker-B7dnMq25.js +1 -0
- package/src/web/public/assets/overview-CX9XZ5EO.js +1 -0
- package/src/web/public/assets/progress-CnHK_jKP.js +3 -0
- package/src/web/public/assets/repos-CyxHTr1L.js +10 -0
- package/src/web/public/assets/{settings-DM779jcF.css → settings-C5xCFk_y.css} +1 -1
- package/src/web/public/assets/settings-DIvkm7lx.js +1 -0
- package/src/web/public/assets/task-CsEvkBW7.js +2 -0
- package/src/web/public/assets/template-slots-BybxMN3v.js +1 -0
- package/src/web/public/assets/template-slots-CNdlIB1I.css +1 -0
- package/src/web/public/assets/templates-BtdK84si.js +2 -0
- package/src/web/public/assets/{upload-D6_jmUnY.js → upload-jOng3KOU.js} +1 -1
- package/src/web/public/index.html +4 -2
- package/src/web/public/assets/Introduction-Bbee0iwv.js +0 -1
- package/src/web/public/assets/cloud-B2DmkIpl.js +0 -3
- package/src/web/public/assets/dir-DSN1ss82.js +0 -3
- package/src/web/public/assets/group-BeYvUIyv.js +0 -1
- package/src/web/public/assets/index-CXeLbq_l.css +0 -1
- package/src/web/public/assets/index-Cz51X3aE.js +0 -50
- package/src/web/public/assets/md-history-CN-7pPNs.js +0 -4
- package/src/web/public/assets/member-picker-tkzaY0OF.js +0 -1
- package/src/web/public/assets/overview-BjqVRU69.js +0 -1
- package/src/web/public/assets/progress-CyAVplmW.js +0 -3
- package/src/web/public/assets/repos-BPj9-JSh.js +0 -11
- package/src/web/public/assets/settings-DR4KWF-R.js +0 -1
- package/src/web/public/assets/task-DN6Yf0IO.js +0 -2
- package/src/web/public/assets/templates-C2o7jy9W.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,114 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录对外可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
|
|
4
4
|
|
|
5
|
+
## [0.19.0] - 2026-11-04 提示词/模板收敛到设置面板 + 面板无页签化(面包屑 + 区域策略)+ DAG 引擎与 goal 插件族
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **提示词 / 模板统一收敛到设置面板一栏(任务 #32)**:此前提示词与模板散在
|
|
10
|
+
「模板与任务工具」区、模板页、项目级设置三处,各抄一份编辑器、风格也不一。
|
|
11
|
+
现在设置面板新增「**提示词 / 模板**」一栏,三样收在一起:
|
|
12
|
+
- **任务模板**:正文里的 `{{槽位}}` 是槽位,**骨架固定**(内置模板不可改结构)
|
|
13
|
+
只填槽位内容——设置面板与模板页都能填(`template update --values`);
|
|
14
|
+
新建任务时正文按槽位值渲染。
|
|
15
|
+
- **回调提示词**(全局,多条):与收尾指令同一份存储与语义。
|
|
16
|
+
- **ack 完成总结提示词**(全局,多条,**新增**):agent 写完成总结(ack)时的
|
|
17
|
+
格式要求,随领取交付的 `instruction` 一起给到 agent;占位符语法与收尾指令
|
|
18
|
+
同款(`{{task.id}}` / `{{goal.*}}` / 段落 `{{#goal}}…{{/goal}}`)。
|
|
19
|
+
**默认不配置 = 交付里不追加任何东西**(默认交付逐字不变)。
|
|
20
|
+
CLI 同口径:`localtask config --ack "<提示词>"`。
|
|
21
|
+
- **共享的提示词编辑器**(`modules/prompts/`):文本框 + 「占位符」插入菜单 +
|
|
22
|
+
已用占位符标记,回调提示词 / ack 提示词 / 持续 get 模板 / 模板槽位共用一份实现
|
|
23
|
+
——此前三套长得不一样的编辑器,现在只有一套。
|
|
24
|
+
- **面板模型收敛(任务 #33)**:工作区统一为四个区域——sidebar / maincontent /
|
|
25
|
+
rightSidebar / float;面板落点由一张**策略表**声明(单例与否一并声明),
|
|
26
|
+
打开新面板即替换主内容区(主区是**单面板位**)。页签与页签条彻底删除:
|
|
27
|
+
面板头部不再有 float / popup 标识,「清空本组全部页签」垃圾桶按钮一并移除;
|
|
28
|
+
顶部新增**面包屑**显示当前位置(如 `[ nx / nx-kv / #28 ]`)。老布局读盘
|
|
29
|
+
自动愈合(关页签条、补右侧栏)。
|
|
30
|
+
- **DAG 依赖与闸门(任务 #34)**:`localtask add --block-by 12,13`(依赖:这些
|
|
31
|
+
任务全部完成后本条才可领取)、`--gate`(闸门:自身未完成前挡住队列里排在
|
|
32
|
+
后面的 pending);`localtask update` 可改依赖 / 设清闸门。领取按 D-first 取
|
|
33
|
+
第一条**可领**的 pending,目录任务列表带「闸门 / 锁定」徽标。
|
|
34
|
+
- **workflow 契约与引擎骨架(任务 #34 阶段 0)**:`core/workflow/` 冻结类型契约
|
|
35
|
+
(workflowId → 主题 key;对外稳定门面只有 submit / get;goal 是插件族:
|
|
36
|
+
block=调度侧、wait/time=消费侧);`localtask` 拆出 store / bus / mutator /
|
|
37
|
+
scheduler 分层(唯一写快照入口 Mutator,原子覆盖);`tasktool/plugins/`
|
|
38
|
+
目录扫描自注册(base / blockgoal / container / taskgoal / timegoal /
|
|
39
|
+
waitgoal),`tasktool/facade/` 收敛 submit / get / wait 门面。
|
|
40
|
+
- **评论任务旁路原文**:工具栏「原文」或 Ctrl/⌘⇧O 在主区右侧并排打开锚定
|
|
41
|
+
原文(`frontmatter.parent`,旧数据从「锚定自…」回推),可单独关掉、不改路由。
|
|
42
|
+
- **验收条 ack 按 md 渲染**:完成总结走正文同一套渲染;过长默认夹紧、
|
|
43
|
+
可展开全文(任务 #20)。
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **面板贴图嵌入修复(任务 #15 再开)**:实时预览的块内光标坐标与源码全文偏移
|
|
48
|
+
分开(`insertAttachmentSnippet` 双坐标),落点禁止进 frontmatter;已落库任务
|
|
49
|
+
贴图后立刻 PATCH 同步服务端,不再等「保存并同步」——修掉「贴图上传成功但
|
|
50
|
+
正文未嵌入、agent 领取 attachments count: 0」。
|
|
51
|
+
- **frontmatter 键自愈**:贴图误插撕出的残留键(`board:` 被撕成 `oard:` / `d:`)
|
|
52
|
+
解析时升回规范键、保存时写回(#15 / #23 案例),组面板不再丢任务。
|
|
53
|
+
- **模板槽位不露 {{标记}}**:新建任务正文一律按槽位值渲染(缺省用内置默认值
|
|
54
|
+
兜底),`{{背景}}` 之类标记不再出现在任务正文里;设置面板「提示词 / 模板」
|
|
55
|
+
栏改左 tab 布局,槽位显示名字不给标记。
|
|
56
|
+
- **右栏可关 + 进程面板跟随**:进程面板可关闭(右栏随之收起);主区切到别的
|
|
57
|
+
任务时已开的进程面板同步跟随(#33 验收反馈);侧栏收起入口挪到品牌行。
|
|
58
|
+
- **面包屑带组名**:目录页面包屑显示 `[ 组 / 目录 / 任务 ]`(层级抽成纯函数
|
|
59
|
+
`crumbs.js`,面板与单测共用)。
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- **float 面板(设置)自调用栈溢出**:`openPanel` 的 float 分支原先递归调
|
|
64
|
+
`openFloatingPanel`,表现是「设置按钮点了没反应」;改为直接走
|
|
65
|
+
「加进主区 → floatTab 浮出」,`openFloatingPanel` 只做委托。
|
|
66
|
+
|
|
67
|
+
## [0.18.0] - 2026-11-04 描述绑定文档 + 描述弹窗
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **描述全部改成绑定的文档,不再是字符串**。三层描述(仓库 / 看板 / 组内成员)以前
|
|
72
|
+
各存一份字符串字段(`repo.desc`、`board.desc`、`board.repoNotes[key]`),现在正文
|
|
73
|
+
一律在自有文档树里,字符串降级为「派生摘要」:
|
|
74
|
+
|
|
75
|
+
| 描述 | 文档位置 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| 仓库(项目) | `repo/<key>/index.md`(主篇)+ `docLinks` 补充篇 |
|
|
78
|
+
| 看板(领域) | `board/<组>.md` |
|
|
79
|
+
| 组内成员 | `group/<组>/<key>.md` |
|
|
80
|
+
|
|
81
|
+
- **写入入口全改为文档**:`repo add --desc` 变成「初始正文 → 写进主篇」;
|
|
82
|
+
`repo update --desc` / `board update --desc` 直接报错并指向 `docs set` / `docs bind`;
|
|
83
|
+
`board note --note` 写 `group/<组>/<key>.md` 再回写摘要。`desc` 字段只在保存文档时
|
|
84
|
+
由 `docs` 单向回写,不再是第三个真相源。
|
|
85
|
+
- **仓库列表的描述列显示文档**:读主篇正文的首段(两行截断),点一下直接开文档页签;
|
|
86
|
+
没写过时显示「未写」,点了就地新建。补充篇的数量以 `+N` 挂在后面。
|
|
87
|
+
- **编辑仓库弹窗**去掉「一句话描述」输入框,改成「项目描述(文档)」一行(显示主篇、
|
|
88
|
+
点击即开)。看板弹窗同理:编辑时只显示文档入口,新建时给的那段仍作为初始正文。
|
|
89
|
+
- **「描述」按钮改为弹窗:左清单 + 右 md 编辑器**。以前点它直接跳文档页签——为了
|
|
90
|
+
改两个字把整页换掉,看板筛选与滚动位置一起丢。现在弹窗左栏列这个仓库的全部描述
|
|
91
|
+
文档(组内描述 / 项目主篇 / 补充篇,各带一行摘要与「未写」标记),右栏是 md 编辑器
|
|
92
|
+
(实时预览 / 源码 / 阅读三态,Ctrl+S 或按钮保存)。切换条目与关窗都先把未保存的
|
|
93
|
+
改动落盘;右上「↗ 页签」保留(读全文、点双链跳转)。
|
|
94
|
+
- 存量数据用 `docs seed --apply` 一次性迁移(字符串 → 约定路径的 md,幂等;
|
|
95
|
+
实测本机 58 份:40 项目 + 17 组内 + 1 组介绍)。
|
|
96
|
+
- **仓库页头部只留可操作的东西**:删掉标题下那段「开发仓库是你在写的 / 参考仓库是
|
|
97
|
+
clone 下来读的 / 看板只是分组」的说明段落,以及紧随其后的三条图例
|
|
98
|
+
(`开发仓库 · 本机路径,不碰 git` 一类)。这两块纯解释性文字占了首屏近 100px,
|
|
99
|
+
把它们去掉后表格从第 130px 开始、首屏多出两行仓库。参考仓库与看板弹窗头部的
|
|
100
|
+
说明文字同样删掉,弹窗第一行直接是操作按钮(克隆 / 同步 / 检查 / 导入导出 /
|
|
101
|
+
存储根),按钮从行首排起而不是被挤到右侧。想解释的留在按钮 `title` 里。
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- `GET /api/repos` 每条记录多一个 `descDocs`:主篇(docId / 标题 / 摘要 / rel / 是否
|
|
106
|
+
已写)+ 补充篇列表,列表页不用再自己拼路径。
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- **文档阅读态标题重复**:正文自带 `# 标题` 时不再额外渲染外层题头。此前同一行字
|
|
111
|
+
在阅读态会出现两次(标题 + 正文 h1),看着像渲染坏了。
|
|
112
|
+
|
|
5
113
|
## [0.17.0] - 2026-11-04 参考仓库/看板改独立弹窗 + 弹窗组件收敛 + 本地任务附件落本机仓
|
|
6
114
|
|
|
7
115
|
### Added
|
|
@@ -97,6 +97,10 @@ wait 必带一句当前现状总结(≤2000 字,连续空白会折叠为单
|
|
|
97
97
|
`{{goal.name}} {{goal.remainingHuman}} {{goal.prompt}} {{task.text}} {{source}} {{name}}…`,
|
|
98
98
|
段落 `{{#goal}}…{{/goal}}` 上下文缺失整段消失;`--no-suffix` 关闭、
|
|
99
99
|
`--reset-suffix` 恢复默认)。字段缺席 = 已关闭或本轮没有可渲染内容。
|
|
100
|
+
- **ack 提示词(完成总结的格式要求)**:`nx-kv localtask config --ack "<提示词>"`
|
|
101
|
+
(占位符同上,多条)。配置后,它会随 `instruction` 一起交给你——按它要求的格式
|
|
102
|
+
写 `localtask done --summary "<完成总结>"` 即可;没配置时 `instruction` 里没有
|
|
103
|
+
这段,格式自便。设置面板「提示词 / 模板」一栏也能配。
|
|
100
104
|
|
|
101
105
|
超时(exit 0,业务结果而非错误):
|
|
102
106
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { edgesFromTasks } from './types.js';
|
|
2
|
+
|
|
3
|
+
export { edgesFromTasks };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 从快照边表收集某节点的前驱 id。
|
|
7
|
+
* @param {import('./types.js').WorkflowSnapshot} snap
|
|
8
|
+
* @param {string|number} nodeId
|
|
9
|
+
*/
|
|
10
|
+
export function predecessorsOf(snap, nodeId) {
|
|
11
|
+
const id = String(nodeId);
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const e of snap?.edges || []) {
|
|
14
|
+
if (String(e.to) === id) out.push(e.from);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 某节点的后继 id。
|
|
21
|
+
* @param {import('./types.js').WorkflowSnapshot} snap
|
|
22
|
+
* @param {string|number} nodeId
|
|
23
|
+
*/
|
|
24
|
+
export function successorsOf(snap, nodeId) {
|
|
25
|
+
const id = String(nodeId);
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const e of snap?.edges || []) {
|
|
28
|
+
if (String(e.from) === id) out.push(e.to);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** workflow EventBus 事件名(冻结) */
|
|
2
|
+
export const EVENTS = Object.freeze({
|
|
3
|
+
WORKFLOW_CREATED: 'workflow:created',
|
|
4
|
+
DAG_MUTATED: 'dag:mutated',
|
|
5
|
+
NODE_STATE_CHANGED: 'node:stateChanged',
|
|
6
|
+
GOAL_BOUND: 'goal:bound',
|
|
7
|
+
GOAL_UNBOUND: 'goal:unbound',
|
|
8
|
+
GOAL_CONFIG_UPDATED: 'goal:configUpdated',
|
|
9
|
+
WORKFLOW_COMPLETED: 'workflow:completed',
|
|
10
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// workflow 目标目录规范(按 nx-kv 分层 + 语义子目录)。
|
|
2
|
+
//
|
|
3
|
+
// 原则
|
|
4
|
+
// 1. 禁止平行顶层 src/{facade,store,mutator,scheduler,plugins}/。
|
|
5
|
+
// 同名语义**必须**落在 modules 或 core/workflow 的子目录里。
|
|
6
|
+
// 2. **全部 goal 同一目录、每种一份**——block / wait / time / task 都在
|
|
7
|
+
// tasktool/plugins/;禁止再拆「core 一份 + modules 一份」。
|
|
8
|
+
// 3. localtask = 写侧(store/mutator/scheduler/bus);
|
|
9
|
+
// tasktool = 消费门面 + goal 族;二者成对,允许互引
|
|
10
|
+
// (scheduler 调 blockgoal → import tasktool/plugins 即可)。
|
|
11
|
+
// 4. 面板只吃 /api;两端同构投影才进 shared/。
|
|
12
|
+
// 5. JS ESM only;子目录均带 index.js 桶文件对外导出。
|
|
13
|
+
//
|
|
14
|
+
// 依赖方向(单向)
|
|
15
|
+
// web → modules → core/workflow → Node 内置
|
|
16
|
+
// tasktool/facade ──▶ localtask/{store,mutator}
|
|
17
|
+
// localtask/scheduler ──▶ tasktool/plugins/blockgoal + core readiness + bus
|
|
18
|
+
// localtask/mutator ──▶ localtask/store
|
|
19
|
+
// tasktool/plugins/runtime ──▶ 同目录各 goal
|
|
20
|
+
//
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// 目标树(✅ 已有 / ○ 待落地 / → 自现状迁入)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
//
|
|
25
|
+
// src/core/workflow/
|
|
26
|
+
// ├── types.js ✅
|
|
27
|
+
// ├── snapshot.example.json ✅
|
|
28
|
+
// ├── layout.js ✅ 本规范
|
|
29
|
+
// ├── readiness.js ✅ 自 dag 下沉
|
|
30
|
+
// ├── edges.js ✅
|
|
31
|
+
// ├── event-names.js ✅
|
|
32
|
+
// └── plugin-registry.js ✅ 按 name / scope 校验
|
|
33
|
+
//
|
|
34
|
+
// src/modules/localtask/
|
|
35
|
+
// ├── index.js / service.js ✅ 模块壳;complete → refreshTopicAfterComplete
|
|
36
|
+
// ├── dag.js ✅ 再导出 readiness
|
|
37
|
+
// ├── engine.js / writes.js ✅ 主题引擎;DAG 日常写经 Mutator
|
|
38
|
+
// ├── store/ ✅ SnapshotStore + memory/file/topic adapter
|
|
39
|
+
// ├── bus/ mutator/ scheduler/ ✅
|
|
40
|
+
//
|
|
41
|
+
// src/modules/tasktool/
|
|
42
|
+
// ├── index.js ✅
|
|
43
|
+
// ├── facade/ ✅ submit/get/wait 再导出
|
|
44
|
+
// ├── plugins/ ✅ 目录扫描 IoC + 实现同目录
|
|
45
|
+
// │ ├── discover.js 扫描注册(跳过 *-store/*-config)
|
|
46
|
+
// │ ├── *-store.js / waitgoal-config 原 timegoal/taskgoal/getloop 迁入
|
|
47
|
+
// │ └── block|wait|time|task goal.js definePlugin
|
|
48
|
+
// ├── facade/ ✅ submit→store+入队;get→AsyncIterable
|
|
49
|
+
// ├── groupctx.js / attach.js ✅
|
|
50
|
+
// └── getloop/timegoal/taskgoal.js ✅ 薄再导出 → plugins/*
|
|
51
|
+
//
|
|
52
|
+
// 为何 blockgoal 也进 tasktool/plugins(不留 core)
|
|
53
|
+
// goal 是插件族,视觉与注册平级;拆到 core 会造成「调度 goal 一套路径、
|
|
54
|
+
// 消费 goal 另一套路径」。localtask↔tasktool 本就可配对互引,scheduler
|
|
55
|
+
// 直接 import blockgoal 即可,无需为避依赖把家族拆开。
|
|
56
|
+
//
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** @type {Readonly<Record<string, string>>} */
|
|
60
|
+
export const TARGET_PATHS = Object.freeze({
|
|
61
|
+
'core types': 'src/core/workflow/types.js',
|
|
62
|
+
'snapshot example': 'src/core/workflow/snapshot.example.json',
|
|
63
|
+
'this layout norm': 'src/core/workflow/layout.js',
|
|
64
|
+
readiness: 'src/core/workflow/readiness.js',
|
|
65
|
+
'plugin registry': 'src/core/workflow/plugin-registry.js',
|
|
66
|
+
'store/': 'src/modules/localtask/store/',
|
|
67
|
+
'store barrel': 'src/modules/localtask/store/index.js',
|
|
68
|
+
'store file-adapter': 'src/modules/localtask/store/file-adapter.js',
|
|
69
|
+
'store memory-adapter': 'src/modules/localtask/store/memory-adapter.js',
|
|
70
|
+
'bus/': 'src/modules/localtask/bus/',
|
|
71
|
+
'bus barrel': 'src/modules/localtask/bus/index.js',
|
|
72
|
+
'mutator/': 'src/modules/localtask/mutator/',
|
|
73
|
+
'mutator barrel': 'src/modules/localtask/mutator/index.js',
|
|
74
|
+
'mutator validate': 'src/modules/localtask/mutator/validate.js',
|
|
75
|
+
'scheduler/': 'src/modules/localtask/scheduler/',
|
|
76
|
+
'scheduler barrel': 'src/modules/localtask/scheduler/index.js',
|
|
77
|
+
'scheduler state-drive': 'src/modules/localtask/scheduler/state-drive.js',
|
|
78
|
+
'facade/': 'src/modules/tasktool/facade/',
|
|
79
|
+
'facade barrel': 'src/modules/tasktool/facade/index.js',
|
|
80
|
+
'facade submit': 'src/modules/tasktool/facade/submit.js',
|
|
81
|
+
'facade get': 'src/modules/tasktool/facade/get.js',
|
|
82
|
+
'facade wait': 'src/modules/tasktool/facade/wait.js',
|
|
83
|
+
'plugins/': 'src/modules/tasktool/plugins/',
|
|
84
|
+
'plugins barrel': 'src/modules/tasktool/plugins/index.js',
|
|
85
|
+
'plugins runtime': 'src/modules/tasktool/plugins/runtime.js',
|
|
86
|
+
blockgoal: 'src/modules/tasktool/plugins/blockgoal.js',
|
|
87
|
+
waitgoal: 'src/modules/tasktool/plugins/waitgoal.js',
|
|
88
|
+
timegoal: 'src/modules/tasktool/plugins/timegoal.js',
|
|
89
|
+
taskgoal: 'src/modules/tasktool/plugins/taskgoal.js',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** 全部 goal 唯一落点(同一目录;禁止第二份) */
|
|
93
|
+
export const GOAL_HOMES = Object.freeze({
|
|
94
|
+
blockgoal: 'src/modules/tasktool/plugins/blockgoal.js',
|
|
95
|
+
waitgoal: 'src/modules/tasktool/plugins/waitgoal.js',
|
|
96
|
+
timegoal: 'src/modules/tasktool/plugins/timegoal.js',
|
|
97
|
+
taskgoal: 'src/modules/tasktool/plugins/taskgoal.js',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** 必须出现的语义子目录(相对仓库根) */
|
|
101
|
+
export const REQUIRED_SEMANTIC_DIRS = Object.freeze([
|
|
102
|
+
'src/modules/localtask/store',
|
|
103
|
+
'src/modules/localtask/bus',
|
|
104
|
+
'src/modules/localtask/mutator',
|
|
105
|
+
'src/modules/localtask/scheduler',
|
|
106
|
+
'src/modules/tasktool/facade',
|
|
107
|
+
'src/modules/tasktool/plugins',
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
/** 禁止的平行顶层 */
|
|
111
|
+
export const FORBIDDEN_TOP_LEVEL = Object.freeze([
|
|
112
|
+
'src/facade',
|
|
113
|
+
'src/store',
|
|
114
|
+
'src/mutator',
|
|
115
|
+
'src/scheduler',
|
|
116
|
+
'src/plugins',
|
|
117
|
+
'src/bus',
|
|
118
|
+
'src/core/types',
|
|
119
|
+
]);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Workflow / Node / Plugin 生命周期(冻结)。插件只挂钩子,不拥有可变状态。
|
|
2
|
+
//
|
|
3
|
+
// ┌─ Workflow ──────────────────────────────────────────────┐
|
|
4
|
+
// │ created ──▶ active ──▶ completed | failed │
|
|
5
|
+
// │ │ │ │
|
|
6
|
+
// │ │ ├─ Mutator 改拓扑/goal → dag:mutated │
|
|
7
|
+
// │ │ ├─ Scheduler onSchedule(永不 onGet) │
|
|
8
|
+
// │ │ └─ Facade get → onGet 消费流 │
|
|
9
|
+
// └─────────────────────────────────────────────────────────┘
|
|
10
|
+
//
|
|
11
|
+
// ┌─ Node ──────────────────────────────────────────────────┐
|
|
12
|
+
// │ pending ──▶ ready ──▶ running ──▶ success | failed │
|
|
13
|
+
// │ ▲ │ │ │
|
|
14
|
+
// │ └─ 屏障加回 │ └─ done / worker │
|
|
15
|
+
// │ └─ Scheduler driveReadyStates │
|
|
16
|
+
// └─────────────────────────────────────────────────────────┘
|
|
17
|
+
//
|
|
18
|
+
// ┌─ Plugin 实例钩子(按绑定点注入)──────────────────────────┐
|
|
19
|
+
// │ onBind Mutator.bindPlugin 时:校验并规范化 config │
|
|
20
|
+
// │ onSchedule Scheduler 轮:仅 scope=control(blockgoal)│
|
|
21
|
+
// │ onGet Facade get:scope=workflow|node 消费流 │
|
|
22
|
+
// │ onUnbind Mutator.unbindPlugin 时:可选清理(无状态) │
|
|
23
|
+
// │ │
|
|
24
|
+
// │ 叠加:workflow.timegoal 包裹 node.waitgoal │
|
|
25
|
+
// │ 约束:调度链禁止调 onGet;消费链禁止写快照(只读 ctx) │
|
|
26
|
+
// └─────────────────────────────────────────────────────────┘
|
|
27
|
+
|
|
28
|
+
/** @typedef {'created'|'active'|'completed'|'failed'} WorkflowPhase */
|
|
29
|
+
/** @typedef {'pending'|'ready'|'running'|'success'|'failed'} NodePhase */
|
|
30
|
+
/** @typedef {'onBind'|'onSchedule'|'onGet'|'onUnbind'} PluginHook */
|
|
31
|
+
|
|
32
|
+
export const WORKFLOW_PHASES = Object.freeze([
|
|
33
|
+
'created',
|
|
34
|
+
'active',
|
|
35
|
+
'completed',
|
|
36
|
+
'failed',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const NODE_PHASES = Object.freeze([
|
|
40
|
+
'pending',
|
|
41
|
+
'ready',
|
|
42
|
+
'running',
|
|
43
|
+
'success',
|
|
44
|
+
'failed',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** 插件可实现的钩子;未实现视为空操作 */
|
|
48
|
+
export const PLUGIN_HOOKS = Object.freeze(['onBind', 'onSchedule', 'onGet', 'onUnbind']);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 哪条执行链会调用哪个钩子。
|
|
52
|
+
* @type {Readonly<Record<'schedule'|'get'|'mutate', PluginHook[]>>}
|
|
53
|
+
*/
|
|
54
|
+
export const HOOK_CHAINS = Object.freeze({
|
|
55
|
+
mutate: Object.freeze(['onBind', 'onUnbind']),
|
|
56
|
+
schedule: Object.freeze(['onSchedule']),
|
|
57
|
+
get: Object.freeze(['onGet']),
|
|
58
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BUILTIN_GOALS, GOAL_SCOPES } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./types.js').GoalScope} GoalScope */
|
|
4
|
+
/** @typedef {import('./types.js').PluginSpec} PluginSpec */
|
|
5
|
+
|
|
6
|
+
/** @type {Map<string, { name: string, scope: GoalScope }>} */
|
|
7
|
+
const registry = new Map();
|
|
8
|
+
|
|
9
|
+
export function registerPluginMeta(name, scope) {
|
|
10
|
+
if (!name || typeof name !== 'string') throw new Error('plugin name required');
|
|
11
|
+
if (!GOAL_SCOPES.includes(scope)) throw new Error(`invalid goal scope: ${scope}`);
|
|
12
|
+
registry.set(name, { name, scope });
|
|
13
|
+
return registry.get(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getPluginMeta(name) {
|
|
17
|
+
return registry.get(name) || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function listPluginMetas() {
|
|
21
|
+
return [...registry.values()];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 校验 PluginSpec:已注册且 scope 匹配(未注册则只认内置名) */
|
|
25
|
+
export function assertPluginSpec(spec) {
|
|
26
|
+
if (!spec || typeof spec !== 'object') throw new Error('PluginSpec required');
|
|
27
|
+
const name = String(spec.name || '');
|
|
28
|
+
if (!name) throw new Error('PluginSpec.name required');
|
|
29
|
+
const meta = registry.get(name);
|
|
30
|
+
const scope = spec.scope || meta?.scope;
|
|
31
|
+
if (!scope || !GOAL_SCOPES.includes(scope)) {
|
|
32
|
+
throw new Error(`PluginSpec.scope invalid for ${name}`);
|
|
33
|
+
}
|
|
34
|
+
if (meta && meta.scope !== scope) {
|
|
35
|
+
throw new Error(`plugin ${name} scope must be ${meta.scope}, got ${scope}`);
|
|
36
|
+
}
|
|
37
|
+
if (!meta && !Object.values(BUILTIN_GOALS).includes(name) && name !== 'taskgoal') {
|
|
38
|
+
throw new Error(`unknown plugin: ${name}`);
|
|
39
|
+
}
|
|
40
|
+
return { name, scope, config: spec.config && typeof spec.config === 'object' ? spec.config : {} };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 内置登记(实现落在 tasktool/plugins;此处只登记元数据)
|
|
44
|
+
registerPluginMeta(BUILTIN_GOALS.blockgoal, 'control');
|
|
45
|
+
registerPluginMeta(BUILTIN_GOALS.waitgoal, 'node');
|
|
46
|
+
registerPluginMeta(BUILTIN_GOALS.timegoal, 'workflow');
|
|
47
|
+
registerPluginMeta('taskgoal', 'node');
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// 主题队列(localtask topic.tasks)↔ WorkflowSnapshot 双向投影。
|
|
2
|
+
// 存盘形状仍是任务行;快照是引擎视图。正文不进快照(payload.textRef)。
|
|
3
|
+
|
|
4
|
+
import { BUILTIN_GOALS, edgesFromTasks, taskToNode } from './types.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./types.js').WorkflowSnapshot} WorkflowSnapshot */
|
|
7
|
+
/** @typedef {import('./types.js').WfNode} WfNode */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} topic localtask 主题桶
|
|
11
|
+
* @returns {WorkflowSnapshot}
|
|
12
|
+
*/
|
|
13
|
+
export function topicToSnapshot(topic) {
|
|
14
|
+
const workflowId = String(topic?.key || topic?.workflowId || '');
|
|
15
|
+
const tasks = Array.isArray(topic?.tasks) ? topic.tasks : [];
|
|
16
|
+
const nodes = tasks.filter((t) => t && !t.deletedAt && !t.migratedTo).map(taskToNode);
|
|
17
|
+
const edges = edgesFromTasks(tasks);
|
|
18
|
+
const revision = Number(topic?.workflowRevision) || 0;
|
|
19
|
+
/** @type {WorkflowSnapshot} */
|
|
20
|
+
return {
|
|
21
|
+
workflowId,
|
|
22
|
+
state: topic?.workflowState || 'active',
|
|
23
|
+
nodes,
|
|
24
|
+
edges,
|
|
25
|
+
workflowPlugins: Array.isArray(topic?.workflowPlugins) ? topic.workflowPlugins : [],
|
|
26
|
+
revision,
|
|
27
|
+
updatedAt: topic?.updatedAt || new Date().toISOString(),
|
|
28
|
+
meta: {
|
|
29
|
+
cwd: topic?.cwd || '',
|
|
30
|
+
name: topic?.name || '',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 把快照写回主题任务行(保守:不改 text;按 id 对齐)。
|
|
37
|
+
* @param {object} topic
|
|
38
|
+
* @param {WorkflowSnapshot} snap
|
|
39
|
+
*/
|
|
40
|
+
export function applySnapshotToTopic(topic, snap) {
|
|
41
|
+
if (!topic || !snap) return topic;
|
|
42
|
+
const tasks = Array.isArray(topic.tasks) ? topic.tasks : (topic.tasks = []);
|
|
43
|
+
const byId = new Map(tasks.map((t) => [String(t.id), t]));
|
|
44
|
+
|
|
45
|
+
for (const node of snap.nodes || []) {
|
|
46
|
+
let task = byId.get(String(node.id));
|
|
47
|
+
if (!task) {
|
|
48
|
+
// 引擎新建的节点 → 补一条占位任务(正文外置占位)
|
|
49
|
+
task = {
|
|
50
|
+
id: Number(node.id) || node.id,
|
|
51
|
+
text: '',
|
|
52
|
+
status: 'pending',
|
|
53
|
+
createdAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
54
|
+
completedAt: '',
|
|
55
|
+
deletedAt: '',
|
|
56
|
+
blockBy: [],
|
|
57
|
+
blocksFollowing: false,
|
|
58
|
+
};
|
|
59
|
+
tasks.push(task);
|
|
60
|
+
byId.set(String(task.id), task);
|
|
61
|
+
}
|
|
62
|
+
syncTaskFromNode(task, node);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 边 → blockBy(显式依赖);闸门边由 blocksFollowing 表达,不写进每条 blockBy
|
|
66
|
+
const gateIds = new Set(
|
|
67
|
+
(snap.nodes || [])
|
|
68
|
+
.filter(
|
|
69
|
+
(n) =>
|
|
70
|
+
n.kind === 'control' ||
|
|
71
|
+
(n.plugins || []).some((p) => p.name === BUILTIN_GOALS.blockgoal),
|
|
72
|
+
)
|
|
73
|
+
.map((n) => String(n.id)),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
for (const task of tasks) {
|
|
77
|
+
if (task.deletedAt || task.migratedTo) continue;
|
|
78
|
+
const incoming = (snap.edges || []).filter((e) => String(e.to) === String(task.id));
|
|
79
|
+
const deps = [];
|
|
80
|
+
for (const e of incoming) {
|
|
81
|
+
// 跳过「闸门挡后续」推出来的边:from 是闸门且闸门在数组序上早于本任务时由 blocksFollowing 覆盖
|
|
82
|
+
if (gateIds.has(String(e.from))) continue;
|
|
83
|
+
deps.push(e.from);
|
|
84
|
+
}
|
|
85
|
+
task.blockBy = deps.map((d) => Number(d)).filter((n) => Number.isFinite(n) && n > 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
topic.workflowRevision = Number(snap.revision) || 0;
|
|
89
|
+
topic.workflowState = snap.state || 'active';
|
|
90
|
+
topic.workflowPlugins = Array.isArray(snap.workflowPlugins) ? snap.workflowPlugins : [];
|
|
91
|
+
if (snap.updatedAt) topic.updatedAt = snap.updatedAt;
|
|
92
|
+
return topic;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {object} task @param {WfNode} node */
|
|
96
|
+
function syncTaskFromNode(task, node) {
|
|
97
|
+
const hasBlock = (node.plugins || []).some((p) => p.name === BUILTIN_GOALS.blockgoal);
|
|
98
|
+
task.blocksFollowing = node.kind === 'control' || hasBlock;
|
|
99
|
+
|
|
100
|
+
if (node.state === 'success') {
|
|
101
|
+
if (!task.completedAt) {
|
|
102
|
+
task.completedAt = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
103
|
+
}
|
|
104
|
+
} else if (node.state === 'running' && task.status !== 'consumed') {
|
|
105
|
+
task.status = 'consumed';
|
|
106
|
+
}
|
|
107
|
+
// pending/ready:不改 status(领取态由 wait/get 管;ready 由 isReadyToClaim 现算)
|
|
108
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// 拓扑就绪(自 localtask/dag.js 下沉)。任务行与快照节点两套入口。
|
|
2
|
+
|
|
3
|
+
/** 规范化 blockBy:正整数 id、去重、丢掉自身 */
|
|
4
|
+
export function normalizeBlockBy(raw, selfId) {
|
|
5
|
+
const self = Number(selfId);
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const x of Array.isArray(raw) ? raw : []) {
|
|
9
|
+
const n = Number(x);
|
|
10
|
+
if (!Number.isFinite(n) || n <= 0) continue;
|
|
11
|
+
if (Number.isFinite(self) && n === self) continue;
|
|
12
|
+
if (seen.has(n)) continue;
|
|
13
|
+
seen.add(n);
|
|
14
|
+
out.push(n);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isAlive(t) {
|
|
20
|
+
return t && !t.deletedAt && !t.migratedTo;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isCompleted(t) {
|
|
24
|
+
return isAlive(t) && !!t.completedAt;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPendingLike(t) {
|
|
28
|
+
return isAlive(t) && t.status !== 'consumed';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 某条 pending 现在能不能领(任务行模型)。
|
|
33
|
+
* @param {object} task
|
|
34
|
+
* @param {object[]} all
|
|
35
|
+
*/
|
|
36
|
+
export function isReadyToClaim(task, all = []) {
|
|
37
|
+
if (!isPendingLike(task)) return false;
|
|
38
|
+
const byId = new Map((all || []).filter(isAlive).map((t) => [Number(t.id), t]));
|
|
39
|
+
|
|
40
|
+
const deps = normalizeBlockBy(task.blockBy, task.id);
|
|
41
|
+
for (const id of deps) {
|
|
42
|
+
const dep = byId.get(id);
|
|
43
|
+
if (!dep) continue;
|
|
44
|
+
if (!isCompleted(dep)) return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const idx = (all || []).indexOf(task);
|
|
48
|
+
const order = all || [];
|
|
49
|
+
const myPos = idx >= 0 ? idx : order.findIndex((t) => Number(t.id) === Number(task.id));
|
|
50
|
+
if (myPos < 0) return true;
|
|
51
|
+
for (let i = 0; i < myPos; i += 1) {
|
|
52
|
+
const prev = order[i];
|
|
53
|
+
if (!isAlive(prev) || !prev.blocksFollowing) continue;
|
|
54
|
+
if (!isCompleted(prev)) return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 按数组序找第一条可领取的 pending;没有 → null */
|
|
60
|
+
export function findFirstReadyPending(tasks = []) {
|
|
61
|
+
for (const t of tasks || []) {
|
|
62
|
+
if (isReadyToClaim(t, tasks)) return t;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 快照节点是否就绪(边 + control/blockgoal 前驱须 success)。
|
|
69
|
+
* @param {import('./types.js').WorkflowSnapshot} snap
|
|
70
|
+
* @param {import('./types.js').WfNode} node
|
|
71
|
+
* @param {(snap: import('./types.js').WorkflowSnapshot, nodeId: string|number) => boolean} [isBarrierOpen]
|
|
72
|
+
*/
|
|
73
|
+
export function isNodeReady(snap, node, isBarrierOpen) {
|
|
74
|
+
if (!node || node.state === 'success' || node.state === 'failed' || node.state === 'running') {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (node.state !== 'pending' && node.state !== 'ready') return false;
|
|
78
|
+
|
|
79
|
+
const preds = (snap.edges || []).filter((e) => String(e.to) === String(node.id));
|
|
80
|
+
const byId = new Map((snap.nodes || []).map((n) => [String(n.id), n]));
|
|
81
|
+
|
|
82
|
+
for (const e of preds) {
|
|
83
|
+
const pred = byId.get(String(e.from));
|
|
84
|
+
if (!pred) continue;
|
|
85
|
+
const isControl =
|
|
86
|
+
pred.kind === 'control' ||
|
|
87
|
+
(pred.plugins || []).some((p) => p.name === 'blockgoal' || p.scope === 'control');
|
|
88
|
+
if (isControl) {
|
|
89
|
+
const open = isBarrierOpen
|
|
90
|
+
? isBarrierOpen(snap, pred.id)
|
|
91
|
+
: pred.state === 'success';
|
|
92
|
+
if (!open) return false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (pred.state !== 'success') return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"workflowId": "local/nx-kv",
|
|
3
|
+
"state": "active",
|
|
4
|
+
"revision": 1,
|
|
5
|
+
"updatedAt": "2026-11-04T08:00:00.000Z",
|
|
6
|
+
"workflowPlugins": [
|
|
7
|
+
{
|
|
8
|
+
"name": "timegoal",
|
|
9
|
+
"scope": "workflow",
|
|
10
|
+
"config": { "duration": "45m", "prompt": "目录级静默超时示例" }
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"nodes": [
|
|
14
|
+
{
|
|
15
|
+
"id": 10,
|
|
16
|
+
"kind": "task",
|
|
17
|
+
"state": "success",
|
|
18
|
+
"plugins": [],
|
|
19
|
+
"payload": { "textRef": true }
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": 11,
|
|
23
|
+
"kind": "control",
|
|
24
|
+
"state": "running",
|
|
25
|
+
"plugins": [{ "name": "blockgoal", "scope": "control", "config": {} }],
|
|
26
|
+
"payload": { "textRef": true }
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": 12,
|
|
30
|
+
"kind": "task",
|
|
31
|
+
"state": "pending",
|
|
32
|
+
"plugins": [
|
|
33
|
+
{
|
|
34
|
+
"name": "waitgoal",
|
|
35
|
+
"scope": "node",
|
|
36
|
+
"config": { "waitMinutes": 3 }
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"payload": { "textRef": true }
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"edges": [
|
|
43
|
+
{ "from": 10, "to": 11 },
|
|
44
|
+
{ "from": 11, "to": 12 }
|
|
45
|
+
]
|
|
46
|
+
}
|