refactor(beaver): 移除Hermes相关引用和迁移代码,完善Beaver后端主线实现

移除了所有Hermes相关的命名引用,包括:
- 从.gitignore中清理相关构建缓存文件
- 将README中的beaver-home路径配置更新
- 完善backend/README.md文档说明Beaver后端主线实现
- 移除Hermes风格的相关注释和兼容性代码
- 清理nanobot环境变量兼容性处理
- 删除技能迁移和服务迁移相关功能代码
- 更新测试用例中相关命名和函数名

BREAKING CHANGE: 移除了Hermes迁移相关API和CLI命令,不再支持nanobot环境变量兼容性
This commit is contained in:
2026-05-14 17:20:32 +08:00
parent b59968167e
commit 3b0af173cc
57 changed files with 245 additions and 4109 deletions

View File

@ -1,5 +1,4 @@
import type {
ProcessActorType,
ProcessArtifact,
ProcessEvent,
ProcessRun,
@ -8,108 +7,42 @@ import type {
} from '@/types';
import { getCurrentAppLocale, pickAppText, type AppLocale } from '@/lib/i18n/core';
const TERMINAL_STATUSES = new Set<OfficeTaskStatus>(['done', 'error', 'cancelled']);
const TERMINAL_STATUSES = new Set<TaskRuntimeStatus>(['done', 'error', 'cancelled']);
const STALE_WAITING_MS = 2 * 60 * 1000;
export type OfficeTaskStatus = ProcessRunStatus | 'blocked';
export type TaskRuntimeStatus = ProcessRunStatus | 'blocked';
export type OfficeZoneId =
| 'reception'
| 'workspace'
| 'collab'
| 'research'
| 'alert'
| 'done';
export interface OfficeProgressView {
mode: 'stage' | 'ratio' | 'status';
export interface TaskRuntimeProgressView {
label: string;
value: number | null;
max: number | null;
stageLabel: string | null;
}
export interface OfficeStatsView {
export interface TaskRuntimeStatsView {
totalRuns: number;
activeRuns: number;
doneRuns: number;
errorRuns: number;
cancelledRuns: number;
memberCount: number;
artifactCount: number;
alertCount: 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 {
export interface TaskRuntimeNodeView {
taskId: string;
runId: string;
parentRunId: string | null;
actorId: string;
actorName: string;
actorType: ProcessActorType;
title: string;
status: OfficeTaskStatus;
status: TaskRuntimeStatus;
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;
export interface TaskRuntimeView {
taskId: string;
sessionId: string | null;
title: string;
status: OfficeTaskStatus;
status: TaskRuntimeStatus;
createdAt: string;
updatedAt: string;
finishedAt: string | null;
@ -117,38 +50,12 @@ export interface OfficeView {
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[];
progress: TaskRuntimeProgressView;
stats: TaskRuntimeStatsView;
tasks: TaskRuntimeNodeView[];
}
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 = {
type BuildTaskRuntimeInput = {
sessions: Session[];
processRuns: ProcessRun[];
processEvents: ProcessEvent[];
@ -235,11 +142,6 @@ function buildChildrenMap(processRuns: ProcessRun[]): Map<string, ProcessRun[]>
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];
@ -279,7 +181,7 @@ function getRunUpdatedAt(
function deriveStageLabel(
run: ProcessRun,
runEvents: ProcessEvent[],
fallbackStatus: OfficeTaskStatus,
fallbackStatus: TaskRuntimeStatus,
locale: AppLocale,
): string | null {
const runMetadataLabel = readMetadataString(run.metadata, [
@ -315,7 +217,7 @@ function deriveRunStatus(
run: ProcessRun,
updatedAt: string,
now: number,
): OfficeTaskStatus {
): TaskRuntimeStatus {
if (run.status !== 'waiting') return run.status;
const updatedTime = toTime(updatedAt);
if (updatedTime !== null && now - updatedTime > STALE_WAITING_MS) {
@ -324,88 +226,16 @@ function deriveRunStatus(
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, locale: AppLocale): string {
if (zoneId === 'reception') return pickAppText(locale, '接待区', 'Reception');
if (zoneId === 'workspace') return pickAppText(locale, '工位区', 'Workspace');
if (zoneId === 'collab') return pickAppText(locale, '协作区', 'Collaboration');
if (zoneId === 'research') return pickAppText(locale, '研究区', 'Research');
if (zoneId === 'alert') return pickAppText(locale, '异常区', 'Alerts');
return pickAppText(locale, '完成区', 'Completed');
}
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[], locale: AppLocale): 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 pickAppText(locale, '任务执行失败', 'Task execution failed');
}
function deriveProgress(
rootRun: ProcessRun,
taskRuns: ProcessRun[],
taskViews: OfficeTaskView[],
locale: AppLocale,
): OfficeProgressView {
): TaskRuntimeProgressView {
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: pickAppText(
locale,
`阶段 ${Math.min(stageValue, stageMax)} / ${stageMax}`,
@ -413,14 +243,12 @@ function deriveProgress(
),
value: stageValue,
max: stageMax,
stageLabel,
};
}
const doneRuns = taskRuns.filter((run) => run.status === 'done').length;
if (taskRuns.length > 0) {
return {
mode: 'ratio',
label: pickAppText(
locale,
`已完成子任务 ${doneRuns} / ${taskRuns.length}`,
@ -428,97 +256,25 @@ function deriveProgress(
),
value: doneRuns,
max: taskRuns.length,
stageLabel: stageLabel ?? taskViews.find((item) => item.isRoot)?.stageLabel ?? null,
};
}
return {
mode: 'status',
label: pickAppText(locale, '等待任务数据', 'Waiting for task data'),
value: null,
max: null,
stageLabel,
};
}
function buildAlerts(
taskViews: OfficeTaskView[],
now: number,
locale: AppLocale,
): OfficeAlertView[] {
const alerts: OfficeAlertView[] = [];
for (const task of taskViews) {
if (task.status === 'error') {
alerts.push({
id: `error:${task.runId}`,
level: 'error',
title: pickAppText(locale, `${task.actorName} 执行失败`, `${task.actorName} failed`),
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: pickAppText(locale, `${task.actorName} 长时间等待`, `${task.actorName} has been waiting for a while`),
description: pickAppText(locale, '该任务长时间无更新,可能存在阻塞。', 'This task has not updated for a while and may be blocked.'),
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: pickAppText(locale, `${task.actorName} 等待时间偏长`, `${task.actorName} has been waiting longer than expected`),
description: pickAppText(locale, '该任务仍处于等待态,建议查看详情确认依赖是否卡住。', 'This task is still waiting. Check the details to confirm whether a dependency is stuck.'),
runId: task.runId,
actorId: task.actorId,
createdAt: task.updatedAt,
});
}
}
}
return alerts.sort((a, b) => compareIsoDesc(a.createdAt, b.createdAt));
function countAlerts(taskViews: TaskRuntimeNodeView[]): number {
return taskViews.filter((task) => task.status === 'error' || task.status === 'blocked').length;
}
function buildZones(members: OfficeMemberView[], tasks: OfficeTaskView[], locale: AppLocale): OfficeZoneView[] {
const ids: OfficeZoneId[] = ['reception', 'workspace', 'collab', 'research', 'alert', 'done'];
return ids.map((id) => ({
id,
label: zoneLabel(id, locale),
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[]>, locale: AppLocale): 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: pickAppText(locale, `${run.actor_name} 分派了 ${children.length} 个子任务`, `${run.actor_name} assigned ${children.length} subtasks`),
};
});
}
export function isOfficeTaskTerminal(status: OfficeTaskStatus): boolean {
export function isTaskRuntimeTerminal(status: TaskRuntimeStatus): boolean {
return TERMINAL_STATUSES.has(status);
}
export function officeTaskStatusLabel(status: OfficeTaskStatus, locale: AppLocale = getCurrentAppLocale()): string {
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');
@ -528,11 +284,11 @@ export function officeTaskStatusLabel(status: OfficeTaskStatus, locale: AppLocal
return pickAppText(locale, '已取消', 'Cancelled');
}
export function buildOfficeView(
export function buildTaskRuntimeView(
taskId: string,
input: BuildOfficeInput,
input: BuildTaskRuntimeInput,
locale: AppLocale = getCurrentAppLocale(),
): OfficeView | null {
): TaskRuntimeView | null {
const { sessions, processRuns, processEvents, processArtifacts } = input;
const runById = new Map(processRuns.map((run) => [run.run_id, run]));
const rootRun = runById.get(taskId);
@ -547,7 +303,7 @@ export function buildOfficeView(
const artifactsByRun = groupByRunId(taskArtifacts);
const now = Date.now();
const taskViews: OfficeTaskView[] = taskRuns
const taskViews: TaskRuntimeNodeView[] = taskRuns
.map((run) => {
const runEvents = eventsByRun.get(run.run_id) ?? [];
const updatedAt = getRunUpdatedAt(run, eventsByRun, artifactsByRun);
@ -560,72 +316,24 @@ export function buildOfficeView(
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, locale),
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;
if (isTaskRuntimeTerminal(a.status) !== isTaskRuntimeTerminal(b.status)) {
return isTaskRuntimeTerminal(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),
@ -633,8 +341,8 @@ export function buildOfficeView(
rootRun.started_at,
]) ?? rootRun.started_at;
const derivedRootStatus = deriveRunStatus(rootRun, updatedAt, now);
const alerts = buildAlerts(taskViews, now, locale);
const progress = deriveProgress(rootRun, taskRuns, taskViews, locale);
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;
@ -646,7 +354,6 @@ export function buildOfficeView(
: null;
return {
officeId: rootRun.run_id,
taskId: rootRun.run_id,
sessionId,
title: rootRun.title || pickAppText(locale, `任务 ${rootRun.run_id.slice(0, 8)}`, `Task ${rootRun.run_id.slice(0, 8)}`),
@ -658,60 +365,13 @@ export function buildOfficeView(
sourceSessionLabel,
rootRunId: rootRun.run_id,
rootActorName: rootRun.actor_name,
currentStageLabel: deriveStageLabel(rootRun, eventsByRun.get(rootRun.run_id) ?? [], derivedRootStatus, locale),
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,
activeRuns: taskViews.filter((task) => !isTaskRuntimeTerminal(task.status)).length,
artifactCount: taskArtifacts.length,
alertCount,
},
alerts,
zones: buildZones(members, taskViews, locale),
members,
tasks: taskViews,
assignments: buildAssignments(taskRuns, childrenMap, locale),
detailRunIds: taskViews.map((task) => task.runId),
};
}
export function buildOfficeTaskList(
input: BuildOfficeInput & { sessionId?: string | null },
locale: AppLocale = getCurrentAppLocale(),
): OfficeTaskListItem[] {
const rootRuns = findRootRuns(input.processRuns);
const offices = rootRuns
.map((rootRun) => buildOfficeView(rootRun.run_id, input, locale))
.filter((office): office is OfficeView => office !== null)
.filter((office) => !input.sessionId || office.sessionId === input.sessionId);
return offices
.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);
});
}