dsh-tree-task-flow 0.3.4 → 0.3.6

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/lib/plan.js CHANGED
@@ -81,28 +81,74 @@ function makeTask(input, now) {
81
81
  * @param now - 注入的时间戳,便于测试。
82
82
  * @returns 完整的计划树对象。
83
83
  */
84
- export function createPlan(input, now = Date.now()) {
84
+ /**
85
+ * 建一个目标:它自己的字段 + 一批任务。
86
+ *
87
+ * 一个会话可以有**多个**目标,它们是最外层的兄弟,按创建顺序排。
88
+ *
89
+ * @param input - `{ title, detail?, tasks: [{ title, detail? }] }`。
90
+ * @param now - 注入的时间戳,便于测试。
91
+ * @returns 目标节点。
92
+ */
93
+ export function createGoal(input, now = Date.now()) {
85
94
  const tasks = Array.isArray(input?.tasks) ? input.tasks : [];
86
95
  if (tasks.length === 0) {
87
96
  throw new Error("至少要有一个任务;请在 tasks 里给出");
88
97
  }
89
98
  return {
90
- version: 1,
99
+ id: newId("g"),
100
+ title: normalizeTitle(input?.title, "(未命名目标)"),
101
+ detail: String(input?.detail ?? ""),
102
+ status: STATUS.PENDING,
103
+ createdAt: now,
104
+ completedAt: null,
105
+ result: null,
106
+ tasks: tasks.map((task) => makeTask(task, now)),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * 建一棵全新的计划树:只含一个目标。
112
+ *
113
+ * @param input - `{ title, detail?, tasks: [{ title, detail? }] }`。
114
+ * @param now - 注入的时间戳,便于测试。
115
+ * @returns 完整的计划树对象。
116
+ */
117
+ export function createPlan(input, now = Date.now()) {
118
+ return {
119
+ version: 2,
91
120
  createdAt: now,
92
121
  updatedAt: now,
93
- goal: {
94
- id: newId("g"),
95
- title: normalizeTitle(input?.title, "(未命名目标)"),
96
- detail: String(input?.detail ?? ""),
97
- status: STATUS.PENDING,
98
- createdAt: now,
99
- completedAt: null,
100
- result: null,
101
- tasks: tasks.map((task) => makeTask(task, now)),
102
- },
122
+ goals: [createGoal(input, now)],
103
123
  };
104
124
  }
105
125
 
126
+ /**
127
+ * 往计划里追加一个目标——同一个会话里再立一个目标走这里,不是重建整棵树。
128
+ *
129
+ * @param plan - 计划树。
130
+ * @param input - 同 `createGoal`。
131
+ * @param now - 时间戳。
132
+ * @returns 新加的目标节点。
133
+ */
134
+ export function addGoal(plan, input, now = Date.now()) {
135
+ const goal = createGoal(input, now);
136
+ if (!Array.isArray(plan.goals)) plan.goals = [];
137
+ plan.goals.push(goal);
138
+ plan.updatedAt = now;
139
+ return goal;
140
+ }
141
+
142
+ /**
143
+ * 计划里的全部目标。
144
+ *
145
+ * @param plan - 计划树。
146
+ * @returns 目标数组;没有计划时是空数组。
147
+ */
148
+ export function goalsOf(plan) {
149
+ return Array.isArray(plan?.goals) ? plan.goals : [];
150
+ }
151
+
106
152
  /**
107
153
  * 按 id 找节点,并给出它在树里的位置。
108
154
  *
@@ -111,18 +157,18 @@ export function createPlan(input, now = Date.now()) {
111
157
  * @returns `{ level, goal, task, step, node }`,找不到返回 null。
112
158
  */
113
159
  export function findNode(plan, id) {
114
- const goal = plan?.goal;
115
- if (!goal) return null;
116
- if (goal.id === id) {
117
- return { level: "goal", goal, task: null, step: null, node: goal };
118
- }
119
- for (const task of goal.tasks ?? []) {
120
- if (task.id === id) {
121
- return { level: "task", goal, task, step: null, node: task };
160
+ for (const goal of goalsOf(plan)) {
161
+ if (goal.id === id) {
162
+ return { level: "goal", goal, task: null, step: null, node: goal };
122
163
  }
123
- for (const step of task.steps ?? []) {
124
- if (step.id === id) {
125
- return { level: "step", goal, task, step, node: step };
164
+ for (const task of goal.tasks ?? []) {
165
+ if (task.id === id) {
166
+ return { level: "task", goal, task, step: null, node: task };
167
+ }
168
+ for (const step of task.steps ?? []) {
169
+ if (step.id === id) {
170
+ return { level: "step", goal, task, step, node: step };
171
+ }
126
172
  }
127
173
  }
128
174
  }
@@ -138,8 +184,8 @@ export function findNode(plan, id) {
138
184
  * `step` 为 `null` 表示当前任务还没拆子任务(需要先 `tree_task_plan`)。
139
185
  */
140
186
  export function activePath(plan) {
141
- const goal = plan?.goal;
142
- if (!goal || !isOpen(goal)) return null;
187
+ const goal = goalsOf(plan).find(isOpen);
188
+ if (!goal) return null;
143
189
  const task = (goal.tasks ?? []).find(isOpen);
144
190
  if (!task) return { goal, task: null, step: null };
145
191
  const step = (task.steps ?? []).find(isOpen);
@@ -156,21 +202,21 @@ export function activePath(plan) {
156
202
  * @returns `{ level, goal, task, step, node }`,没有检查点时返回 null。
157
203
  */
158
204
  export function checkpointOf(plan) {
159
- const goal = plan?.goal;
160
- if (!goal || !isOpen(goal)) return null;
161
-
162
- const tasks = goal.tasks ?? [];
163
- const openTask = tasks.find(isOpen);
205
+ const active = activePath(plan);
206
+ if (active === null) return null;
207
+ const { goal } = active;
164
208
 
165
- if (openTask === undefined) {
166
- // 目标下已经没有未结束的任务,但目标自己还开着。
209
+ if (active.task === null) {
210
+ // 这个目标下已经没有未结束的任务,但目标自己还开着。
211
+ const tasks = goal.tasks ?? [];
167
212
  return tasks.length > 0 ? { level: "goal", goal, task: null, step: null, node: goal } : null;
168
213
  }
169
214
 
170
- const steps = openTask.steps ?? [];
215
+ if (active.step !== null) return null;
216
+
217
+ const steps = active.task.steps ?? [];
171
218
  if (steps.length === 0) return null;
172
- if (steps.some(isOpen)) return null;
173
- return { level: "task", goal, task: openTask, step: null, node: openTask };
219
+ return { level: "task", goal, task: active.task, step: null, node: active.task };
174
220
  }
175
221
 
176
222
  /**
@@ -257,9 +303,11 @@ export function complete(plan, id, result, now = Date.now()) {
257
303
  }
258
304
 
259
305
  /**
260
- * 丢弃一个节点。丢弃任务会连同它的子任务一起丢弃。
306
+ * 丢弃一个节点。丢弃目标会连同它下面的任务与子任务一起丢弃;丢弃任务会带上它的子任务。
261
307
  *
262
308
  * 改需求就用这个丢弃 + `tree_task_plan` 追加新子节点,旧节点留在树里可查。
309
+ * 已经结束的子节点保持原样——丢的是"还要不要做",不是改写历史。
310
+ * 已经**完成**的节点不能丢(那会毁掉它交出的结果),丢过的也不能再丢一次。
263
311
  *
264
312
  * @param plan - 计划树。
265
313
  * @param id - 节点 id。
@@ -269,16 +317,29 @@ export function complete(plan, id, result, now = Date.now()) {
269
317
  export function dropNode(plan, id, now = Date.now()) {
270
318
  const found = findNode(plan, id);
271
319
  if (!found) return { ok: false, reason: `找不到节点 ${id}` };
272
- if (found.level === "goal") {
273
- return { ok: false, reason: "不能丢弃整个目标;请用 tree_task_create 重建计划" };
320
+
321
+ if (found.node.status === STATUS.DONE) {
322
+ return { ok: false, reason: `节点 ${id} 已经完成,丢弃会毁掉它的结果记录` };
323
+ }
324
+ if (found.node.status === STATUS.DROPPED) {
325
+ return { ok: false, reason: `节点 ${id} 已经被丢弃` };
274
326
  }
275
327
 
276
328
  const dropped = [`${levelLabel(found.level)}「${found.node.title}」`];
277
329
  found.node.status = STATUS.DROPPED;
278
- for (const step of found.node.steps ?? []) {
279
- if (!isClosed(step)) {
280
- step.status = STATUS.DROPPED;
281
- dropped.push(`子任务「${step.title}」`);
330
+
331
+ // 目标是这一层唯一的"有孩子"的节点里最外那一层:丢它就把整支带走。
332
+ const tasks = found.level === "goal" ? (found.node.tasks ?? []) : [found.node];
333
+ for (const task of tasks) {
334
+ if (task !== found.node && !isClosed(task)) {
335
+ task.status = STATUS.DROPPED;
336
+ dropped.push(`任务「${task.title}」`);
337
+ }
338
+ for (const step of task.steps ?? []) {
339
+ if (!isClosed(step)) {
340
+ step.status = STATUS.DROPPED;
341
+ dropped.push(`子任务「${step.title}」`);
342
+ }
282
343
  }
283
344
  }
284
345
  plan.updatedAt = now;
@@ -334,13 +395,68 @@ export function levelLabel(level) {
334
395
  return "子任务";
335
396
  }
336
397
 
337
- /** 检查点那句话:把判断权交还给模型,不假装替它规定了出路。 */
398
+ /**
399
+ * 注入消息里的层级称呼。树渲染走 `levelLabel`(目标 / 任务 / 子任务),
400
+ * 注入消息里 L3 一律叫"步骤"。
401
+ */
402
+ const NOTICE_WORD = { goal: "目标", task: "任务", step: "步骤" };
403
+
404
+ /**
405
+ * 节点在注入消息里的自称:目标不带标题,其余带「标题」。
406
+ *
407
+ * @param level - `goal` / `task` / `step`。
408
+ * @param node - 节点。
409
+ * @returns 自称文本。
410
+ */
411
+ function subject(level, node) {
412
+ return level === "goal" ? `目标${node.id}` : `${NOTICE_WORD[level]}「${node.title}」${node.id}`;
413
+ }
414
+
415
+ /**
416
+ * 一条完成通告:这个节点做完了、过程归档了。
417
+ *
418
+ * 末尾**不带标点**——调用方按需要在后面接","或"。"再拼下一步提示。
419
+ * 不重复提交内容:`result` 就在这次 `tree_task_done` 调用的参数里,再抄一遍是白占位置。
420
+ *
421
+ * @param level - `goal` / `task` / `step`。
422
+ * @param node - 节点。
423
+ * @returns 通告文本。
424
+ */
425
+ export function doneNotice(level, node) {
426
+ const self = subject(level, node);
427
+ if (node.status !== STATUS.DONE) return `${self}已被丢弃`;
428
+ return `${self}已经完成,执行过程已归档`;
429
+ }
430
+
431
+ /** 注入消息与 `tree_task_done` 结果共用的抬头。 */
432
+ export const NOTICE_PREFIX = "tree_task消息:";
433
+
434
+ /**
435
+ * `tree_task_done` 的返回内容:下一步该干什么。
436
+ *
437
+ * 提示走工具结果本身,不再另外伪造一条用户消息;"哪个节点完成了、过程已归档"
438
+ * 由折叠落下的那条通告负责。两者合起来才是一条完整的话,所以这里只给后半句。
439
+ *
440
+ * @param plan - 计划树。
441
+ * @returns 返回文本。
442
+ */
443
+ export function doneReply(plan) {
444
+ return `${NOTICE_PREFIX}${renderContinue(plan)}`;
445
+ }
446
+
447
+ /**
448
+ * 检查点那句话:这一层的下一级都结束了,是收尾还是再补几个。
449
+ *
450
+ * @param checkpoint - `checkpointOf(plan)` 的结果。
451
+ * @returns 检查点文本。
452
+ */
338
453
  function checkpointNote(checkpoint) {
339
- const label = levelLabel(checkpoint.level);
454
+ const level = checkpoint.level;
455
+ const childWord = level === "goal" ? "任务" : "步骤";
456
+ const self = subject(level, checkpoint.node);
340
457
  return (
341
- `${label}「${checkpoint.node.title}」的子节点已全部结束,转入检查点。` +
342
- `此刻轮到你的判断:用 tree_task_plan 继续给 ${checkpoint.node.id} 拆子节点,` +
343
- `或者用 tree_task_done(id="${checkpoint.node.id}", result="…") 提交这个${label}的结果并完成它。`
458
+ `检查点:${self} 的${childWord}已全部结束,请确认${self} 是否完成,` +
459
+ `如果完成请调用tree_task_done,如果还需继续,可使用 tree_task_plan 新增${childWord}继续。`
344
460
  );
345
461
  }
346
462
 
@@ -364,66 +480,76 @@ function firstLine(text, limit = 120) {
364
480
  * @returns 多行文本。
365
481
  */
366
482
  export function renderTree(plan) {
367
- if (!plan?.goal) return "(还没有计划。先用 tree_task_create 建立目标与任务。)";
483
+ const goals = goalsOf(plan);
484
+ if (goals.length === 0) return "(还没有计划。先用 tree_task_create 建立目标与任务。)";
368
485
  const active = activePath(plan);
369
486
  const checkpoint = checkpointOf(plan);
370
487
  const lines = [];
371
- const goal = plan.goal;
372
-
373
- lines.push(`${mark(goal)} 目标 ${goal.id} ${goal.title}`);
374
- if (goal.detail) lines.push(` ${goal.detail}`);
375
- if (goal.result) lines.push(` 结果: ${firstLine(goal.result.text)}`);
376
-
377
- for (const task of goal.tasks ?? []) {
378
- const taskHere = active?.task?.id === task.id || checkpoint?.node?.id === task.id;
379
- lines.push(` ${mark(task)} 任务 ${task.id} ${task.title}${taskHere ? " ← 当前" : ""}`);
380
- if (task.result) lines.push(` 结果: ${firstLine(task.result.text)}`);
381
- const steps = task.steps ?? [];
382
- if (steps.length === 0 && isOpen(task)) {
383
- lines.push(" (还没有子节点 → 调用 tree_task_plan)");
384
- }
385
- for (const step of steps) {
386
- lines.push(` ${mark(step)} 子任务 ${step.id} ${step.title}${active?.step?.id === step.id ? " ← 当前" : ""}`);
387
- if (step.result) lines.push(` 结果: ${firstLine(step.result.text)}`);
488
+
489
+ for (const goal of goals) {
490
+ lines.push(`${mark(goal)} 目标 ${goal.id} ${goal.title}`);
491
+ if (goal.detail) lines.push(` ${goal.detail}`);
492
+ if (goal.result) lines.push(` 结果: ${firstLine(goal.result.text)}`);
493
+
494
+ for (const task of goal.tasks ?? []) {
495
+ const taskHere = active?.task?.id === task.id || checkpoint?.node?.id === task.id;
496
+ lines.push(` ${mark(task)} 任务 ${task.id} ${task.title}${taskHere ? " ← 当前" : ""}`);
497
+ if (task.result) lines.push(` 结果: ${firstLine(task.result.text)}`);
498
+ const steps = task.steps ?? [];
499
+ if (steps.length === 0 && isOpen(task)) {
500
+ lines.push(" (还没有子节点 → 调用 tree_task_plan)");
501
+ }
502
+ for (const step of steps) {
503
+ lines.push(` ${mark(step)} 子任务 ${step.id} ${step.title}${active?.step?.id === step.id ? " ← 当前" : ""}`);
504
+ if (step.result) lines.push(` 结果: ${firstLine(step.result.text)}`);
505
+ }
388
506
  }
389
507
  }
390
508
  return lines.join("\n");
391
509
  }
392
510
 
393
511
  /**
394
- * 生成"接下来做什么"的指令文本,给自动续行用。
512
+ * 注入消息里的收尾提示。它接在完成通告后面,所以自带前置标点(`lead`)。
513
+ *
514
+ * 三种落点:还有同级就报下一个;同级到头就进检查点;整棵树完了就让汇报。
395
515
  *
396
516
  * @param plan - 计划树。
397
- * @returns 指令文本;整棵树已结束时说明已经做完。
517
+ * @returns `{ lead, text }`;`lead` 是要接在通告末尾的标点。
398
518
  */
399
- export function renderContinue(plan) {
519
+ export function continueNote(plan) {
400
520
  const checkpoint = checkpointOf(plan);
401
- if (checkpoint !== null) {
402
- const label = levelLabel(checkpoint.level);
403
- return [
404
- `检查点:${label}「${checkpoint.node.title}」(${checkpoint.node.id}) 的子节点已全部结束。`,
405
- `此刻轮到你的判断:用 tree_task_plan 继续给 ${checkpoint.node.id} 拆子节点,` +
406
- `或者用 tree_task_done(id="${checkpoint.node.id}", result="…") 提交这个${label}的结果并完成它。`,
407
- ].join("\n");
408
- }
521
+ if (checkpoint !== null) return { lead: "。", text: checkpointNote(checkpoint) };
409
522
 
410
523
  const active = activePath(plan);
411
- if (active === null) {
412
- return "计划树上的所有节点都已完成。请向用户汇报最终结果,不要再调用 tree_task_* 工具。";
413
- }
524
+ if (active === null) return { lead: ",", text: "请汇报最终结果。" };
414
525
 
415
- if (active.task !== null && active.step === null) {
416
- return `当前任务「${active.task.title}」(${active.task.id}) 还没有子节点。请调用 tree_task_plan 为它列出子任务,然后开始执行第一个。`;
526
+ if (active.step !== null) {
527
+ return {
528
+ lead: ",",
529
+ text: `接下来需要进行步骤:「${active.step.title}」(${active.step.id})。`,
530
+ };
417
531
  }
418
532
 
419
- if (active.step !== null) {
420
- const step = active.step;
421
- return [
422
- `请继续执行下一个子任务。`,
423
- `当前子任务:${step.id}「${step.title}」${step.detail ? ` —— ${step.detail}` : ""}`,
424
- `做完之后调用 tree_task_done(id="${step.id}", result="…"),在 result 里写清它产出了什么。`,
425
- ].join("\n");
533
+ if (active.task !== null) {
534
+ return {
535
+ lead: ",",
536
+ text: `接下来需要进行任务:「${active.task.title}」(${active.task.id})。`,
537
+ };
426
538
  }
427
539
 
428
- return `请继续推进任务「${active.task?.title ?? "(未知)"}」。`;
540
+ // 目标下面还没拆任务,或者刚从上一个目标轮到这一个。
541
+ return {
542
+ lead: ",",
543
+ text: `接下来需要进行目标:「${active.goal.title}」(${active.goal.id})。`,
544
+ };
545
+ }
546
+
547
+ /**
548
+ * 同一句话独立成句。自动续行那边没有完成通告可接,直接用 `text`。
549
+ *
550
+ * @param plan - 计划树。
551
+ * @returns 指令文本。
552
+ */
553
+ export function renderContinue(plan) {
554
+ return continueNote(plan).text;
429
555
  }
package/lib/prompt.js CHANGED
@@ -23,18 +23,7 @@ export const SECTION_NAME = "dsh-tree-task-flow";
23
23
  export const DEFAULT_ORDER = 1800;
24
24
 
25
25
  /** 段落正文。 */
26
- export const SECTION_TEXT = `## 树形任务流(tree_task_* 工具组)
27
-
28
- 一件事需要多步才能做完时,用这组工具把它组织成**目标 → 任务 → 子任务**三级树,
29
- 而不是把计划写在回复正文里,也不要再用 todo_write 另记一份。层级固定三级,
30
- 子任务是最低一级,不能再往下拆。
31
-
32
- - **完成必须提交能独立看懂的 result。** 这个节点从开始到现在的执行过程会被整段
33
- 折叠掉,只留下这条结果;同级已完成的汇总不会被后来的折叠卷走。要看全貌就调
34
- tree_task_status,不要凭印象猜。
35
- - **子任务全部结束后,父节点不会自动完成。** 那一刻轮到你的判断:继续用
36
- tree_task_plan 拆子节点,还是用 tree_task_done 提交结果收尾。插件不会替你决定
37
- 这一层做完没有。`;
26
+ export const SECTION_TEXT = "`tree_task_*` 工具适用长线任务,当目标和资料分析完成后,如果用户要求或判断任务较为复杂,可以将任务执行计划分为目标、任务交付节点、任务执行步骤三级写入任务树,借助工具来推进项目;完成步骤或任务时提交后续步骤需要的 result,内容可以是:产物/结论/问题/有哪些需要注意的坑。";
38
27
 
39
28
  /**
40
29
  * 注册段落。调用方必须包在 `ctx.effect()` 里,否则热更新后会重复注册而抛错。