移除了所有Hermes相关的命名引用,包括: - 从.gitignore中清理相关构建缓存文件 - 将README中的beaver-home路径配置更新 - 完善backend/README.md文档说明Beaver后端主线实现 - 移除Hermes风格的相关注释和兼容性代码 - 清理nanobot环境变量兼容性处理 - 删除技能迁移和服务迁移相关功能代码 - 更新测试用例中相关命名和函数名 BREAKING CHANGE: 移除了Hermes迁移相关API和CLI命令,不再支持nanobot环境变量兼容性
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import type {
|
|
ProcessArtifact,
|
|
ProcessEvent,
|
|
ProcessRun,
|
|
ProcessRunStatus,
|
|
Session,
|
|
} from '@/types';
|
|
import { getCurrentAppLocale, pickAppText, type AppLocale } from '@/lib/i18n/core';
|
|
|
|
const TERMINAL_STATUSES = new Set<TaskRuntimeStatus>(['done', 'error', 'cancelled']);
|
|
const STALE_WAITING_MS = 2 * 60 * 1000;
|
|
|
|
export type TaskRuntimeStatus = ProcessRunStatus | 'blocked';
|
|
|
|
export interface TaskRuntimeProgressView {
|
|
label: string;
|
|
value: number | null;
|
|
max: number | null;
|
|
}
|
|
|
|
export interface TaskRuntimeStatsView {
|
|
totalRuns: number;
|
|
activeRuns: number;
|
|
artifactCount: number;
|
|
alertCount: number;
|
|
}
|
|
|
|
export interface TaskRuntimeNodeView {
|
|
taskId: string;
|
|
runId: string;
|
|
actorName: string;
|
|
title: string;
|
|
status: TaskRuntimeStatus;
|
|
stageLabel: string | null;
|
|
summary: string | null;
|
|
updatedAt: string;
|
|
childTaskIds: string[];
|
|
isRoot: boolean;
|
|
}
|
|
|
|
export interface TaskRuntimeView {
|
|
taskId: string;
|
|
sessionId: string | null;
|
|
title: string;
|
|
status: TaskRuntimeStatus;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
finishedAt: string | null;
|
|
durationMs: number | null;
|
|
sourceSessionLabel: string;
|
|
rootRunId: string;
|
|
rootActorName: string;
|
|
progress: TaskRuntimeProgressView;
|
|
stats: TaskRuntimeStatsView;
|
|
tasks: TaskRuntimeNodeView[];
|
|
}
|
|
|
|
type BuildTaskRuntimeInput = {
|
|
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, locale: AppLocale): string {
|
|
if (!sessionId) return pickAppText(locale, '未关联会话', 'No session linked');
|
|
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 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: TaskRuntimeStatus,
|
|
locale: AppLocale,
|
|
): 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 pickAppText(locale, '执行中', 'Running');
|
|
if (fallbackStatus === 'waiting') return pickAppText(locale, '等待中', 'Waiting');
|
|
if (fallbackStatus === 'queued') return pickAppText(locale, '排队中', 'Queued');
|
|
if (fallbackStatus === 'done') return pickAppText(locale, '已完成', 'Done');
|
|
if (fallbackStatus === 'error') return pickAppText(locale, '失败', 'Error');
|
|
if (fallbackStatus === 'cancelled') return pickAppText(locale, '已取消', 'Cancelled');
|
|
if (fallbackStatus === 'blocked') return pickAppText(locale, '阻塞', 'Blocked');
|
|
return null;
|
|
}
|
|
|
|
function deriveRunStatus(
|
|
run: ProcessRun,
|
|
updatedAt: string,
|
|
now: number,
|
|
): TaskRuntimeStatus {
|
|
if (run.status !== 'waiting') return run.status;
|
|
const updatedTime = toTime(updatedAt);
|
|
if (updatedTime !== null && now - updatedTime > STALE_WAITING_MS) {
|
|
return 'blocked';
|
|
}
|
|
return 'waiting';
|
|
}
|
|
|
|
function deriveProgress(
|
|
rootRun: ProcessRun,
|
|
taskRuns: ProcessRun[],
|
|
locale: AppLocale,
|
|
): TaskRuntimeProgressView {
|
|
const stageValue = readMetadataNumber(rootRun.metadata, ['stage_index', 'step_index', 'phase_index']);
|
|
const stageMax = readMetadataNumber(rootRun.metadata, ['stage_total', 'step_total', 'phase_total']);
|
|
|
|
if (stageValue !== null && stageMax !== null && stageMax > 0) {
|
|
return {
|
|
label: pickAppText(
|
|
locale,
|
|
`阶段 ${Math.min(stageValue, stageMax)} / ${stageMax}`,
|
|
`Stage ${Math.min(stageValue, stageMax)} / ${stageMax}`
|
|
),
|
|
value: stageValue,
|
|
max: stageMax,
|
|
};
|
|
}
|
|
|
|
const doneRuns = taskRuns.filter((run) => run.status === 'done').length;
|
|
if (taskRuns.length > 0) {
|
|
return {
|
|
label: pickAppText(
|
|
locale,
|
|
`已完成子任务 ${doneRuns} / ${taskRuns.length}`,
|
|
`Subtasks completed ${doneRuns} / ${taskRuns.length}`
|
|
),
|
|
value: doneRuns,
|
|
max: taskRuns.length,
|
|
};
|
|
}
|
|
|
|
return {
|
|
label: pickAppText(locale, '等待任务数据', 'Waiting for task data'),
|
|
value: null,
|
|
max: null,
|
|
};
|
|
}
|
|
|
|
function countAlerts(taskViews: TaskRuntimeNodeView[]): number {
|
|
return taskViews.filter((task) => task.status === 'error' || task.status === 'blocked').length;
|
|
}
|
|
|
|
export function isTaskRuntimeTerminal(status: TaskRuntimeStatus): boolean {
|
|
return TERMINAL_STATUSES.has(status);
|
|
}
|
|
|
|
export function taskRuntimeStatusLabel(status: TaskRuntimeStatus, locale: AppLocale = getCurrentAppLocale()): string {
|
|
if (status === 'queued') return pickAppText(locale, '排队中', 'Queued');
|
|
if (status === 'running') return pickAppText(locale, '进行中', 'In Progress');
|
|
if (status === 'waiting') return pickAppText(locale, '等待中', 'Waiting');
|
|
if (status === 'blocked') return pickAppText(locale, '阻塞', 'Blocked');
|
|
if (status === 'done') return pickAppText(locale, '已完成', 'Done');
|
|
if (status === 'error') return pickAppText(locale, '失败', 'Error');
|
|
return pickAppText(locale, '已取消', 'Cancelled');
|
|
}
|
|
|
|
export function buildTaskRuntimeView(
|
|
taskId: string,
|
|
input: BuildTaskRuntimeInput,
|
|
locale: AppLocale = getCurrentAppLocale(),
|
|
): TaskRuntimeView | 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: TaskRuntimeNodeView[] = 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, locale);
|
|
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,
|
|
actorName: run.actor_name,
|
|
title: run.title,
|
|
status,
|
|
stageLabel,
|
|
summary: firstString(run.summary),
|
|
updatedAt,
|
|
childTaskIds,
|
|
isRoot: run.run_id === rootRun.run_id,
|
|
};
|
|
})
|
|
.sort((a, b) => {
|
|
if (a.isRoot !== b.isRoot) return a.isRoot ? -1 : 1;
|
|
if (isTaskRuntimeTerminal(a.status) !== isTaskRuntimeTerminal(b.status)) {
|
|
return isTaskRuntimeTerminal(a.status) ? 1 : -1;
|
|
}
|
|
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 alertCount = countAlerts(taskViews);
|
|
const progress = deriveProgress(rootRun, taskRuns, locale);
|
|
const sourceSessionLabel = getSessionLabel(sessions, sessionId, locale);
|
|
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 {
|
|
taskId: rootRun.run_id,
|
|
sessionId,
|
|
title: rootRun.title || pickAppText(locale, `任务 ${rootRun.run_id.slice(0, 8)}`, `Task ${rootRun.run_id.slice(0, 8)}`),
|
|
status: derivedRootStatus,
|
|
createdAt,
|
|
updatedAt,
|
|
finishedAt,
|
|
durationMs,
|
|
sourceSessionLabel,
|
|
rootRunId: rootRun.run_id,
|
|
rootActorName: rootRun.actor_name,
|
|
progress,
|
|
stats: {
|
|
totalRuns: taskRuns.length,
|
|
activeRuns: taskViews.filter((task) => !isTaskRuntimeTerminal(task.status)).length,
|
|
artifactCount: taskArtifacts.length,
|
|
alertCount,
|
|
},
|
|
tasks: taskViews,
|
|
};
|
|
}
|