nx-kv 0.19.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 +105 -0
- package/assets/kv-get/references/wait-workflow.md +3 -2
- package/package.json +1 -1
- package/src/core/workflow/project.js +9 -3
- package/src/core/workflow/readiness.js +109 -14
- package/src/core/workflow/types.js +4 -1
- package/src/modules/localtask/dag.js +2 -0
- package/src/modules/localtask/index.js +46 -13
- package/src/modules/localtask/service.js +179 -29
- package/src/modules/localtask/writes.js +14 -3
- package/src/modules/tasktool/add.js +1 -22
- package/src/modules/tasktool/group-store.js +28 -0
- package/src/modules/tasktool/update.js +70 -0
- package/src/web/frontend/App.jsx +7 -6
- package/src/web/frontend/components/icons.jsx +11 -0
- package/src/web/frontend/components/layout/breadcrumb.jsx +16 -4
- package/src/web/frontend/components/layout/crumbs.js +51 -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/sidebar/index.jsx +2 -2
- package/src/web/frontend/components/sidebar/side-state.js +3 -2
- package/src/web/frontend/modules/prompts/prompt-editor.jsx +110 -26
- package/src/web/frontend/modules/prompts/style.css +15 -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 -66
- package/src/web/frontend/page/dir/task.jsx +36 -2
- package/src/web/frontend/page/group/index.jsx +188 -22
- package/src/web/frontend/page/overview/index.jsx +2 -0
- package/src/web/frontend/page/repos/index.jsx +2 -0
- package/src/web/frontend/page/settings/index.jsx +101 -41
- package/src/web/frontend/page/settings/prompts-section.jsx +21 -31
- package/src/web/frontend/page/settings/style.css +57 -17
- package/src/web/frontend/page/templates/index.jsx +2 -0
- package/src/web/frontend/panels.jsx +6 -0
- package/src/web/frontend/style-v2.css +225 -7
- package/src/web/frontend/workspace/areas.js +1 -0
- package/src/web/frontend/workspace.js +14 -4
- package/src/web/public/assets/{Introduction-C4qC8N7g.js → Introduction-BnbM3LXZ.js} +1 -1
- package/src/web/public/assets/{attach-DcyGhCUW.js → attach-v8BxRJ7p.js} +1 -1
- 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-fhYYxphP.css → index-Cg0WaaAi.css} +1 -1
- package/src/web/public/assets/{index-DidFyMnw.js → index-DFHqf4ue.js} +4 -4
- package/src/web/public/assets/{md-history-C29-dF29.js → md-history-DDQvoTFy.js} +3 -3
- package/src/web/public/assets/{member-picker-B7dnMq25.js → member-picker-zypl1Wuc.js} +1 -1
- package/src/web/public/assets/overview-uWj8gdmN.js +1 -0
- package/src/web/public/assets/{progress-CnHK_jKP.js → progress-D2cyYcIw.js} +2 -2
- 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-BybxMN3v.js → template-slots-BjfVnHFQ.js} +1 -1
- package/src/web/public/assets/{template-slots-CNdlIB1I.css → template-slots-UVa4FnPn.css} +1 -1
- package/src/web/public/assets/templates-B9VLtDMa.js +2 -0
- package/src/web/public/assets/{upload-jOng3KOU.js → upload-DvoSV9Cl.js} +1 -1
- package/src/web/public/index.html +3 -3
- package/src/web/public/assets/cloud-D0SRl665.js +0 -3
- package/src/web/public/assets/dir-B4egV6kV.js +0 -3
- package/src/web/public/assets/group-Cbd1PVuv.js +0 -1
- package/src/web/public/assets/overview-CX9XZ5EO.js +0 -1
- package/src/web/public/assets/repos-CyxHTr1L.js +0 -10
- package/src/web/public/assets/settings-C5xCFk_y.css +0 -1
- package/src/web/public/assets/settings-DIvkm7lx.js +0 -1
- package/src/web/public/assets/task-CsEvkBW7.js +0 -2
- package/src/web/public/assets/templates-BtdK84si.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,111 @@
|
|
|
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
|
+
|
|
5
110
|
## [0.19.0] - 2026-11-04 提示词/模板收敛到设置面板 + 面板无页签化(面包屑 + 区域策略)+ DAG 引擎与 goal 插件族
|
|
6
111
|
|
|
7
112
|
### Added
|
|
@@ -96,9 +96,10 @@ 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 <序号>` 按条增删)。字段缺席 = 已关闭或本轮没有可渲染内容。
|
|
100
101
|
- **ack 提示词(完成总结的格式要求)**:`nx-kv localtask config --ack "<提示词>"`
|
|
101
|
-
|
|
102
|
+
(占位符同上;多条用 `--ack-list "第一条,第二条"`(逗号分隔)整体替换)。配置后,它会随 `instruction` 一起交给你——按它要求的格式
|
|
102
103
|
写 `localtask done --summary "<完成总结>"` 即可;没配置时 `instruction` 里没有
|
|
103
104
|
这段,格式自便。设置面板「提示词 / 模板」一栏也能配。
|
|
104
105
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// 主题队列(localtask topic.tasks)↔ WorkflowSnapshot 双向投影。
|
|
2
2
|
// 存盘形状仍是任务行;快照是引擎视图。正文不进快照(payload.textRef)。
|
|
3
|
+
// 跨队列 blockBy("topicKey:id")不进引擎边,投影回写时必须保留。
|
|
3
4
|
|
|
4
5
|
import { BUILTIN_GOALS, edgesFromTasks, taskToNode } from './types.js';
|
|
6
|
+
import { normalizeBlockBy, parseDepRef } from './readiness.js';
|
|
5
7
|
|
|
6
8
|
/** @typedef {import('./types.js').WorkflowSnapshot} WorkflowSnapshot */
|
|
7
9
|
/** @typedef {import('./types.js').WfNode} WfNode */
|
|
@@ -75,14 +77,18 @@ export function applySnapshotToTopic(topic, snap) {
|
|
|
75
77
|
|
|
76
78
|
for (const task of tasks) {
|
|
77
79
|
if (task.deletedAt || task.migratedTo) continue;
|
|
80
|
+
const keepForeign = normalizeBlockBy(task.blockBy, task.id).filter(
|
|
81
|
+
(d) => parseDepRef(d)?.kind === 'foreign',
|
|
82
|
+
);
|
|
78
83
|
const incoming = (snap.edges || []).filter((e) => String(e.to) === String(task.id));
|
|
79
|
-
const
|
|
84
|
+
const localDeps = [];
|
|
80
85
|
for (const e of incoming) {
|
|
81
86
|
// 跳过「闸门挡后续」推出来的边:from 是闸门且闸门在数组序上早于本任务时由 blocksFollowing 覆盖
|
|
82
87
|
if (gateIds.has(String(e.from))) continue;
|
|
83
|
-
|
|
88
|
+
const n = Number(e.from);
|
|
89
|
+
if (Number.isFinite(n) && n > 0) localDeps.push(n);
|
|
84
90
|
}
|
|
85
|
-
task.blockBy =
|
|
91
|
+
task.blockBy = normalizeBlockBy([...localDeps, ...keepForeign], task.id);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
topic.workflowRevision = Number(snap.revision) || 0;
|
|
@@ -1,17 +1,90 @@
|
|
|
1
1
|
// 拓扑就绪(自 localtask/dag.js 下沉)。任务行与快照节点两套入口。
|
|
2
|
+
//
|
|
3
|
+
// blockBy 支持:
|
|
4
|
+
// - 本地 id(number)——同队列依赖(兼容旧存盘)
|
|
5
|
+
// - 跨队列引用 `"<topicKey>:<taskId>"`——组编排跨项目边;get 时跨 topic 解析
|
|
2
6
|
|
|
3
|
-
/**
|
|
7
|
+
/** topicKey:id —— key 允许字母数字与 _-(含 __GLOBAL__) */
|
|
8
|
+
const FOREIGN_RE = /^([A-Za-z0-9_-]+):(\d+)$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{ kind: 'local', id: number } | { kind: 'foreign', key: string, id: number }} DepRef
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 解析单条依赖引用。
|
|
16
|
+
* @param {unknown} x
|
|
17
|
+
* @returns {DepRef | null}
|
|
18
|
+
*/
|
|
19
|
+
export function parseDepRef(x) {
|
|
20
|
+
if (typeof x === 'number' && Number.isFinite(x) && x > 0) {
|
|
21
|
+
return { kind: 'local', id: Math.trunc(x) };
|
|
22
|
+
}
|
|
23
|
+
const s = String(x ?? '').trim();
|
|
24
|
+
if (!s) return null;
|
|
25
|
+
const m = FOREIGN_RE.exec(s);
|
|
26
|
+
if (m) return { kind: 'foreign', key: m[1], id: Number(m[2]) };
|
|
27
|
+
// 纯数字字符串 → 本地 id(避免把 "12" 当 foreign)
|
|
28
|
+
if (/^\d+$/.test(s)) {
|
|
29
|
+
const n = Number(s);
|
|
30
|
+
if (Number.isFinite(n) && n > 0) return { kind: 'local', id: n };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {DepRef} ref @returns {number|string} */
|
|
36
|
+
export function formatDepRef(ref) {
|
|
37
|
+
if (!ref) return '';
|
|
38
|
+
if (ref.kind === 'foreign') return `${ref.key}:${ref.id}`;
|
|
39
|
+
return ref.id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 规范化 blockBy:本地正整数 id + 跨队列 "key:id"、去重、丢掉本地自身。
|
|
44
|
+
* @param {unknown} raw
|
|
45
|
+
* @param {number|string} [selfId]
|
|
46
|
+
* @returns {Array<number|string>}
|
|
47
|
+
*/
|
|
4
48
|
export function normalizeBlockBy(raw, selfId) {
|
|
5
49
|
const self = Number(selfId);
|
|
6
50
|
const seen = new Set();
|
|
7
51
|
const out = [];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
52
|
+
|
|
53
|
+
/** @type {unknown[]} */
|
|
54
|
+
let items = [];
|
|
55
|
+
if (raw == null || raw === '') items = [];
|
|
56
|
+
else if (Array.isArray(raw)) items = raw;
|
|
57
|
+
else if (typeof raw === 'string' && /[,,\s]/.test(raw) && !FOREIGN_RE.test(raw.trim())) {
|
|
58
|
+
items = raw.split(/[,,\s]+/).filter(Boolean);
|
|
59
|
+
} else {
|
|
60
|
+
items = [raw];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 数组元素里也可能是逗号串(HTTP 偶发)
|
|
64
|
+
const flat = [];
|
|
65
|
+
for (const x of items) {
|
|
66
|
+
if (typeof x === 'string' && /[,,]/.test(x) && !FOREIGN_RE.test(x.trim())) {
|
|
67
|
+
flat.push(...x.split(/[,,\s]+/).filter(Boolean));
|
|
68
|
+
} else {
|
|
69
|
+
flat.push(x);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const x of flat) {
|
|
74
|
+
const ref = parseDepRef(x);
|
|
75
|
+
if (!ref) continue;
|
|
76
|
+
if (ref.kind === 'local') {
|
|
77
|
+
if (Number.isFinite(self) && ref.id === self) continue;
|
|
78
|
+
const token = String(ref.id);
|
|
79
|
+
if (seen.has(`L:${token}`)) continue;
|
|
80
|
+
seen.add(`L:${token}`);
|
|
81
|
+
out.push(ref.id);
|
|
82
|
+
} else {
|
|
83
|
+
const token = `${ref.key}:${ref.id}`;
|
|
84
|
+
if (seen.has(`F:${token}`)) continue;
|
|
85
|
+
seen.add(`F:${token}`);
|
|
86
|
+
out.push(token);
|
|
87
|
+
}
|
|
15
88
|
}
|
|
16
89
|
return out;
|
|
17
90
|
}
|
|
@@ -28,18 +101,36 @@ function isPendingLike(t) {
|
|
|
28
101
|
return isAlive(t) && t.status !== 'consumed';
|
|
29
102
|
}
|
|
30
103
|
|
|
104
|
+
/**
|
|
105
|
+
* @typedef {object} ReadyOpts
|
|
106
|
+
* @property {(topicKey: string, taskId: number) => object|null|undefined} [resolveForeign]
|
|
107
|
+
* 跨队列依赖解析;缺席或返回空 = fail-open(挡不住)
|
|
108
|
+
*/
|
|
109
|
+
|
|
31
110
|
/**
|
|
32
111
|
* 某条 pending 现在能不能领(任务行模型)。
|
|
33
112
|
* @param {object} task
|
|
34
113
|
* @param {object[]} all
|
|
114
|
+
* @param {ReadyOpts} [opts]
|
|
35
115
|
*/
|
|
36
|
-
export function isReadyToClaim(task, all = []) {
|
|
116
|
+
export function isReadyToClaim(task, all = [], opts = {}) {
|
|
37
117
|
if (!isPendingLike(task)) return false;
|
|
38
118
|
const byId = new Map((all || []).filter(isAlive).map((t) => [Number(t.id), t]));
|
|
39
119
|
|
|
40
120
|
const deps = normalizeBlockBy(task.blockBy, task.id);
|
|
41
|
-
for (const
|
|
42
|
-
const
|
|
121
|
+
for (const d of deps) {
|
|
122
|
+
const ref = parseDepRef(d);
|
|
123
|
+
if (!ref) continue;
|
|
124
|
+
let dep = null;
|
|
125
|
+
if (ref.kind === 'local') {
|
|
126
|
+
dep = byId.get(ref.id) || null;
|
|
127
|
+
} else if (typeof opts.resolveForeign === 'function') {
|
|
128
|
+
const found = opts.resolveForeign(ref.key, ref.id);
|
|
129
|
+
dep = found && isAlive(found) ? found : null;
|
|
130
|
+
} else {
|
|
131
|
+
// 无解析器:跨队列依赖无法验证 → fail-open(与「依赖缺失」同口径)
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
43
134
|
if (!dep) continue;
|
|
44
135
|
if (!isCompleted(dep)) return false;
|
|
45
136
|
}
|
|
@@ -56,10 +147,14 @@ export function isReadyToClaim(task, all = []) {
|
|
|
56
147
|
return true;
|
|
57
148
|
}
|
|
58
149
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
150
|
+
/**
|
|
151
|
+
* 按数组序找第一条可领取的 pending;没有 → null
|
|
152
|
+
* @param {object[]} tasks
|
|
153
|
+
* @param {ReadyOpts} [opts]
|
|
154
|
+
*/
|
|
155
|
+
export function findFirstReadyPending(tasks = [], opts = {}) {
|
|
61
156
|
for (const t of tasks || []) {
|
|
62
|
-
if (isReadyToClaim(t, tasks)) return t;
|
|
157
|
+
if (isReadyToClaim(t, tasks, opts)) return t;
|
|
63
158
|
}
|
|
64
159
|
return null;
|
|
65
160
|
}
|
|
@@ -155,7 +155,10 @@ export const edgesFromTasks = (tasks = []) => {
|
|
|
155
155
|
const alive = (tasks || []).filter((t) => t && !t.deletedAt && !t.migratedTo);
|
|
156
156
|
for (const t of alive) {
|
|
157
157
|
for (const dep of Array.isArray(t.blockBy) ? t.blockBy : []) {
|
|
158
|
-
|
|
158
|
+
// 引擎快照只认本队列数字边;跨队列 "key:id" 留给 get/ready 解析
|
|
159
|
+
const n = Number(dep);
|
|
160
|
+
if (!Number.isFinite(n) || n <= 0 || String(dep).includes(':')) continue;
|
|
161
|
+
edges.push({ from: n, to: t.id });
|
|
159
162
|
}
|
|
160
163
|
}
|
|
161
164
|
for (let i = 0; i < alive.length; i += 1) {
|
|
@@ -203,8 +203,14 @@ export default {
|
|
|
203
203
|
key: KEY,
|
|
204
204
|
text: { type: 'string', hint: '修改后的任务正文(Markdown);不给则只重开不改内容' },
|
|
205
205
|
},
|
|
206
|
-
run: (ctx) =>
|
|
207
|
-
|
|
206
|
+
run: async (ctx) => {
|
|
207
|
+
// 重开也可能带着改过的归属(--text 的 frontmatter.project 变了)→ 同样搬家
|
|
208
|
+
const { reopenTask } = await import('../tasktool/update.js');
|
|
209
|
+
return reopenTask({ id: ctx.id, cwd: ctx.cwd, key: ctx.key, text: ctx.text });
|
|
210
|
+
},
|
|
211
|
+
render: (d) =>
|
|
212
|
+
`已重开任务 #${d.id} @ ${d.name}(回到待领取,第 ${d.task.reopenCount} 次重开)` +
|
|
213
|
+
(d.moved && d.movedFrom ? `;已改归属项目,原 ${d.movedFrom.name} #${d.movedFrom.id}` : ''),
|
|
208
214
|
},
|
|
209
215
|
{
|
|
210
216
|
// 完成门禁:带时间目标的任务未到点拒绝标记完成(提示派 sub agent 重提需求)。
|
|
@@ -314,10 +320,11 @@ export default {
|
|
|
314
320
|
// CLI、HTTP、面板提交同走这一条链路。
|
|
315
321
|
const { submitTask } = await import('../tasktool/add.js');
|
|
316
322
|
const rawBlock = ctx.blockBy ?? ctx['block-by'];
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
// 支持本地 id 与跨队列 "topicKey:id"(组编排边);空 → undefined
|
|
324
|
+
const { normalizeBlockBy } = await import('./dag.js');
|
|
325
|
+
const blockBy = rawBlock != null && String(rawBlock).trim()
|
|
326
|
+
? normalizeBlockBy(String(rawBlock), 0)
|
|
327
|
+
: [];
|
|
321
328
|
return submitTask({
|
|
322
329
|
text: ctx.text,
|
|
323
330
|
cwd: ctx.cwd,
|
|
@@ -355,7 +362,7 @@ export default {
|
|
|
355
362
|
clearBlockBy: { type: 'boolean', hint: '同 --clear-block-by' },
|
|
356
363
|
gate: { type: 'boolean', hint: '设/清闸门(true=挡住后续;false=取消闸门)' },
|
|
357
364
|
},
|
|
358
|
-
run: (ctx) => {
|
|
365
|
+
run: async (ctx) => {
|
|
359
366
|
const gp = ctx.goalPrompt ?? ctx['goal-prompt'];
|
|
360
367
|
const cg = ctx.clearGoal ?? ctx['clear-goal'];
|
|
361
368
|
const hasGoalInput = ctx.duration != null || gp != null || cg;
|
|
@@ -363,12 +370,17 @@ export default {
|
|
|
363
370
|
const clearBlockBy = !!(ctx.clearBlockBy ?? ctx['clear-block-by']);
|
|
364
371
|
let blockBy;
|
|
365
372
|
if (!clearBlockBy && rawBlock !== undefined) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.
|
|
373
|
+
// 本地 id + "topicKey:id";数组 body(面板)与逗号串都走 normalizeBlockBy
|
|
374
|
+
const { normalizeBlockBy } = await import('./dag.js');
|
|
375
|
+
blockBy = normalizeBlockBy(
|
|
376
|
+
Array.isArray(rawBlock) ? rawBlock : String(rawBlock || ''),
|
|
377
|
+
ctx.id,
|
|
378
|
+
);
|
|
370
379
|
}
|
|
371
|
-
|
|
380
|
+
// 改归属即搬家(任务 #43):正文 effective project 变了 → 任务迁到新项目的队列。
|
|
381
|
+
// 编排在 tasktool/update.js,与派任务(tasktool/add.js)同一套落点判定。
|
|
382
|
+
const { updateTask: editTask } = await import('../tasktool/update.js');
|
|
383
|
+
return editTask({
|
|
372
384
|
id: ctx.id,
|
|
373
385
|
text: ctx.text,
|
|
374
386
|
cwd: ctx.cwd,
|
|
@@ -380,7 +392,11 @@ export default {
|
|
|
380
392
|
});
|
|
381
393
|
},
|
|
382
394
|
render: (d) =>
|
|
383
|
-
|
|
395
|
+
(d.moved
|
|
396
|
+
? `已改归属项目:迁到主题「${d.name}」#${d.task.id}` +
|
|
397
|
+
(d.movedFrom ? `(原 ${d.movedFrom.name} #${d.movedFrom.id})` : '')
|
|
398
|
+
: `已更新主题「${d.name}」#${d.task.id}`) +
|
|
399
|
+
`: ${d.task.text}` +
|
|
384
400
|
(d.goalChanged === 'set'
|
|
385
401
|
? `(时间目标:${d.task.timegoal.duration}${d.task.timegoal.prompt ? ' · 附加要求已固化' : ''})`
|
|
386
402
|
: d.goalChanged === 'cleared'
|
|
@@ -553,10 +569,27 @@ export default {
|
|
|
553
569
|
type: 'string',
|
|
554
570
|
hint: 'ack 完成总结提示词(agent 完成任务时写完成总结的格式要求;随领取交付的 instruction 给到 agent)。占位符同 --suffix;传空 = 关闭。设置面板「提示词 / 模板」一栏也能配',
|
|
555
571
|
},
|
|
572
|
+
// **整体替换**多条列表(面板「提示词 / 模板」一栏用这个保存;服务层本就吃列表,
|
|
573
|
+
// 但此前 action 只暴露单条 flag → 面板保存 400 且静默无效,2026-11-04 #41 实测发现)。
|
|
574
|
+
'suffix-list': { type: 'array', hint: '整体替换收尾指令列表(多条,一项一条)' },
|
|
575
|
+
suffixList: { type: 'array', hint: '同 --suffix-list(驼峰写法,HTTP body 用)' },
|
|
576
|
+
'ack-list': { type: 'array', hint: '整体替换 ack 完成总结提示词列表(多条)' },
|
|
577
|
+
ackList: { type: 'array', hint: '同 --ack-list(驼峰写法,HTTP body 用)' },
|
|
556
578
|
},
|
|
557
579
|
run: async (ctx) => {
|
|
558
580
|
const patch = { timeoutSec: ctx.timeout };
|
|
559
581
|
if (ctx.ack !== undefined) patch.ackPrompt = ctx.ack;
|
|
582
|
+
const suffixList = ctx.suffixList ?? ctx['suffix-list'];
|
|
583
|
+
const ackList = ctx.ackList ?? ctx['ack-list'];
|
|
584
|
+
if (suffixList !== undefined || ackList !== undefined) {
|
|
585
|
+
if (ctx.suffix !== undefined || ctx['no-suffix'] || ctx['reset-suffix']
|
|
586
|
+
|| ctx['suffix-add'] !== undefined || ctx['suffix-remove'] !== undefined) {
|
|
587
|
+
throw badInput('--suffix-list 不能与 --suffix / --no-suffix / --reset-suffix / --suffix-add / --suffix-remove 同用');
|
|
588
|
+
}
|
|
589
|
+
if (ctx.ack !== undefined) throw badInput('--ack-list 不能与 --ack 同用');
|
|
590
|
+
if (suffixList !== undefined) patch.taskSuffix = suffixList;
|
|
591
|
+
if (ackList !== undefined) patch.ackPrompt = ackList;
|
|
592
|
+
}
|
|
560
593
|
const add = ctx['suffix-add'];
|
|
561
594
|
const removeN = ctx['suffix-remove'];
|
|
562
595
|
if (add !== undefined || removeN !== undefined) {
|