feat(tasks): 任务列表支持批量修改分类

勾选多个任务后可一次性移动到目标分类,或选「未分类」把它们移出当前分类。

- db: 新增 SetTasksCategory,在单个事务里校验分类存在性并 UPDATE ... WHERE
  id=ANY(...),返回真正被更新的 id 和刷新后的分类(含 task_count)。分类检查
  放在事务内,避免并发删除把 UPDATE 变成外键冲突。单批上限 100。
- server: 新增 POST /api/tasks/category/batch。写入原子,响应里的逐任务失败
  只可能是勾选后又被删掉的任务;顺带把 category_id 的解析(必填但可为 null)
  抽成 parseCategoryIDField 与单任务接口共用。
- server: 把批量 id 的 trim/归一/去重逻辑从 controlTasksBatch 抽成
  normalizeBatchTaskIDs,两个批量端点共用同一套重复判定。
- manager: SetTasksCategory 在 taskStateMu 内完成 DB 写入与运行中任务 DTO
  刷新,防止与单任务更新交错后留下过期的 category_name。
- web: 选中工具栏新增「修改分类」对话框(需确认目标,避免误点批量改动);
  「未分类」哨兵值统一提取为 UNCATEGORIZED_VALUE。

验证: go build / vet / gofmt、db 与 server 新增测试连本地 PG 实跑通过、
tsc --noEmit、biome check、next build 均通过。
This commit is contained in:
Autumn-27 committed 2026-08-24 09:16:44 -04:00
1 parent 3eca390089
commit fea78b2d6f
12 files changed
+607 -47

No files matched your search

+7
View File
@@ -12,6 +12,7 @@ import type {
AgentTrigger,
Asset,
Audit,
BatchCategoryItem,
BatchControlItem,
ChatAttachment,
CommandRecord,
@@ -218,6 +219,12 @@ export const api = {
deleteTaskCategory: (id: number) => del<{ deleted: number }>(`/task-categories/${id}`),
updateTaskCategory: (taskId: string, categoryId?: number) =>
patch<Task>(`/tasks/${taskId}/category`, { category_id: categoryId ?? null }),
// categoryId 省略/undefined = 移出分类(后端收到 null)
updateTasksCategory: (taskIds: string[], categoryId?: number) =>
post<{ items: BatchCategoryItem[]; category: TaskCategory | null }>("/tasks/category/batch", {
task_ids: taskIds,
category_id: categoryId ?? null,
}),
taskTemplates: () => get<{ templates: TaskTemplate[] }>("/task-templates").then((r) => arr(r.templates)),
createTaskTemplate: (input: Pick<TaskTemplate, "name" | "description" | "goal">) =>
post<TaskTemplate>("/task-templates", input),
+19
View File
@@ -356,6 +356,25 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
}
return { deleted: categoryID };
}
if (path === "/tasks/category/batch" && m === "POST") {
const requested = Array.isArray(b.task_ids) ? b.task_ids.map(String) : [];
const taskIDs = [...new Set(requested)];
if (taskIDs.length === 0 || taskIDs.length > 100) throw new Error("task_ids 数量必须为 1-100");
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("任务分类不存在");
const items = taskIDs.map((id) => {
const task = mockTasks.find((item) => item.id === id);
if (!task) return { id, ok: false, error: "task not found" };
task.category_id = category?.id;
task.category_name = category?.name;
return { id, ok: true };
});
return {
items,
category: category ? mockTaskCategorySnapshot().find((item) => item.id === category.id) : null,
};
}
if (seg[0] === "tasks" && seg[2] === "category" && seg.length === 3 && m === "PATCH") {
const task = mockTasks.find((item) => item.id === seg[1]);
if (!task) throw new Error("任务不存在");
+7
View File
@@ -624,6 +624,13 @@ export interface BatchControlItem {
error?: string;
}
// 批量改分类的逐任务结果。失败只可能是任务已被删除,分类本身的写入是原子的。
export interface BatchCategoryItem {
id: string;
ok: boolean;
error?: string;
}
// Whole-task (all agents) token aggregate.
export interface TokenTotal {
input_tokens: number;