dsh-tree-task-flow 0.3.14 → 0.3.15

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.
@@ -1,11 +1,12 @@
1
1
  # 这个文件只是示例,插件不需要它——**安装即启用**,装完就能用。
2
2
  #
3
- # **两个开关平时不用改这里。** 打开 DSH 的设置面板,导航里「树形任务流」那一页点一下
4
- # 就行——那是活的,改完立刻生效,而且**盖过这里写的值**。优先级是:
3
+ # **两个开关平时不用改这里。** 打开侧边栏的「插件」页,在「已安装」里点
4
+ # `dsh-tree-task-flow` 那张卡,页面上就是这两个开关——那是活的,改完立刻生效,
5
+ # 而且**盖过这里写的值**。优先级是:
5
6
  #
6
- # 设置面板里改过的 > profile 里的 config > DEFAULT_CONFIG
7
+ # 插件页里改过的 > profile 里的 config > DEFAULT_CONFIG
7
8
  #
8
- # 这里留给"连设置面板也不想点"的场合(比如要一次性写进 profile 发给别人)。
9
+ # 这里留给"连插件页也不想点"的场合(比如要一次性写进 profile 发给别人)。
9
10
  # 想改成默认以外的东西时,把下面这段抄进 profile 自己的 cordis.patch.yml
10
11
  # (通常是 $DSH_HOME/profiles/<名字>/cordis.patch.yml):
11
12
  #
@@ -22,13 +23,13 @@
22
23
  # 关掉后插件不注册工具、不加提示词、不折叠上下文、不续行,一个字节都不碰会话。
23
24
  enabled: true
24
25
  # 模型停下时是否自动续行。**默认 false(不续)。**
25
- # 它会在你没看着的时候自己往下跑,所以默认交给设置面板里的人来点——
26
- # 设置面板里的那一页是个活的开关,改完立刻生效,不用重启。
26
+ # 它会在你没看着的时候自己往下跑,所以默认交给插件页里的人来点——
27
+ # 插件页里的那两个开关是活的,改完立刻生效,不用重启。
27
28
  autoContinue: false
28
29
  # 连续自动续行的轮次上限,达到就停;只有真人发言才重置配额。
29
30
  maxAutoRounds: 5
30
31
  # 意外中断后是否自动拉起(见 README「意外中断自动拉起」)。默认 true:
31
- # 它只在「这一轮不是正常结束」时动手,正常停下不受影响。同样能在设置面板里开关,
32
+ # 它只在「这一轮不是正常结束」时动手,正常停下不受影响。同样能在插件页里开关,
32
33
  # 而且改完立刻生效——这一项是"要不要救",与上一项"要不要往前推"是两件事。
33
34
  interruptRecovery: true
34
35
  # 网络/API 类中断的拉起时刻,单位毫秒,默认 [1分钟, 5分钟, 10分钟, 30分钟];
package/cordis.patch.yml CHANGED
@@ -12,16 +12,16 @@
12
12
  # 字段一次写全。可覆盖的字段见 DEFAULT_CONFIG:
13
13
  #
14
14
  # enabled 总开关 默认 true(写 false 关掉)
15
- # autoContinue 是否允许自动续行 默认 false(设置面板里能改)
15
+ # autoContinue 是否允许自动续行 默认 false(插件页里能改)
16
16
  # maxAutoRounds 连续自动续行轮次上限 默认 5
17
- # interruptRecovery 意外中断是否自动拉起 默认 true(设置面板里能改)
17
+ # interruptRecovery 意外中断是否自动拉起 默认 true(插件页里能改)
18
18
  # interruptDelaysMs 网络类中断拉起时刻 默认 null(= 1/5/10/30 分钟)
19
19
  # promptOrder 工具说明段顺序 默认 1800
20
20
  # rootDir 插件状态目录 默认 $DSH_HOME/dsh-task-tree
21
21
  #
22
22
  # 两个布尔开关(autoContinue / interruptRecovery)平时不用在这里改:
