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.
- package/README.md +119 -98
- package/client.js +590 -675
- package/config.example.yml +8 -7
- package/cordis.patch.yml +4 -4
- package/lib/commands.js +57 -25
- package/lib/http.js +181 -46
- package/lib/index.js +1 -1
- package/lib/notes.js +173 -0
- package/lib/pause.js +44 -0
- package/lib/prompt.js +20 -4
- package/lib/region.js +375 -58
- package/lib/render.js +311 -0
- package/lib/requirement.js +914 -0
- package/lib/store.js +50 -7
- package/lib/tools.js +588 -144
- package/package.json +2 -2
package/config.example.yml
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# 这个文件只是示例,插件不需要它——**安装即启用**,装完就能用。
|
|
2
2
|
#
|
|
3
|
-
# **两个开关平时不用改这里。**
|
|
4
|
-
#
|
|
3
|
+
# **两个开关平时不用改这里。** 打开侧边栏的「插件」页,在「已安装」里点
|
|
4
|
+
# `dsh-tree-task-flow` 那张卡,页面上就是这两个开关——那是活的,改完立刻生效,
|
|
5
|
+
# 而且**盖过这里写的值**。优先级是:
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
+
# 插件页里改过的 > profile 里的 config > DEFAULT_CONFIG
|
|
7
8
|
#
|
|
8
|
-
# 这里留给"
|
|
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
|
-
#
|
|
24
|
-
#
|
|
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, "",
|
|
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
|
|
159
|
-
|
|
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: [
|
|
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
|
|
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(
|
|
213
|
+
// 删的是任务或步骤:它所在的那条需求还在,只给它那条需求的列表。
|
|
214
|
+
lines.push(listText(plan));
|
|
184
215
|
} else {
|
|
185
|
-
const
|
|
216
|
+
const left = goalsOf(plan).length;
|
|
186
217
|
lines.push(
|
|
187
|
-
|
|
188
|
-
? "
|
|
189
|
-
: `这个会话还剩 ${
|
|
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 = [
|
|
231
|
+
const lines = [];
|
|
201
232
|
|
|
202
233
|
if (enabled !== true) {
|
|
203
|
-
lines.push(
|
|
234
|
+
lines.push(DISABLED_NOTE);
|
|
204
235
|
} else if (plan === null) {
|
|
205
|
-
lines.push("
|
|
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,
|
|
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
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
//
|
|
146
|
-
//
|
|
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 === "
|
|
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