dsh-tree-task-flow 0.3.13 → 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 +157 -91
- package/client.js +629 -512
- package/config.example.yml +13 -6
- package/cordis.patch.yml +5 -3
- package/lib/commands.js +91 -18
- package/lib/http.js +228 -47
- package/lib/index.js +24 -16
- package/lib/interrupt.js +18 -0
- package/lib/notes.js +173 -0
- package/lib/pause.js +84 -15
- package/lib/plan.js +48 -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/settings.js +53 -0
- package/lib/store.js +91 -7
- package/lib/tools.js +602 -111
- package/package.json +2 -2
package/lib/render.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 需求的渲染:给 `tree_task_status` 用的几种输出。
|
|
3
|
+
*
|
|
4
|
+
* 与 `lib/plan.js` 里那套「目标 / 任务 / 子任务」的树渲染分开:那是给**面板与旧调用点**用的,
|
|
5
|
+
* 这一套说的是「需求」的语言——列表一行一条、详情分节、只展开当前进行中的那一条。
|
|
6
|
+
*
|
|
7
|
+
* 两个原则:
|
|
8
|
+
*
|
|
9
|
+
* 1. **常驻开销要小**。列表一行 = 编号 + 状态 + 标题 + 简介,别的一个字都不带;
|
|
10
|
+
* 版本、任务结果、笔记这些要主动查才给。
|
|
11
|
+
* 2. **不截断**。给了正文就整段输出——截掉半句等于没交付(与 tree_task_done 的 result 同一个理由)。
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-tree-task-flow/render
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { STATUS } from "./plan.js";
|
|
17
|
+
import {
|
|
18
|
+
REQUIREMENT_STATUS,
|
|
19
|
+
currentVersion,
|
|
20
|
+
isDraft,
|
|
21
|
+
isOpenRequirement,
|
|
22
|
+
missingPrerequisites,
|
|
23
|
+
progressOf,
|
|
24
|
+
} from "./requirement.js";
|
|
25
|
+
|
|
26
|
+
/** 状态的显示名。 */
|
|
27
|
+
export function statusLabel(requirement) {
|
|
28
|
+
if (isDraft(requirement)) return "草稿";
|
|
29
|
+
if (requirement.status === REQUIREMENT_STATUS.DONE) return "已完成";
|
|
30
|
+
if (requirement.status === REQUIREMENT_STATUS.DROPPED) return "已放弃";
|
|
31
|
+
return "未完成";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 任务 / 步骤的勾选标记,写法与 plan.js 保持一致。 */
|
|
35
|
+
function mark(node) {
|
|
36
|
+
if (node.status === STATUS.DONE) return "[x]";
|
|
37
|
+
if (node.status === STATUS.DROPPED) return "[-]";
|
|
38
|
+
return "[ ]";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 缩进。 */
|
|
42
|
+
const INDENT = " ";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 把多行文本作为一条列表项的续行挂上去。
|
|
46
|
+
*
|
|
47
|
+
* @param lines - 输出行数组。
|
|
48
|
+
* @param pad - 缩进。
|
|
49
|
+
* @param text - 正文。
|
|
50
|
+
*/
|
|
51
|
+
function appendBlock(lines, pad, text) {
|
|
52
|
+
for (const row of String(text ?? "").replace(/\r\n?/gu, "\n").split("\n")) {
|
|
53
|
+
lines.push(row === "" ? "" : `${pad}${row}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 列表。
|
|
59
|
+
*
|
|
60
|
+
* @param plan - 计划。
|
|
61
|
+
* @param rows - 行数据:`{ active, archived, drafts }`。
|
|
62
|
+
* @param active - 当前进行中的需求(展开那一条)。
|
|
63
|
+
* @returns 多行 Markdown。
|
|
64
|
+
*/
|
|
65
|
+
export function renderList(plan, rows, active) {
|
|
66
|
+
const lines = [];
|
|
67
|
+
const counts = `活跃 ${rows.active.length} 条 · 已归档 ${rows.archived.length} 条 · 草稿 ${rows.drafts.length} 条`;
|
|
68
|
+
lines.push(`需求列表(${counts})`);
|
|
69
|
+
lines.push("");
|
|
70
|
+
|
|
71
|
+
if (rows.active.length === 0 && rows.archived.length === 0 && rows.drafts.length === 0) {
|
|
72
|
+
lines.push("这个会话还没有任何需求。用户说一句要做什么,先用 tree_task_create 记成草稿,沟通清楚后确认。");
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const requirement of rows.active) {
|
|
77
|
+
const here = active !== null && active.id === requirement.id;
|
|
78
|
+
lines.push(`- ${listLineBody(requirement)}${here ? " ← 当前" : ""}`);
|
|
79
|
+
if (requirement.intro) appendBlock(lines, `${INDENT}`, requirement.intro);
|
|
80
|
+
const missing = missingPrerequisites(plan, requirement);
|
|
81
|
+
if (missing.length > 0) {
|
|
82
|
+
lines.push(
|
|
83
|
+
`${INDENT}缺前置:${missing.map((item) => `${item.id}(${missingLabel(item.state)})`).join("、")}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (requirement.pending !== null) {
|
|
87
|
+
lines.push(`${INDENT}待确认变动:${requirement.pending.change || "(未说明改了什么)"}`);
|
|
88
|
+
}
|
|
89
|
+
if (here) appendProgress(lines, requirement);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (rows.archived.length > 0) {
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push("已归档(不占常驻,可按编号查):");
|
|
95
|
+
for (const requirement of rows.archived) {
|
|
96
|
+
lines.push(`- ${listLineBody(requirement)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (rows.drafts.length > 0) {
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push("草稿(还没跟用户确认,不算真需求):");
|
|
103
|
+
for (const requirement of rows.drafts) {
|
|
104
|
+
lines.push(
|
|
105
|
+
`- ${requirement.id} ${requirement.title || "(还没有标题)"} — 确认它用 tree_task_requirement action=confirm`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 「简」版列表:一条需求一行,不带简介、不带进度。
|
|
115
|
+
*
|
|
116
|
+
* 给**需求完成**那一刻用——那一下上下文被整条折掉,返回里要能一眼看出还剩哪几条。
|
|
117
|
+
*
|
|
118
|
+
* @param rows - 行数据:`{ active, archived, drafts }`。
|
|
119
|
+
* @param active - 当前进行中的需求(打「当前」标记)。
|
|
120
|
+
* @returns 多行 Markdown。
|
|
121
|
+
*/
|
|
122
|
+
export function renderBriefList(rows, active) {
|
|
123
|
+
const counts = `活跃 ${rows.active.length} 条 · 已归档 ${rows.archived.length} 条 · 草稿 ${rows.drafts.length} 条`;
|
|
124
|
+
const lines = [`需求列表(${counts})`];
|
|
125
|
+
if (rows.active.length === 0) {
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push("没有活跃需求了。");
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
for (const requirement of rows.active) {
|
|
131
|
+
const here = active !== null && active.id === requirement.id;
|
|
132
|
+
lines.push(`- ${listLineBody(requirement)}${here ? " ← 当前" : ""}`);
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 一行需求去掉勾选框之后的内容(勾选框由调用方拼)。
|
|
139
|
+
*
|
|
140
|
+
* @param requirement - 需求。
|
|
141
|
+
* @returns 文本。
|
|
142
|
+
*/
|
|
143
|
+
function listLineBody(requirement) {
|
|
144
|
+
const bits = [`**${requirement.id}** · ${statusLabel(requirement)}`, requirement.title || "(还没有标题)"];
|
|
145
|
+
if (requirement.archived === true) bits.push("【已归档】");
|
|
146
|
+
return bits.join(" ");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 前置状态的短说法。 */
|
|
150
|
+
function missingLabel(state) {
|
|
151
|
+
if (state === "open") return "未完成";
|
|
152
|
+
if (state === "dropped") return "已放弃";
|
|
153
|
+
if (state === "missing") return "已删除";
|
|
154
|
+
return state;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 展开一条需求的进度:一级任务、二级步骤。
|
|
159
|
+
*
|
|
160
|
+
* @param lines - 输出行数组。
|
|
161
|
+
* @param requirement - 需求。
|
|
162
|
+
*/
|
|
163
|
+
function appendProgress(lines, requirement) {
|
|
164
|
+
const rows = progressOf(requirement);
|
|
165
|
+
if (rows.length === 0) {
|
|
166
|
+
lines.push(`${INDENT}还没有任务,用 tree_task_plan 给它拆任务。`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (const { task, steps } of rows) {
|
|
170
|
+
lines.push(`${INDENT}- ${mark(task)} ${task.title}`);
|
|
171
|
+
for (const step of steps) {
|
|
172
|
+
lines.push(`${INDENT}${INDENT}- ${mark(step)} ${step.title}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 前置那一行:满足的只写编号,没满足的在编号后面标状态。
|
|
179
|
+
*
|
|
180
|
+
* 不能"列出全部编号"再另起一句"缺谁"——那会把同一个编号说两遍
|
|
181
|
+
* (`g-x(缺 g-x(未完成))`),实测在真会话里就这么显示过。
|
|
182
|
+
*
|
|
183
|
+
* @param plan - 计划。
|
|
184
|
+
* @param requirement - 需求。
|
|
185
|
+
* @returns 一行文本。
|
|
186
|
+
*/
|
|
187
|
+
function prerequisiteLine(plan, requirement) {
|
|
188
|
+
const ids = requirement.prerequisites ?? [];
|
|
189
|
+
if (ids.length === 0) return "无";
|
|
190
|
+
const missing = new Map(missingPrerequisites(plan, requirement).map((item) => [item.id, item.state]));
|
|
191
|
+
return ids.map((id) => (missing.has(id) ? `${id}(${missingLabel(missing.get(id))})` : id)).join("、");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 需求详情。
|
|
196
|
+
*
|
|
197
|
+
* `sections` 决定给哪些节:`detail`(简介 + 详情)、`versions`、`tasks`(任务与结果)、
|
|
198
|
+
* `notes` 由调用方另外拼。默认给 `detail` 与 `tasks`。
|
|
199
|
+
*
|
|
200
|
+
* @param plan - 计划。
|
|
201
|
+
* @param requirement - 需求。
|
|
202
|
+
* @param sections - 要的节名数组。
|
|
203
|
+
* @returns 多行 Markdown。
|
|
204
|
+
*/
|
|
205
|
+
export function renderDetail(plan, requirement, sections) {
|
|
206
|
+
const wanted = new Set(Array.isArray(sections) && sections.length > 0 ? sections : ["detail", "tasks"]);
|
|
207
|
+
const lines = [];
|
|
208
|
+
|
|
209
|
+
const head = [`## ${requirement.id} · ${requirement.title || "(还没有标题)"}`];
|
|
210
|
+
const meta = [statusLabel(requirement)];
|
|
211
|
+
if (requirement.archived === true) meta.push("已归档");
|
|
212
|
+
// 草稿还没有版本历史,写 v0 只会让人以为出错了。
|
|
213
|
+
if (!isDraft(requirement)) meta.push(`v${currentVersion(requirement)}`);
|
|
214
|
+
head.push(meta.join(" · "));
|
|
215
|
+
lines.push(head.join(" "));
|
|
216
|
+
lines.push("");
|
|
217
|
+
|
|
218
|
+
if (wanted.has("detail")) {
|
|
219
|
+
if (requirement.intro) lines.push(`**简介**:${requirement.intro}`);
|
|
220
|
+
lines.push(`**前置**:${prerequisiteLine(plan, requirement)}`);
|
|
221
|
+
if (requirement.detail) {
|
|
222
|
+
lines.push("");
|
|
223
|
+
lines.push("**详情**:");
|
|
224
|
+
lines.push(requirement.detail);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (requirement.pending !== null) {
|
|
229
|
+
lines.push("");
|
|
230
|
+
lines.push("**等确认的变动**(用户提交的暂定版本):");
|
|
231
|
+
if (requirement.pending.title !== requirement.title) lines.push(`- 标题:${requirement.pending.title}`);
|
|
232
|
+
if (requirement.pending.intro !== requirement.intro) lines.push(`- 简介:${requirement.pending.intro}`);
|
|
233
|
+
if (requirement.pending.detail !== requirement.detail) lines.push(`- 详情有改动`);
|
|
234
|
+
if (requirement.pending.question) lines.push(`- 上次问到:${requirement.pending.question}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (wanted.has("versions")) {
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push("**版本历史**:");
|
|
240
|
+
for (const version of requirement.versions ?? []) {
|
|
241
|
+
const who = version.by === "user" ? "用户" : version.by === "ai" ? "AI" : "迁移";
|
|
242
|
+
lines.push(`- v${version.n}(${who}):${version.change}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (wanted.has("tasks")) {
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push("**任务**:");
|
|
249
|
+
const tasks = requirement.tasks ?? [];
|
|
250
|
+
if (tasks.length === 0) {
|
|
251
|
+
lines.push("- 还没有任务,用 tree_task_plan 拆");
|
|
252
|
+
}
|
|
253
|
+
for (const task of tasks) {
|
|
254
|
+
lines.push(`- ${mark(task)} ${task.title} \`${task.id}\``);
|
|
255
|
+
for (const step of task.steps ?? []) {
|
|
256
|
+
lines.push(`${INDENT}- ${mark(step)} ${step.title} \`${step.id}\``);
|
|
257
|
+
}
|
|
258
|
+
if (task.result?.text) {
|
|
259
|
+
lines.push(`${INDENT}**结果**:`);
|
|
260
|
+
appendBlock(lines, `${INDENT}${INDENT}`, task.result.text);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (requirement.result?.text) {
|
|
266
|
+
lines.push("");
|
|
267
|
+
lines.push("**总结**:");
|
|
268
|
+
lines.push(requirement.result.text);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 笔记列表。
|
|
276
|
+
*
|
|
277
|
+
* @param items - 笔记数组(已按需要筛过)。
|
|
278
|
+
* @param query - 用过的关键词,拼在抬头里。
|
|
279
|
+
* @returns 多行 Markdown。
|
|
280
|
+
*/
|
|
281
|
+
export function renderNotes(items, query) {
|
|
282
|
+
if (items.length === 0) {
|
|
283
|
+
return String(query ?? "").trim() === ""
|
|
284
|
+
? "这个会话还没有笔记。写一条用 tree_task_note:给 key 和 text。"
|
|
285
|
+
: `没有找到包含「${query}」的笔记。`;
|
|
286
|
+
}
|
|
287
|
+
const head = String(query ?? "").trim() === "" ? `笔记 ${items.length} 条` : `包含「${query}」的笔记 ${items.length} 条`;
|
|
288
|
+
const lines = [head];
|
|
289
|
+
for (const item of items) {
|
|
290
|
+
lines.push(`- **${item.key}** (r${item.rev}):${item.text}`);
|
|
291
|
+
}
|
|
292
|
+
return lines.join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 一条完成通告用到的提示拼装:下一步 + 那几行提示。
|
|
297
|
+
*
|
|
298
|
+
* @param next - 下一步那句话。
|
|
299
|
+
* @param hints - 提示行数组。
|
|
300
|
+
* @returns 文本。
|
|
301
|
+
*/
|
|
302
|
+
export function composeReply(next, hints) {
|
|
303
|
+
const lines = [next];
|
|
304
|
+
for (const hint of hints ?? []) lines.push(hint);
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 需求是否还开着(列表里要展开的那一条只可能是它)。 */
|
|
309
|
+
export function isLive(requirement) {
|
|
310
|
+
return isOpenRequirement(requirement) || isDraft(requirement);
|
|
311
|
+
}
|