```
feat(agent): 添加对持久化子智能体的支持并增强委派管理 添加了持久化子智能体的完整生命周期管理功能,包括创建、更新、删除和查询API接口。 新增了子智能体的JSON-RPC通信协议支持,实现了远程调用和任务管理功能。 同时增强了委派管理器的功能: - 添加了对本地委派、插件委派和本地回退的开关控制 - 实现了持久化子智能体任务的自动检测和本地执行保护 - 增加了对不同委派类型的权限验证机制 修改了智能体注册表以支持插件智能体的条件性包含,并更新了工具注册逻辑以支持可选工具。 BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。 ```
This commit is contained in:
704
app-instance/frontend/lib/office.ts
Normal file
704
app-instance/frontend/lib/office.ts
Normal file
@ -0,0 +1,704 @@
|
||||
import type {
|
||||
ProcessActorType,
|
||||
ProcessArtifact,
|
||||
ProcessEvent,
|
||||
ProcessRun,
|
||||
ProcessRunStatus,
|
||||
Session,
|
||||
} from '@/types';
|
||||
|
||||
const TERMINAL_STATUSES = new Set<OfficeTaskStatus>(['done', 'error', 'cancelled']);
|
||||
const STALE_WAITING_MS = 2 * 60 * 1000;
|
||||
|
||||
export type OfficeTaskStatus = ProcessRunStatus | 'blocked';
|
||||
|
||||
export type OfficeZoneId =
|
||||
| 'reception'
|
||||
| 'workspace'
|
||||
| 'collab'
|
||||
| 'research'
|
||||
| 'alert'
|
||||
| 'done';
|
||||
|
||||
export interface OfficeProgressView {
|
||||
mode: 'stage' | 'ratio' | 'status';
|
||||
label: string;
|
||||
value: number | null;
|
||||
max: number | null;
|
||||
stageLabel: string | null;
|
||||
}
|
||||
|
||||
export interface OfficeStatsView {
|
||||
totalRuns: number;
|
||||
activeRuns: number;
|
||||
doneRuns: number;
|
||||
errorRuns: number;
|
||||
cancelledRuns: number;
|
||||
memberCount: number;
|
||||
artifactCount: number;
|
||||
}
|
||||
|
||||
export interface OfficeZoneView {
|
||||
id: OfficeZoneId;
|
||||
label: string;
|
||||
memberIds: string[];
|
||||
taskIds: string[];
|
||||
tone: 'neutral' | 'info' | 'warn' | 'danger' | 'success';
|
||||
}
|
||||
|
||||
export interface OfficeMemberView {
|
||||
memberId: string;
|
||||
actorId: string;
|
||||
actorName: string;
|
||||
actorType: ProcessActorType;
|
||||
status: OfficeTaskStatus;
|
||||
zoneId: OfficeZoneId;
|
||||
currentRunId: string;
|
||||
currentTitle: string;
|
||||
stageLabel: string | null;
|
||||
summary: string | null;
|
||||
startedAt: string | null;
|
||||
updatedAt: string | null;
|
||||
finishedAt: string | null;
|
||||
childRunIds: string[];
|
||||
artifactCount: number;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
export interface OfficeTaskView {
|
||||
taskId: string;
|
||||
runId: string;
|
||||
parentRunId: string | null;
|
||||
actorId: string;
|
||||
actorName: string;
|
||||
actorType: ProcessActorType;
|
||||
title: string;
|
||||
status: OfficeTaskStatus;
|
||||
stageLabel: string | null;
|
||||
summary: string | null;
|
||||
startedAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string | null;
|
||||
childTaskIds: string[];
|
||||
artifactCount: number;
|
||||
errorText: string | null;
|
||||
isRoot: boolean;
|
||||
}
|
||||
|
||||
export interface OfficeAssignmentView {
|
||||
ownerRunId: string;
|
||||
ownerActorName: string;
|
||||
assigneeRunIds: string[];
|
||||
assigneeActorNames: string[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface OfficeAlertView {
|
||||
id: string;
|
||||
level: 'info' | 'warn' | 'error';
|
||||
title: string;
|
||||
description: string | null;
|
||||
runId: string | null;
|
||||
actorId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface OfficeView {
|
||||
officeId: string;
|
||||
taskId: string;
|
||||
sessionId: string | null;
|
||||
title: string;
|
||||
status: OfficeTaskStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string | null;
|
||||
durationMs: number | null;
|
||||
sourceSessionLabel: string;
|
||||
rootRunId: string;
|
||||
rootActorName: string;
|
||||
currentStageLabel: string | null;
|
||||
progress: OfficeProgressView;
|
||||
stats: OfficeStatsView;
|
||||
alerts: OfficeAlertView[];
|
||||
zones: OfficeZoneView[];
|
||||
members: OfficeMemberView[];
|
||||
tasks: OfficeTaskView[];
|
||||
assignments: OfficeAssignmentView[];
|
||||
detailRunIds: string[];
|
||||
}
|
||||
|
||||
export interface OfficeTaskListItem {
|
||||
officeId: string;
|
||||
taskId: string;
|
||||
sessionId: string | null;
|
||||
sessionLabel: string;
|
||||
title: string;
|
||||
status: OfficeTaskStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
finishedAt: string | null;
|
||||
rootRunId: string;
|
||||
rootActorName: string;
|
||||
memberCount: number;
|
||||
activeRuns: number;
|
||||
errorCount: number;
|
||||
artifactCount: number;
|
||||
currentStageLabel: string | null;
|
||||
progress: OfficeProgressView;
|
||||
}
|
||||
|
||||
type BuildOfficeInput = {
|
||||
sessions: Session[];
|
||||
processRuns: ProcessRun[];
|
||||
processEvents: ProcessEvent[];
|
||||
processArtifacts: ProcessArtifact[];
|
||||
};
|
||||
|
||||
function toTime(value?: string | null): number | null {
|
||||
if (!value) return null;
|
||||
const parsed = new Date(value).getTime();
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function compareIsoDesc(a?: string | null, b?: string | null): number {
|
||||
return (toTime(b) ?? 0) - (toTime(a) ?? 0);
|
||||
}
|
||||
|
||||
function firstString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function firstNumber(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function readMetadataString(metadata: Record<string, unknown> | undefined, keys: string[]): string | null {
|
||||
for (const key of keys) {
|
||||
const value = firstString(metadata?.[key]);
|
||||
if (value) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function readMetadataNumber(metadata: Record<string, unknown> | undefined, keys: string[]): number | null {
|
||||
for (const key of keys) {
|
||||
const value = firstNumber(metadata?.[key]);
|
||||
if (value !== null) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function latestTimestamp(values: Array<string | null | undefined>): string | null {
|
||||
let selected: string | null = null;
|
||||
let selectedTime = -1;
|
||||
for (const value of values) {
|
||||
const time = toTime(value);
|
||||
if (time === null || time <= selectedTime) continue;
|
||||
selected = value ?? null;
|
||||
selectedTime = time;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
function getSessionLabel(sessions: Session[], sessionId: string | null): string {
|
||||
if (!sessionId) return '未关联会话';
|
||||
const session = sessions.find((item) => item.key === sessionId);
|
||||
if (!session) return sessionId;
|
||||
return session.path?.trim() || session.key;
|
||||
}
|
||||
|
||||
function groupByRunId<T extends { run_id: string }>(items: T[]): Map<string, T[]> {
|
||||
const map = new Map<string, T[]>();
|
||||
for (const item of items) {
|
||||
const collection = map.get(item.run_id);
|
||||
if (collection) {
|
||||
collection.push(item);
|
||||
continue;
|
||||
}
|
||||
map.set(item.run_id, [item]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildChildrenMap(processRuns: ProcessRun[]): Map<string, ProcessRun[]> {
|
||||
const map = new Map<string, ProcessRun[]>();
|
||||
for (const run of processRuns) {
|
||||
if (!run.parent_run_id) continue;
|
||||
const children = map.get(run.parent_run_id);
|
||||
if (children) {
|
||||
children.push(run);
|
||||
continue;
|
||||
}
|
||||
map.set(run.parent_run_id, [run]);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function findRootRuns(processRuns: ProcessRun[]): ProcessRun[] {
|
||||
const runIds = new Set(processRuns.map((run) => run.run_id));
|
||||
return processRuns.filter((run) => !run.parent_run_id || !runIds.has(run.parent_run_id));
|
||||
}
|
||||
|
||||
function collectRunTree(rootRun: ProcessRun, childrenMap: Map<string, ProcessRun[]>): ProcessRun[] {
|
||||
const collected: ProcessRun[] = [];
|
||||
const stack = [rootRun];
|
||||
const seen = new Set<string>();
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (!current || seen.has(current.run_id)) continue;
|
||||
seen.add(current.run_id);
|
||||
collected.push(current);
|
||||
const children = childrenMap.get(current.run_id) ?? [];
|
||||
for (let index = children.length - 1; index >= 0; index -= 1) {
|
||||
stack.push(children[index]);
|
||||
}
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
function getRunUpdatedAt(
|
||||
run: ProcessRun,
|
||||
eventsByRun: Map<string, ProcessEvent[]>,
|
||||
artifactsByRun: Map<string, ProcessArtifact[]>,
|
||||
): string {
|
||||
const eventTimes = (eventsByRun.get(run.run_id) ?? []).map((event) => event.created_at);
|
||||
const artifactTimes = (artifactsByRun.get(run.run_id) ?? []).map((artifact) => artifact.created_at);
|
||||
return (
|
||||
latestTimestamp([
|
||||
...eventTimes,
|
||||
...artifactTimes,
|
||||
run.finished_at,
|
||||
run.started_at,
|
||||
]) ?? run.started_at
|
||||
);
|
||||
}
|
||||
|
||||
function deriveStageLabel(
|
||||
run: ProcessRun,
|
||||
runEvents: ProcessEvent[],
|
||||
fallbackStatus: OfficeTaskStatus,
|
||||
): string | null {
|
||||
const runMetadataLabel = readMetadataString(run.metadata, [
|
||||
'stage_label',
|
||||
'stage',
|
||||
'phase_label',
|
||||
'step_label',
|
||||
]);
|
||||
if (runMetadataLabel) return runMetadataLabel;
|
||||
|
||||
const sortedEvents = [...runEvents].sort((a, b) => compareIsoDesc(a.created_at, b.created_at));
|
||||
for (const event of sortedEvents) {
|
||||
const label = readMetadataString(event.metadata, [
|
||||
'stage_label',
|
||||
'stage',
|
||||
'phase_label',
|
||||
'step_label',
|
||||
]);
|
||||
if (label) return label;
|
||||
}
|
||||
|
||||
if (fallbackStatus === 'running') return '执行中';
|
||||
if (fallbackStatus === 'waiting') return '等待中';
|
||||
if (fallbackStatus === 'queued') return '排队中';
|
||||
if (fallbackStatus === 'done') return '已完成';
|
||||
if (fallbackStatus === 'error') return '失败';
|
||||
if (fallbackStatus === 'cancelled') return '已取消';
|
||||
if (fallbackStatus === 'blocked') return '阻塞';
|
||||
return null;
|
||||
}
|
||||
|
||||
function deriveRunStatus(
|
||||
run: ProcessRun,
|
||||
updatedAt: string,
|
||||
now: number,
|
||||
): OfficeTaskStatus {
|
||||
if (run.status !== 'waiting') return run.status;
|
||||
const updatedTime = toTime(updatedAt);
|
||||
if (updatedTime !== null && now - updatedTime > STALE_WAITING_MS) {
|
||||
return 'blocked';
|
||||
}
|
||||
return 'waiting';
|
||||
}
|
||||
|
||||
function mapZoneId(status: OfficeTaskStatus, actorType: ProcessActorType): OfficeZoneId {
|
||||
if (status === 'queued') return 'reception';
|
||||
if (status === 'waiting' || status === 'blocked') return actorType === 'mcp' ? 'research' : 'collab';
|
||||
if (status === 'running') return actorType === 'mcp' ? 'research' : 'workspace';
|
||||
if (status === 'done') return 'collab';
|
||||
return 'alert';
|
||||
}
|
||||
|
||||
function zoneLabel(zoneId: OfficeZoneId): string {
|
||||
if (zoneId === 'reception') return '接待区';
|
||||
if (zoneId === 'workspace') return '工位区';
|
||||
if (zoneId === 'collab') return '协作区';
|
||||
if (zoneId === 'research') return '研究区';
|
||||
if (zoneId === 'alert') return '异常区';
|
||||
return '完成区';
|
||||
}
|
||||
|
||||
function zoneTone(zoneId: OfficeZoneId): OfficeZoneView['tone'] {
|
||||
if (zoneId === 'workspace' || zoneId === 'research') return 'info';
|
||||
if (zoneId === 'collab' || zoneId === 'reception') return 'warn';
|
||||
if (zoneId === 'alert') return 'danger';
|
||||
if (zoneId === 'done') return 'success';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
function taskStatusPriority(status: OfficeTaskStatus): number {
|
||||
if (status === 'running') return 6;
|
||||
if (status === 'blocked') return 5;
|
||||
if (status === 'waiting') return 4;
|
||||
if (status === 'queued') return 3;
|
||||
if (status === 'error') return 2;
|
||||
if (status === 'cancelled') return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function selectDisplayRun(
|
||||
runs: ProcessRun[],
|
||||
eventsByRun: Map<string, ProcessEvent[]>,
|
||||
artifactsByRun: Map<string, ProcessArtifact[]>,
|
||||
now: number,
|
||||
): { run: ProcessRun; status: OfficeTaskStatus; updatedAt: string } {
|
||||
const sorted = [...runs]
|
||||
.map((run) => {
|
||||
const updatedAt = getRunUpdatedAt(run, eventsByRun, artifactsByRun);
|
||||
const status = deriveRunStatus(run, updatedAt, now);
|
||||
return { run, status, updatedAt };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const byStatus = taskStatusPriority(b.status) - taskStatusPriority(a.status);
|
||||
if (byStatus !== 0) return byStatus;
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
function deriveErrorText(run: ProcessRun, runEvents: ProcessEvent[]): string | null {
|
||||
if (run.status !== 'error') return null;
|
||||
const direct = firstString(run.summary);
|
||||
if (direct) return direct;
|
||||
const sortedEvents = [...runEvents].sort((a, b) => compareIsoDesc(a.created_at, b.created_at));
|
||||
for (const event of sortedEvents) {
|
||||
if (event.status === 'error' && firstString(event.text)) {
|
||||
return event.text!.trim();
|
||||
}
|
||||
}
|
||||
return '任务执行失败';
|
||||
}
|
||||
|
||||
function deriveProgress(
|
||||
rootRun: ProcessRun,
|
||||
taskRuns: ProcessRun[],
|
||||
taskViews: OfficeTaskView[],
|
||||
): OfficeProgressView {
|
||||
const stageValue = readMetadataNumber(rootRun.metadata, ['stage_index', 'step_index', 'phase_index']);
|
||||
const stageMax = readMetadataNumber(rootRun.metadata, ['stage_total', 'step_total', 'phase_total']);
|
||||
const stageLabel = readMetadataString(rootRun.metadata, ['stage_label', 'stage', 'phase_label', 'step_label']);
|
||||
|
||||
if (stageValue !== null && stageMax !== null && stageMax > 0) {
|
||||
return {
|
||||
mode: 'ratio',
|
||||
label: `阶段 ${Math.min(stageValue, stageMax)} / ${stageMax}`,
|
||||
value: stageValue,
|
||||
max: stageMax,
|
||||
stageLabel,
|
||||
};
|
||||
}
|
||||
|
||||
const doneRuns = taskRuns.filter((run) => run.status === 'done').length;
|
||||
if (taskRuns.length > 0) {
|
||||
return {
|
||||
mode: 'ratio',
|
||||
label: `已完成子任务 ${doneRuns} / ${taskRuns.length}`,
|
||||
value: doneRuns,
|
||||
max: taskRuns.length,
|
||||
stageLabel: stageLabel ?? taskViews.find((item) => item.isRoot)?.stageLabel ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'status',
|
||||
label: '等待任务数据',
|
||||
value: null,
|
||||
max: null,
|
||||
stageLabel,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAlerts(
|
||||
taskViews: OfficeTaskView[],
|
||||
now: number,
|
||||
): OfficeAlertView[] {
|
||||
const alerts: OfficeAlertView[] = [];
|
||||
|
||||
for (const task of taskViews) {
|
||||
if (task.status === 'error') {
|
||||
alerts.push({
|
||||
id: `error:${task.runId}`,
|
||||
level: 'error',
|
||||
title: `${task.actorName} 执行失败`,
|
||||
description: task.errorText,
|
||||
runId: task.runId,
|
||||
actorId: task.actorId,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
} else if (task.status === 'blocked') {
|
||||
alerts.push({
|
||||
id: `blocked:${task.runId}`,
|
||||
level: 'warn',
|
||||
title: `${task.actorName} 长时间等待`,
|
||||
description: '该任务长时间无更新,可能存在阻塞。',
|
||||
runId: task.runId,
|
||||
actorId: task.actorId,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
} else if (task.status === 'waiting') {
|
||||
const updatedTime = toTime(task.updatedAt);
|
||||
if (updatedTime !== null && now - updatedTime > STALE_WAITING_MS) {
|
||||
alerts.push({
|
||||
id: `stale:${task.runId}`,
|
||||
level: 'warn',
|
||||
title: `${task.actorName} 等待时间偏长`,
|
||||
description: '该任务仍处于等待态,建议查看详情确认依赖是否卡住。',
|
||||
runId: task.runId,
|
||||
actorId: task.actorId,
|
||||
createdAt: task.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alerts.sort((a, b) => compareIsoDesc(a.createdAt, b.createdAt));
|
||||
}
|
||||
|
||||
function buildZones(members: OfficeMemberView[], tasks: OfficeTaskView[]): OfficeZoneView[] {
|
||||
const ids: OfficeZoneId[] = ['reception', 'workspace', 'collab', 'research', 'alert', 'done'];
|
||||
return ids.map((id) => ({
|
||||
id,
|
||||
label: zoneLabel(id),
|
||||
memberIds: members.filter((member) => member.zoneId === id).map((member) => member.memberId),
|
||||
taskIds: tasks.filter((task) => mapZoneId(task.status, task.actorType) === id).map((task) => task.taskId),
|
||||
tone: zoneTone(id),
|
||||
}));
|
||||
}
|
||||
|
||||
function buildAssignments(taskRuns: ProcessRun[], childrenMap: Map<string, ProcessRun[]>): OfficeAssignmentView[] {
|
||||
return taskRuns
|
||||
.filter((run) => (childrenMap.get(run.run_id) ?? []).length > 0)
|
||||
.map((run) => {
|
||||
const children = childrenMap.get(run.run_id) ?? [];
|
||||
return {
|
||||
ownerRunId: run.run_id,
|
||||
ownerActorName: run.actor_name,
|
||||
assigneeRunIds: children.map((item) => item.run_id),
|
||||
assigneeActorNames: children.map((item) => item.actor_name),
|
||||
label: `${run.actor_name} 分派了 ${children.length} 个子任务`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function isOfficeTaskTerminal(status: OfficeTaskStatus): boolean {
|
||||
return TERMINAL_STATUSES.has(status);
|
||||
}
|
||||
|
||||
export function officeTaskStatusLabel(status: OfficeTaskStatus): string {
|
||||
if (status === 'queued') return '排队中';
|
||||
if (status === 'running') return '进行中';
|
||||
if (status === 'waiting') return '等待中';
|
||||
if (status === 'blocked') return '阻塞';
|
||||
if (status === 'done') return '已完成';
|
||||
if (status === 'error') return '失败';
|
||||
return '已取消';
|
||||
}
|
||||
|
||||
export function buildOfficeView(
|
||||
taskId: string,
|
||||
input: BuildOfficeInput,
|
||||
): OfficeView | null {
|
||||
const { sessions, processRuns, processEvents, processArtifacts } = input;
|
||||
const runById = new Map(processRuns.map((run) => [run.run_id, run]));
|
||||
const rootRun = runById.get(taskId);
|
||||
if (!rootRun) return null;
|
||||
|
||||
const childrenMap = buildChildrenMap(processRuns);
|
||||
const taskRuns = collectRunTree(rootRun, childrenMap);
|
||||
const taskRunIds = new Set(taskRuns.map((run) => run.run_id));
|
||||
const taskEvents = processEvents.filter((event) => taskRunIds.has(event.run_id));
|
||||
const taskArtifacts = processArtifacts.filter((artifact) => taskRunIds.has(artifact.run_id));
|
||||
const eventsByRun = groupByRunId(taskEvents);
|
||||
const artifactsByRun = groupByRunId(taskArtifacts);
|
||||
const now = Date.now();
|
||||
|
||||
const taskViews: OfficeTaskView[] = taskRuns
|
||||
.map((run) => {
|
||||
const runEvents = eventsByRun.get(run.run_id) ?? [];
|
||||
const updatedAt = getRunUpdatedAt(run, eventsByRun, artifactsByRun);
|
||||
const status = deriveRunStatus(run, updatedAt, now);
|
||||
const stageLabel = deriveStageLabel(run, runEvents, status);
|
||||
const childTaskIds = (childrenMap.get(run.run_id) ?? [])
|
||||
.filter((child) => taskRunIds.has(child.run_id))
|
||||
.map((child) => child.run_id);
|
||||
|
||||
return {
|
||||
taskId: run.run_id,
|
||||
runId: run.run_id,
|
||||
parentRunId: run.parent_run_id ?? null,
|
||||
actorId: run.actor_id,
|
||||
actorName: run.actor_name,
|
||||
actorType: run.actor_type,
|
||||
title: run.title,
|
||||
status,
|
||||
stageLabel,
|
||||
summary: firstString(run.summary),
|
||||
startedAt: run.started_at,
|
||||
updatedAt,
|
||||
finishedAt: run.finished_at ?? null,
|
||||
childTaskIds,
|
||||
artifactCount: (artifactsByRun.get(run.run_id) ?? []).length,
|
||||
errorText: deriveErrorText(run, runEvents),
|
||||
isRoot: run.run_id === rootRun.run_id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.isRoot !== b.isRoot) return a.isRoot ? -1 : 1;
|
||||
if (isOfficeTaskTerminal(a.status) !== isOfficeTaskTerminal(b.status)) {
|
||||
return isOfficeTaskTerminal(a.status) ? 1 : -1;
|
||||
}
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
|
||||
const actorRuns = new Map<string, ProcessRun[]>();
|
||||
for (const run of taskRuns) {
|
||||
const collection = actorRuns.get(run.actor_id);
|
||||
if (collection) {
|
||||
collection.push(run);
|
||||
continue;
|
||||
}
|
||||
actorRuns.set(run.actor_id, [run]);
|
||||
}
|
||||
|
||||
const members: OfficeMemberView[] = Array.from(actorRuns.entries())
|
||||
.map(([actorId, runs]) => {
|
||||
const display = selectDisplayRun(runs, eventsByRun, artifactsByRun, now);
|
||||
const currentRun = display.run;
|
||||
const currentTask = taskViews.find((task) => task.runId === currentRun.run_id);
|
||||
return {
|
||||
memberId: actorId,
|
||||
actorId,
|
||||
actorName: currentRun.actor_name,
|
||||
actorType: currentRun.actor_type,
|
||||
status: display.status,
|
||||
zoneId: mapZoneId(display.status, currentRun.actor_type),
|
||||
currentRunId: currentRun.run_id,
|
||||
currentTitle: currentRun.title,
|
||||
stageLabel: currentTask?.stageLabel ?? null,
|
||||
summary: currentTask?.summary ?? null,
|
||||
startedAt: currentRun.started_at ?? null,
|
||||
updatedAt: display.updatedAt,
|
||||
finishedAt: currentRun.finished_at ?? null,
|
||||
childRunIds: (childrenMap.get(currentRun.run_id) ?? []).map((child) => child.run_id),
|
||||
artifactCount: runs.reduce((count, run) => count + (artifactsByRun.get(run.run_id) ?? []).length, 0),
|
||||
isPrimary: currentRun.run_id === rootRun.run_id,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;
|
||||
const byStatus = taskStatusPriority(b.status) - taskStatusPriority(a.status);
|
||||
if (byStatus !== 0) return byStatus;
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
|
||||
const sessionId = rootRun.session_id ?? taskRuns.find((run) => run.session_id)?.session_id ?? null;
|
||||
const updatedAt = latestTimestamp([
|
||||
...taskViews.map((task) => task.updatedAt),
|
||||
rootRun.finished_at,
|
||||
rootRun.started_at,
|
||||
]) ?? rootRun.started_at;
|
||||
const derivedRootStatus = deriveRunStatus(rootRun, updatedAt, now);
|
||||
const alerts = buildAlerts(taskViews, now);
|
||||
const progress = deriveProgress(rootRun, taskRuns, taskViews);
|
||||
const sourceSessionLabel = getSessionLabel(sessions, sessionId);
|
||||
const createdAt = rootRun.started_at;
|
||||
const finishedAt = rootRun.finished_at ?? null;
|
||||
const durationStart = toTime(createdAt);
|
||||
const durationEnd = toTime(finishedAt ?? updatedAt);
|
||||
const durationMs =
|
||||
durationStart !== null && durationEnd !== null && durationEnd >= durationStart
|
||||
? durationEnd - durationStart
|
||||
: null;
|
||||
|
||||
return {
|
||||
officeId: rootRun.run_id,
|
||||
taskId: rootRun.run_id,
|
||||
sessionId,
|
||||
title: rootRun.title || `Task ${rootRun.run_id.slice(0, 8)}`,
|
||||
status: derivedRootStatus,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
finishedAt,
|
||||
durationMs,
|
||||
sourceSessionLabel,
|
||||
rootRunId: rootRun.run_id,
|
||||
rootActorName: rootRun.actor_name,
|
||||
currentStageLabel: deriveStageLabel(rootRun, eventsByRun.get(rootRun.run_id) ?? [], derivedRootStatus),
|
||||
progress,
|
||||
stats: {
|
||||
totalRuns: taskRuns.length,
|
||||
activeRuns: taskViews.filter((task) => !isOfficeTaskTerminal(task.status)).length,
|
||||
doneRuns: taskViews.filter((task) => task.status === 'done').length,
|
||||
errorRuns: taskViews.filter((task) => task.status === 'error').length,
|
||||
cancelledRuns: taskViews.filter((task) => task.status === 'cancelled').length,
|
||||
memberCount: members.length,
|
||||
artifactCount: taskArtifacts.length,
|
||||
},
|
||||
alerts,
|
||||
zones: buildZones(members, taskViews),
|
||||
members,
|
||||
tasks: taskViews,
|
||||
assignments: buildAssignments(taskRuns, childrenMap),
|
||||
detailRunIds: taskViews.map((task) => task.runId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOfficeTaskList(
|
||||
input: BuildOfficeInput & { sessionId?: string | null },
|
||||
): OfficeTaskListItem[] {
|
||||
const rootRuns = findRootRuns(input.processRuns);
|
||||
const filteredRoots = input.sessionId
|
||||
? rootRuns.filter((run) => run.session_id === input.sessionId)
|
||||
: rootRuns;
|
||||
|
||||
return filteredRoots
|
||||
.map((rootRun) => buildOfficeView(rootRun.run_id, input))
|
||||
.filter((office): office is OfficeView => office !== null)
|
||||
.map((office) => ({
|
||||
officeId: office.officeId,
|
||||
taskId: office.taskId,
|
||||
sessionId: office.sessionId,
|
||||
sessionLabel: office.sourceSessionLabel,
|
||||
title: office.title,
|
||||
status: office.status,
|
||||
createdAt: office.createdAt,
|
||||
updatedAt: office.updatedAt,
|
||||
finishedAt: office.finishedAt,
|
||||
rootRunId: office.rootRunId,
|
||||
rootActorName: office.rootActorName,
|
||||
memberCount: office.members.length,
|
||||
activeRuns: office.stats.activeRuns,
|
||||
errorCount: office.stats.errorRuns,
|
||||
artifactCount: office.stats.artifactCount,
|
||||
currentStageLabel: office.currentStageLabel,
|
||||
progress: office.progress,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (isOfficeTaskTerminal(a.status) !== isOfficeTaskTerminal(b.status)) {
|
||||
return isOfficeTaskTerminal(a.status) ? 1 : -1;
|
||||
}
|
||||
return compareIsoDesc(a.updatedAt, b.updatedAt);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user