23
- # **DSH 设置面板**的导航里有一页叫「树形任务流」,点一下就行——**改完立刻生效、
24
- # 并且盖过这里写的值**(设置面板 > profile config > DEFAULT_CONFIG)。
23
+ # **侧边栏「插件」页**里点开 `dsh-tree-task-flow` 这张卡,页面上就有这两个开关——
24
+ # **改完立刻生效、并且盖过这里写的值**(插件页 > profile config > DEFAULT_CONFIG)。
25
25
  - insert:
26
26
  - id: task-tree
27
27
  name: 'dsh-tree-task-flow'
package/lib/commands.js CHANGED
@@ -20,16 +20,26 @@ import {
20
20
  renderGoal,
21
21
  renderTree,
22
22
  } from "./plan.js";
23
+ import { renderList } from "./render.js";
24
+ import {
25
+ activeRequirement,
26
+ deleteRequirement,
27
+ dropRequirement,
28
+ listActive,
29
+ listArchived,
30
+ listDrafts,
31
+ requirementOf,
32
+ } from "./requirement.js";
23
33
 
24
34
  /** 用法说明,status 和 help 都会带上。 */
25
35
  const USAGE = [
26
- "/task-tree 查看本会话的任务树与检查点状态",
27
- "/task-tree done <id> <结果> 提交某个节点的结果并完成它",
28
- "/task-tree drop <id> 丢弃某个节点:留着可查,标记为不做了",
29
- "/task-tree delete <id> 删除某个节点:连子节点一起从树里摘掉,不看状态",
36
+ "/task-tree 查看本会话的需求列表与当前进度",
37
+ "/task-tree done <id> <结果> 提交某个节点(需求 / 任务 / 步骤)的结果并完成它",
38
+ "/task-tree drop <id> 放弃:留着可查,标记为不做了",
39
+ "/task-tree delete <id> 删除:连下面的一起摘掉,不看状态",
30
40
  "/task-tree pause 暂停:当前工具跑完后停住,不再往下推进",
31
41
  "/task-tree resume 继续:从停住的地方接着跑,不插消息",
32
- "/task-tree reset 清掉本会话的计划树",
42
+ "/task-tree reset 清掉本会话的计划(需求、任务全没)",
33
43
  ].join("\n");
34
44
 
35
45
  /** 插件被配置关掉时唯一要说的话:现在是关的,以及怎么打开。 */
@@ -37,6 +47,25 @@ const DISABLED_NOTE =
37
47
  "dsh-tree-task-flow 已被配置**关闭**(config 里 enabled 不是 true):它没有注册工具," +
38
48
  "也不会折叠任何上下文。要打开请在 profile 的 cordis.patch.yml 里把 enabled 设为 true。";
39
49
 
