mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
🔨 chore(task): add participants to task.list response (#13778)
* ✨ feat(task): add participants array to task.list response Return a participants array per task (id / type / avatar / name) so clients can show avatar groups on task cards. For now participants only contains the assignee agent; future iterations can aggregate comment authors and topic executors. Also extract TaskItem into @lobechat/types as an explicit type definition so it no longer relies on drizzle schema inference. * ♻️ refactor(task): extract NewTask to @lobechat/types Remove the drizzle $inferInsert NewTask from schemas and define it explicitly in @lobechat/types alongside TaskItem. * ✅ test(task): cover participants in task.list response
This commit is contained in:
parent
f439fb913a
commit
f0f2feb015
5 changed files with 141 additions and 7 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import type {
|
||||
CheckpointConfig,
|
||||
NewTask,
|
||||
TaskItem,
|
||||
WorkspaceData,
|
||||
WorkspaceDocNode,
|
||||
WorkspaceTreeNode,
|
||||
|
|
@ -8,7 +10,7 @@ import { and, desc, eq, inArray, isNotNull, isNull, ne, sql } from 'drizzle-orm'
|
|||
|
||||
import { merge } from '@/utils/merge';
|
||||
|
||||
import type { NewTask, NewTaskComment, TaskCommentItem, TaskItem } from '../schemas/task';
|
||||
import type { NewTaskComment, TaskCommentItem } from '../schemas/task';
|
||||
import { taskComments, taskDependencies, taskDocuments, tasks } from '../schemas/task';
|
||||
import type { LobeChatDatabase } from '../type';
|
||||
|
||||
|
|
@ -307,7 +309,7 @@ export class TaskModel {
|
|||
SELECT * FROM task_tree
|
||||
`);
|
||||
|
||||
return result.rows as TaskItem[];
|
||||
return result.rows as unknown as TaskItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -101,9 +101,6 @@ export const tasks = pgTable(
|
|||
],
|
||||
);
|
||||
|
||||
export type NewTask = typeof tasks.$inferInsert;
|
||||
export type TaskItem = typeof tasks.$inferSelect;
|
||||
|
||||
// ── Task Dependencies ────────────────────────────────────
|
||||
|
||||
export const taskDependencies = pgTable(
|
||||
|
|
|
|||
|
|
@ -38,6 +38,85 @@ export interface TaskTopicHandoff {
|
|||
title?: string;
|
||||
}
|
||||
|
||||
// ── Task list item (shared between router response and client) ──
|
||||
|
||||
export interface TaskParticipant {
|
||||
avatar: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'user' | 'agent';
|
||||
}
|
||||
|
||||
export interface TaskItem {
|
||||
accessedAt: Date;
|
||||
assigneeAgentId: string | null;
|
||||
assigneeUserId: string | null;
|
||||
completedAt: Date | null;
|
||||
config: unknown;
|
||||
context: unknown;
|
||||
createdAt: Date;
|
||||
createdByAgentId: string | null;
|
||||
createdByUserId: string;
|
||||
currentTopicId: string | null;
|
||||
description: string | null;
|
||||
error: string | null;
|
||||
heartbeatInterval: number | null;
|
||||
heartbeatTimeout: number | null;
|
||||
id: string;
|
||||
identifier: string;
|
||||
instruction: string;
|
||||
lastHeartbeatAt: Date | null;
|
||||
maxTopics: number | null;
|
||||
name: string | null;
|
||||
parentTaskId: string | null;
|
||||
priority: number | null;
|
||||
schedulePattern: string | null;
|
||||
scheduleTimezone: string | null;
|
||||
seq: number;
|
||||
sortOrder: number | null;
|
||||
startedAt: Date | null;
|
||||
status: string;
|
||||
totalTopics: number | null;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type TaskListItem = TaskItem & {
|
||||
participants: TaskParticipant[];
|
||||
};
|
||||
|
||||
export interface NewTask {
|
||||
accessedAt?: Date;
|
||||
assigneeAgentId?: string | null;
|
||||
assigneeUserId?: string | null;
|
||||
completedAt?: Date | null;
|
||||
config?: unknown;
|
||||
context?: unknown;
|
||||
createdAt?: Date;
|
||||
createdByAgentId?: string | null;
|
||||
createdByUserId: string;
|
||||
currentTopicId?: string | null;
|
||||
description?: string | null;
|
||||
error?: string | null;
|
||||
heartbeatInterval?: number | null;
|
||||
heartbeatTimeout?: number | null;
|
||||
id?: string;
|
||||
identifier: string;
|
||||
instruction: string;
|
||||
lastHeartbeatAt?: Date | null;
|
||||
maxTopics?: number | null;
|
||||
name?: string | null;
|
||||
parentTaskId?: string | null;
|
||||
priority?: number | null;
|
||||
schedulePattern?: string | null;
|
||||
scheduleTimezone?: string | null;
|
||||
seq: number;
|
||||
sortOrder?: number | null;
|
||||
startedAt?: Date | null;
|
||||
status?: string;
|
||||
totalTopics?: number | null;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
// ── Task Detail (shared across CLI, viewTask tool, task.detail router) ──
|
||||
|
||||
export interface TaskDetailSubtask {
|
||||
|
|
|
|||
|
|
@ -419,6 +419,31 @@ describe('Task Router Integration', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('list participants', () => {
|
||||
it('should populate participants from assignee agent', async () => {
|
||||
const { agents } = await import('@/database/schemas');
|
||||
const { eq } = await import('drizzle-orm');
|
||||
await serverDB
|
||||
.update(agents)
|
||||
.set({ avatar: 'avatar.png', title: 'Agent One' })
|
||||
.where(eq(agents.id, testAgentId));
|
||||
|
||||
await caller.create({ assigneeAgentId: testAgentId, instruction: 'Task A' });
|
||||
await caller.create({ instruction: 'Task without assignee' });
|
||||
|
||||
const list = await caller.list({});
|
||||
expect(list.data).toHaveLength(2);
|
||||
|
||||
const assigned = list.data.find((t) => t.assigneeAgentId === testAgentId)!;
|
||||
expect(assigned.participants).toEqual([
|
||||
{ avatar: 'avatar.png', id: testAgentId, name: 'Agent One', type: 'agent' },
|
||||
]);
|
||||
|
||||
const unassigned = list.data.find((t) => !t.assigneeAgentId)!;
|
||||
expect(unassigned.participants).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('heartbeat timeout detection', () => {
|
||||
it('should auto-detect timeout on detail and pause task', async () => {
|
||||
const task = await caller.create({
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@ import { TaskIdentifier as TaskSkillIdentifier } from '@lobechat/builtin-skills'
|
|||
import { BriefIdentifier } from '@lobechat/builtin-tool-brief';
|
||||
import { NotebookIdentifier } from '@lobechat/builtin-tool-notebook';
|
||||
import { buildTaskRunPrompt } from '@lobechat/prompts';
|
||||
import type { TaskTopicHandoff, WorkspaceData } from '@lobechat/types';
|
||||
import type {
|
||||
TaskListItem,
|
||||
TaskParticipant,
|
||||
TaskTopicHandoff,
|
||||
WorkspaceData,
|
||||
} from '@lobechat/types';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AgentModel } from '@/database/models/agent';
|
||||
import { BriefModel } from '@/database/models/brief';
|
||||
import { TaskModel } from '@/database/models/task';
|
||||
import { TaskTopicModel } from '@/database/models/taskTopic';
|
||||
|
|
@ -21,6 +27,7 @@ const taskProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
|
|||
const { ctx } = opts;
|
||||
return opts.next({
|
||||
ctx: {
|
||||
agentModel: new AgentModel(ctx.serverDB, ctx.userId),
|
||||
briefModel: new BriefModel(ctx.serverDB, ctx.userId),
|
||||
taskLifecycle: new TaskLifecycleService(ctx.serverDB, ctx.userId),
|
||||
taskModel: new TaskModel(ctx.serverDB, ctx.userId),
|
||||
|
|
@ -721,7 +728,31 @@ export const taskRouter = router({
|
|||
try {
|
||||
const model = ctx.taskModel;
|
||||
const result = await model.list(input);
|
||||
return { data: result.tasks, success: true, total: result.total };
|
||||
|
||||
const assigneeIds = [
|
||||
...new Set(result.tasks.map((t) => t.assigneeAgentId).filter((id): id is string => !!id)),
|
||||
];
|
||||
const agents =
|
||||
assigneeIds.length > 0 ? await ctx.agentModel.getAgentAvatarsByIds(assigneeIds) : [];
|
||||
const agentMap = new Map(agents.map((a) => [a.id, a]));
|
||||
|
||||
const data: TaskListItem[] = result.tasks.map((task) => {
|
||||
const participants: TaskParticipant[] = [];
|
||||
if (task.assigneeAgentId) {
|
||||
const agent = agentMap.get(task.assigneeAgentId);
|
||||
if (agent) {
|
||||
participants.push({
|
||||
avatar: agent.avatar,
|
||||
id: agent.id,
|
||||
name: agent.title ?? '',
|
||||
type: 'agent',
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ...task, participants };
|
||||
});
|
||||
|
||||
return { data, success: true, total: result.total };
|
||||
} catch (error) {
|
||||
console.error('[task:list]', error);
|
||||
throw new TRPCError({
|
||||
|
|
|
|||
Loading…
Reference in a new issue