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/lib/notes.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * 笔记:跟工作有关、但不属于任何一条需求的事实与偏好。
3
+ *
4
+ * **跟着会话走**,不设全局层、不跨会话带——用户开新会话就不管旧会话了,这是设计不是缺陷。
5
+ * 偏好是"对产出的要求",不是"关于用户的知识",所以写进这里就够,不必另立体系。
6
+ *
7
+ * 纯逻辑模块:不碰 DSH、不碰磁盘。
8
+ *
9
+ * ## 为什么笔记要有 `key` 和 `rev`
10
+ *
11
+ * 折叠会把一段过程收成一条通告,通告里要带上"这段里动过的笔记"。于是同一条笔记可能:
12
+ * 先在较早的通告里出现(rev 1),后来又被改过(rev 2),两份通告都还在上下文里。
13
+ * 给每条笔记一个**短名字**和一个**修订号**,再在静态提示词里写死一句"同一名字以修订号大的
14
+ * 为准",模型就不会拿旧值当新值。
15
+ *
16
+ * `seq` 记的是写这条笔记那一刻会话表层的末尾节点,折叠时靠它判断"这条笔记是不是落在
17
+ * 这个区间里"。
18
+ *
19
+ * @module dsh-tree-task-flow/notes
20
+ */
21
+
22
+ /** 一条笔记的形状。 */
23
+ export function makeNote(input, now = Date.now()) {
24
+ return {
25
+ key: String(input?.key ?? "").trim(),
26
+ text: String(input?.text ?? "").trim(),
27
+ rev: 1,
28
+ at: now,
29
+ seq: typeof input?.seq === "number" ? input.seq : null,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * 读出来的笔记表补齐成当前形状。坏文件、缺字段一律当空,不让它拖垮会话。
35
+ *
36
+ * @param state - 磁盘上读到的对象,可能是 null。
37
+ * @returns `{ version, items, updatedAt }`。
38
+ */
39
+ export function normalizeNotes(state) {
40
+ if (state === null || typeof state !== "object") {
41
+ return { version: 1, items: [], updatedAt: null };
42
+ }
43
+ const items = Array.isArray(state.items)
44
+ ? state.items
45
+ .filter((item) => item !== null && typeof item === "object")
46
+ .map((item) => ({
47
+ key: String(item.key ?? "").trim(),
48
+ text: String(item.text ?? ""),
49
+ rev: Number.isFinite(item.rev) ? item.rev : 1,
50
+ at: Number.isFinite(item.at) ? item.at : null,
51
+ seq: Number.isFinite(item.seq) ? item.seq : null,
52
+ }))
53
+ .filter((item) => item.key !== "")
54
+ : [];
55
+ return {
56
+ version: 1,
57
+ items,
58
+ updatedAt: Number.isFinite(state.updatedAt) ? state.updatedAt : null,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * 写一条笔记:**同名就覆盖、修订号 +1**,不同名就新增。
64
+ *
65
+ * 当场返回结果——`tree_task_note` 的返回直接用它,不用等折叠。
66
+ *
67
+ * @param state - 笔记表(会被就地修改)。
68
+ * @param input - `{ key, text, seq? }`。
69
+ * @param now - 时间戳。
70
+ * @returns `{ ok, reason?, note?, created? }`。
71
+ */
72
+ export function upsertNote(state, input, now = Date.now()) {
73
+ const key = String(input?.key ?? "").trim();
74
+ const text = String(input?.text ?? "").trim();
75
+ if (key === "") return { ok: false, reason: "笔记要有一个短名字(key),以后按它找" };
76
+ if (text === "") return { ok: false, reason: "笔记正文不能为空" };
77
+
78
+ const existing = state.items.find((item) => item.key === key);
79
+ if (existing === undefined) {
80
+ const note = makeNote({ key, text, seq: input?.seq }, now);
81
+ state.items.push(note);
82
+ state.updatedAt = now;
83
+ return { ok: true, note, created: true };
84
+ }
85
+
86
+ existing.text = text;
87
+ existing.rev += 1;
88
+ existing.at = now;
89
+ if (typeof input?.seq === "number") existing.seq = input.seq;
90
+ state.updatedAt = now;
91
+ return { ok: true, note: existing, created: false };
92
+ }
93
+
94
+ /**
95
+ * 查笔记:不给条件就是全部,给了就在名字与正文里找。
96
+ *
97
+ * @param state - 笔记表。
98
+ * @param query - 关键词(不分大小写)。
99
+ * @returns 笔记数组,按最后修改时间倒序。
100
+ */
101
+ export function findNotes(state, query) {
102
+ const word = String(query ?? "").trim().toLowerCase();
103
+ const items = word === ""
104
+ ? [...state.items]
105
+ : state.items.filter(
106
+ (item) =>
107
+ item.key.toLowerCase().includes(word) || item.text.toLowerCase().includes(word),
108
+ );
109
+ return items.sort((a, b) => Number(b.at ?? 0) - Number(a.at ?? 0));
110
+ }
111
+
112
+ /**
113
+ * 某个表层区间里的笔记:`seq` 落在 `(from, to]` 里的那几条。
114
+ *
115
+ * 折叠时用它决定"这段通告要带哪些笔记"。没有 `seq` 的老笔记(迁移前写的)一律算进来——
116
+ * 宁可多带一条,也不能让笔记跟着被收走的过程一起消失。
117
+ *
118
+ * @param state - 笔记表。
119
+ * @param from - 区间起点 seq(不含)。
120
+ * @param to - 区间终点 seq(含)。
121
+ * @returns 笔记数组,按写入顺序。
122
+ */
123
+ export function notesInRange(state, from, to) {
124
+ return state.items
125
+ .filter((item) => item.seq === null || (item.seq > from && item.seq <= to))
126
+ .sort((a, b) => Number(a.seq ?? 0) - Number(b.seq ?? 0));
127
+ }
128
+
129
+ /**
130
+ * 把笔记渲染成通告里那一段。
131
+ *
132
+ * 形状是**固定且可再解析的**:一行 `· 名字 (r修订号):正文`。二次折叠时靠它把上一层通告里的
133
+ * 笔记捡回来,所以别随手改格式(`parseNoteLines` 认的就是它)。
134
+ *
135
+ * @param items - 笔记数组。
136
+ * @returns 多行文本;没有笔记时是空串。
137
+ */
138
+ export function renderNoteLines(items) {
139
+ if (!Array.isArray(items) || items.length === 0) return "";
140
+ return items.map((item) => `· ${item.key} (r${item.rev}):${oneLine(item.text)}`).join("\n");
141
+ }
142
+
143
+ /**
144
+ * 从一段通告正文里把笔记捡回来。
145
+ *
146
+ * @param text - 通告正文。
147
+ * @returns 笔记数组 `{ key, rev, text }`。
148
+ */
149
+ export function parseNoteLines(text) {
150
+ const rows = [];
151
+ const pattern = /^· (.+?) \(r(\d+)\):(.*)$/u;
152
+ for (const line of String(text ?? "").split("\n")) {
153
+ const match = pattern.exec(line.trim());
154
+ if (match === null) continue;
155
+ rows.push({ key: match[1], rev: Number(match[2]), text: match[3] });
156
+ }
157
+ return rows;
158
+ }
159
+
160
+ /**
161
+ * 正文压成一行:通告里每条笔记只占一行,换行会把它拆成两条,再解析就错位了。
162
+ *
163
+ * @param text - 原文。
164
+ * @returns 单行文本。
165
+ */
166
+ function oneLine(text) {
167
+ return String(text ?? "")
168
+ .replace(/\r\n?/gu, "\n")
169
+ .split("\n")
170
+ .map((line) => line.trim())
171
+ .filter((line) => line !== "")
172
+ .join(" / ");
173
+ }
package/lib/pause.js CHANGED
@@ -276,5 +276,49 @@ export function registerPause(ctx, { store = null, autoContinue = null } = {}) {
276
276
  isPaused(sessionId) {
277
277
  return heldNow(sessionId);
278
278
  },
279
+
280
+ /**
281
+ * 往会话里排一句话,把它推起来。**面板上的「开始」按钮走这里。**
282
+ *
283
+ * 与 `resume()` 的区别:`resume` 是"把按住的东西放开",这里的语义是"从这条需求
284
+ * 开工"。会话正忙时不排——它自己会在下一个岔口继续;没被加载时也不排,回一句
285
+ * "先在界面上打开它"。
286
+ *
287
+ * @param sessionId - 目标会话。
288
+ * @param text - 要排进去的那句话。
289
+ * @param label - 界面上那行摘要的裸标签。
290
+ * @returns `{ woke, reason, text }`。
291
+ */
292
+ nudge(sessionId, text, label = "开始需求") {
293
+ if (sessionId === undefined) {
294
+ return { woke: false, reason: "no-session", text: "拿不到当前会话。" };
295
+ }
296
+ const agent = liveAgent(sessionId);
297
+ if (agent === null) {
298
+ return {
299
+ woke: false,
300
+ reason: "unloaded",
301
+ text: "这个会话还没被加载:先在界面上打开它,或在其中发一条消息。",
302
+ };
303
+ }
304
+ if (agent.status !== "idle") {
305
+ return {
306
+ woke: false,
307
+ reason: "busy",
308
+ text: "这个会话正在跑,不用再推它;它做完手上这一步就会接着往下走。",
309
+ };
310
+ }
311
+ try {
312
+ agent.followup({
313
+ id: `tt-nudge-${sessionId}-${Date.now().toString(36)}`,
314
+ role: "user",
315
+ content: [{ type: "text", text: String(text ?? "") }],
316
+ source: pluginSource(label),
317
+ });
318
+ } catch (error) {
319
+ return { woke: false, reason: "error", text: `唤醒失败:${String(error?.message ?? error)}` };
320
+ }
321
+ return { woke: true, reason: "woke", text: "已让它开始。" };
322
+ },
279
323
  };
280
324
  }
package/lib/prompt.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 系统提示词段落:**只有一段**,讲的是"什么时候用这组工具、完成时守什么规矩"。
2
+ * 系统提示词段落:**只有一段**,讲的是"这组工具是什么、一次工作怎么开头、完成时守什么时机"。
3
3
  *
4
- * 工具清单**不在这里**——每个工具的用法由它自己的 `description` 承担。同一句话
4
+ * 工具清单与填法**不在这里**——每个工具怎么用由它自己的 `description` 承担。同一句话
5
5
  * 写在两处,只会让它在每一次请求里占两份位置,而模型真要调用时读的始终是
6
6
  * `description`。计划当前的状态同样不在这里,由 `tree_task_status` 现取。
7
7
  *
@@ -22,8 +22,24 @@ export const SECTION_NAME = "dsh-tree-task-flow";
22
22
  */
23
23
  export const DEFAULT_ORDER = 1800;
24
24
 
25
- /** 段落正文。 */
26
- export const SECTION_TEXT = "`tree_task_*` 工具适用长线任务,当目标和资料分析完成后,如果用户要求或判断任务较为复杂,可以将任务执行计划分为目标、任务、任务执行步骤三级写入任务树,借助工具来推进项目;将目标按照可独立交付的节点分为多个任务,任务节点需要准确表述清楚需求;将任务按照“需一次性处理并交付产物”分为多个步骤;完成步骤或任务时提交后续步骤需要的 result,提交内容可以是:产物/结论/问题/有哪些需要注意的坑;执行过程上下文将会在完成时被隐藏。";
25
+ /**
26
+ * 段落正文。
27
+ *
28
+ * 只讲这次工作按什么顺序走、什么时候该动哪个工具,不讲工具怎么填——填法归各自的
29
+ * `description`。三个入口工具(状态、建草稿、改需求)在这里点名,其余仍交给描述:
30
+ * 光说"用工具"换不到指代明确,十来个字不值得省。
31
+ *
32
+ * 流水线:需求草稿 → update 多次修改(仍是草稿)→ confirm 生成版本 → 读资料 → start。
33
+ * 任务与步骤的拆分口径归 `plan`,result 写法归 `done`,笔记覆盖归 `note`。
34
+ */
35
+ export const SECTION_TEXT =
36
+ "`tree_task_*` 工具记录这个会话的工作结构。需求记录用户提出的要求;任务与步骤记录实现路径;" +
37
+ "笔记记录不属于任何单条需求的工作事实与偏好。\n\n" +
38
+ "开工前先调用 tree_task_status 取回需求列表。用户提出要求后,先用 tree_task_create 记录需求草稿;" +
39
+ "草稿阶段用 tree_task_requirement 的 update 动作逐次修改正文,每次只给出要改的字段,修改后仍是草稿;" +
40
+ "标题与简介确定后,用同一工具的 confirm 动作确认并生成版本。确认之后,调用 tree_task_start 开始工作," +
41
+ "调用前先读取所需的文件与文档。\n\n" +
42
+ "每完成一个步骤、任务或需求,调用一次 tree_task_done。";
27
43
 
28
44
  /**
29
45
  * 注册段落。调用方必须包在 `ctx.effect()` 里,否则热更新后会重复注册而抛错。