50
+ /**
51
+ * 需求列表的文本:与面板、工具共用同一份渲染(`render.renderList`)。
52
+ *
53
+ * @param plan - 计划树。
54
+ * @returns 多行 Markdown。
55
+ */
56
+ function listText(plan) {
57
+ const pick = (rows) => rows.map((row) => requirementOf(plan, row.id)).filter((item) => item !== null);
58
+ return renderList(
59
+ plan,
60
+ {
61
+ active: pick(listActive(plan)),
62
+ archived: pick(listArchived(plan)),
63
+ drafts: listDrafts(plan),
64
+ },
65
+ activeRequirement(plan),
66
+ );
67
+ }
68
+
40
69
  /**
41
70
  * 把检查点那句话拼出来。措辞与 `plan.js` 的 `renderContinue` 一致。
42
71
  *
@@ -140,12 +169,12 @@ export function registerCommands(ctx, { store, cfg, pause = null, enabled = fals
140
169
  };
141
170
  }
142
171
  const plan = store.readPlan(sessionId);
143
- if (plan === null) return { kind: "error", text: "本会话还没有计划。" };
172
+ if (plan === null) return { kind: "error", text: "本会话还没有需求。" };
144
173
  const outcome = complete(plan, id, result);
145
174
  if (!outcome.ok) return { kind: "error", text: `没有完成:${outcome.reason}` };
146
175
  store.writePlan(sessionId, plan);
147
- // `complete` 已经把检查点提示放进 events 了,这里只补上折叠后的树。
148
- return { kind: "success", text: [...outcome.events, "", renderTree(plan)].join("\n") };
176
+ // `complete` 已经把检查点提示放进 events 了,这里只补上最新的需求列表。
177
+ return { kind: "success", text: [...outcome.events, "", listText(plan)].join("\n") };
149
178
  }
150
179
 
151
180
  if (verb === "drop") {
@@ -154,13 +183,14 @@ export function registerCommands(ctx, { store, cfg, pause = null, enabled = fals
154
183
  return { kind: "error", text: `用法:/task-tree drop <节点 id>\n\n${USAGE}` };
155
184
  }
156
185
  const plan = store.readPlan(sessionId);
157
- if (plan === null) return { kind: "error", text: "本会话还没有计划。" };
158
- const result = dropNode(plan, id);
159
- if (!result.ok) return { kind: "error", text: `没有丢弃:${result.reason}` };
186
+ if (plan === null) return { kind: "error", text: "本会话还没有需求。" };
187
+ const isRequirement = requirementOf(plan, id) !== null;
188
+ const result = isRequirement ? dropRequirement(plan, id) : dropNode(plan, id);
189
+ if (!result.ok) return { kind: "error", text: `没有放弃:${result.reason}` };
160
190
  store.writePlan(sessionId, plan);
161
191
  return {
162
192
  kind: "success",
163
- text: [`已丢弃:${result.dropped.join("、")}`, "", renderTree(plan)].join("\n"),
193
+ text: [`已放弃:${result.dropped.join("、")}`, "", listText(plan)].join("\n"),
164
194
  };
165
195
  }
166
196
 
@@ -170,23 +200,24 @@ export function registerCommands(ctx, { store, cfg, pause = null, enabled = fals
170
200
  return { kind: "error", text: `用法:/task-tree delete <节点 id>\n\n${USAGE}` };
171
201
  }
172
202
  const plan = store.readPlan(sessionId);
173
- if (plan === null) return { kind: "error", text: "本会话还没有计划。" };
174
- // 摘掉之后就查不到"这次删的是哪个目标"了,先记一份。
203
+ if (plan === null) return { kind: "error", text: "本会话还没有需求。" };
204
+ // 摘掉之后就查不到"这次动的是哪条需求"了,先记一份。
175
205
  const touched = findNode(plan, id);
176
- const result = deleteNode(plan, id);
206
+ const isRequirement = requirementOf(plan, id) !== null;
207
+ const result = isRequirement ? deleteRequirement(plan, id) : deleteNode(plan, id);
177
208
  if (!result.ok) return { kind: "error", text: `没有删除:${result.reason}` };
178
209
  store.writePlan(sessionId, plan);
179
210
 
180
211
  const lines = [`已删除:${result.removed.join("、")}`, ""];
181
212
  if (touched !== null && touched.level !== "goal") {
182
- // 删的是任务或子任务:它所在的那个目标还在,只给它一个目标的树。
183
- lines.push(renderGoal(touched.goal, plan, true));
213
+ // 删的是任务或步骤:它所在的那条需求还在,只给它那条需求的列表。
214
+ lines.push(listText(plan));
184
215
  } else {
185
- const restGoal = goalsOf(plan).length;
216
+ const left = goalsOf(plan).length;
186
217
  lines.push(
187
- restGoal === 0
188
- ? "计划树现在空了。"
189
- : `这个会话还剩 ${restGoal} 个目标,要看全部用 /task-tree status。`,
218
+ left === 0
219
+ ? "这个会话现在没有需求了。"
220
+ : `这个会话还剩 ${left} 条需求,要看全部用 /task-tree status。`,
190
221
  );
191
222
  }
192
223
  return { kind: "success", text: lines.join("\n") };
@@ -197,14 +228,15 @@ export function registerCommands(ctx, { store, cfg, pause = null, enabled = fals
197
228
  }
198
229
 
199
230
  const plan = store.readPlan(sessionId);
200
- const lines = [renderTree(plan)];
231
+ const lines = [];
201
232
 
202
233
  if (enabled !== true) {
203
- lines.push("", DISABLED_NOTE);
234
+ lines.push(DISABLED_NOTE);
204
235
  } else if (plan === null) {
205
- lines.push("", "本会话还没有计划。用 tree_task_plan 建立目标与任务。");
236
+ lines.push("本会话还没有需求。用 tree_task_create 记一条需求草稿,跟用户确认之后才算真需求。");
206
237
  } else {
207
- // 检查点优先于"当前节点":那一刻要推进的不是某个子任务,
238
+ lines.push(listText(plan));
239
+ // 检查点优先于"当前节点":那一刻要推进的不是某个步骤,
208
240
  // 而是"这一层到底做完了没有"这个判断。
209
241
  const checkpoint = checkpointOf(plan);
210
242
  const active = activePath(plan);
package/lib/http.js CHANGED
@@ -23,8 +23,23 @@
23
23
  * @module dsh-tree-task-flow/http
24
24
  */
