nx-kv 0.1.1 → 0.2.0

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,280 +1,288 @@
1
- // 清单模块:四把 key(open / done / freeze / topics)上的任务管理。
2
- //
3
- // 声明了 `resource: 'todo'`,因此必须齐备 CRUD 五操作且两端可调用 ——
4
- // tests/unit/registry.test.mjs 会据此断言。非 CRUD 的动作(完成/冻结/归档/topic/prompt)
5
- // 用动作动词命名,不受五动词约束,但同样两端可达。
6
- import * as service from './service.js';
7
-
8
- // 所有动作都接受 --group 覆盖当前工作空间;默认取本机配置里的当前值
9
- const GROUP = { type: 'number', hint: '工作空间 id(默认取当前)' };
10
-
11
- // 完成时间显示:只取到分钟,表格里塞不下完整时间戳
12
- const shortTime = (s) => (s ? String(s).slice(0, 16).replace('T', ' ') : '');
13
-
14
- function renderTaskList(d) {
15
- const groups = [
16
- ['open', '待办'],
17
- ['done', '已完成'],
18
- ['freeze', '冻结'],
19
- ].filter(([k]) => d[k]);
20
-
21
- const lines = [];
22
- if (d.topic) lines.push(`topic=${d.topic}`);
23
- for (const [key, label] of groups) {
24
- const items = d[key];
25
- if (!items) continue;
26
- if (!items.length) {
27
- lines.push(`${label}: (空)`);
28
- continue;
29
- }
30
- lines.push(`${label}(${items.length}):`);
31
- const w = Math.max(...items.map((t) => String(t.id).length));
32
- for (const t of items) {
33
- const mark = key === 'done' ? `✓ ${shortTime(t.doneAt)}` : key === 'freeze' ? `❄ ${shortTime(t.frozenAt)}` : '';
34
- lines.push(` ${String(t.id).padStart(w)} [${t.topic}] ${t.text}${mark ? ' ' + mark : ''}`);
35
- if (t.note) lines.push(` ${' '.repeat(w)} note: ${t.note.slice(0, 100)}`);
36
- }
37
- }
38
- return lines.join('\n');
39
- }
40
-
41
- const renderTask = (t) =>
42
- [
43
- `#${t.id} [${t.topic}] ${t.text}`,
44
- ` 状态: ${t.doneAt ? '已完成' : t.frozenAt ? '冻结' : '待办'}${t.bucket ? `(在 ${t.bucket})` : ''}`,
45
- ` 创建: ${t.createdAt}`,
46
- t.doneAt ? ` 完成: ${t.doneAt}` : '',
47
- t.frozenAt ? ` 冻结: ${t.frozenAt}` : '',
48
- t.note ? ` 备注: ${t.note}` : '',
49
- ]
50
- .filter(Boolean)
51
- .join('\n');
52
-
53
- export default {
54
- id: 'todo',
55
- title: '清单(= Web「清单」页)',
56
- order: 30,
57
- view: () => import('./view.jsx'),
58
-
59
- // CRUD 资源声明:任务的增删查改必须齐备,且 CLI 与面板都能调用
60
- resource: 'todo',
61
-
62
- actions: [
63
- // ─── CRUD 五操作 ───────────────────────────────────────────
64
- {
65
- id: 'todo.list',
66
- cli: ['todo', 'list'],
67
- http: ['GET', '/api/todo'],
68
- summary: '列出任务(--topic / --status 过滤;默认三个桶全列)',
69
- flags: {
70
- topic: { type: 'string', hint: '只看某个主题' },
71
- status: { type: 'string', enum: ['open', 'done', 'freeze', 'all'], default: 'all' },
72
- group: GROUP,
73
- },
74
- run: (ctx) => service.listTodos({ topic: ctx.topic, status: ctx.status, groupId: ctx.group }),
75
- render: renderTaskList,
76
- },
77
- {
78
- id: 'todo.get',
79
- cli: ['todo', 'get'],
80
- http: ['GET', '/api/todo/:id'],
81
- summary: '查看单个任务(自动在待办/已完成/冻结里找)',
82
- args: ['id'],
83
- flags: {
84
- topic: { type: 'string', hint: '消歧:同 id 有多条时用主题定位' },
85
- pick: { type: 'number', hint: '消歧:同 id 多条时按编号选(配合报错里的列表)' },
86
- group: GROUP,
87
- },
88
- run: (ctx) => service.getTodo(Number(ctx.id), { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
89
- render: renderTask,
90
- },
91
- {
92
- id: 'todo.add',
93
- cli: ['todo', 'add'],
94
- http: ['POST', '/api/todo'],
95
- summary: '新增任务到待办(topic 不在快捷列表时自动补上)',
96
- args: ['text'],
97
- flags: { topic: { type: 'string', required: true, hint: '主题,路由维度' }, group: GROUP },
98
- run: (ctx) => service.addTodo({ topic: ctx.topic, text: ctx.text, groupId: ctx.group }),
99
- render: (d) =>
100
- `已加入待办: #${d.id} [${d.task.topic}] ${d.task.text}` +
101
- (d.topicAdded ? `\n(已把 ${d.task.topic} 加入快捷 topic)` : ''),
102
- },
103
- {
104
- id: 'todo.update',
105
- cli: ['todo', 'update'],
106
- http: ['PATCH', '/api/todo/:id'],
107
- summary: '编辑任务(只改传入的字段)',
108
- args: ['id'],
109
- flags: {
110
- topic: { type: 'string', hint: '改成这个主题' },
111
- text: { type: 'string' },
112
- note: { type: 'string' },
113
- matchTopic: { type: 'string', hint: '消歧:同 id 有多条时用主题定位' },
114
- pick: { type: 'number', hint: '消歧:同 id 多条时按编号选(配合报错里的列表)' },
115
- group: GROUP,
116
- },
117
- run: (ctx) =>
118
- service.updateTodo(Number(ctx.id), {
119
- topic: ctx.topic,
120
- text: ctx.text,
121
- note: ctx.note,
122
- matchTopic: ctx.matchTopic,
123
- pick: ctx.pick,
124
- groupId: ctx.group,
125
- }),
126
- render: (d) => `已更新 #${d.task.id}(在 ${d.bucket})`,
127
- },
128
- {
129
- id: 'todo.remove',
130
- cli: ['todo', 'remove'],
131
- http: ['DELETE', '/api/todo/:id'],
132
- summary: '删除任务(只删命中的那一条)',
133
- args: ['id'],
134
- flags: {
135
- topic: { type: 'string', hint: '消歧:同 id 有多条时用主题定位' },
136
- pick: { type: 'number', hint: '消歧:同 id 多条时按编号选(配合报错里的列表)' },
137
- group: GROUP,
138
- },
139
- run: (ctx) => service.removeTodo(Number(ctx.id), { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
140
- render: (d) => `已删除 #${d.removed.id} [${d.removed.topic}] ${d.removed.text}`,
141
- },
142
-
143
- // ─── 状态流转 ──────────────────────────────────────────────
144
- {
145
- id: 'todo.done',
146
- cli: ['todo', 'done'],
147
- http: ['POST', '/api/todo/:id/done'],
148
- summary: '标记完成(--result 写进 note 作为完成结果)',
149
- args: ['id'],
150
- flags: {
151
- result: { type: 'string', hint: '完成结果摘要' },
152
- topic: { type: 'string', hint: '消歧:同 id 有多条时用主题定位' },
153
- pick: { type: 'number', hint: '消歧:同 id 多条时按编号选(配合报错里的列表)' },
154
- group: GROUP,
155
- },
156
- run: (ctx) =>
157
- service.doneTodo(Number(ctx.id), { result: ctx.result, topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
158
- render: (d) => `已完成 #${d.task.id} [${d.task.topic}] ${d.task.text}\n ${d.task.doneAt}`,
159
- },
160
- {
161
- id: 'todo.freeze',
162
- cli: ['todo', 'freeze'],
163
- http: ['POST', '/api/todo/:id/freeze'],
164
- summary: '冻结任务(id 保留,解冻时校验冲突)',
165
- args: ['id'],
166
- flags: {
167
- topic: { type: 'string', hint: '消歧:同 id 有多条时用主题定位' },
168
- pick: { type: 'number', hint: '消歧:同 id 多条时按编号选(配合报错里的列表)' },
169
- group: GROUP,
170
- },
171
- run: (ctx) => service.freezeTodo(Number(ctx.id), { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
172
- render: (d) => `已冻结 #${d.task.id} [${d.task.topic}] ${d.task.text}`,
173
- },
174
- {
175
- id: 'todo.unfreeze',
176
- cli: ['todo', 'unfreeze'],
177
- http: ['POST', '/api/todo/:id/unfreeze'],
178
- summary: '解冻回待办(id 撞车时自动换新 id)',
179
- args: ['id'],
180
- flags: {
181
- topic: { type: 'string', hint: '消歧:同 id 有多条时用主题定位' },
182
- pick: { type: 'number', hint: '消歧:同 id 多条时按编号选(配合报错里的列表)' },
183
- group: GROUP,
184
- },
185
- run: (ctx) => service.unfreezeTodo(Number(ctx.id), { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
186
- render: (d) =>
187
- `已解冻 #${d.task.id} [${d.task.topic}] ${d.task.text}` +
188
- (d.reIded ? '\n(原 id 与现有待办冲突,已分配新 id)' : ''),
189
- },
190
- {
191
- id: 'todo.archive',
192
- cli: ['todo', 'archive'],
193
- http: ['POST', '/api/todo/archive'],
194
- summary: '把已完成里较旧的条目归档到冷 key(默认 30 天前)',
195
- flags: {
196
- before: { type: 'string', hint: '截止日期,如 2026-08-01' },
197
- group: GROUP,
198
- },
199
- run: (ctx) => service.archiveDone({ before: ctx.before, groupId: ctx.group }),
200
- render: (d) =>
201
- d.moved
202
- ? `已归档 ${d.moved} 条 -> ${d.coldKey}(已完成剩 ${d.remaining} 条,冷 key 共 ${d.total} 条)`
203
- : '没有需要归档的条目',
204
- },
205
-
206
- // ─── 快捷 topic(动作集合,非 CRUD)───────────────────────
207
- {
208
- id: 'topic.list',
209
- cli: ['topic', 'list'],
210
- http: ['GET', '/api/topics'],
211
- summary: '快捷主题列表(含各状态任务数)',
212
- flags: { group: GROUP },
213
- run: (ctx) => service.listTopics({ groupId: ctx.group }),
214
- render: (list) => {
215
- if (!list.length) return '(暂无快捷主题)';
216
- const w = Math.max(...list.map((t) => t.name.length));
217
- return list
218
- .map((t) => `${t.name.padEnd(w)} 待办 ${t.open} 完成 ${t.done} 冻结 ${t.freeze}`)
219
- .join('\n');
220
- },
221
- },
222
- {
223
- id: 'topic.add',
224
- cli: ['topic', 'add'],
225
- http: ['POST', '/api/topics'],
226
- summary: '加入快捷主题',
227
- args: ['name'],
228
- flags: { group: GROUP },
229
- run: (ctx) => service.addTopic(ctx.name, { groupId: ctx.group }),
230
- render: (d) => (d.added ? `已加入快捷主题: ${d.name}` : `${d.name} 已在快捷列表中`),
231
- },
232
- {
233
- id: 'topic.remove',
234
- cli: ['topic', 'remove'],
235
- http: ['DELETE', '/api/topics'],
236
- summary: '移出快捷主题(不动已有任务;仍被任务使用时会在结果里提示)',
237
- args: ['name'],
238
- flags: { group: GROUP },
239
- run: (ctx) => service.removeTopic(ctx.name, { groupId: ctx.group }),
240
- render: (d) => {
241
- if (!d.removed) return `${d.name} 本就不在快捷列表中`;
242
- return d.inUse
243
- ? `已移出快捷主题: ${d.name}\n注意: 仍有 ${d.inUse} 条任务在用它,主题名不会消失,只是不再出现在候选里`
244
- : `已移出快捷主题: ${d.name}`;
245
- },
246
- },
247
-
248
- // ─── 主题提示词(给 agent 的上下文)───────────────────────
249
- {
250
- id: 'prompt.get',
251
- cli: ['prompt', 'get'],
252
- http: ['GET', '/api/prompts/:topic'],
253
- summary: '读取某主题的上下文提示词',
254
- args: ['topic'],
255
- flags: { group: GROUP },
256
- run: (ctx) => service.getPrompt(ctx.topic, { groupId: ctx.group }),
257
- render: (d) => (d.hasPrompt ? d.prompt : `(${d.topic} 没有配置提示词)`),
258
- },
259
- {
260
- id: 'prompt.set',
261
- cli: ['prompt', 'set'],
262
- http: ['POST', '/api/prompts/:topic'],
263
- summary: '写入某主题的上下文提示词(覆盖)',
264
- args: ['topic', 'text'],
265
- flags: { group: GROUP },
266
- run: (ctx) => service.setPrompt(ctx.topic, ctx.text, { groupId: ctx.group }),
267
- render: (d) => `已写入 ${d.topic} 的提示词(${d.bytes} 字节)`,
268
- },
269
- {
270
- id: 'prompt.remove',
271
- cli: ['prompt', 'remove'],
272
- http: ['DELETE', '/api/prompts/:topic'],
273
- summary: '删除某主题的提示词',
274
- args: ['topic'],
275
- flags: { group: GROUP },
276
- run: (ctx) => service.removePrompt(ctx.topic, { groupId: ctx.group }),
277
- render: (d) => (d.removed ? `已删除 ${d.topic} 的提示词` : `${d.topic} 本就没有提示词`),
278
- },
279
- ],
280
- };
1
+ // 清单模块:四把 key(open / done / freeze / topics)上的任务管理。
2
+ //
3
+ // 声明了 `resource: 'todo'`,因此必须齐备 CRUD 五操作且两端可调用 ——
4
+ // tests/unit/registry.test.mjs 会据此断言。非 CRUD 的动作(完成/冻结/归档/topic/prompt)
5
+ // 用动作动词命名,不受五动词约束,但同样两端可达。
6
+ //
7
+ // ⚠️ 定位一律用**内容**(`--ref`),不要用 id。
8
+ //
9
+ // id 只是给人看的元数据:分配只扫「待办 + 冻结」,所以它会被复用;
10
+ // 同一主题里也本来就有内容重复的任务。按 id 定位迟早改错数据。
11
+ // 详见 service.js 的「定位」一节。
12
+ import * as service from './service.js';
13
+
14
+ // 所有动作都接受 --group 覆盖当前工作空间;默认取本机配置里的当前值
15
+ const GROUP = { type: 'number', hint: '工作空间 id(默认取当前)' };
16
+
17
+ // 完成时间显示:只取到分钟,表格里塞不下完整时间戳
18
+ const shortTime = (s) => (s ? String(s).slice(0, 16).replace('T', ' ') : '');
19
+
20
+ function renderTaskList(d) {
21
+ const groups = [
22
+ ['open', '待办'],
23
+ ['done', '已完成'],
24
+ ['freeze', '冻结'],
25
+ ].filter(([k]) => d[k]);
26
+
27
+ const lines = [];
28
+ if (d.topic) lines.push(`topic=${d.topic}`);
29
+ for (const [key, label] of groups) {
30
+ const items = d[key];
31
+ if (!items) continue;
32
+ if (!items.length) {
33
+ lines.push(`${label}: (空)`);
34
+ continue;
35
+ }
36
+ lines.push(`${label}(${items.length}):`);
37
+ const w = Math.max(...items.map((t) => String(t.id).length));
38
+ for (const t of items) {
39
+ const mark = key === 'done' ? `✓ ${shortTime(t.doneAt)}` : key === 'freeze' ? `❄ ${shortTime(t.frozenAt)}` : '';
40
+ // id 只作展示位(对齐列的宽度由它决定),**操作时不要用它定位** ——
41
+ // 定位一律用 --ref <内容>,见文件头的说明。
42
+ lines.push(` ${String(t.id).padStart(w)} [${t.topic}] ${t.text}${mark ? ' ' + mark : ''}`);
43
+ if (t.note) lines.push(` ${' '.repeat(w)} note: ${t.note.slice(0, 100)}`);
44
+ }
45
+ }
46
+ return lines.join('\n');
47
+ }
48
+
49
+ const renderTask = (t) =>
50
+ [
51
+ `[${t.topic}] ${t.text}`,
52
+ ` 状态: ${t.doneAt ? '已完成' : t.frozenAt ? '冻结' : '待办'}${t.bucket ? `(在 ${t.bucket})` : ''}`,
53
+ ` 创建: ${t.createdAt}`,
54
+ t.doneAt ? ` 完成: ${t.doneAt}` : '',
55
+ t.frozenAt ? ` 冻结: ${t.frozenAt}` : '',
56
+ t.note ? ` 备注: ${t.note}` : '',
57
+ ]
58
+ .filter(Boolean)
59
+ .join('\n');
60
+
61
+ export default {
62
+ id: 'todo',
63
+ title: '清单(= Web「清单」页)',
64
+ order: 30,
65
+ view: () => import('./view.jsx'),
66
+
67
+ // CRUD 资源声明:任务的增删查改必须齐备,且 CLI 与面板都能调用
68
+ resource: 'todo',
69
+
70
+ actions: [
71
+ // ─── CRUD 五操作 ───────────────────────────────────────────
72
+ {
73
+ id: 'todo.list',
74
+ cli: ['todo', 'list'],
75
+ http: ['GET', '/api/todo'],
76
+ summary: '列出任务(--topic / --status 过滤;默认三个桶全列)',
77
+ flags: {
78
+ topic: { type: 'string', hint: '只看某个主题' },
79
+ status: { type: 'string', enum: ['open', 'done', 'freeze', 'all'], default: 'all' },
80
+ group: GROUP,
81
+ },
82
+ run: (ctx) => service.listTodos({ topic: ctx.topic, status: ctx.status, groupId: ctx.group }),
83
+ render: renderTaskList,
84
+ },
85
+ {
86
+ id: 'todo.get',
87
+ cli: ['todo', 'get'],
88
+ http: ['GET', '/api/todo/item'],
89
+ summary: '查看单个任务(按内容定位;自动在待办/已完成/冻结里找)',
90
+ flags: {
91
+ ref: { type: 'string', required: true, hint: '任务内容(整串)' },
92
+ topic: { type: 'string', hint: '消歧:同内容有多条时用主题收窄' },
93
+ pick: { type: 'number', hint: '消歧:同内容多条时按编号选(配合报错里的列表)' },
94
+ group: GROUP,
95
+ },
96
+ run: (ctx) => service.getTodo(ctx.ref, { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
97
+ render: renderTask,
98
+ },
99
+ {
100
+ id: 'todo.add',
101
+ cli: ['todo', 'add'],
102
+ http: ['POST', '/api/todo'],
103
+ summary: '新增任务到待办(topic 不在快捷列表时自动补上)',
104
+ args: ['text'],
105
+ flags: { topic: { type: 'string', required: true, hint: '主题,路由维度' }, group: GROUP },
106
+ run: (ctx) => service.addTodo({ topic: ctx.topic, text: ctx.text, groupId: ctx.group }),
107
+ render: (d) =>
108
+ `已加入待办: [${d.task.topic}] ${d.task.text}` +
109
+ (d.topicAdded ? `\n(已把 ${d.task.topic} 加入快捷 topic)` : ''),
110
+ },
111
+ {
112
+ id: 'todo.update',
113
+ cli: ['todo', 'update'],
114
+ http: ['PATCH', '/api/todo/item'],
115
+ summary: '编辑任务(按内容定位,只改传入的字段)',
116
+ flags: {
117
+ ref: { type: 'string', required: true, hint: '要改的那条的内容(整串)' },
118
+ topic: { type: 'string', hint: '改成这个主题' },
119
+ text: { type: 'string', hint: '改成这个内容' },
120
+ note: { type: 'string' },
121
+ matchTopic: { type: 'string', hint: '消歧:同内容有多条时用主题收窄' },
122
+ pick: { type: 'number', hint: '消歧:同内容多条时按编号选(配合报错里的列表)' },
123
+ group: GROUP,
124
+ },
125
+ run: (ctx) =>
126
+ service.updateTodo(ctx.ref, {
127
+ topic: ctx.topic,
128
+ text: ctx.text,
129
+ note: ctx.note,
130
+ matchTopic: ctx.matchTopic,
131
+ pick: ctx.pick,
132
+ groupId: ctx.group,
133
+ }),
134
+ render: (d) => `已更新(在 ${d.bucket}): [${d.task.topic}] ${d.task.text}`,
135
+ },
136
+ {
137
+ id: 'todo.remove',
138
+ cli: ['todo', 'remove'],
139
+ http: ['DELETE', '/api/todo/item'],
140
+ summary: '删除任务(按内容定位,只删命中的那一条)',
141
+ flags: {
142
+ ref: { type: 'string', required: true, hint: '要删的那条的内容(整串)' },
143
+ topic: { type: 'string', hint: '消歧:同内容有多条时用主题收窄' },
144
+ pick: { type: 'number', hint: '消歧:同内容多条时按编号选(配合报错里的列表)' },
145
+ group: GROUP,
146
+ },
147
+ run: (ctx) => service.removeTodo(ctx.ref, { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
148
+ render: (d) => `已删除 [${d.removed.topic}] ${d.removed.text}`,
149
+ },
150
+
151
+ // ─── 状态流转 ──────────────────────────────────────────────
152
+ {
153
+ id: 'todo.done',
154
+ cli: ['todo', 'done'],
155
+ http: ['POST', '/api/todo/done'],
156
+ summary: '标记完成(--ref 定位;--result 写进 note 作为完成结果)',
157
+ flags: {
158
+ ref: { type: 'string', required: true, hint: '要完成的那条的内容(整串)' },
159
+ result: { type: 'string', hint: '完成结果摘要' },
160
+ topic: { type: 'string', hint: '消歧:同内容有多条时用主题收窄' },
161
+ pick: { type: 'number', hint: '消歧:同内容多条时按编号选(配合报错里的列表)' },
162
+ group: GROUP,
163
+ },
164
+ run: (ctx) =>
165
+ service.doneTodo(ctx.ref, { result: ctx.result, topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
166
+ render: (d) => `已完成 [${d.task.topic}] ${d.task.text}\n ${d.task.doneAt}`,
167
+ },
168
+ {
169
+ id: 'todo.freeze',
170
+ cli: ['todo', 'freeze'],
171
+ http: ['POST', '/api/todo/freeze'],
172
+ summary: '冻结任务(按内容定位;id 保留,解冻时校验冲突)',
173
+ flags: {
174
+ ref: { type: 'string', required: true, hint: '要冻结的那条的内容(整串)' },
175
+ topic: { type: 'string', hint: '消歧:同内容有多条时用主题收窄' },
176
+ pick: { type: 'number', hint: '消歧:同内容多条时按编号选(配合报错里的列表)' },
177
+ group: GROUP,
178
+ },
179
+ run: (ctx) => service.freezeTodo(ctx.ref, { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
180
+ render: (d) => `已冻结 [${d.task.topic}] ${d.task.text}`,
181
+ },
182
+ {
183
+ id: 'todo.unfreeze',
184
+ cli: ['todo', 'unfreeze'],
185
+ http: ['POST', '/api/todo/unfreeze'],
186
+ summary: '解冻回待办(按内容定位;id 撞车时自动换新 id)',
187
+ flags: {
188
+ ref: { type: 'string', required: true, hint: '要解冻的那条的内容(整串)' },
189
+ topic: { type: 'string', hint: '消歧:同内容有多条时用主题收窄' },
190
+ pick: { type: 'number', hint: '消歧:同内容多条时按编号选(配合报错里的列表)' },
191
+ group: GROUP,
192
+ },
193
+ run: (ctx) => service.unfreezeTodo(ctx.ref, { topic: ctx.topic, pick: ctx.pick, groupId: ctx.group }),
194
+ render: (d) =>
195
+ `已解冻 [${d.task.topic}] ${d.task.text}` +
196
+ (d.reIded ? '\n(原 id 与现有待办冲突,已分配新 id)' : ''),
197
+ },
198
+ {
199
+ id: 'todo.archive',
200
+ cli: ['todo', 'archive'],
201
+ http: ['POST', '/api/todo/archive'],
202
+ summary: '把已完成里较旧的条目归档到冷 key(默认 30 天前)',
203
+ flags: {
204
+ before: { type: 'string', hint: '截止日期,如 2026-08-01' },
205
+ group: GROUP,
206
+ },
207
+ run: (ctx) => service.archiveDone({ before: ctx.before, groupId: ctx.group }),
208
+ render: (d) =>
209
+ d.moved
210
+ ? `已归档 ${d.moved} 条 -> ${d.coldKey}(已完成剩 ${d.remaining} 条,冷 key 共 ${d.total} 条)`
211
+ : '没有需要归档的条目',
212
+ },
213
+
214
+ // ─── 快捷 topic(动作集合,非 CRUD)───────────────────────
215
+ {
216
+ id: 'topic.list',
217
+ cli: ['topic', 'list'],
218
+ http: ['GET', '/api/topics'],
219
+ summary: '快捷主题列表(含各状态任务数)',
220
+ flags: { group: GROUP },
221
+ run: (ctx) => service.listTopics({ groupId: ctx.group }),
222
+ render: (list) => {
223
+ if (!list.length) return '(暂无快捷主题)';
224
+ const w = Math.max(...list.map((t) => t.name.length));
225
+ return list
226
+ .map((t) => `${t.name.padEnd(w)} 待办 ${t.open} 完成 ${t.done} 冻结 ${t.freeze}`)
227
+ .join('\n');
228
+ },
229
+ },
230
+ {
231
+ id: 'topic.add',
232
+ cli: ['topic', 'add'],
233
+ http: ['POST', '/api/topics'],
234
+ summary: '加入快捷主题',
235
+ args: ['name'],
236
+ flags: { group: GROUP },
237
+ run: (ctx) => service.addTopic(ctx.name, { groupId: ctx.group }),
238
+ render: (d) => (d.added ? `已加入快捷主题: ${d.name}` : `${d.name} 已在快捷列表中`),
239
+ },
240
+ {
241
+ id: 'topic.remove',
242
+ cli: ['topic', 'remove'],
243
+ http: ['DELETE', '/api/topics'],
244
+ summary: '移出快捷主题(不动已有任务;仍被任务使用时会在结果里提示)',
245
+ args: ['name'],
246
+ flags: { group: GROUP },
247
+ run: (ctx) => service.removeTopic(ctx.name, { groupId: ctx.group }),
248
+ render: (d) => {
249
+ if (!d.removed) return `${d.name} 本就不在快捷列表中`;
250
+ return d.inUse
251
+ ? `已移出快捷主题: ${d.name}\n注意: 仍有 ${d.inUse} 条任务在用它,主题名不会消失,只是不再出现在候选里`
252
+ : `已移出快捷主题: ${d.name}`;
253
+ },
254
+ },
255
+
256
+ // ─── 主题提示词(给 agent 的上下文)───────────────────────
257
+ {
258
+ id: 'prompt.get',
259
+ cli: ['prompt', 'get'],
260
+ http: ['GET', '/api/prompts/:topic'],
261
+ summary: '读取某主题的上下文提示词',
262
+ args: ['topic'],
263
+ flags: { group: GROUP },
264
+ run: (ctx) => service.getPrompt(ctx.topic, { groupId: ctx.group }),
265
+ render: (d) => (d.hasPrompt ? d.prompt : `(${d.topic} 没有配置提示词)`),
266
+ },
267
+ {
268
+ id: 'prompt.set',
269
+ cli: ['prompt', 'set'],
270
+ http: ['POST', '/api/prompts/:topic'],
271
+ summary: '写入某主题的上下文提示词(覆盖)',
272
+ args: ['topic', 'text'],
273
+ flags: { group: GROUP },
274
+ run: (ctx) => service.setPrompt(ctx.topic, ctx.text, { groupId: ctx.group }),
275
+ render: (d) => `已写入 ${d.topic} 的提示词(${d.bytes} 字节)`,
276
+ },
277
+ {
278
+ id: 'prompt.remove',
279
+ cli: ['prompt', 'remove'],
280
+ http: ['DELETE', '/api/prompts/:topic'],
281
+ summary: '删除某主题的提示词',
282
+ args: ['topic'],
283
+ flags: { group: GROUP },
284
+ run: (ctx) => service.removePrompt(ctx.topic, { groupId: ctx.group }),
285
+ render: (d) => (d.removed ? `已删除 ${d.topic} 的提示词` : `${d.topic} 本就没有提示词`),
286
+ },
287
+ ],
288
+ };