mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
🔨 chore: return full brief data in task activities (#13914)
* ✨ feat: return full brief data in task activities (LOBE-7266) The activity feed for tasks previously emitted a stripped `brief` row that concatenated `resolvedAction` and `resolvedComment` and omitted everything BriefCard needs (taskId, topicId, agentId, cronJobId, agents, actions, artifacts, readAt, resolvedAt, etc.). Map the full `BriefItem` into each activity row and reuse `BriefService.enrichBriefsWithAgents` to populate the participant avatars. The CLI and prompt formatter now compose the action + comment display string themselves. * 🐛 fix: degrade gracefully when brief agent enrichment fails getTaskDetail was calling BriefService.enrichBriefsWithAgents inside Promise.all without a fallback, so a failure in the agent-tree lookup would reject the whole request — a regression vs. the existing .catch(() => []) pattern used by other activity reads in this method. Fall back to agentless briefs on error so the task detail keeps rendering.
This commit is contained in:
parent
828175f8f0
commit
34b60e1842
5 changed files with 143 additions and 12 deletions
|
|
@ -466,7 +466,12 @@ export function registerTaskCommand(program: Command) {
|
|||
: act.priority === 'normal'
|
||||
? pc.yellow(' [normal]')
|
||||
: '';
|
||||
const resolved = act.resolvedAction ? pc.green(` ✏️ ${act.resolvedAction}`) : '';
|
||||
const resolvedLabel = act.resolvedAction
|
||||
? act.resolvedComment
|
||||
? `${act.resolvedAction}: ${act.resolvedComment}`
|
||||
: act.resolvedAction
|
||||
: '';
|
||||
const resolved = resolvedLabel ? pc.green(` ✏️ ${resolvedLabel}`) : '';
|
||||
const typeLabel = pc.dim(`[${act.briefType}]`);
|
||||
console.log(
|
||||
` ${icon} ${pc.dim(ago.padStart(7))} Brief ${typeLabel} ${act.title}${pri}${resolved}${idSuffix}`,
|
||||
|
|
|
|||
|
|
@ -210,7 +210,12 @@ export const formatTaskDetail = (t: TaskDetailData): string => {
|
|||
` 💬 ${act.time || ''} Topic #${act.seq || '?'} ${act.title || 'Untitled'} ${statusIcon(status)} ${status}${idSuffix}`,
|
||||
);
|
||||
} else if (act.type === 'brief') {
|
||||
const resolved = act.resolvedAction ? ` ✏️ ${act.resolvedAction}` : '';
|
||||
const resolvedLabel = act.resolvedAction
|
||||
? act.resolvedComment
|
||||
? `${act.resolvedAction}: ${act.resolvedComment}`
|
||||
: act.resolvedAction
|
||||
: '';
|
||||
const resolved = resolvedLabel ? ` ✏️ ${resolvedLabel}` : '';
|
||||
const priStr = act.priority ? ` [${act.priority}]` : '';
|
||||
lines.push(
|
||||
` ${briefIcon(act.briefType || '')} ${act.time || ''} Brief [${act.briefType}] ${act.title}${priStr}${resolved}${idSuffix}`,
|
||||
|
|
|
|||
|
|
@ -146,20 +146,38 @@ export interface TaskDetailActivityAuthor {
|
|||
type: 'agent' | 'user';
|
||||
}
|
||||
|
||||
export interface TaskDetailActivityAgent {
|
||||
avatar: string | null;
|
||||
backgroundColor: string | null;
|
||||
id: string;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export interface TaskDetailActivity {
|
||||
actions?: unknown;
|
||||
agentId?: string | null;
|
||||
agents?: TaskDetailActivityAgent[];
|
||||
artifacts?: unknown;
|
||||
author?: TaskDetailActivityAuthor;
|
||||
briefType?: string;
|
||||
content?: string;
|
||||
createdAt?: string;
|
||||
cronJobId?: string | null;
|
||||
id?: string;
|
||||
priority?: string | null;
|
||||
readAt?: string | null;
|
||||
resolvedAction?: string | null;
|
||||
resolvedAt?: string | null;
|
||||
resolvedComment?: string | null;
|
||||
seq?: number | null;
|
||||
status?: string | null;
|
||||
summary?: string;
|
||||
taskId?: string | null;
|
||||
time?: string;
|
||||
title?: string;
|
||||
topicId?: string | null;
|
||||
type: 'brief' | 'comment' | 'topic';
|
||||
userId?: string | null;
|
||||
}
|
||||
|
||||
export interface TaskDetailData {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ describe('TaskService', () => {
|
|||
getDependencies: vi.fn(),
|
||||
getDependenciesByTaskIds: vi.fn(),
|
||||
getReviewConfig: vi.fn(),
|
||||
getTreeAgentIdsForTaskIds: vi.fn().mockResolvedValue({}),
|
||||
getTreePinnedDocuments: vi.fn(),
|
||||
resolve: vi.fn(),
|
||||
};
|
||||
|
|
@ -874,7 +875,7 @@ describe('TaskService', () => {
|
|||
expect(result?.workspace).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should build brief activities with resolvedAction concatenated with resolvedComment', async () => {
|
||||
it('should build brief activities with full BriefItem fields', async () => {
|
||||
const task = {
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
|
|
@ -896,14 +897,23 @@ describe('TaskService', () => {
|
|||
|
||||
const briefs = [
|
||||
{
|
||||
actions: [{ key: 'approve', label: '✅', type: 'resolve' }],
|
||||
agentId: 'agent-1',
|
||||
artifacts: ['doc_1'],
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
cronJobId: null,
|
||||
id: 'brief-1',
|
||||
priority: 'urgent',
|
||||
readAt: new Date('2024-01-01T01:00:00Z'),
|
||||
resolvedAction: 'approved',
|
||||
resolvedAt: new Date('2024-01-01T02:00:00Z'),
|
||||
resolvedComment: 'looks good',
|
||||
summary: 'Review brief',
|
||||
taskId: 'task_001',
|
||||
title: 'Approval',
|
||||
topicId: null,
|
||||
type: 'decision',
|
||||
userId: 'user-1',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -917,22 +927,38 @@ describe('TaskService', () => {
|
|||
mockTaskModel.findByIds.mockResolvedValue([]);
|
||||
mockTaskModel.getCheckpointConfig.mockReturnValue({});
|
||||
mockTaskModel.getReviewConfig.mockReturnValue(undefined);
|
||||
mockTaskModel.getTreeAgentIdsForTaskIds.mockResolvedValue({ task_001: ['agent-1'] });
|
||||
mockAgentModel.getAgentAvatarsByIds.mockResolvedValue([
|
||||
{ avatar: 'avatar.png', backgroundColor: '#fff', id: 'agent-1', title: 'Agent One' },
|
||||
]);
|
||||
|
||||
const service = new TaskService(db, userId);
|
||||
const result = await service.getTaskDetail('TASK-1');
|
||||
|
||||
expect(result?.activities?.[0]).toMatchObject({
|
||||
actions: [{ key: 'approve', label: '✅', type: 'resolve' }],
|
||||
agentId: 'agent-1',
|
||||
agents: [
|
||||
{ avatar: 'avatar.png', backgroundColor: '#fff', id: 'agent-1', title: 'Agent One' },
|
||||
],
|
||||
artifacts: ['doc_1'],
|
||||
briefType: 'decision',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
id: 'brief-1',
|
||||
priority: 'urgent',
|
||||
resolvedAction: 'approved: looks good',
|
||||
readAt: '2024-01-01T01:00:00.000Z',
|
||||
resolvedAction: 'approved',
|
||||
resolvedAt: '2024-01-01T02:00:00.000Z',
|
||||
resolvedComment: 'looks good',
|
||||
summary: 'Review brief',
|
||||
taskId: 'task_001',
|
||||
title: 'Approval',
|
||||
type: 'brief',
|
||||
userId: 'user-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use only resolvedAction when resolvedComment is absent', async () => {
|
||||
it('should keep resolvedAction and resolvedComment as separate fields', async () => {
|
||||
const task = {
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
|
|
@ -981,6 +1007,66 @@ describe('TaskService', () => {
|
|||
|
||||
expect(result?.activities?.[0]).toMatchObject({
|
||||
resolvedAction: 'retry',
|
||||
resolvedComment: null,
|
||||
type: 'brief',
|
||||
});
|
||||
});
|
||||
|
||||
it('should still return task detail when brief agent enrichment fails', async () => {
|
||||
const task = {
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: null,
|
||||
createdAt: null,
|
||||
description: null,
|
||||
error: null,
|
||||
heartbeatInterval: null,
|
||||
heartbeatTimeout: null,
|
||||
id: 'task_001',
|
||||
identifier: 'TASK-1',
|
||||
instruction: null,
|
||||
lastHeartbeatAt: null,
|
||||
name: 'Task 1',
|
||||
parentTaskId: null,
|
||||
priority: 'normal',
|
||||
status: 'todo',
|
||||
totalTopics: 0,
|
||||
};
|
||||
|
||||
const briefs = [
|
||||
{
|
||||
agentId: 'agent-1',
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
id: 'brief-1',
|
||||
priority: 'normal',
|
||||
resolvedAction: null,
|
||||
resolvedComment: null,
|
||||
summary: 'Brief',
|
||||
taskId: 'task_001',
|
||||
title: 'Brief A',
|
||||
type: 'insight',
|
||||
},
|
||||
];
|
||||
|
||||
mockTaskModel.resolve.mockResolvedValue(task);
|
||||
mockTaskModel.findAllDescendants.mockResolvedValue([]);
|
||||
mockTaskModel.getDependencies.mockResolvedValue([]);
|
||||
mockTaskTopicModel.findWithHandoff.mockResolvedValue([]);
|
||||
mockBriefModel.findByTaskId.mockResolvedValue(briefs);
|
||||
mockTaskModel.getComments.mockResolvedValue([]);
|
||||
mockTaskModel.getTreePinnedDocuments.mockResolvedValue({ nodeMap: {}, tree: [] });
|
||||
mockTaskModel.findByIds.mockResolvedValue([]);
|
||||
mockTaskModel.getCheckpointConfig.mockReturnValue({});
|
||||
mockTaskModel.getReviewConfig.mockReturnValue(undefined);
|
||||
mockTaskModel.getTreeAgentIdsForTaskIds.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
const service = new TaskService(db, userId);
|
||||
const result = await service.getTaskDetail('TASK-1');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.activities).toHaveLength(1);
|
||||
expect(result?.activities?.[0]).toMatchObject({
|
||||
agents: [],
|
||||
id: 'brief-1',
|
||||
type: 'brief',
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,11 +15,14 @@ import { TaskTopicModel } from '@/database/models/taskTopic';
|
|||
import { UserModel } from '@/database/models/user';
|
||||
import type { LobeChatDatabase } from '@/database/type';
|
||||
|
||||
import { BriefService } from '../brief';
|
||||
|
||||
const emptyWorkspace: WorkspaceData = { nodeMap: {}, tree: [] };
|
||||
|
||||
export class TaskService {
|
||||
private agentModel: AgentModel;
|
||||
private briefModel: BriefModel;
|
||||
private briefService: BriefService;
|
||||
private db: LobeChatDatabase;
|
||||
private taskModel: TaskModel;
|
||||
private taskTopicModel: TaskTopicModel;
|
||||
|
|
@ -30,6 +33,7 @@ export class TaskService {
|
|||
this.taskModel = new TaskModel(db, userId);
|
||||
this.taskTopicModel = new TaskTopicModel(db, userId);
|
||||
this.briefModel = new BriefModel(db, userId);
|
||||
this.briefService = new BriefService(db, userId);
|
||||
}
|
||||
|
||||
async getTaskDetail(taskIdOrIdentifier: string): Promise<TaskDetailData | null> {
|
||||
|
|
@ -135,7 +139,12 @@ export class TaskService {
|
|||
if (c.authorUserId) userIds.add(c.authorUserId);
|
||||
}
|
||||
|
||||
const authorMap = await this.resolveAuthors(agentIds, userIds);
|
||||
const [authorMap, enrichedBriefs] = await Promise.all([
|
||||
this.resolveAuthors(agentIds, userIds),
|
||||
this.briefService
|
||||
.enrichBriefsWithAgents(briefs)
|
||||
.catch(() => briefs.map((b) => ({ ...b, agents: [] }))),
|
||||
]);
|
||||
|
||||
const activities: TaskDetailActivity[] = [
|
||||
...topics.map((t) => ({
|
||||
|
|
@ -147,20 +156,28 @@ export class TaskService {
|
|||
title: (t.handoff as TaskTopicHandoff | null)?.title || 'Untitled',
|
||||
type: 'topic' as const,
|
||||
})),
|
||||
...briefs.map((b) => ({
|
||||
...enrichedBriefs.map((b) => ({
|
||||
actions: b.actions ?? undefined,
|
||||
agentId: b.agentId,
|
||||
agents: b.agents,
|
||||
artifacts: b.artifacts ?? undefined,
|
||||
author: b.agentId ? authorMap.get(b.agentId) : undefined,
|
||||
briefType: b.type,
|
||||
createdAt: toISO(b.createdAt),
|
||||
cronJobId: b.cronJobId,
|
||||
id: b.id,
|
||||
priority: b.priority,
|
||||
resolvedAction: b.resolvedAction
|
||||
? b.resolvedComment
|
||||
? `${b.resolvedAction}: ${b.resolvedComment}`
|
||||
: b.resolvedAction
|
||||
: undefined,
|
||||
readAt: toISO(b.readAt),
|
||||
resolvedAction: b.resolvedAction,
|
||||
resolvedAt: toISO(b.resolvedAt),
|
||||
resolvedComment: b.resolvedComment,
|
||||
summary: b.summary,
|
||||
taskId: b.taskId,
|
||||
time: toISO(b.createdAt),
|
||||
title: b.title,
|
||||
topicId: b.topicId,
|
||||
type: 'brief' as const,
|
||||
userId: b.userId,
|
||||
})),
|
||||
...comments.map((c) => ({
|
||||
agentId: c.authorAgentId,
|
||||
|
|
|
|||
Loading…
Reference in a new issue