25
25
 
26
- import { activePath, checkpointOf, complete, deleteNode, dropNode, levelLabel } from "./plan.js";
26
+ import { activePath, complete, deleteNode, dropNode, levelLabel } from "./plan.js";
27
27
  import { SETTING_KEYS, settingsSnapshot } from "./settings.js";
28
+ import {
29
+ addDraft,
30
+ currentVersion,
31
+ deleteRequirement,
32
+ dropRequirement,
33
+ listActive,
34
+ listArchived,
35
+ listDrafts,
36
+ missingPrerequisites,
37
+ nextTaskOf,
38
+ progressOf,
39
+ requirementOf,
40
+ setArchived,
41
+ updateByUser,
42
+ } from "./requirement.js";
28
43
 
29
44
  /** 统一的 JSON 响应。 */
30
45
  function sendJson(response, status, body) {
@@ -47,30 +62,98 @@ async function readJsonBody(request) {
47
62
  }
48
63
  }
49
64
 
50
- /** 活动路径压成面板要的形状:三层各自的 id,没到那层就是 null。 */
51
- function activeInfo(plan) {
52
- const active = activePath(plan);
53
- if (active === null) return null;
65
+ /**
66
+ * 一条需求压成面板要的形状。
67
+ *
68
+ * 列表上只显示标题、简介与状态;`detail` 与 `versions` 也一起给——面板是本机的,
69
+ * 展开时不用再发一次请求,而"要不要展示"是面板自己的事。
70
+ *
71
+ * @param plan - 计划树。
72
+ * @param requirement - 需求节点。
73
+ * @returns 面板用的行对象。
74
+ */
75
+ function rowOf(plan, requirement) {
76
+ const task = nextTaskOf(requirement);
54
77
  return {
55
- goalId: active.goal.id,
56
- taskId: active.task?.id ?? null,
57
- stepId: active.step?.id ?? null,
78
+ id: requirement.id,
79
+ title: requirement.title,
80
+ intro: requirement.intro,
81
+ detail: requirement.detail,
82
+ status: requirement.status,
83
+ archived: requirement.archived === true,
84
+ pending: requirement.pending
85
+ ? {
86
+ title: requirement.pending.title,
87
+ intro: requirement.pending.intro,
88
+ detail: requirement.pending.detail,
89
+ change: requirement.pending.change,
90
+ question: requirement.pending.question,
91
+ at: requirement.pending.at,
92
+ }
93
+ : null,
94
+ missing: missingPrerequisites(plan, requirement).map((item) => ({
95
+ id: item.id,
96
+ state: item.state,
97
+ })),
98
+ prerequisites: requirement.prerequisites ?? [],
99
+ nextTask: task === null ? null : { id: task.id, title: task.title },
100
+ version: currentVersion(requirement),
101
+ changes: (requirement.versions ?? []).length,
102
+ updatedAt: requirement.updatedAt ?? null,
103
+ result: requirement.result?.text ?? null,
104
+ tasks: (requirement.tasks ?? []).map((each) => ({
105
+ id: each.id,
106
+ title: each.title,
107
+ status: each.status,
108
+ result: each.result?.text ?? null,
109
+ steps: (each.steps ?? []).map((step) => ({
110
+ id: step.id,
111
+ title: step.title,
112
+ status: step.status,
113
+ result: step.result?.text ?? null,
114
+ })),
115
+ })),
58
116
  };
59
117
  }
60
118
 
61
- /** 检查点压成面板要的形状;没有检查点就是 null。 */
62
- function checkpointInfo(plan) {
63
- const found = checkpointOf(plan);
64
- if (found === null) return null;
119
+ /**
120
+ * 面板要的整份数据:两个列表 + 草稿区 + 当前进行中的展开。
121
+ *
122
+ * 「当前进行中」由**最后执行的那个任务**推导(`activeRequirement`),面板不另存状态。
123
+ *
124
+ * @param plan - 计划树。
125
+ * @returns 面板用的数据;没有计划时三个列表都是空的。
126
+ */
127
+ function panelData(plan) {
128
+ if (plan === null) {
129
+ return {
130
+ requirements: { active: [], archived: [], drafts: [] },
131
+ current: null,
132
+ progress: [],
133
+ };
134
+ }
135
+ const active = listActive(plan).map((row) => rowOf(plan, requirementOf(plan, row.id)));
136
+ const archived = listArchived(plan).map((row) => rowOf(plan, requirementOf(plan, row.id)));
137
+ const drafts = listDrafts(plan).map((requirement) => rowOf(plan, requirement));
138
+ // 当前进行中的那一条:它的任务与步骤要在列表里就地展开。
139
+ const live = active.find((row) => row.status === "pending") ?? null;
140
+ const current = live ?? null;
65
141
  return {
66
- id: found.node.id,
67
- level: found.level,
68
- label: levelLabel(found.level),
69
- title: found.node.title,
142
+ requirements: { active, archived, drafts },
143
+ current: current === null ? null : current.id,
144
+ progress:
145
+ current === null
146
+ ? []
147
+ : progressOf(requirementOf(plan, current.id)).map(({ task, steps }) => ({
148
+ id: task.id,
149
+ title: task.title,
150
+ status: task.status,
151
+ steps: steps.map((step) => ({ id: step.id, title: step.title, status: step.status })),
152
+ })),
70
153
  };
71
154
  }
72
155
 
73
- /** 一个节点提交过的结果;还没提交就是 null。 */
156
+ /** 一条节点提交过的结果;还没提交就是 null。 */
74
157
  function resultOf(node) {
75
158
  if (!node?.result) return null;
76
159
  return {
@@ -81,23 +164,9 @@ function resultOf(node) {
81
164
  };
82
165
  }
83
166
 
84
- /** 活动路径上每一层各自提交的结果。 */
85
- function levelResults(plan) {
86
- const active = activePath(plan);
87
- return {
88
- goal: resultOf(active?.goal),
89
- task: resultOf(active?.task),
90
- step: resultOf(active?.step),
91
- };
92
- }
93
-
94
167
  /** 一次写操作之后,面板需要的全部新状态。 */
95
168
  function afterWrite(plan) {
96
- return {
97
- active: activeInfo(plan),
98
- checkpoint: checkpointInfo(plan),
99
- results: levelResults(plan),
100
- };
169
+ return panelData(plan);
101
170
  }
102
171
 
103
172
  /**
@@ -111,7 +180,7 @@ function afterWrite(plan) {
111
180
  export function registerHttp(ctx, { store, autoContinue, pause = null, cfg = null }) {
112
181
  ctx.inject(["webServer"], (host) => {
113
182
  host.effect(() => {
114
- // 一棵树的全部信息:结构 + 每层结果 + 检查点 + 当前路径 + 三层折叠游标。
183
+ // 面板要的全部数据:需求两个列表 + 草稿区 + 当前进行中的展开 + 按住状态。
115
184
  host.webServer.register(
116
185
  {
117
186
  kind: "exact",
@@ -121,18 +190,11 @@ export function registerHttp(ctx, { store, autoContinue, pause = null, cfg = nul
121
190
  const url = new URL(request.url ?? "/", "http://localhost");
122
191
  const sessionId = url.searchParams.get("sessionId") ?? "";
123
192
  const plan = store.readPlan(sessionId);
124
- if (plan === null) return sendJson(response, 404, { ok: false, error: "这个会话没有计划" });
125
193
 
126
- const cursors = store.readSurfaces(sessionId);
127
194
  sendJson(response, 200, {
128
195
  ok: true,
129
- plan,
196
+ sessionId,
130
197
  ...afterWrite(plan),
131
- cursors: {
132
- goal: cursors.goal ?? null,
133
- task: cursors.task ?? null,
134
- step: cursors.step ?? null,
135
- },
136
198
  stopped: autoContinue?.isStopped?.(sessionId) === true,
137
199
  paused: pause?.isPaused?.(sessionId) === true,
138
200
  settings: settingsSnapshot(cfg),
@@ -142,8 +204,8 @@ export function registerHttp(ctx, { store, autoContinue, pause = null, cfg = nul
142
204
  "dsh-tree-task-flow: tree",
143
205
  );
144
206
 
145
- // DSH 设置面板里那一页用的接口:**跟会话无关**,只回两个开关的当前值与总开关
146
- // 状态。设置面板没有"当前会话"这个概念,所以它不能走 `/tree`。
207
+ // 插件管理里本插件那一页用的接口:**跟会话无关**,只回两个开关的当前值与总开关
208
+ // 状态。那一页不是某个会话的界面,没有"当前会话"这个概念,所以它不能走 `/tree`。
147
209
  host.webServer.register(
148
210
  {
149
211
  kind: "exact",
@@ -202,6 +264,75 @@ export function registerHttp(ctx, { store, autoContinue, pause = null, cfg = nul
202
264
  return sendJson(response, 200, { ok: true, settings: settingsSnapshot(cfg) });
203
265
  }
204
266
 
267
+ // 面板上新建需求:**只产出草稿**,等 AI 沟通确认之后才算真需求。
268
+ if (action === "draft") {
269
+ const plan = store.readPlan(sessionId) ?? {
270
+ version: 3,
271
+ createdAt: Date.now(),
272
+ updatedAt: Date.now(),
273
+ goals: [],
274
+ };
275
+ const requirement = addDraft(plan, {
276
+ title: body.title,
277
+ intro: body.intro,
278
+ detail: body.detail,
279
+ });
280
+ store.writePlan(sessionId, plan);
281
+ return sendJson(response, 200, { ok: true, id: requirement.id, ...afterWrite(plan) });
282
+ }
283
+
284
+ // 用户改需求正文:**只落暂定版本**,不动正在生效的正文(`docs/09` §3.3)。
285
+ // 点这个按钮**不叫醒 AI**——它下次被唤起时会先看到这条待确认。
286
+ if (action === "pending") {
287
+ const plan = store.readPlan(sessionId);
288
+ if (plan === null) return sendJson(response, 404, { ok: false, error: "这个会话还没有需求" });
289
+ const outcome = updateByUser(plan, String(body.id ?? ""), {
290
+ title: body.title,
291
+ intro: body.intro,
292
+ detail: body.detail,
293
+ change: body.change,
294
+ });
295
+ if (!outcome.ok) return sendJson(response, 200, { ok: false, error: outcome.reason });
296
+ store.writePlan(sessionId, plan);
297
+ return sendJson(response, 200, {
298
+ ok: true,
299
+ cancelled: outcome.cancelled === true,
300
+ message: outcome.cancelled === true ? "改回和当前版本一样了,暂定版本已撤掉。" : "已提交这个版本的改动,等 AI 确认。",
301
+ ...afterWrite(plan),
302
+ });
303
+ }
304
+
305
+ if (action === "archive" || action === "unarchive") {
306
+ const plan = store.readPlan(sessionId);
307
+ if (plan === null) return sendJson(response, 404, { ok: false, error: "这个会话还没有需求" });
308
+ const outcome = setArchived(plan, String(body.id ?? ""), action === "archive");
309
+ if (!outcome.ok) return sendJson(response, 200, { ok: false, error: outcome.reason });
310
+ store.writePlan(sessionId, plan);
311
+ return sendJson(response, 200, { ok: true, ...afterWrite(plan) });
312
+ }
313
+
314
+ // 点某条需求的「开始」:这是**能叫醒 AI 的三种动作之一**。
315
+ if (action === "start") {
316
+ const plan = store.readPlan(sessionId);
317
+ const requirement = plan === null ? null : requirementOf(plan, String(body.id ?? ""));
318
+ if (requirement === null) {
319
+ return sendJson(response, 200, { ok: false, error: "找不到这条需求" });
320
+ }
321
+ const outcome = pause?.nudge?.(
322
+ sessionId,
323
+ `开始做需求 ${requirement.id}「${requirement.title}」。` +
324
+ "先用 tree_task_start 开始它(会折叠上一次折叠之后的过程,并给出这条需求的详情),再按计划往下推。",
325
+ "开始需求",
326
+ ) ?? { woke: false, reason: "unavailable", text: "插件没在运行,叫不醒会话。" };
327
+ return sendJson(response, 200, {
328
+ ok: true,
329
+ woke: outcome.woke === true,
330
+ message: outcome.text,
331
+ paused: pause?.isPaused?.(sessionId) === true,
332
+ stopped: autoContinue?.isStopped?.(sessionId) === true,
333
+ });
334
+ }
335
+
205
336
  if (action === "reset") {
206
337
  store.writePlan(sessionId, null);
207
338
  // 计划没了,游标也必须跟着清:游标里记的是"这一层总结到哪儿了",
@@ -220,9 +351,13 @@ export function registerHttp(ctx, { store, autoContinue, pause = null, cfg = nul
220
351
  const plan = store.readPlan(sessionId);
221
352
  if (plan === null) return sendJson(response, 404, { ok: false, error: "这个会话没有计划" });
222
353
  const id = String(body.id ?? "");
354
+ const found = requirementOf(plan, id) !== null;
223
355
  // 完成必须带结果:插件要用它替换掉这个节点的执行过程。
224
- const outcome =
225
- action === "done"
356
+ const outcome = found
357
+ ? action === "drop"
358
+ ? dropRequirement(plan, id)
359
+ : deleteRequirement(plan, id)
360
+ : action === "done"
226
361
  ? complete(plan, id, body.result)
227
362
  : action === "drop"
228
363
  ? dropNode(plan, id)
@@ -242,7 +377,7 @@ export function registerHttp(ctx, { store, autoContinue, pause = null, cfg = nul
242
377
  "dsh-tree-task-flow: action",
243
378
  );
244
379
 
245
- ctx.logger?.info?.("dsh-tree-task-flow: 设置面板接口已挂载");
380
+ ctx.logger?.info?.("dsh-tree-task-flow: 插件配置页接口已挂载");
246
381
  }, "dsh-tree-task-flow: http");
247
382
  });
248
383
  }
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * dsh-tree-task-flow —— 三级任务树:完成时提交结果,同时把执行过程折叠掉。
2
+ * dsh-tree-task-flow —— 三级任务树:完成[步骤/任务/需求]并折叠过程。
3
3
  *
4
4
  * 与 DSH 内置任务系统的差别在于**上下文的收放方式**:
5
5
  *