nx-kv 0.18.0 → 0.20.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 +167 -0
- package/assets/kv-get/references/wait-workflow.md +6 -1
- 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 +114 -0
- package/src/core/workflow/readiness.js +193 -0
- package/src/core/workflow/snapshot.example.json +46 -0
- package/src/core/workflow/types.js +172 -0
- package/src/modules/localtask/bus/index.js +41 -0
- package/src/modules/localtask/dag.js +8 -0
- package/src/modules/localtask/engine.js +49 -0
- package/src/modules/localtask/index.js +81 -7
- 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 +306 -31
- 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 +131 -0
- package/src/modules/tasktool/add.js +12 -23
- 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/group-store.js +28 -0
- 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/update.js +70 -0
- 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 +20 -50
- package/src/web/frontend/components/icons.jsx +11 -0
- package/src/web/frontend/components/layout/breadcrumb.jsx +38 -0
- package/src/web/frontend/components/layout/crumbs.js +85 -0
- package/src/web/frontend/components/layout/route-crumb.jsx +33 -0
- package/src/web/frontend/components/layout/route-history.js +27 -0
- package/src/web/frontend/components/md/md-panes.jsx +10 -6
- 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/sidebar/index.jsx +6 -5
- package/src/web/frontend/components/sidebar/side-state.js +3 -2
- 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 +253 -0
- package/src/web/frontend/modules/prompts/style.css +78 -0
- package/src/web/frontend/modules/prompts/template-slots.jsx +87 -0
- package/src/web/frontend/page/cloud/index.jsx +2 -0
- package/src/web/frontend/page/dag/index.jsx +775 -0
- package/src/web/frontend/page/dir/dag-editor.jsx +180 -0
- package/src/web/frontend/page/dir/index.jsx +77 -2
- package/src/web/frontend/page/dir/landing.jsx +175 -55
- package/src/web/frontend/page/dir/task.jsx +239 -40
- package/src/web/frontend/page/group/index.jsx +196 -24
- package/src/web/frontend/page/overview/index.jsx +2 -0
- package/src/web/frontend/page/progress/index.jsx +19 -3
- package/src/web/frontend/page/repos/index.jsx +2 -0
- package/src/web/frontend/page/settings/index.jsx +113 -172
- package/src/web/frontend/page/settings/prompts-section.jsx +239 -0
- package/src/web/frontend/page/settings/style.css +85 -6
- package/src/web/frontend/page/templates/index.jsx +29 -0
- package/src/web/frontend/panels.jsx +25 -10
- package/src/web/frontend/style-v2.css +244 -7
- package/src/web/frontend/workspace/areas.js +56 -0
- package/src/web/frontend/workspace.js +286 -11
- package/src/web/public/assets/Introduction-BnbM3LXZ.js +1 -0
- package/src/web/public/assets/attach-BZrdOH46.css +1 -0
- package/src/web/public/assets/attach-v8BxRJ7p.js +1 -0
- package/src/web/public/assets/cloud-DlVOlEuX.js +3 -0
- package/src/web/public/assets/dag-B_EovnBK.js +1 -0
- package/src/web/public/assets/dir-DOQkmLNI.js +3 -0
- package/src/web/public/assets/group-CQ9U4NiQ.js +1 -0
- package/src/web/public/assets/index-Cg0WaaAi.css +1 -0
- package/src/web/public/assets/index-DFHqf4ue.js +50 -0
- package/src/web/public/assets/md-history-DDQvoTFy.js +5 -0
- package/src/web/public/assets/member-picker-zypl1Wuc.js +1 -0
- package/src/web/public/assets/overview-uWj8gdmN.js +1 -0
- package/src/web/public/assets/progress-D2cyYcIw.js +3 -0
- package/src/web/public/assets/repos-lSywsxfo.js +10 -0
- package/src/web/public/assets/route-crumb-BI31PVXo.js +1 -0
- package/src/web/public/assets/settings-BVcCJUvg.js +3 -0
- package/src/web/public/assets/settings-CRK86fnJ.css +1 -0
- package/src/web/public/assets/task-Dj77xNZX.js +2 -0
- package/src/web/public/assets/template-slots-BjfVnHFQ.js +1 -0
- package/src/web/public/assets/template-slots-UVa4FnPn.css +1 -0
- package/src/web/public/assets/templates-B9VLtDMa.js +2 -0
- package/src/web/public/assets/{upload-BupH8dMK.js → upload-DvoSV9Cl.js} +1 -1
- package/src/web/public/index.html +4 -2
- package/src/web/public/assets/Introduction-CLTMLBZ5.js +0 -1
- package/src/web/public/assets/cloud-B9_h1Ym7.js +0 -3
- package/src/web/public/assets/dir-BLUQranN.js +0 -3
- package/src/web/public/assets/group-DIeOm80x.js +0 -1
- package/src/web/public/assets/index-DA98QEzx.css +0 -1
- package/src/web/public/assets/index-t7PgSf_t.js +0 -50
- package/src/web/public/assets/md-history-CDVA0KIl.js +0 -5
- package/src/web/public/assets/member-picker-9MAmKm9d.js +0 -1
- package/src/web/public/assets/overview-DNLRHNzP.js +0 -1
- package/src/web/public/assets/progress-C0MiaOW0.js +0 -3
- package/src/web/public/assets/repos--i4W5fzj.js +0 -10
- package/src/web/public/assets/settings-DM779jcF.css +0 -1
- package/src/web/public/assets/settings-DSx2Wsto.js +0 -1
- package/src/web/public/assets/task-D2-CWZXv.js +0 -2
- package/src/web/public/assets/templates-DD6m8DT4.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,173 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录对外可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
|
|
4
4
|
|
|
5
|
+
## [0.20.0] - 2026-11-04 任务编排(DAG)进面板 + 面包屑/设置面板收拾 + 改归属项目即搬家
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **任务编排(DAG)进面板(任务 #40)**:目录落地页任务表与项目组「本组任务」列表支持编排——
|
|
10
|
+
- **拖动排序**:目录任务表待领取行可拖动换位(落点=上半行前/下半行后,青绿线指示),
|
|
11
|
+
走既有 `localtask reorder`(已领取行是固定历史,不可拖);
|
|
12
|
+
- **编排编辑器**(行尾「⋮」→「编排依赖…」):闸门开关、上游依赖(我等谁)、
|
|
13
|
+
下游任务(谁等我,逐条改对方 blockBy)三段勾选,保存前客户端查环——
|
|
14
|
+
会成环的组合直接禁用;闸门开启时上游段禁用并清空、下游自动覆盖(引擎约束对齐:
|
|
15
|
+
闸门的隐含边与上游边成环,`applySnapshotToTopic` 亦把闸门显式后继按冗余剪除);
|
|
16
|
+
- **组页按队列表达**:组任务横跨多个成员队列,编排以**该任务所属队列**为作用域
|
|
17
|
+
(候选/闸门下游都取本队列),闸门×N 与锁定徽章上屏;
|
|
18
|
+
- 闸门徽章升级:显示被挡后续数(`闸门×N`,悬停列出 #id)。
|
|
19
|
+
- 组任务行尾新增「⋮」行菜单(编排依赖 / 闸门开关),待领取行悬停可见。
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **设置面板观感重做(任务 #41 重开反馈「ui 太丑了」→ 二轮「更丑了」)**:交互语义不变,只收拾
|
|
24
|
+
密度与层级——「提示词 / 模板」栏的竖排 tab 侧栏改成顶部一排药丸、内容限宽;提示词条目行去描边、
|
|
25
|
+
编号不用圆圈、**收起标题洗掉原始模板标记**(不再直接甩 `{{#goal}}〔…〕` 给人看);操作条分隔线分组;
|
|
26
|
+
项目级设置从大卡片改成紧凑行(项目多时一屏 8 个而不是 2 个),「持续 get」关着时只留一枚行内小开关;
|
|
27
|
+
「提示词 / 模板」栏不再套卡(盒中盒显脏)。:交互语义不变,只收拾
|
|
28
|
+
密度与层级——「提示词 / 模板」栏的竖排 tab 侧栏改成顶部一排药丸、内容限宽;
|
|
29
|
+
提示词条目行去描边、编号不用圆圈;操作条分隔线分组;项目级设置从大卡片改成
|
|
30
|
+
紧凑行(项目多时一屏 8 个而不是 2 个),「持续 get」关着时只留一枚行内小开关。
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **面包屑移到 maincontent 面板内顶部(任务 #44)**:路径条不再占窗口顶部通栏
|
|
35
|
+
(那是侧栏的地盘),改成挂在**当前主面板**顶部的一枚小药丸——目录任务表 /
|
|
36
|
+
任务详情页 / 项目组 / 全部目录总览 / 云端总览 / 仓库管理 / 任务模板 各面板
|
|
37
|
+
根部都有一条;MD 工作台里则直接放进顶部工具栏(`mdw-top`)内、标题左侧;
|
|
38
|
+
「← 返回」随路径条一起搬进面板(按来处返回的行为不变)。:路径条不再占窗口顶部通栏
|
|
39
|
+
(那是侧栏的地盘),改成挂在**当前主面板**顶部的一枚小药丸——目录任务表 /
|
|
40
|
+
任务详情页 / 项目组 / 全部目录总览 / 云端总览 / 仓库管理 / 任务模板 各面板
|
|
41
|
+
根部都有一条;「← 返回」随路径条一起搬进面板(按来处返回的行为不变)。
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **改归属项目即搬家(任务 #43「提交任务后发现归属项目填错了,重新修改提交无效」)**:
|
|
46
|
+
此前「归属决定落点」只在提交那一刻生效——提交后发现归属填错,改正文里的 project 再保存,
|
|
47
|
+
正文改了队列没动:任务还挂在旧项目队列里,旧项目的 agent 照旧领走,新项目的 agent 永远
|
|
48
|
+
看不见,组面板/目录页也都显示旧归属。现在 `localtask update --text` / `localtask reopen --text`
|
|
49
|
+
/ 面板「保存并同步 / 重做」走与派任务同一套落点判定:effective frontmatter.project 解析到
|
|
50
|
+
哪个登记项目,任务就迁到哪个队列;介绍引用(projectIntro/projectDesc)随归属刷新。
|
|
51
|
+
约束如实说明:已领取的任务不能改归属(要改先 reopen);带队列内依赖/闸门的任务搬运会
|
|
52
|
+
跨队列断链,先解除再改(明确报错,不静默);搬家后任务拿到**新队列的 id**(面板页签自动
|
|
53
|
+
跟到新号,CLI 输出标注迁移);指向它的跨队列引用(`topicKey:id`)改写到新地址。
|
|
54
|
+
另自愈历史脏数据:归属字段与所在队列对不上(上一轮只改字段没搬队列的残留)时,
|
|
55
|
+
保存即搬到归属队列;已领取/带队列内依赖的脏数据不搬也不拦普通编辑。
|
|
56
|
+
|
|
57
|
+
- **顶部面包屑真正可见 + 补返回键(任务 #42)**:面包屑此前**渲染了却看不见**——
|
|
58
|
+
FlexLayout 的根是绝对定位(inset:0),铺满工作区把 26px 的路径条盖在下面
|
|
59
|
+
(侧栏面板压在上面,路径条点不到也看不见)。现在工作区有面包屑时布局整体下移一条;
|
|
60
|
+
侧栏也不再出页签条(#33「无页签化」的残留,页签条同样压在路径条上)。
|
|
61
|
+
面包屑条左侧新增「← 返回」:按**来处**返回(会话内路由历史栈)——
|
|
62
|
+
从项目组点任务(任务住在 dir 队列里,详情页落在 dir 作用域)时,返回会回到**组**
|
|
63
|
+
而不是掉进目录;没有上一级时不画返回键。
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **可视化编排面板(任务 #40 验收反馈重开)**:目录与项目组**共用**一块编排面板,
|
|
68
|
+
从任务面板**头部右侧的绿色超链**进入(开在右侧主内容区):
|
|
69
|
+
- **节点 = 小圆角卡片**,画布带网格底;节点纵向位置 = 队列顺序;
|
|
70
|
+
- **拖拽排序**:拖节点即改队列顺序(已领取节点是固定历史,不可拖);
|
|
71
|
+
- **连线建立有向依赖**:从节点左侧把手拉到另一个节点 = 后者等前者(写入 blockBy);
|
|
72
|
+
依赖边以曲线 + 箭头画在画布左侧,节点上显示「等 #N」芯片(可一键解除);
|
|
73
|
+
- **节点工具**:设为闸门(挡住后续)/ 加时间目标(timegoal)/ 打开任务;
|
|
74
|
+
- 组视图进来可在**成员队列之间切换**(DAG 的排序/依赖/闸门都是队列内的);
|
|
75
|
+
- 编排只改被碰过的那几项,没编排过的节点保持原队列序。
|
|
76
|
+
- **设置面板交互收敛(任务 #41)**:
|
|
77
|
+
- **左边 tab 悬停即切换**(设置外层分区导航 + 「提示词 / 模板」栏内的三个分类),
|
|
78
|
+
点击保留为兜底;少一次点击。
|
|
79
|
+
- **提示词条目默认收起成标题行**(首行截断 + 字数),**点击才展开编辑**(单开手风琴);
|
|
80
|
+
「添加一条…」新增后直接展开待填。
|
|
81
|
+
- **说明文字全部删除**:设置面板右侧的静态说明段(工作区/任务工具/项目级等)与
|
|
82
|
+
提示词栏的提示段一并清掉,只留状态与操作。
|
|
83
|
+
- **项目级「持续 get」一句话配置**:选模板(内置默认 + 全局回调提示词条目)→ 改一句话
|
|
84
|
+
(`%s` = 任务编号,存取时与 `{{task.id}}` 互转)→ 添加;不再手写整段模板。
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
- `localtask config --suffix-list "一条,二条"` / `--ack-list "一条,二条"`:**整体替换**多条
|
|
89
|
+
提示词列表(逗号分隔;HTTP body 传数组)。此前 action 只暴露单条 `--suffix`/`--ack`,
|
|
90
|
+
面板的多条保存无处可传(#41 实测发现并修)。
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- **首次打开设置面板爆栈**(任务 #41 实测发现,#33 遗留):`openPanel` 按区域分发时
|
|
95
|
+
FLOAT → `openFloatingPanel` → `openPanel`(未带 area)→ 又判成 FLOAT → 无限递归
|
|
96
|
+
("Maximum call stack size exceeded")。老布局因已存着设置浮窗、走 existing 早退而侥幸
|
|
97
|
+
没踩到,全新档案首次打开必崩。内层调用显式指定 `AREA.MAIN` 后收敛。
|
|
98
|
+
- **回调 / ack 提示词在面板上保存不了**(任务 #41 实测发现,#32 遗留):面板打的是
|
|
99
|
+
`taskSuffix`/`ackPrompt` 字段,action 未声明 → 请求 400 且**静默无效**(面板回显旧列表,
|
|
100
|
+
看起来像保存成功)。改为 `suffixList`/`ackList`(对应上面新增的列表 flag)后真正落库。
|
|
101
|
+
- 侧栏搜索框降高响应式(任务 #38):`label.sbox` 28px→24px 显式定高;「+」按钮
|
|
102
|
+
`flex:none` 永不被挤掉;侧栏按实际像素宽分档降级(≤256 收间距 / ≤195 藏撤销重做 /
|
|
103
|
+
≤150 藏标题+放大镜 / ≤125 藏品牌字+匹配数),高频件(搜索框、+、收起/关闭、齿轮)
|
|
104
|
+
永远在场;`nx-title` 窄宽折两行一并修复。
|
|
105
|
+
- 面板 maximize 键全线下线(任务 #39):`tabSetEnableMaximize=false` 一关全断
|
|
106
|
+
(页签条 Maximize/Restore 按钮、右键菜单最大化项、双击标题栏最大化都走
|
|
107
|
+
`canMaximize()` 门控);连带清理死翻译与死 CSS 变量。
|
|
108
|
+
- 目录/组任务行的「⋮」菜单点击不再冒泡打开任务详情(stopPropagation)。
|
|
109
|
+
|
|
110
|
+
## [0.19.0] - 2026-11-04 提示词/模板收敛到设置面板 + 面板无页签化(面包屑 + 区域策略)+ DAG 引擎与 goal 插件族
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
|
|
114
|
+
- **提示词 / 模板统一收敛到设置面板一栏(任务 #32)**:此前提示词与模板散在
|
|
115
|
+
「模板与任务工具」区、模板页、项目级设置三处,各抄一份编辑器、风格也不一。
|
|
116
|
+
现在设置面板新增「**提示词 / 模板**」一栏,三样收在一起:
|
|
117
|
+
- **任务模板**:正文里的 `{{槽位}}` 是槽位,**骨架固定**(内置模板不可改结构)
|
|
118
|
+
只填槽位内容——设置面板与模板页都能填(`template update --values`);
|
|
119
|
+
新建任务时正文按槽位值渲染。
|
|
120
|
+
- **回调提示词**(全局,多条):与收尾指令同一份存储与语义。
|
|
121
|
+
- **ack 完成总结提示词**(全局,多条,**新增**):agent 写完成总结(ack)时的
|
|
122
|
+
格式要求,随领取交付的 `instruction` 一起给到 agent;占位符语法与收尾指令
|
|
123
|
+
同款(`{{task.id}}` / `{{goal.*}}` / 段落 `{{#goal}}…{{/goal}}`)。
|
|
124
|
+
**默认不配置 = 交付里不追加任何东西**(默认交付逐字不变)。
|
|
125
|
+
CLI 同口径:`localtask config --ack "<提示词>"`。
|
|
126
|
+
- **共享的提示词编辑器**(`modules/prompts/`):文本框 + 「占位符」插入菜单 +
|
|
127
|
+
已用占位符标记,回调提示词 / ack 提示词 / 持续 get 模板 / 模板槽位共用一份实现
|
|
128
|
+
——此前三套长得不一样的编辑器,现在只有一套。
|
|
129
|
+
- **面板模型收敛(任务 #33)**:工作区统一为四个区域——sidebar / maincontent /
|
|
130
|
+
rightSidebar / float;面板落点由一张**策略表**声明(单例与否一并声明),
|
|
131
|
+
打开新面板即替换主内容区(主区是**单面板位**)。页签与页签条彻底删除:
|
|
132
|
+
面板头部不再有 float / popup 标识,「清空本组全部页签」垃圾桶按钮一并移除;
|
|
133
|
+
顶部新增**面包屑**显示当前位置(如 `[ nx / nx-kv / #28 ]`)。老布局读盘
|
|
134
|
+
自动愈合(关页签条、补右侧栏)。
|
|
135
|
+
- **DAG 依赖与闸门(任务 #34)**:`localtask add --block-by 12,13`(依赖:这些
|
|
136
|
+
任务全部完成后本条才可领取)、`--gate`(闸门:自身未完成前挡住队列里排在
|
|
137
|
+
后面的 pending);`localtask update` 可改依赖 / 设清闸门。领取按 D-first 取
|
|
138
|
+
第一条**可领**的 pending,目录任务列表带「闸门 / 锁定」徽标。
|
|
139
|
+
- **workflow 契约与引擎骨架(任务 #34 阶段 0)**:`core/workflow/` 冻结类型契约
|
|
140
|
+
(workflowId → 主题 key;对外稳定门面只有 submit / get;goal 是插件族:
|
|
141
|
+
block=调度侧、wait/time=消费侧);`localtask` 拆出 store / bus / mutator /
|
|
142
|
+
scheduler 分层(唯一写快照入口 Mutator,原子覆盖);`tasktool/plugins/`
|
|
143
|
+
目录扫描自注册(base / blockgoal / container / taskgoal / timegoal /
|
|
144
|
+
waitgoal),`tasktool/facade/` 收敛 submit / get / wait 门面。
|
|
145
|
+
- **评论任务旁路原文**:工具栏「原文」或 Ctrl/⌘⇧O 在主区右侧并排打开锚定
|
|
146
|
+
原文(`frontmatter.parent`,旧数据从「锚定自…」回推),可单独关掉、不改路由。
|
|
147
|
+
- **验收条 ack 按 md 渲染**:完成总结走正文同一套渲染;过长默认夹紧、
|
|
148
|
+
可展开全文(任务 #20)。
|
|
149
|
+
|
|
150
|
+
### Changed
|
|
151
|
+
|
|
152
|
+
- **面板贴图嵌入修复(任务 #15 再开)**:实时预览的块内光标坐标与源码全文偏移
|
|
153
|
+
分开(`insertAttachmentSnippet` 双坐标),落点禁止进 frontmatter;已落库任务
|
|
154
|
+
贴图后立刻 PATCH 同步服务端,不再等「保存并同步」——修掉「贴图上传成功但
|
|
155
|
+
正文未嵌入、agent 领取 attachments count: 0」。
|
|
156
|
+
- **frontmatter 键自愈**:贴图误插撕出的残留键(`board:` 被撕成 `oard:` / `d:`)
|
|
157
|
+
解析时升回规范键、保存时写回(#15 / #23 案例),组面板不再丢任务。
|
|
158
|
+
- **模板槽位不露 {{标记}}**:新建任务正文一律按槽位值渲染(缺省用内置默认值
|
|
159
|
+
兜底),`{{背景}}` 之类标记不再出现在任务正文里;设置面板「提示词 / 模板」
|
|
160
|
+
栏改左 tab 布局,槽位显示名字不给标记。
|
|
161
|
+
- **右栏可关 + 进程面板跟随**:进程面板可关闭(右栏随之收起);主区切到别的
|
|
162
|
+
任务时已开的进程面板同步跟随(#33 验收反馈);侧栏收起入口挪到品牌行。
|
|
163
|
+
- **面包屑带组名**:目录页面包屑显示 `[ 组 / 目录 / 任务 ]`(层级抽成纯函数
|
|
164
|
+
`crumbs.js`,面板与单测共用)。
|
|
165
|
+
|
|
166
|
+
### Fixed
|
|
167
|
+
|
|
168
|
+
- **float 面板(设置)自调用栈溢出**:`openPanel` 的 float 分支原先递归调
|
|
169
|
+
`openFloatingPanel`,表现是「设置按钮点了没反应」;改为直接走
|
|
170
|
+
「加进主区 → floatTab 浮出」,`openFloatingPanel` 只做委托。
|
|
171
|
+
|
|
5
172
|
## [0.18.0] - 2026-11-04 描述绑定文档 + 描述弹窗
|
|
6
173
|
|
|
7
174
|
### Changed
|
|
@@ -96,7 +96,12 @@ wait 必带一句当前现状总结(≤2000 字,连续空白会折叠为单
|
|
|
96
96
|
- **配置**:`nx-kv localtask config --suffix "<模板>"`(占位符
|
|
97
97
|
`{{goal.name}} {{goal.remainingHuman}} {{goal.prompt}} {{task.text}} {{source}} {{name}}…`,
|
|
98
98
|
段落 `{{#goal}}…{{/goal}}` 上下文缺失整段消失;`--no-suffix` 关闭、
|
|
99
|
-
`--reset-suffix`
|
|
99
|
+
`--reset-suffix` 恢复默认;**多条**用 `--suffix-list "第一条,第二条"`(逗号分隔)整体替换,
|
|
100
|
+
或 `--suffix-add` / `--suffix-remove <序号>` 按条增删)。字段缺席 = 已关闭或本轮没有可渲染内容。
|
|
101
|
+
- **ack 提示词(完成总结的格式要求)**:`nx-kv localtask config --ack "<提示词>"`
|
|
102
|
+
(占位符同上;多条用 `--ack-list "第一条,第二条"`(逗号分隔)整体替换)。配置后,它会随 `instruction` 一起交给你——按它要求的格式
|
|
103
|
+
写 `localtask done --summary "<完成总结>"` 即可;没配置时 `instruction` 里没有
|
|
104
|
+
这段,格式自便。设置面板「提示词 / 模板」一栏也能配。
|
|
100
105
|
|
|
101
106
|
超时(exit 0,业务结果而非错误):
|
|
102
107
|
|
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,114 @@
|
|
|
1
|
+
// 主题队列(localtask topic.tasks)↔ WorkflowSnapshot 双向投影。
|
|
2
|
+
// 存盘形状仍是任务行;快照是引擎视图。正文不进快照(payload.textRef)。
|
|
3
|
+
// 跨队列 blockBy("topicKey:id")不进引擎边,投影回写时必须保留。
|
|
4
|
+
|
|
5
|
+
import { BUILTIN_GOALS, edgesFromTasks, taskToNode } from './types.js';
|
|
6
|
+
import { normalizeBlockBy, parseDepRef } from './readiness.js';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('./types.js').WorkflowSnapshot} WorkflowSnapshot */
|
|
9
|
+
/** @typedef {import('./types.js').WfNode} WfNode */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} topic localtask 主题桶
|
|
13
|
+
* @returns {WorkflowSnapshot}
|
|
14
|
+
*/
|
|
15
|
+
export function topicToSnapshot(topic) {
|
|
16
|
+
const workflowId = String(topic?.key || topic?.workflowId || '');
|
|
17
|
+
const tasks = Array.isArray(topic?.tasks) ? topic.tasks : [];
|
|
18
|
+
const nodes = tasks.filter((t) => t && !t.deletedAt && !t.migratedTo).map(taskToNode);
|
|
19
|
+
const edges = edgesFromTasks(tasks);
|
|
20
|
+
const revision = Number(topic?.workflowRevision) || 0;
|
|
21
|
+
/** @type {WorkflowSnapshot} */
|
|
22
|
+
return {
|
|
23
|
+
workflowId,
|
|
24
|
+
state: topic?.workflowState || 'active',
|
|
25
|
+
nodes,
|
|
26
|
+
edges,
|
|
27
|
+
workflowPlugins: Array.isArray(topic?.workflowPlugins) ? topic.workflowPlugins : [],
|
|
28
|
+
revision,
|
|
29
|
+
updatedAt: topic?.updatedAt || new Date().toISOString(),
|
|
30
|
+
meta: {
|
|
31
|
+
cwd: topic?.cwd || '',
|
|
32
|
+
name: topic?.name || '',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 把快照写回主题任务行(保守:不改 text;按 id 对齐)。
|
|
39
|
+
* @param {object} topic
|
|
40
|
+
* @param {WorkflowSnapshot} snap
|
|
41
|
+
*/
|
|
42
|
+
export function applySnapshotToTopic(topic, snap) {
|
|
43
|
+
if (!topic || !snap) return topic;
|
|
44
|
+
const tasks = Array.isArray(topic.tasks) ? topic.tasks : (topic.tasks = []);
|
|
45
|
+
const byId = new Map(tasks.map((t) => [String(t.id), t]));
|
|
46
|
+
|
|
47
|
+
for (const node of snap.nodes || []) {
|
|
48
|
+
let task = byId.get(String(node.id));
|
|
49
|
+
if (!task) {
|
|
50
|
+
// 引擎新建的节点 → 补一条占位任务(正文外置占位)
|
|
51
|
+
task = {
|
|
52
|
+
id: Number(node.id) || node.id,
|
|
53
|
+
text: '',
|
|
54
|
+
status: 'pending',
|
|
55
|
+
createdAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
56
|
+
completedAt: '',
|
|
57
|
+
deletedAt: '',
|
|
58
|
+
blockBy: [],
|
|
59
|
+
blocksFollowing: false,
|
|
60
|
+
};
|
|
61
|
+
tasks.push(task);
|
|
62
|
+
byId.set(String(task.id), task);
|
|
63
|
+
}
|
|
64
|
+
syncTaskFromNode(task, node);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 边 → blockBy(显式依赖);闸门边由 blocksFollowing 表达,不写进每条 blockBy
|
|
68
|
+
const gateIds = new Set(
|
|
69
|
+
(snap.nodes || [])
|
|
70
|
+
.filter(
|
|
71
|
+
(n) =>
|
|
72
|
+
n.kind === 'control' ||
|
|
73
|
+
(n.plugins || []).some((p) => p.name === BUILTIN_GOALS.blockgoal),
|
|
74
|
+
)
|
|
75
|
+
.map((n) => String(n.id)),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for (const task of tasks) {
|
|
79
|
+
if (task.deletedAt || task.migratedTo) continue;
|
|
80
|
+
const keepForeign = normalizeBlockBy(task.blockBy, task.id).filter(
|
|
81
|
+
(d) => parseDepRef(d)?.kind === 'foreign',
|
|
82
|
+
);
|
|
83
|
+
const incoming = (snap.edges || []).filter((e) => String(e.to) === String(task.id));
|
|
84
|
+
const localDeps = [];
|
|
85
|
+
for (const e of incoming) {
|
|
86
|
+
// 跳过「闸门挡后续」推出来的边:from 是闸门且闸门在数组序上早于本任务时由 blocksFollowing 覆盖
|
|
87
|
+
if (gateIds.has(String(e.from))) continue;
|
|
88
|
+
const n = Number(e.from);
|
|
89
|
+
if (Number.isFinite(n) && n > 0) localDeps.push(n);
|
|
90
|
+
}
|
|
91
|
+
task.blockBy = normalizeBlockBy([...localDeps, ...keepForeign], task.id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
topic.workflowRevision = Number(snap.revision) || 0;
|
|
95
|
+
topic.workflowState = snap.state || 'active';
|
|
96
|
+
topic.workflowPlugins = Array.isArray(snap.workflowPlugins) ? snap.workflowPlugins : [];
|
|
97
|
+
if (snap.updatedAt) topic.updatedAt = snap.updatedAt;
|
|
98
|
+
return topic;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @param {object} task @param {WfNode} node */
|
|
102
|
+
function syncTaskFromNode(task, node) {
|
|
103
|
+
const hasBlock = (node.plugins || []).some((p) => p.name === BUILTIN_GOALS.blockgoal);
|
|
104
|
+
task.blocksFollowing = node.kind === 'control' || hasBlock;
|
|
105
|
+
|
|
106
|
+
if (node.state === 'success') {
|
|
107
|
+
if (!task.completedAt) {
|
|
108
|
+
task.completedAt = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
109
|
+
}
|
|
110
|
+
} else if (node.state === 'running' && task.status !== 'consumed') {
|
|
111
|
+
task.status = 'consumed';
|
|
112
|
+
}
|
|
113
|
+
// pending/ready:不改 status(领取态由 wait/get 管;ready 由 isReadyToClaim 现算)
|
|
114
|
+
}
|