@becrafter/prompt-manager 0.0.19 → 0.1.2
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 +145 -234
- package/app/desktop/assets/app.1.png +0 -0
- package/app/desktop/assets/app.png +0 -0
- package/app/desktop/assets/icons/icon.icns +0 -0
- package/app/desktop/assets/icons/icon.ico +0 -0
- package/app/desktop/assets/icons/icon.png +0 -0
- package/app/desktop/assets/icons/tray.png +0 -0
- package/app/desktop/assets/tray.1.png +0 -0
- package/app/desktop/assets/tray.png +0 -0
- package/app/desktop/main.js +27 -0
- package/app/desktop/package-lock.json +201 -4
- package/app/desktop/package.json +23 -29
- package/app/desktop/src/services/module-loader.js +43 -22
- package/app/desktop/src/services/runtime-manager.js +172 -23
- package/app/desktop/src/services/update-manager.js +6 -7
- package/app/desktop/src/ui/admin-window-manager.js +757 -0
- package/app/desktop/src/ui/splash-manager.js +253 -0
- package/app/desktop/src/ui/tray-manager.js +8 -24
- package/app/desktop/src/utils/icon-manager.js +39 -47
- package/app/desktop/src/utils/resource-paths.js +0 -23
- package/app/desktop/src/utils/resource-sync.js +260 -0
- package/app/desktop/src/utils/runtime-sync.js +241 -0
- package/examples/prompts/recommend/human_3-0_growth_diagnostic_coach_prompt.yaml +105 -0
- package/package.json +16 -13
- package/packages/admin-ui/.babelrc +3 -0
- package/packages/admin-ui/admin.html +237 -4784
- package/packages/admin-ui/css/main.css +2592 -0
- package/packages/admin-ui/css/recommended-prompts.css +610 -0
- package/packages/admin-ui/package-lock.json +6973 -0
- package/packages/admin-ui/package.json +36 -0
- package/packages/admin-ui/src/codemirror.js +53 -0
- package/packages/admin-ui/src/index.js +3188 -0
- package/packages/admin-ui/webpack.config.js +76 -0
- package/packages/resources/tools/chrome-devtools/README.md +310 -0
- package/packages/resources/tools/chrome-devtools/chrome-devtools.tool.js +1703 -0
- package/packages/resources/tools/file-reader/README.md +289 -0
- package/packages/resources/tools/file-reader/file-reader.tool.js +1545 -0
- package/packages/resources/tools/filesystem/README.md +359 -0
- package/packages/resources/tools/filesystem/filesystem.tool.js +514 -160
- package/packages/resources/tools/ollama-remote/README.md +192 -0
- package/packages/resources/tools/ollama-remote/ollama-remote.tool.js +421 -0
- package/packages/resources/tools/pdf-reader/README.md +236 -0
- package/packages/resources/tools/pdf-reader/pdf-reader.tool.js +565 -0
- package/packages/resources/tools/playwright/README.md +306 -0
- package/packages/resources/tools/playwright/playwright.tool.js +1186 -0
- package/packages/resources/tools/todolist/README.md +394 -0
- package/packages/resources/tools/todolist/todolist.tool.js +1312 -0
- package/packages/server/README.md +142 -0
- package/packages/server/api/admin.routes.js +42 -11
- package/packages/server/api/surge.routes.js +43 -0
- package/packages/server/app.js +119 -14
- package/packages/server/index.js +39 -0
- package/packages/server/mcp/mcp.server.js +324 -105
- package/packages/server/mcp/sequential-thinking.handler.js +318 -0
- package/packages/server/mcp/think-plan.handler.js +274 -0
- package/packages/server/middlewares/auth.middleware.js +6 -0
- package/packages/server/package.json +51 -0
- package/packages/server/server.js +37 -1
- package/packages/server/toolm/index.js +9 -0
- package/packages/server/toolm/package-installer.service.js +267 -0
- package/packages/server/toolm/test-tools.js +264 -0
- package/packages/server/toolm/tool-context.service.js +334 -0
- package/packages/server/toolm/tool-dependency.service.js +168 -0
- package/packages/server/toolm/tool-description-generator-optimized.service.js +375 -0
- package/packages/server/toolm/tool-description-generator.service.js +312 -0
- package/packages/server/toolm/tool-environment.service.js +200 -0
- package/packages/server/toolm/tool-execution.service.js +277 -0
- package/packages/server/toolm/tool-loader.service.js +219 -0
- package/packages/server/toolm/tool-logger.service.js +223 -0
- package/packages/server/toolm/tool-manager.handler.js +65 -0
- package/packages/server/toolm/tool-manual-generator.service.js +389 -0
- package/packages/server/toolm/tool-mode-handlers.service.js +224 -0
- package/packages/server/toolm/tool-storage.service.js +111 -0
- package/packages/server/toolm/tool-sync.service.js +138 -0
- package/packages/server/toolm/tool-utils.js +20 -0
- package/packages/server/toolm/tool-yaml-parser.service.js +81 -0
- package/packages/server/toolm/validate-system.js +421 -0
- package/packages/server/utils/config.js +49 -5
- package/packages/server/utils/util.js +65 -10
- package/scripts/build-icons.js +99 -69
- package/scripts/build.sh +57 -0
- package/scripts/surge/CNAME +1 -0
- package/scripts/surge/README.md +47 -0
- package/scripts/surge/package-lock.json +34 -0
- package/scripts/surge/package.json +20 -0
- package/scripts/surge/sync-to-surge.js +151 -0
- package/app/desktop/assets/icons/icon_1024x1024.png +0 -0
- package/app/desktop/assets/icons/icon_128x128.png +0 -0
- package/app/desktop/assets/icons/icon_16x16.png +0 -0
- package/app/desktop/assets/icons/icon_24x24.png +0 -0
- package/app/desktop/assets/icons/icon_256x256.png +0 -0
- package/app/desktop/assets/icons/icon_32x32.png +0 -0
- package/app/desktop/assets/icons/icon_48x48.png +0 -0
- package/app/desktop/assets/icons/icon_512x512.png +0 -0
- package/app/desktop/assets/icons/icon_64x64.png +0 -0
- package/app/desktop/assets/icons/icon_96x96.png +0 -0
- package/packages/admin-ui/js/closebrackets.min.js +0 -8
- package/packages/admin-ui/js/codemirror.min.js +0 -8
- package/packages/admin-ui/js/js-yaml.min.js +0 -2
- package/packages/admin-ui/js/markdown.min.js +0 -8
- package/packages/resources/tools/index.js +0 -16
- package/packages/server/mcp/toolx.handler.js +0 -131
- package/scripts/icns-builder/package.json +0 -12
- /package/packages/server/mcp/{mcp.handler.js → prompt.handler.js} +0 -0
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TodoList Tool - 基于本地 JSON 文件的轻量任务管理工具
|
|
3
|
+
*
|
|
4
|
+
* 核心功能:
|
|
5
|
+
* - 支持会话任务(临时,默认)和项目任务(持久化)
|
|
6
|
+
* - 快速查询:今日任务、待办、已完成、逾期
|
|
7
|
+
* - 批量操作:减少模型调用次数
|
|
8
|
+
* - 任务统计:完成率、按优先级/标签统计
|
|
9
|
+
*
|
|
10
|
+
* 设计理念:
|
|
11
|
+
* - 默认创建会话任务,快速记录临时待办
|
|
12
|
+
* - 指定 project_id 创建项目任务,跨会话可用
|
|
13
|
+
* - 数据隔离:会话任务和项目任务完全隔离
|
|
14
|
+
*
|
|
15
|
+
* 技术说明:
|
|
16
|
+
* - 使用纯 JavaScript JSON 文件存储,无需任何原生依赖
|
|
17
|
+
* - 文件分离:活跃任务存储在 tasks.json,归档任务存储在 archived.json
|
|
18
|
+
* - 所有写操作都会立即刷新到磁盘,确保数据安全
|
|
19
|
+
* - 自动数据清理:过期会话任务和超量归档任务自动清理
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import os from 'os';
|
|
24
|
+
import { randomUUID } from 'crypto';
|
|
25
|
+
import { promises as fs } from 'fs';
|
|
26
|
+
|
|
27
|
+
const DEFAULT_LIMIT = 50;
|
|
28
|
+
const DEFAULT_PRIORITY = 2;
|
|
29
|
+
const DATA_DIR_NAME = 'data';
|
|
30
|
+
const TASK_FILE_NAME = 'tasks.json';
|
|
31
|
+
const ARCHIVED_FILE_NAME = 'archived.json';
|
|
32
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
33
|
+
|
|
34
|
+
const getEnvNumber = (value, fallback, min = 1) => {
|
|
35
|
+
const parsed = Number(value);
|
|
36
|
+
if (!Number.isFinite(parsed)) {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
return Math.max(min, parsed);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const SESSION_TTL_DAYS = getEnvNumber(process.env?.TODOLIST_SESSION_TTL_DAYS, 7, 1); // 会话任务保留天数
|
|
43
|
+
const MAX_ARCHIVED_PER_SCOPE = getEnvNumber(process.env?.TODOLIST_MAX_ARCHIVED, 200, 50); // 各作用域最多保留归档任务条数
|
|
44
|
+
|
|
45
|
+
class TaskStore {
|
|
46
|
+
constructor(activeFilePath, archivedFilePath, activeTasks, archivedTasks, logger) {
|
|
47
|
+
this.activeFilePath = activeFilePath;
|
|
48
|
+
this.archivedFilePath = archivedFilePath;
|
|
49
|
+
this.activeTasks = activeTasks;
|
|
50
|
+
this.archivedTasks = archivedTasks;
|
|
51
|
+
this.logger = logger;
|
|
52
|
+
this.activeModified = false;
|
|
53
|
+
this.archivedModified = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static async load(toolDir, logger) {
|
|
57
|
+
const dataDir = path.join(toolDir, DATA_DIR_NAME);
|
|
58
|
+
const activePath = path.join(dataDir, TASK_FILE_NAME);
|
|
59
|
+
const archivedPath = path.join(dataDir, ARCHIVED_FILE_NAME);
|
|
60
|
+
await fs.mkdir(dataDir, { recursive: true });
|
|
61
|
+
|
|
62
|
+
let activeTasks = [];
|
|
63
|
+
let archivedTasks = [];
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs.readFile(activePath, 'utf-8');
|
|
67
|
+
const parsed = JSON.parse(content);
|
|
68
|
+
if (Array.isArray(parsed.tasks)) {
|
|
69
|
+
activeTasks = parsed.tasks.map(task => TaskStore.normalizeTask(task)).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT') {
|
|
73
|
+
// 如果是 JSON 解析错误,尝试备份文件
|
|
74
|
+
if (error instanceof SyntaxError) {
|
|
75
|
+
try {
|
|
76
|
+
const backupPath = activePath + '.backup.' + Date.now();
|
|
77
|
+
await fs.copyFile(activePath, backupPath);
|
|
78
|
+
logger?.error('活跃任务文件 JSON 格式错误,已备份', {
|
|
79
|
+
error: error.message,
|
|
80
|
+
backup: backupPath
|
|
81
|
+
});
|
|
82
|
+
} catch (backupError) {
|
|
83
|
+
logger?.error('读取活跃任务文件失败且备份失败', {
|
|
84
|
+
error: error.message,
|
|
85
|
+
backupError: backupError.message
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
logger?.warn('读取活跃任务文件失败,使用空任务列表', {
|
|
90
|
+
error: error.message
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const content = await fs.readFile(archivedPath, 'utf-8');
|
|
98
|
+
const parsed = JSON.parse(content);
|
|
99
|
+
if (Array.isArray(parsed.tasks)) {
|
|
100
|
+
archivedTasks = parsed.tasks.map(task => TaskStore.normalizeTask(task)).filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.code !== 'ENOENT') {
|
|
104
|
+
// 如果是 JSON 解析错误,尝试备份文件
|
|
105
|
+
if (error instanceof SyntaxError) {
|
|
106
|
+
try {
|
|
107
|
+
const backupPath = archivedPath + '.backup.' + Date.now();
|
|
108
|
+
await fs.copyFile(archivedPath, backupPath);
|
|
109
|
+
logger?.error('归档任务文件 JSON 格式错误,已备份', {
|
|
110
|
+
error: error.message,
|
|
111
|
+
backup: backupPath
|
|
112
|
+
});
|
|
113
|
+
} catch (backupError) {
|
|
114
|
+
logger?.error('读取归档任务文件失败且备份失败', {
|
|
115
|
+
error: error.message,
|
|
116
|
+
backupError: backupError.message
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
logger?.warn('读取归档任务文件失败,使用空任务列表', {
|
|
121
|
+
error: error.message
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const store = new TaskStore(activePath, archivedPath, activeTasks, archivedTasks, logger);
|
|
128
|
+
store.pruneExpiredData();
|
|
129
|
+
if (store.activeModified || store.archivedModified) {
|
|
130
|
+
await store.save(true);
|
|
131
|
+
}
|
|
132
|
+
return store;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getAllTasks() {
|
|
136
|
+
return [...this.activeTasks, ...this.archivedTasks];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getActiveTasks() {
|
|
140
|
+
return this.activeTasks;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getArchivedTasks() {
|
|
144
|
+
return this.archivedTasks;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
findTask(taskId) {
|
|
148
|
+
let task = this.activeTasks.find(t => t.id === taskId);
|
|
149
|
+
if (!task) {
|
|
150
|
+
task = this.archivedTasks.find(t => t.id === taskId);
|
|
151
|
+
}
|
|
152
|
+
return task;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
addTask(task) {
|
|
156
|
+
if (task.status === 'archived') {
|
|
157
|
+
this.archivedTasks.push(task);
|
|
158
|
+
this.archivedModified = true;
|
|
159
|
+
} else {
|
|
160
|
+
this.activeTasks.push(task);
|
|
161
|
+
this.activeModified = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
updateTask(taskId, updates) {
|
|
166
|
+
let task = this.activeTasks.find(t => t.id === taskId);
|
|
167
|
+
let isArchived = false;
|
|
168
|
+
|
|
169
|
+
if (!task) {
|
|
170
|
+
task = this.archivedTasks.find(t => t.id === taskId);
|
|
171
|
+
isArchived = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!task) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const wasArchived = task.status === 'archived';
|
|
179
|
+
Object.assign(task, updates);
|
|
180
|
+
const isNowArchived = task.status === 'archived';
|
|
181
|
+
|
|
182
|
+
if (wasArchived !== isNowArchived) {
|
|
183
|
+
if (isNowArchived) {
|
|
184
|
+
this.activeTasks = this.activeTasks.filter(t => t.id !== taskId);
|
|
185
|
+
this.archivedTasks.push(task);
|
|
186
|
+
this.activeModified = true;
|
|
187
|
+
this.archivedModified = true;
|
|
188
|
+
} else {
|
|
189
|
+
this.archivedTasks = this.archivedTasks.filter(t => t.id !== taskId);
|
|
190
|
+
this.activeTasks.push(task);
|
|
191
|
+
this.activeModified = true;
|
|
192
|
+
this.archivedModified = true;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
if (isArchived || isNowArchived) {
|
|
196
|
+
this.archivedModified = true;
|
|
197
|
+
} else {
|
|
198
|
+
this.activeModified = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
removeTask(taskId) {
|
|
206
|
+
const activeIndex = this.activeTasks.findIndex(t => t.id === taskId);
|
|
207
|
+
if (activeIndex !== -1) {
|
|
208
|
+
this.activeTasks.splice(activeIndex, 1);
|
|
209
|
+
this.activeModified = true;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const archivedIndex = this.archivedTasks.findIndex(t => t.id === taskId);
|
|
214
|
+
if (archivedIndex !== -1) {
|
|
215
|
+
this.archivedTasks.splice(archivedIndex, 1);
|
|
216
|
+
this.archivedModified = true;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static normalizeTask(task) {
|
|
224
|
+
if (!task || typeof task !== 'object') {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const normalized = { ...task };
|
|
229
|
+
|
|
230
|
+
if (typeof normalized.tags === 'string') {
|
|
231
|
+
try {
|
|
232
|
+
normalized.tags = JSON.parse(normalized.tags);
|
|
233
|
+
} catch {
|
|
234
|
+
normalized.tags = [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!Array.isArray(normalized.tags)) {
|
|
239
|
+
normalized.tags = [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (normalized.session_id === undefined) {
|
|
243
|
+
normalized.session_id = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (normalized.project_id === undefined) {
|
|
247
|
+
normalized.project_id = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (typeof normalized.priority !== 'number') {
|
|
251
|
+
normalized.priority = DEFAULT_PRIORITY;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (typeof normalized.sort !== 'number') {
|
|
255
|
+
normalized.sort = 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return normalized;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
markActiveModified() {
|
|
262
|
+
this.activeModified = true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
markArchivedModified() {
|
|
266
|
+
this.archivedModified = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async save(force = false) {
|
|
270
|
+
if (this.activeModified || force) {
|
|
271
|
+
const payload = {
|
|
272
|
+
tasks: this.activeTasks
|
|
273
|
+
};
|
|
274
|
+
await fs.mkdir(path.dirname(this.activeFilePath), { recursive: true });
|
|
275
|
+
// 使用临时文件+重命名确保原子性写入
|
|
276
|
+
const tempPath = this.activeFilePath + '.tmp';
|
|
277
|
+
try {
|
|
278
|
+
await fs.writeFile(tempPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
279
|
+
await fs.rename(tempPath, this.activeFilePath);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// 如果重命名失败,尝试清理临时文件
|
|
282
|
+
await fs.unlink(tempPath).catch(() => {});
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
this.activeModified = false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (this.archivedModified || force) {
|
|
289
|
+
const payload = {
|
|
290
|
+
tasks: this.archivedTasks
|
|
291
|
+
};
|
|
292
|
+
await fs.mkdir(path.dirname(this.archivedFilePath), { recursive: true });
|
|
293
|
+
// 使用临时文件+重命名确保原子性写入
|
|
294
|
+
const tempPath = this.archivedFilePath + '.tmp';
|
|
295
|
+
try {
|
|
296
|
+
await fs.writeFile(tempPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
297
|
+
await fs.rename(tempPath, this.archivedFilePath);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// 如果重命名失败,尝试清理临时文件
|
|
300
|
+
await fs.unlink(tempPath).catch(() => {});
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
this.archivedModified = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pruneExpiredData() {
|
|
308
|
+
const beforeActive = this.activeTasks.length;
|
|
309
|
+
const cutoff = Date.now() - SESSION_TTL_DAYS * DAY_MS;
|
|
310
|
+
|
|
311
|
+
this.activeTasks = this.activeTasks.filter(task => {
|
|
312
|
+
if (!task) return false;
|
|
313
|
+
if (task.project_id || !task.session_id) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
const lastActive = Date.parse(task.updated_at || task.created_at || '');
|
|
317
|
+
if (Number.isNaN(lastActive)) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
return lastActive >= cutoff;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (this.activeTasks.length !== beforeActive) {
|
|
324
|
+
this.logger?.info?.('已清理过期会话任务', {
|
|
325
|
+
removed: beforeActive - this.activeTasks.length,
|
|
326
|
+
session_ttl_days: SESSION_TTL_DAYS
|
|
327
|
+
});
|
|
328
|
+
this.activeModified = true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const archivedByScope = new Map();
|
|
332
|
+
this.archivedTasks.forEach(task => {
|
|
333
|
+
if (!task) return;
|
|
334
|
+
const scopeKey = task.project_id
|
|
335
|
+
? `project:${task.project_id}`
|
|
336
|
+
: `session:${task.session_id || 'null'}`;
|
|
337
|
+
if (!archivedByScope.has(scopeKey)) {
|
|
338
|
+
archivedByScope.set(scopeKey, []);
|
|
339
|
+
}
|
|
340
|
+
archivedByScope.get(scopeKey).push(task);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
archivedByScope.forEach((items, scopeKey) => {
|
|
344
|
+
if (items.length <= MAX_ARCHIVED_PER_SCOPE) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const sorted = [...items].sort((a, b) => {
|
|
348
|
+
const timeA = Date.parse(a.updated_at || a.created_at || 0);
|
|
349
|
+
const timeB = Date.parse(b.updated_at || b.created_at || 0);
|
|
350
|
+
return timeB - timeA;
|
|
351
|
+
});
|
|
352
|
+
const toRemove = new Set(
|
|
353
|
+
sorted.slice(MAX_ARCHIVED_PER_SCOPE).map(task => task.id)
|
|
354
|
+
);
|
|
355
|
+
const beforeCount = this.archivedTasks.length;
|
|
356
|
+
this.archivedTasks = this.archivedTasks.filter(task => !toRemove.has(task.id));
|
|
357
|
+
if (beforeCount !== this.archivedTasks.length) {
|
|
358
|
+
this.logger?.info?.('归档任务达到上限,已清理旧数据', {
|
|
359
|
+
scope: scopeKey,
|
|
360
|
+
removed: beforeCount - this.archivedTasks.length,
|
|
361
|
+
max_archived_per_scope: MAX_ARCHIVED_PER_SCOPE
|
|
362
|
+
});
|
|
363
|
+
this.archivedModified = true;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export default {
|
|
370
|
+
/**
|
|
371
|
+
* 获取工具依赖
|
|
372
|
+
*/
|
|
373
|
+
getDependencies() {
|
|
374
|
+
return {
|
|
375
|
+
uuid: '^9.0.0'
|
|
376
|
+
};
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 获取工具元信息
|
|
381
|
+
*/
|
|
382
|
+
getMetadata() {
|
|
383
|
+
return {
|
|
384
|
+
id: 'todolist',
|
|
385
|
+
name: 'TodoList',
|
|
386
|
+
description:
|
|
387
|
+
'基于本地 JSON 文件存储的 TodoList 工具,默认创建会话任务(临时),也可指定项目创建持久化任务。支持快速查询、任务统计、批量操作等功能。',
|
|
388
|
+
version: '1.0.0',
|
|
389
|
+
category: 'utility',
|
|
390
|
+
tags: ['todo', 'todolist', 'task', 'management', 'json', 'batch'],
|
|
391
|
+
scenarios: [
|
|
392
|
+
'快速添加会话任务(临时待办)',
|
|
393
|
+
'创建项目任务(持久化)',
|
|
394
|
+
'查询今日任务、待办、已完成',
|
|
395
|
+
'批量创建和更新任务',
|
|
396
|
+
'按标签和优先级筛选任务',
|
|
397
|
+
'查看任务统计信息',
|
|
398
|
+
'跨会话管理项目任务'
|
|
399
|
+
],
|
|
400
|
+
limitations: [
|
|
401
|
+
'仅支持本地存储,不支持云端同步',
|
|
402
|
+
'会话任务在会话断开后失效(数据保留但不关联新会话)',
|
|
403
|
+
'项目自动管理,无需手动创建/删除',
|
|
404
|
+
'自然语言日期解析能力有限'
|
|
405
|
+
]
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 获取参数 Schema
|
|
411
|
+
*/
|
|
412
|
+
getSchema() {
|
|
413
|
+
return {
|
|
414
|
+
parameters: {
|
|
415
|
+
type: 'object',
|
|
416
|
+
properties: {
|
|
417
|
+
method: {
|
|
418
|
+
type: 'string',
|
|
419
|
+
description: '操作方法',
|
|
420
|
+
enum: [
|
|
421
|
+
'add_task',
|
|
422
|
+
'list_tasks',
|
|
423
|
+
'update_task',
|
|
424
|
+
'complete_task',
|
|
425
|
+
'archive_task',
|
|
426
|
+
'batch_tasks',
|
|
427
|
+
'reorder_tasks',
|
|
428
|
+
'get_statistics',
|
|
429
|
+
'list_projects'
|
|
430
|
+
]
|
|
431
|
+
},
|
|
432
|
+
content: { type: 'string', description: '任务内容' },
|
|
433
|
+
description: { type: 'string', description: '任务描述(可选)' },
|
|
434
|
+
priority: {
|
|
435
|
+
type: 'number',
|
|
436
|
+
enum: [1, 2, 3, 4],
|
|
437
|
+
description: '优先级:1=低, 2=中, 3=高, 4=紧急'
|
|
438
|
+
},
|
|
439
|
+
due_date: { type: 'string', description: '截止日期(ISO 8601 或自然语言)' },
|
|
440
|
+
project_id: {
|
|
441
|
+
type: 'string',
|
|
442
|
+
description: '项目ID(不指定则创建会话任务,指定则创建项目任务)'
|
|
443
|
+
},
|
|
444
|
+
sort: { type: 'number', description: '排序值(默认自动递增)' },
|
|
445
|
+
tags: { type: 'array', items: { type: 'string' }, description: '标签数组' },
|
|
446
|
+
quick_filter: {
|
|
447
|
+
type: 'string',
|
|
448
|
+
enum: ['today', 'pending', 'completed', 'overdue', 'all'],
|
|
449
|
+
description: '快捷筛选'
|
|
450
|
+
},
|
|
451
|
+
status: {
|
|
452
|
+
type: 'string',
|
|
453
|
+
enum: ['pending', 'completed', 'archived', 'all'],
|
|
454
|
+
description: '状态筛选(与 quick_filter 互斥)'
|
|
455
|
+
},
|
|
456
|
+
sort_by: {
|
|
457
|
+
type: 'string',
|
|
458
|
+
enum: ['sort', 'created_at', 'due_date', 'priority'],
|
|
459
|
+
description: '排序方式(默认created_at)'
|
|
460
|
+
},
|
|
461
|
+
sort_order: {
|
|
462
|
+
type: 'string',
|
|
463
|
+
enum: ['asc', 'desc'],
|
|
464
|
+
description: '排序方向(默认desc)'
|
|
465
|
+
},
|
|
466
|
+
limit: { type: 'number', description: '返回数量限制(默认50)' },
|
|
467
|
+
task_id: { type: 'string', description: '任务ID' },
|
|
468
|
+
operations: {
|
|
469
|
+
type: 'array',
|
|
470
|
+
description: '操作数组',
|
|
471
|
+
items: {
|
|
472
|
+
type: 'object',
|
|
473
|
+
properties: {
|
|
474
|
+
action: { type: 'string', enum: ['add', 'update', 'archive', 'complete'] },
|
|
475
|
+
task_id: { type: 'string' },
|
|
476
|
+
content: { type: 'string' },
|
|
477
|
+
description: { type: 'string' },
|
|
478
|
+
priority: { type: 'number', enum: [1, 2, 3, 4] },
|
|
479
|
+
due_date: { type: 'string' },
|
|
480
|
+
sort: { type: 'number' },
|
|
481
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
482
|
+
status: { type: 'string', enum: ['pending', 'completed', 'archived'] },
|
|
483
|
+
project_id: { type: 'string' }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
transaction: { type: 'boolean', description: '是否使用严格事务(默认false)' },
|
|
488
|
+
task_ids: {
|
|
489
|
+
type: 'array',
|
|
490
|
+
items: { type: 'string' },
|
|
491
|
+
description: '任务ID数组(按新顺序排列)'
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
required: ['method']
|
|
495
|
+
},
|
|
496
|
+
environment: {
|
|
497
|
+
type: 'object',
|
|
498
|
+
properties: {},
|
|
499
|
+
required: []
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 获取业务错误定义
|
|
506
|
+
*/
|
|
507
|
+
getBusinessErrors() {
|
|
508
|
+
return [
|
|
509
|
+
{
|
|
510
|
+
code: 'TASK_NOT_FOUND',
|
|
511
|
+
description: '任务不存在',
|
|
512
|
+
match: /任务不存在|Task not found/i,
|
|
513
|
+
solution: '请检查任务ID是否正确',
|
|
514
|
+
retryable: false
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
code: 'INVALID_PROJECT_ID',
|
|
518
|
+
description: '无效的项目ID',
|
|
519
|
+
match: /无效的项目|Invalid project_id/i,
|
|
520
|
+
solution: '项目ID格式不正确,请使用字母数字和连字符(如 work、personal)。不指定 project_id 则创建会话任务',
|
|
521
|
+
retryable: false
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
code: 'DATABASE_ERROR',
|
|
525
|
+
description: '数据存储失败',
|
|
526
|
+
match: /database|存储|保存/i,
|
|
527
|
+
solution: '请检查工具目录读写权限,或查看日志获取详细信息',
|
|
528
|
+
retryable: true
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
code: 'INVALID_DATE_FORMAT',
|
|
532
|
+
description: '日期格式错误',
|
|
533
|
+
match: /日期格式|date format/i,
|
|
534
|
+
solution: '请使用 ISO 8601 格式(如 2025-01-15)或自然语言(如 tomorrow)',
|
|
535
|
+
retryable: false
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
code: 'INVALID_OPERATION',
|
|
539
|
+
description: '无效的操作类型',
|
|
540
|
+
match: /无效的操作|Invalid operation/i,
|
|
541
|
+
solution: '操作类型必须是 add、update、archive 或 complete',
|
|
542
|
+
retryable: false
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
code: 'MISSING_REQUIRED_PARAM',
|
|
546
|
+
description: '缺少必需参数',
|
|
547
|
+
match: /缺少必需参数|Missing required/i,
|
|
548
|
+
solution: '请检查操作参数,确保必需参数已提供',
|
|
549
|
+
retryable: false
|
|
550
|
+
}
|
|
551
|
+
];
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 执行工具
|
|
556
|
+
*/
|
|
557
|
+
async execute(params) {
|
|
558
|
+
const { api } = this;
|
|
559
|
+
|
|
560
|
+
api?.logger?.info('TodoList 工具执行开始', {
|
|
561
|
+
method: params.method,
|
|
562
|
+
tool: this.__toolName
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
try {
|
|
566
|
+
const store = await this.initializeDatabase();
|
|
567
|
+
|
|
568
|
+
switch (params.method) {
|
|
569
|
+
case 'add_task':
|
|
570
|
+
return await this.addTask(store, params);
|
|
571
|
+
case 'list_tasks':
|
|
572
|
+
return await this.listTasks(store, params);
|
|
573
|
+
case 'update_task':
|
|
574
|
+
return await this.updateTask(store, params);
|
|
575
|
+
case 'complete_task':
|
|
576
|
+
return await this.completeTask(store, params);
|
|
577
|
+
case 'archive_task':
|
|
578
|
+
return await this.archiveTask(store, params);
|
|
579
|
+
case 'batch_tasks':
|
|
580
|
+
return await this.batchTasks(store, params);
|
|
581
|
+
case 'reorder_tasks':
|
|
582
|
+
return await this.reorderTasks(store, params);
|
|
583
|
+
case 'get_statistics':
|
|
584
|
+
return await this.getStatistics(store, params);
|
|
585
|
+
case 'list_projects':
|
|
586
|
+
return await this.listProjects(store, params);
|
|
587
|
+
default:
|
|
588
|
+
throw new Error(`不支持的方法: ${params.method}`);
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
const errorMessage = error?.message || String(error) || '未知错误';
|
|
592
|
+
const errorStack = error?.stack || '无堆栈信息';
|
|
593
|
+
|
|
594
|
+
api?.logger?.error('TodoList 工具执行失败', {
|
|
595
|
+
error: errorMessage,
|
|
596
|
+
stack: errorStack,
|
|
597
|
+
method: params.method,
|
|
598
|
+
errorType: error?.constructor?.name
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
if (error instanceof Error) {
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
throw new Error(errorMessage);
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* 初始化数据库
|
|
611
|
+
*/
|
|
612
|
+
async initializeDatabase() {
|
|
613
|
+
const toolDir = this.__toolDir || path.join(os.homedir(), '.prompt-manager', 'toolbox', this.__toolName || 'todolist');
|
|
614
|
+
const store = await TaskStore.load(toolDir, this.api?.logger);
|
|
615
|
+
return store;
|
|
616
|
+
},
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* 保存数据库到文件
|
|
620
|
+
*/
|
|
621
|
+
async saveDatabase(store) {
|
|
622
|
+
await store.save();
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* 解析日期(支持 ISO 8601 和简单自然语言)
|
|
627
|
+
*/
|
|
628
|
+
parseDate(dateString) {
|
|
629
|
+
if (!dateString) return null;
|
|
630
|
+
|
|
631
|
+
const isoDate = new Date(dateString);
|
|
632
|
+
if (!isNaN(isoDate.getTime())) {
|
|
633
|
+
return isoDate.toISOString().split('T')[0];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const today = new Date();
|
|
637
|
+
const lower = dateString.toLowerCase().trim();
|
|
638
|
+
|
|
639
|
+
if (lower === 'today' || lower === '今天') {
|
|
640
|
+
return today.toISOString().split('T')[0];
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (lower === 'tomorrow' || lower === '明天') {
|
|
644
|
+
const tomorrow = new Date(today);
|
|
645
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
646
|
+
return tomorrow.toISOString().split('T')[0];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (lower.startsWith('in ')) {
|
|
650
|
+
const match = lower.match(/in (\d+) days?/);
|
|
651
|
+
if (match) {
|
|
652
|
+
const days = parseInt(match[1], 10);
|
|
653
|
+
if (!isNaN(days)) {
|
|
654
|
+
const future = new Date(today);
|
|
655
|
+
future.setDate(future.getDate() + days);
|
|
656
|
+
return future.toISOString().split('T')[0];
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 无法解析时返回 null,而不是原字符串
|
|
662
|
+
return null;
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* 获取当前会话ID
|
|
667
|
+
*/
|
|
668
|
+
async getCurrentSessionId() {
|
|
669
|
+
const { api } = this;
|
|
670
|
+
|
|
671
|
+
if (api?.context?.sessionId) {
|
|
672
|
+
return api.context.sessionId;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const storedSessionId = api?.storage?.getItem('todolist_session_id');
|
|
676
|
+
if (storedSessionId) {
|
|
677
|
+
return storedSessionId;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const newSessionId = randomUUID();
|
|
681
|
+
if (api?.storage) {
|
|
682
|
+
api.storage.setItem('todolist_session_id', newSessionId);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return newSessionId;
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* 获取下一个排序值
|
|
690
|
+
*/
|
|
691
|
+
getNextSort(store, projectId, sessionId) {
|
|
692
|
+
const allTasks = store.getAllTasks();
|
|
693
|
+
const scopedTasks = allTasks.filter(task => this.isSameScope(task, projectId, sessionId));
|
|
694
|
+
if (scopedTasks.length === 0) {
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const maxSort = scopedTasks.reduce((max, task) => Math.max(max, typeof task.sort === 'number' ? task.sort : 0), -1);
|
|
699
|
+
return maxSort + 1;
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
isSameScope(task, projectId, sessionId) {
|
|
703
|
+
if (projectId) {
|
|
704
|
+
return task.project_id === projectId && !task.session_id;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return task.session_id === sessionId && !task.project_id;
|
|
708
|
+
},
|
|
709
|
+
|
|
710
|
+
normalizeTagsInput(tags) {
|
|
711
|
+
if (!tags || !Array.isArray(tags) || tags.length === 0) {
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return Array.from(new Set(tags.filter(tag => typeof tag === 'string' && tag.trim() !== '').map(tag => tag.trim())));
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
formatTask(task) {
|
|
719
|
+
return {
|
|
720
|
+
task_id: task.id,
|
|
721
|
+
content: task.content,
|
|
722
|
+
description: task.description || undefined,
|
|
723
|
+
status: task.status,
|
|
724
|
+
priority: task.priority,
|
|
725
|
+
project_id: task.project_id || null,
|
|
726
|
+
session_id: task.session_id || null,
|
|
727
|
+
sort: task.sort,
|
|
728
|
+
tags: task.tags && task.tags.length > 0 ? [...task.tags] : undefined,
|
|
729
|
+
due_date: task.due_date || undefined,
|
|
730
|
+
created_at: task.created_at,
|
|
731
|
+
updated_at: task.updated_at,
|
|
732
|
+
completed_at: task.completed_at || undefined
|
|
733
|
+
};
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* 添加任务
|
|
738
|
+
*/
|
|
739
|
+
async addTask(store, params, options = {}) {
|
|
740
|
+
if (!params.content) {
|
|
741
|
+
throw new Error('缺少必需参数: content');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const taskId = randomUUID();
|
|
745
|
+
const now = new Date().toISOString();
|
|
746
|
+
const projectId = params.project_id || null;
|
|
747
|
+
const sessionId = projectId ? null : await this.getCurrentSessionId();
|
|
748
|
+
const sort = params.sort !== undefined ? params.sort : this.getNextSort(store, projectId, sessionId);
|
|
749
|
+
const dueDate = params.due_date ? this.parseDate(params.due_date) : null;
|
|
750
|
+
const tags = this.normalizeTagsInput(params.tags);
|
|
751
|
+
|
|
752
|
+
const task = {
|
|
753
|
+
id: taskId,
|
|
754
|
+
content: params.content,
|
|
755
|
+
description: params.description || null,
|
|
756
|
+
status: 'pending',
|
|
757
|
+
priority: params.priority || DEFAULT_PRIORITY,
|
|
758
|
+
project_id: projectId,
|
|
759
|
+
session_id: sessionId,
|
|
760
|
+
sort,
|
|
761
|
+
tags,
|
|
762
|
+
due_date: dueDate,
|
|
763
|
+
created_at: now,
|
|
764
|
+
updated_at: now,
|
|
765
|
+
completed_at: null
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
store.addTask(task);
|
|
769
|
+
|
|
770
|
+
if (!options.skipSave) {
|
|
771
|
+
await this.saveDatabase(store);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
this.api?.logger?.info('任务添加成功', { taskId, projectId, sessionId });
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
task_id: taskId,
|
|
778
|
+
content: params.content,
|
|
779
|
+
description: params.description,
|
|
780
|
+
priority: task.priority,
|
|
781
|
+
status: 'pending',
|
|
782
|
+
project_id: projectId,
|
|
783
|
+
session_id: sessionId,
|
|
784
|
+
sort,
|
|
785
|
+
tags: tags.length > 0 ? [...tags] : undefined,
|
|
786
|
+
due_date: dueDate,
|
|
787
|
+
created_at: now
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* 查询任务列表
|
|
793
|
+
*/
|
|
794
|
+
async listTasks(store, params) {
|
|
795
|
+
const projectId = params.project_id || null;
|
|
796
|
+
const sessionId = projectId ? null : await this.getCurrentSessionId();
|
|
797
|
+
|
|
798
|
+
const allTasks = store.getAllTasks();
|
|
799
|
+
let scopedTasks = allTasks.filter(task => this.isSameScope(task, projectId, sessionId));
|
|
800
|
+
|
|
801
|
+
const today = new Date().toISOString().split('T')[0];
|
|
802
|
+
|
|
803
|
+
if (params.quick_filter) {
|
|
804
|
+
scopedTasks = scopedTasks.filter(task => {
|
|
805
|
+
switch (params.quick_filter) {
|
|
806
|
+
case 'today':
|
|
807
|
+
return task.status === 'pending' && task.due_date === today;
|
|
808
|
+
case 'pending':
|
|
809
|
+
return task.status === 'pending';
|
|
810
|
+
case 'completed':
|
|
811
|
+
return task.status === 'completed';
|
|
812
|
+
case 'overdue':
|
|
813
|
+
return task.status === 'pending' && task.due_date && task.due_date < today;
|
|
814
|
+
case 'all':
|
|
815
|
+
return true;
|
|
816
|
+
default:
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
} else if (params.status) {
|
|
821
|
+
if (params.status !== 'all') {
|
|
822
|
+
scopedTasks = scopedTasks.filter(task => task.status === params.status);
|
|
823
|
+
}
|
|
824
|
+
} else {
|
|
825
|
+
scopedTasks = scopedTasks.filter(task => task.status === 'pending');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (params.priority) {
|
|
829
|
+
scopedTasks = scopedTasks.filter(task => task.priority === params.priority);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (params.tags && Array.isArray(params.tags) && params.tags.length > 0) {
|
|
833
|
+
scopedTasks = scopedTasks.filter(task => {
|
|
834
|
+
if (!task.tags || task.tags.length === 0) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
return params.tags.some(tag => task.tags.includes(tag));
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const sortBy = params.sort_by || 'created_at';
|
|
842
|
+
const sortOrder = params.sort_order === 'asc' ? 1 : -1;
|
|
843
|
+
|
|
844
|
+
scopedTasks.sort((a, b) => {
|
|
845
|
+
const valueA = a[sortBy] ?? null;
|
|
846
|
+
const valueB = b[sortBy] ?? null;
|
|
847
|
+
|
|
848
|
+
if (valueA === valueB) {
|
|
849
|
+
return 0;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (valueA === null) {
|
|
853
|
+
return 1 * sortOrder;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (valueB === null) {
|
|
857
|
+
return -1 * sortOrder;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (valueA > valueB) {
|
|
861
|
+
return 1 * sortOrder;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return -1 * sortOrder;
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
const total = scopedTasks.length;
|
|
868
|
+
const limit = params.limit || DEFAULT_LIMIT;
|
|
869
|
+
const tasks = scopedTasks.slice(0, limit).map(task => this.formatTask(task));
|
|
870
|
+
|
|
871
|
+
this.api?.logger?.info('任务查询成功', {
|
|
872
|
+
total,
|
|
873
|
+
count: tasks.length,
|
|
874
|
+
projectId,
|
|
875
|
+
sessionId
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
return {
|
|
879
|
+
total,
|
|
880
|
+
tasks
|
|
881
|
+
};
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* 更新任务
|
|
886
|
+
*/
|
|
887
|
+
async updateTask(store, params, options = {}) {
|
|
888
|
+
if (!params.task_id) {
|
|
889
|
+
throw new Error('缺少必需参数: task_id');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const task = store.findTask(params.task_id);
|
|
893
|
+
if (!task) {
|
|
894
|
+
throw new Error('任务不存在');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const updates = {};
|
|
898
|
+
const now = new Date().toISOString();
|
|
899
|
+
|
|
900
|
+
if (params.content !== undefined) {
|
|
901
|
+
updates.content = params.content;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (params.description !== undefined) {
|
|
905
|
+
updates.description = params.description || null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (params.priority !== undefined) {
|
|
909
|
+
updates.priority = params.priority;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (params.due_date !== undefined) {
|
|
913
|
+
updates.due_date = params.due_date ? this.parseDate(params.due_date) : null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (params.status !== undefined) {
|
|
917
|
+
updates.status = params.status;
|
|
918
|
+
if (params.status === 'completed' && task.status !== 'completed') {
|
|
919
|
+
updates.completed_at = now;
|
|
920
|
+
} else if (params.status !== 'completed' && task.status === 'completed') {
|
|
921
|
+
updates.completed_at = null;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (params.sort !== undefined) {
|
|
926
|
+
updates.sort = params.sort;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (params.tags !== undefined) {
|
|
930
|
+
updates.tags = this.normalizeTagsInput(params.tags);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (Object.keys(updates).length === 0) {
|
|
934
|
+
return this.formatTask(task);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
updates.updated_at = now;
|
|
938
|
+
const updated = store.updateTask(params.task_id, updates);
|
|
939
|
+
if (!updated) {
|
|
940
|
+
throw new Error('任务不存在');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!options.skipSave) {
|
|
944
|
+
await this.saveDatabase(store);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
this.api?.logger?.info('任务更新成功', { task_id: params.task_id });
|
|
948
|
+
|
|
949
|
+
const updatedTask = store.findTask(params.task_id);
|
|
950
|
+
return this.formatTask(updatedTask);
|
|
951
|
+
},
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* 完成任务
|
|
955
|
+
*/
|
|
956
|
+
async completeTask(store, params, options = {}) {
|
|
957
|
+
if (!params.task_id) {
|
|
958
|
+
throw new Error('缺少必需参数: task_id');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const task = store.findTask(params.task_id);
|
|
962
|
+
if (!task) {
|
|
963
|
+
throw new Error('任务不存在');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const now = new Date().toISOString();
|
|
967
|
+
const updated = store.updateTask(params.task_id, {
|
|
968
|
+
status: 'completed',
|
|
969
|
+
completed_at: now,
|
|
970
|
+
updated_at: now
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
if (!updated) {
|
|
974
|
+
throw new Error('任务不存在');
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (!options.skipSave) {
|
|
978
|
+
await this.saveDatabase(store);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
this.api?.logger?.info('任务完成', { task_id: params.task_id });
|
|
982
|
+
|
|
983
|
+
return {
|
|
984
|
+
task_id: params.task_id,
|
|
985
|
+
status: 'completed',
|
|
986
|
+
completed_at: now
|
|
987
|
+
};
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* 归档任务
|
|
992
|
+
*/
|
|
993
|
+
async archiveTask(store, params, options = {}) {
|
|
994
|
+
if (!params.task_id) {
|
|
995
|
+
throw new Error('缺少必需参数: task_id');
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const task = store.findTask(params.task_id);
|
|
999
|
+
if (!task) {
|
|
1000
|
+
throw new Error('任务不存在');
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const now = new Date().toISOString();
|
|
1004
|
+
const updated = store.updateTask(params.task_id, {
|
|
1005
|
+
status: 'archived',
|
|
1006
|
+
updated_at: now
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
if (!updated) {
|
|
1010
|
+
throw new Error('任务不存在');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (!options.skipSave) {
|
|
1014
|
+
await this.saveDatabase(store);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
this.api?.logger?.info('任务归档', { task_id: params.task_id });
|
|
1018
|
+
|
|
1019
|
+
return {
|
|
1020
|
+
task_id: params.task_id,
|
|
1021
|
+
status: 'archived',
|
|
1022
|
+
updated_at: now
|
|
1023
|
+
};
|
|
1024
|
+
},
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* 批量操作
|
|
1028
|
+
*/
|
|
1029
|
+
async batchTasks(store, params) {
|
|
1030
|
+
if (!params.operations || !Array.isArray(params.operations) || params.operations.length === 0) {
|
|
1031
|
+
throw new Error('缺少必需参数: operations');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const projectId = params.project_id || null;
|
|
1035
|
+
const sessionId = projectId ? null : await this.getCurrentSessionId();
|
|
1036
|
+
const useTransaction = params.transaction === true;
|
|
1037
|
+
|
|
1038
|
+
const results = [];
|
|
1039
|
+
let succeeded = 0;
|
|
1040
|
+
let failed = 0;
|
|
1041
|
+
|
|
1042
|
+
if (useTransaction) {
|
|
1043
|
+
const snapshot = {
|
|
1044
|
+
active: JSON.parse(JSON.stringify(store.getActiveTasks())),
|
|
1045
|
+
archived: JSON.parse(JSON.stringify(store.getArchivedTasks()))
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
try {
|
|
1049
|
+
for (let i = 0; i < params.operations.length; i++) {
|
|
1050
|
+
const op = params.operations[i];
|
|
1051
|
+
const result = await this.performBatchOperation(store, op, {
|
|
1052
|
+
projectId,
|
|
1053
|
+
sessionId,
|
|
1054
|
+
skipSave: true
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
results.push({
|
|
1058
|
+
index: i,
|
|
1059
|
+
action: op.action,
|
|
1060
|
+
success: true,
|
|
1061
|
+
task_id: result?.task_id || result?.task?.id || null,
|
|
1062
|
+
error: null
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
succeeded++;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
await this.saveDatabase(store);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
const normalizedActive = snapshot.active.map(task => TaskStore.normalizeTask(task)).filter(Boolean);
|
|
1071
|
+
const normalizedArchived = snapshot.archived.map(task => TaskStore.normalizeTask(task)).filter(Boolean);
|
|
1072
|
+
|
|
1073
|
+
store.activeTasks = normalizedActive;
|
|
1074
|
+
store.archivedTasks = normalizedArchived;
|
|
1075
|
+
store.activeModified = true;
|
|
1076
|
+
store.archivedModified = true;
|
|
1077
|
+
await this.saveDatabase(store);
|
|
1078
|
+
|
|
1079
|
+
for (let i = 0; i < params.operations.length; i++) {
|
|
1080
|
+
results.push({
|
|
1081
|
+
index: i,
|
|
1082
|
+
action: params.operations[i].action,
|
|
1083
|
+
success: false,
|
|
1084
|
+
task_id: params.operations[i].task_id || null,
|
|
1085
|
+
error: error.message
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
failed = params.operations.length;
|
|
1090
|
+
|
|
1091
|
+
return {
|
|
1092
|
+
success: false,
|
|
1093
|
+
total: params.operations.length,
|
|
1094
|
+
succeeded: 0,
|
|
1095
|
+
failed,
|
|
1096
|
+
results
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
} else {
|
|
1100
|
+
for (let i = 0; i < params.operations.length; i++) {
|
|
1101
|
+
const op = params.operations[i];
|
|
1102
|
+
|
|
1103
|
+
try {
|
|
1104
|
+
const result = await this.performBatchOperation(store, op, {
|
|
1105
|
+
projectId,
|
|
1106
|
+
sessionId,
|
|
1107
|
+
skipSave: false
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
results.push({
|
|
1111
|
+
index: i,
|
|
1112
|
+
action: op.action,
|
|
1113
|
+
success: true,
|
|
1114
|
+
task_id: result?.task_id || result?.task?.id || null,
|
|
1115
|
+
error: null
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
succeeded++;
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
results.push({
|
|
1121
|
+
index: i,
|
|
1122
|
+
action: op.action,
|
|
1123
|
+
success: false,
|
|
1124
|
+
task_id: op.task_id || null,
|
|
1125
|
+
error: error.message
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
failed++;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
this.api?.logger?.info('批量操作完成', {
|
|
1134
|
+
total: params.operations.length,
|
|
1135
|
+
succeeded,
|
|
1136
|
+
failed
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
return {
|
|
1140
|
+
success: failed === 0,
|
|
1141
|
+
total: params.operations.length,
|
|
1142
|
+
succeeded,
|
|
1143
|
+
failed,
|
|
1144
|
+
results
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
|
|
1148
|
+
async performBatchOperation(store, op, options) {
|
|
1149
|
+
const payload = { ...op };
|
|
1150
|
+
|
|
1151
|
+
if (op.action === 'add') {
|
|
1152
|
+
payload.project_id = op.project_id || options.projectId;
|
|
1153
|
+
return await this.addTask(store, payload, { skipSave: options.skipSave });
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (op.action === 'update') {
|
|
1157
|
+
return await this.updateTask(store, payload, { skipSave: options.skipSave });
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (op.action === 'complete') {
|
|
1161
|
+
return await this.completeTask(store, payload, { skipSave: options.skipSave });
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (op.action === 'archive') {
|
|
1165
|
+
return await this.archiveTask(store, payload, { skipSave: options.skipSave });
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
throw new Error(`无效的操作类型: ${op.action}`);
|
|
1169
|
+
},
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* 调整排序
|
|
1173
|
+
*/
|
|
1174
|
+
async reorderTasks(store, params) {
|
|
1175
|
+
if (!params.task_ids || !Array.isArray(params.task_ids) || params.task_ids.length === 0) {
|
|
1176
|
+
throw new Error('缺少必需参数: task_ids');
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const projectId = params.project_id || null;
|
|
1180
|
+
const sessionId = projectId ? null : await this.getCurrentSessionId();
|
|
1181
|
+
const now = new Date().toISOString();
|
|
1182
|
+
|
|
1183
|
+
const total = params.task_ids.length;
|
|
1184
|
+
params.task_ids.forEach((taskId, index) => {
|
|
1185
|
+
const task = store.findTask(taskId);
|
|
1186
|
+
if (task && this.isSameScope(task, projectId, sessionId)) {
|
|
1187
|
+
store.updateTask(taskId, {
|
|
1188
|
+
sort: total - index - 1,
|
|
1189
|
+
updated_at: now
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
await this.saveDatabase(store);
|
|
1195
|
+
|
|
1196
|
+
this.api?.logger?.info('排序调整完成', { total, projectId, sessionId });
|
|
1197
|
+
|
|
1198
|
+
return {
|
|
1199
|
+
success: true,
|
|
1200
|
+
total,
|
|
1201
|
+
task_ids: params.task_ids
|
|
1202
|
+
};
|
|
1203
|
+
},
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* 获取统计信息
|
|
1207
|
+
*/
|
|
1208
|
+
async getStatistics(store, params) {
|
|
1209
|
+
const allTasks = store.getAllTasks();
|
|
1210
|
+
let scopedTasks;
|
|
1211
|
+
|
|
1212
|
+
if (params.project_id === null) {
|
|
1213
|
+
scopedTasks = allTasks.filter(task => task.project_id && !task.session_id);
|
|
1214
|
+
} else if (params.project_id) {
|
|
1215
|
+
scopedTasks = allTasks.filter(task => task.project_id === params.project_id && !task.session_id);
|
|
1216
|
+
} else {
|
|
1217
|
+
const sessionId = await this.getCurrentSessionId();
|
|
1218
|
+
scopedTasks = allTasks.filter(task => task.session_id === sessionId && !task.project_id);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const pending = scopedTasks.filter(task => task.status === 'pending').length;
|
|
1222
|
+
const completed = scopedTasks.filter(task => task.status === 'completed').length;
|
|
1223
|
+
const archived = scopedTasks.filter(task => task.status === 'archived').length;
|
|
1224
|
+
const overdue = scopedTasks.filter(task => task.status === 'pending' && task.due_date && task.due_date < new Date().toISOString().split('T')[0]).length;
|
|
1225
|
+
|
|
1226
|
+
const byPriority = { 1: 0, 2: 0, 3: 0, 4: 0 };
|
|
1227
|
+
scopedTasks.forEach(task => {
|
|
1228
|
+
if (byPriority[task.priority] !== undefined) {
|
|
1229
|
+
byPriority[task.priority] += 1;
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
const byTag = {};
|
|
1234
|
+
scopedTasks.forEach(task => {
|
|
1235
|
+
if (!task.tags) return;
|
|
1236
|
+
task.tags.forEach(tag => {
|
|
1237
|
+
byTag[tag] = (byTag[tag] || 0) + 1;
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
const total = scopedTasks.length;
|
|
1242
|
+
const completionRate = total > 0 ? completed / total : 0;
|
|
1243
|
+
|
|
1244
|
+
this.api?.logger?.info('统计信息获取成功', {
|
|
1245
|
+
total,
|
|
1246
|
+
project_id: params.project_id
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
return {
|
|
1250
|
+
total,
|
|
1251
|
+
pending,
|
|
1252
|
+
completed,
|
|
1253
|
+
archived,
|
|
1254
|
+
overdue,
|
|
1255
|
+
completion_rate: completionRate,
|
|
1256
|
+
by_priority: byPriority,
|
|
1257
|
+
by_tag: byTag
|
|
1258
|
+
};
|
|
1259
|
+
},
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* 列出项目
|
|
1263
|
+
*/
|
|
1264
|
+
async listProjects(store) {
|
|
1265
|
+
const projectMap = new Map();
|
|
1266
|
+
const allTasks = store.getAllTasks();
|
|
1267
|
+
|
|
1268
|
+
allTasks.forEach(task => {
|
|
1269
|
+
if (task.project_id && !task.session_id) {
|
|
1270
|
+
const stats = projectMap.get(task.project_id) || {
|
|
1271
|
+
project_id: task.project_id,
|
|
1272
|
+
task_count: 0,
|
|
1273
|
+
pending_count: 0,
|
|
1274
|
+
completed_count: 0
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
if (task.status === 'pending' || task.status === 'completed') {
|
|
1278
|
+
stats.task_count += 1;
|
|
1279
|
+
if (task.status === 'pending') {
|
|
1280
|
+
stats.pending_count += 1;
|
|
1281
|
+
} else if (task.status === 'completed') {
|
|
1282
|
+
stats.completed_count += 1;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
projectMap.set(task.project_id, stats);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const projects = Array.from(projectMap.values()).filter(project => project.task_count > 0).sort((a, b) => a.project_id.localeCompare(b.project_id));
|
|
1291
|
+
|
|
1292
|
+
const sessionId = await this.getCurrentSessionId();
|
|
1293
|
+
const sessionTasks = allTasks.filter(task => task.session_id === sessionId && !task.project_id);
|
|
1294
|
+
|
|
1295
|
+
const sessionStats = {
|
|
1296
|
+
session_id: sessionId,
|
|
1297
|
+
task_count: sessionTasks.length,
|
|
1298
|
+
pending_count: sessionTasks.filter(task => task.status === 'pending').length,
|
|
1299
|
+
completed_count: sessionTasks.filter(task => task.status === 'completed').length
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
this.api?.logger?.info('项目列表获取成功', {
|
|
1303
|
+
projectCount: projects.length,
|
|
1304
|
+
sessionId
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
return {
|
|
1308
|
+
projects,
|
|
1309
|
+
current_session: sessionStats
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
};
|