dsh-worktodo 0.1.0 → 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.
package/src/tools.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { parseNaturalDue } from './parse-due.mjs';
2
+
1
3
  /**
2
4
  * 面向模型的 worktodo_* 工具。
3
5
  *
@@ -22,6 +24,7 @@
22
24
  */
23
25
  export const TOOL_NAMES = Object.freeze([
24
26
  'worktodo_add',
27
+ 'worktodo_add_many',
25
28
  'worktodo_list',
26
29
  'worktodo_update',
27
30
  'worktodo_done',
@@ -89,6 +92,7 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
89
92
  parameters: {
90
93
  title: { type: 'string', required: true, description: '待办内容,一句简短的祈使句,例如「把 v0.1.0 发到 npm」。' },
91
94
  due: { type: 'string', description: '截止日期:YYYY-MM-DD / today / tomorrow / +3d。省略表示没有截止日期。' },
95
+ dueTime: { type: 'string', description: '截止时间 HH:MM(可选,只有给了 due 才有意义;例如 14:30)。' },
92
96
  priority: { type: 'string', enum: ['high', 'normal', 'low'], description: '优先级,默认 normal。真正卡住别人的事才给 high。' },
93
97
  project: { type: 'string', description: '项目或领域标签,例如 dsh-worktodo。省略表示不归类。' },
94
98
  notes: { type: 'string', description: '补充说明:背景、链接、验收标准。默认空。' },
@@ -102,6 +106,7 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
102
106
  id: { type: 'string', required: true },
103
107
  title: { type: 'string', required: true },
104
108
  due: { type: 'string' },
109
+ dueTime: { type: 'string' },
105
110
  priority: { type: 'string', required: true },
106
111
  project: { type: 'string' },
107
112
  status: { type: 'string', required: true },
@@ -110,15 +115,16 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
110
115
  },
111
116
  render: (_args, value) => [{
112
117
  type: 'text',
113
- text: `已记入工作待办:${value.title}(${value.id}${value.due ? `,截止 ${value.due}` : ''})`
118
+ text: `已记入工作待办:${value.title}(${value.id}${value.due ? `,截止 ${value.due}${value.dueTime ? ` ${value.dueTime}` : ''}` : ''})`
114
119
  + (value.stats ? `|当前未完成 ${value.stats.open} 条,逾期 ${value.stats.overdue} 条` : ''),
115
120
  }],
116
121
  },
117
122
  execute(args) {
118
- return service.create(args, { source: 'agent' }).then(unwrap).then((result) => withStats(compact({
123
+ return service.create(withParsedDue(args), { source: 'agent' }).then(unwrap).then((result) => withStats(compact({
119
124
  id: result.todo.id,
120
125
  title: result.todo.title,
121
126
  due: result.todo.due ?? undefined,
127
+ dueTime: result.todo.dueTime ?? undefined,
122
128
  priority: result.todo.priority,
123
129
  project: result.todo.project ?? undefined,
124
130
  status: result.todo.status,
@@ -127,6 +133,112 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
127
133
  presentCall: (args) => ({ card: 'generic', title: '登记工作待办', kind: 'other', rawInput: args.title }),
128
134
  })));
129
135
 
136
+ // ---- 批量新增(AI 友好:一口气登记多条)----
137
+ disposers.push(ctx.tools.register(defineTool({
138
+ name: 'worktodo_add_many',
139
+ description:
140
+ '一次登记多条工作待办(用户一口气说了好几件事、或你整理出一串要跟进的活时用它;分开一条条调 worktodo_add 也行,但这样更省调用)。'
141
+ + '某条不合法只跳过那一条并把原因回传,不影响其余条目。',
142
+ parameters: {
143
+ items: {
144
+ type: 'array',
145
+ required: true,
146
+ description: '要登记的事;每项至少给 title。',
147
+ items: {
148
+ type: 'object',
149
+ additionalProperties: false,
150
+ properties: {
151
+ title: { type: 'string', required: true, description: '待办内容(一句简短的祈使句)。' },
152
+ due: { type: 'string', description: '截止日期:YYYY-MM-DD / today / tomorrow / +3d。' },
153
+ dueTime: { type: 'string', description: '截止时间 HH:MM(可选)。' },
154
+ priority: { type: 'string', enum: ['high', 'normal', 'low'], description: '默认 normal。' },
155
+ project: { type: 'string', description: '项目或领域标签。' },
156
+ notes: { type: 'string', description: '背景、链接、验收标准。' },
157
+ status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: '默认 pending。' },
158
+ },
159
+ },
160
+ },
161
+ },
162
+ output: {
163
+ schema: {
164
+ type: 'object',
165
+ additionalProperties: false,
166
+ properties: {
167
+ count: { type: 'number', required: true },
168
+ created: {
169
+ type: 'array',
170
+ required: true,
171
+ items: {
172
+ type: 'object',
173
+ additionalProperties: false,
174
+ properties: {
175
+ id: { type: 'string', required: true },
176
+ title: { type: 'string', required: true },
177
+ due: { type: 'string' },
178
+ dueTime: { type: 'string' },
179
+ priority: { type: 'string', required: true },
180
+ project: { type: 'string' },
181
+ status: { type: 'string', required: true },
182
+ },
183
+ },
184
+ },
185
+ skipped: {
186
+ type: 'array',
187
+ required: true,
188
+ items: {
189
+ type: 'object',
190
+ additionalProperties: false,
191
+ properties: {
192
+ index: { type: 'number', required: true },
193
+ title: { type: 'string', required: true },
194
+ reason: { type: 'string', required: true },
195
+ },
196
+ },
197
+ },
198
+ stats: { type: 'object', additionalProperties: true },
199
+ },
200
+ },
201
+ render: (_args, value) => [{
202
+ type: 'text',
203
+ text: `已记入工作待办 ${value.count} 条`
204
+ + (value.skipped.length ? `,跳过 ${value.skipped.length} 条(${value.skipped.map((entry) => `#${entry.index} ${entry.reason}`).join(';')})` : '')
205
+ + (value.stats ? `|当前未完成 ${value.stats.open} 条,逾期 ${value.stats.overdue} 条` : ''),
206
+ }],
207
+ },
208
+ async execute(args) {
209
+ const created = [];
210
+ const skipped = [];
211
+ const items = Array.isArray(args.items) ? args.items : [];
212
+ for (const [index, item] of items.entries()) {
213
+ try {
214
+ const result = unwrap(await service.create(withParsedDue(item ?? {}), { source: 'agent' }));
215
+ created.push(compact({
216
+ id: result.todo.id,
217
+ title: result.todo.title,
218
+ due: result.todo.due ?? undefined,
219
+ dueTime: result.todo.dueTime ?? undefined,
220
+ priority: result.todo.priority,
221
+ project: result.todo.project ?? undefined,
222
+ status: result.todo.status,
223
+ }));
224
+ } catch (error) {
225
+ skipped.push({
226
+ index,
227
+ title: typeof item?.title === 'string' ? item.title : '',
228
+ reason: String(error?.message ?? error),
229
+ });
230
+ }
231
+ }
232
+ return withStats(compact({ count: created.length, created, skipped }));
233
+ },
234
+ presentCall: (args) => ({
235
+ card: 'generic',
236
+ title: `登记 ${Array.isArray(args.items) ? args.items.length : 0} 条工作待办`,
237
+ kind: 'other',
238
+ rawInput: '',
239
+ }),
240
+ })));
241
+
130
242
  // ---- 查询 ----
131
243
  disposers.push(ctx.tools.register(defineTool({
132
244
  name: 'worktodo_list',
@@ -138,6 +250,7 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
138
250
  filter: { type: 'string', enum: ['open', 'today', 'overdue', 'week', 'doing', 'completed', 'all'], description: '默认 open(未完成)。' },
139
251
  project: { type: 'string', description: '只看某个项目标签。' },
140
252
  query: { type: 'string', description: '按标题或备注里的关键词过滤。' },
253
+ sort: { type: 'string', enum: ['manual', 'priority', 'created'], description: '排序:manual=尊重用户拖动过的顺序(默认,用户看到的顺序);priority=高优先级在前;created=最近添加的在前。' },
141
254
  },
142
255
  output: {
143
256
  schema: {
@@ -156,7 +269,7 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
156
269
  lines: result.todos.length
157
270
  ? result.todos.map((t) => {
158
271
  const box = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]';
159
- const bits = [t.priority === 'high' ? '高' : t.priority === 'low' ? '低' : null, t.due ? `${t.due}${t.due < result.today && t.status !== 'completed' ? '(逾期)' : ''}` : null, t.project ? `#${t.project}` : null].filter(Boolean);
272
+ const bits = [t.priority === 'high' ? '高' : t.priority === 'low' ? '低' : null, t.due ? `${t.due}${t.dueTime ? ` ${t.dueTime}` : ''}${t.due < result.today && t.status !== 'completed' ? '(逾期)' : ''}` : null, t.project ? `#${t.project}` : null].filter(Boolean);
160
273
  return `${box} ${t.id} ${t.title}${bits.length ? ` (${bits.join(', ')})` : ''}`;
161
274
  }).join('\n')
162
275
  : '(没有符合条件的待办)',
@@ -177,7 +290,8 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
177
290
  parameters: {
178
291
  ref: { type: 'string', required: true, description: '待办的 id(推荐)或标题。' },
179
292
  title: { type: 'string', description: '新标题。' },
180
- due: { type: 'string', description: '新截止日期;传 none 清空。' },
293
+ due: { type: 'string', description: '新截止日期;传 none 清空(清空日期会一并清掉时间)。' },
294
+ dueTime: { type: 'string', description: '新截止时间 HH:MM;传 none 清空。' },
181
295
  priority: { type: 'string', enum: ['high', 'normal', 'low'] },
182
296
  project: { type: 'string', description: '新项目标签;传 none 清空。' },
183
297
  notes: { type: 'string', description: '新备注。' },
@@ -342,4 +456,23 @@ export async function registerWorkTodoTools({ ctx, service, logger }) {
342
456
  }
343
457
  },
344
458
  };
459
+ }/**
460
+ * 让工具也能"说人话":标题里写了「明天下午三点」这类说法时,交给与面板**同一份**解析器
461
+ * (`src/parse-due.mjs`)。显式传了 `due` 就以显式为准,绝不被标题覆盖。
462
+ * @param {object} input
463
+ * @returns {object} 落库用的参数(标题里的日期词会被摘掉)
464
+ */
465
+ export function withParsedDue(input = {}) {
466
+ const explicit = input.due !== undefined && input.due !== null && input.due !== '';
467
+ if (explicit) return input;
468
+ const parsed = parseNaturalDue(String(input.title ?? ''), Date.now());
469
+ if (!parsed.matched.length) return input;
470
+ return {
471
+ ...input,
472
+ title: parsed.title,
473
+ due: parsed.due ?? undefined,
474
+ dueTime: input.dueTime ?? parsed.dueTime ?? undefined,
475
+ };
345
476
  }
477
+
478
+