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:
@ -1,9 +1,6 @@
|
||||
// Nanobot API client — single-user direct mode.
|
||||
// Beaver API client - single-user direct mode.
|
||||
|
||||
import type {
|
||||
AuthzBackendRecord,
|
||||
AuthzChannelSettings,
|
||||
AuthzRegisterBackendResponse,
|
||||
AuthzStatus,
|
||||
AuthUser,
|
||||
ActiveTask,
|
||||
@ -12,11 +9,8 @@ import type {
|
||||
ChatMessage,
|
||||
CronJob,
|
||||
FileAttachment,
|
||||
Marketplace,
|
||||
MarketplacePlugin,
|
||||
NotificationDetail,
|
||||
NotificationRun,
|
||||
PluginInfo,
|
||||
ProviderConfigPayload,
|
||||
Session,
|
||||
SessionDetail,
|
||||
@ -33,7 +27,6 @@ import type {
|
||||
SkillHubVersionsResponse,
|
||||
SkillLearningCandidate,
|
||||
SkillReviewRecord,
|
||||
SlashCommand,
|
||||
SessionProcessProjection,
|
||||
SystemStatus,
|
||||
TokenResponse,
|
||||
@ -55,8 +48,8 @@ import { getCurrentAppLocale, pickAppText } from '@/lib/i18n/core';
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL?.trim();
|
||||
const WS_URL = process.env.NEXT_PUBLIC_WS_URL?.trim();
|
||||
const DEFAULT_API_URL = 'http://127.0.0.1:18080';
|
||||
const ACCESS_TOKEN_KEY = 'nanobot_access_token';
|
||||
const REFRESH_TOKEN_KEY = 'nanobot_refresh_token';
|
||||
const ACCESS_TOKEN_KEY = 'beaver_access_token';
|
||||
const REFRESH_TOKEN_KEY = 'beaver_refresh_token';
|
||||
const REQUEST_TIMEOUT_MS = 8000;
|
||||
const OUTLOOK_REQUEST_TIMEOUT_MS = 45000;
|
||||
const SKILL_LEARNING_REQUEST_TIMEOUT_MS = 120000;
|
||||
@ -636,16 +629,6 @@ export async function updateProviderConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function restartSystem(): Promise<{
|
||||
ok: boolean;
|
||||
restarting: boolean;
|
||||
detail: string;
|
||||
}> {
|
||||
return fetchJSON('/api/system/restart', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cron (proxied)
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -861,14 +844,6 @@ export async function rollbackPublishedSkill(
|
||||
});
|
||||
}
|
||||
|
||||
export async function listCommands(): Promise<SlashCommand[]> {
|
||||
return fetchJSON('/api/commands');
|
||||
}
|
||||
|
||||
export async function listPlugins(): Promise<PluginInfo[]> {
|
||||
return fetchJSON('/api/plugins');
|
||||
}
|
||||
|
||||
export async function listAgents(): Promise<UiAgentDescriptor[]> {
|
||||
return fetchJSON('/api/agents');
|
||||
}
|
||||
@ -957,18 +932,6 @@ export async function deleteSubagent(subagentId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelDelegation(runId: string): Promise<{ ok: boolean; run_id: string }> {
|
||||
return fetchJSON(`/api/delegations/${encodeURIComponent(runId)}/cancel`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function retryDelegation(runId: string): Promise<{ ok: boolean; run_id: string }> {
|
||||
return fetchJSON(`/api/delegations/${encodeURIComponent(runId)}/retry`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function listMcpServers(): Promise<UiMcpServerDescriptor[]> {
|
||||
return fetchJSON('/api/mcp/servers');
|
||||
}
|
||||
@ -1022,122 +985,6 @@ export async function getAuthzStatus(): Promise<AuthzStatus> {
|
||||
return fetchJSON('/api/authz/status');
|
||||
}
|
||||
|
||||
export async function bindLocalBackendIdentity(payload: {
|
||||
backend_id: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
name?: string;
|
||||
public_base_url?: string;
|
||||
authz_base_url?: string;
|
||||
authz_enabled?: boolean;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
return fetchJSON('/api/authz/local-backend/bind', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAuthzBackends(): Promise<AuthzBackendRecord[]> {
|
||||
return fetchJSON('/api/authz/backends');
|
||||
}
|
||||
|
||||
export async function registerAuthzBackend(payload: {
|
||||
name?: string;
|
||||
backend_id?: string;
|
||||
base_url?: string;
|
||||
save_to_backend?: boolean;
|
||||
authz_base_url?: string;
|
||||
}): Promise<AuthzRegisterBackendResponse> {
|
||||
return fetchJSON('/api/authz/backends/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}`);
|
||||
}
|
||||
|
||||
export async function enableAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/enable`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function disableAuthzBackend(backendId: string): Promise<AuthzBackendRecord> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/disable`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function rotateAuthzBackendSecret(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/rotate-secret`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthzBackendPermissions(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`);
|
||||
}
|
||||
|
||||
export async function setAuthzBackendPermissions(
|
||||
backendId: string,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/permissions`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthzBackendOutlookSettings(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`);
|
||||
}
|
||||
|
||||
export async function setAuthzBackendOutlookSettings(
|
||||
backendId: string,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAuthzBackendOutlookSettings(backendId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/backends/${encodeURIComponent(backendId)}/settings/outlook`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAuthzChannelSettings(): Promise<Record<string, AuthzChannelSettings>> {
|
||||
return fetchJSON('/api/authz/channel-settings');
|
||||
}
|
||||
|
||||
export async function getAuthzChannelSettings(channelId: string): Promise<AuthzChannelSettings> {
|
||||
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`);
|
||||
}
|
||||
|
||||
export async function setAuthzChannelSettings(
|
||||
channelId: string,
|
||||
payload: {
|
||||
configured?: boolean;
|
||||
config?: Record<string, unknown>;
|
||||
secrets?: Record<string, unknown>;
|
||||
}
|
||||
): Promise<AuthzChannelSettings> {
|
||||
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAuthzChannelSettings(channelId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/authz/channel-settings/${encodeURIComponent(channelId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function testMcpServer(serverId: string): Promise<Record<string, unknown>> {
|
||||
return fetchJSON(`/api/mcp/servers/${encodeURIComponent(serverId)}/test`, {
|
||||
method: 'POST',
|
||||
@ -1282,10 +1129,6 @@ export async function uploadSkill(file: File): Promise<Skill> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function migrateSkills(): Promise<{ included: Array<Record<string, unknown>>; skipped: Array<Record<string, unknown>> }> {
|
||||
return fetchJSON('/api/skills/migrate', { method: 'POST', timeoutMs: 45000 });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SkillHub marketplace
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -1356,50 +1199,6 @@ export async function installSkillHubSkill(
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Marketplace (proxied)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function listMarketplaces(): Promise<Marketplace[]> {
|
||||
return fetchJSON('/api/marketplaces');
|
||||
}
|
||||
|
||||
export async function addMarketplace(source: string): Promise<Marketplace> {
|
||||
return fetchJSON('/api/marketplaces', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ source }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeMarketplace(name: string): Promise<void> {
|
||||
await fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateMarketplace(name: string): Promise<Marketplace> {
|
||||
return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/update`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function listMarketplacePlugins(name: string): Promise<MarketplacePlugin[]> {
|
||||
return fetchJSON(`/api/marketplaces/${encodeURIComponent(name)}/plugins`);
|
||||
}
|
||||
|
||||
export async function installMarketplacePlugin(marketplaceName: string, pluginName: string): Promise<void> {
|
||||
await fetchJSON(
|
||||
`/api/marketplaces/${encodeURIComponent(marketplaceName)}/plugins/${encodeURIComponent(pluginName)}/install`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
}
|
||||
|
||||
export async function uninstallPlugin(pluginName: string): Promise<void> {
|
||||
await fetchJSON(`/api/plugins/${encodeURIComponent(pluginName)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Files (proxied)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { OfficeTaskStatus } from '@/lib/office';
|
||||
import type { TaskRuntimeStatus } from '@/lib/task-runtime';
|
||||
import type { ProcessArtifact, ProcessRun } from '@/types';
|
||||
import { getCurrentAppLocale, pickAppText, type AppLocale } from '@/lib/i18n/core';
|
||||
import type { WsStatus } from '@/lib/api';
|
||||
|
||||
export function appStatusLabel(
|
||||
status: ProcessRun['status'] | OfficeTaskStatus | string,
|
||||
status: ProcessRun['status'] | TaskRuntimeStatus | string,
|
||||
locale: AppLocale = getCurrentAppLocale()
|
||||
): string {
|
||||
if (status === 'queued') return pickAppText(locale, '排队中', 'Queued');
|
||||
@ -63,12 +63,12 @@ export function appArtifactPreview(artifact: ProcessArtifact, locale: AppLocale
|
||||
|
||||
export function appConnectionStatusLabel(
|
||||
wsStatus: WsStatus,
|
||||
nanobotReady: boolean | null,
|
||||
beaverReady: boolean | null,
|
||||
locale: AppLocale = getCurrentAppLocale()
|
||||
): string {
|
||||
const isOnline = wsStatus === 'connected' && nanobotReady === true;
|
||||
const isChecking = wsStatus === 'connected' && nanobotReady === null;
|
||||
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && nanobotReady === false);
|
||||
const isOnline = wsStatus === 'connected' && beaverReady === true;
|
||||
const isChecking = wsStatus === 'connected' && beaverReady === null;
|
||||
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && beaverReady === false);
|
||||
|
||||
if (isOnline) return pickAppText(locale, '已连接', 'Connected');
|
||||
if (isChecking) return pickAppText(locale, '检查中', 'Checking');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export const APP_LOCALE_COOKIE = 'nanobot_locale';
|
||||
export const APP_LOCALE_STORAGE_KEY = 'nanobot_locale';
|
||||
export const APP_LOCALE_COOKIE = 'beaver_locale';
|
||||
export const APP_LOCALE_STORAGE_KEY = 'beaver_locale';
|
||||
|
||||
export const APP_LOCALES = ['zh-CN', 'en-US'] as const;
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import type {
|
||||
} from '@/types';
|
||||
import type { WsStatus } from '@/lib/api';
|
||||
|
||||
const ACTIVE_SESSION_STORAGE_KEY = 'nanobot_active_session_id';
|
||||
const ACTIVE_SESSION_STORAGE_KEY = 'beaver_active_session_id';
|
||||
|
||||
function getInitialSessionId(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
@ -40,7 +40,7 @@ interface ChatStore {
|
||||
streamingContent: string;
|
||||
wsStatus: WsStatus;
|
||||
isThinking: boolean;
|
||||
nanobotReady: boolean | null;
|
||||
beaverReady: boolean | null;
|
||||
sessions: Session[];
|
||||
processRuns: ProcessRun[];
|
||||
processEvents: ProcessEvent[];
|
||||
@ -68,7 +68,7 @@ interface ChatStore {
|
||||
clearMessages: () => void;
|
||||
setWsStatus: (status: WsStatus) => void;
|
||||
setIsThinking: (thinking: boolean) => void;
|
||||
setNanobotReady: (ready: boolean | null) => void;
|
||||
setBeaverReady: (ready: boolean | null) => void;
|
||||
resetProcessState: () => void;
|
||||
ingestProcessEvent: (event: ProcessWsEvent) => void;
|
||||
setSessionProcess: (sessionId: string, projection: SessionProcessProjection) => void;
|
||||
@ -135,7 +135,7 @@ export const useChatStore = create<ChatStore>((set) => ({
|
||||
streamingContent: '',
|
||||
wsStatus: 'disconnected',
|
||||
isThinking: false,
|
||||
nanobotReady: null,
|
||||
beaverReady: null,
|
||||
sessions: [],
|
||||
processRuns: [],
|
||||
processEvents: [],
|
||||
@ -175,7 +175,7 @@ export const useChatStore = create<ChatStore>((set) => ({
|
||||
clearMessages: () => set({ messages: [], streamingContent: '' }),
|
||||
setWsStatus: (status) => set({ wsStatus: status }),
|
||||
setIsThinking: (thinking) => set({ isThinking: thinking }),
|
||||
setNanobotReady: (ready) => set({ nanobotReady: ready }),
|
||||
setBeaverReady: (ready) => set({ beaverReady: ready }),
|
||||
resetProcessState: () =>
|
||||
set({
|
||||
processRuns: [],
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { buildOfficeTaskList, buildOfficeView } from '@/lib/office';
|
||||
import { buildTaskRuntimeView } from '@/lib/task-runtime';
|
||||
import type { ProcessArtifact, ProcessEvent, ProcessRun, Session } from '@/types';
|
||||
|
||||
describe('office view builders', () => {
|
||||
describe('runtime view builders', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-03-24T12:00:00.000Z'));
|
||||
@ -13,7 +13,7 @@ describe('office view builders', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('builds an office view from a root run tree', () => {
|
||||
it('builds a runtime view from a root run tree', () => {
|
||||
const sessions: Session[] = [
|
||||
{
|
||||
key: 'web:default',
|
||||
@ -118,26 +118,21 @@ describe('office view builders', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const office = buildOfficeView('run-root', {
|
||||
const runtime = buildTaskRuntimeView('run-root', {
|
||||
sessions,
|
||||
processRuns,
|
||||
processEvents,
|
||||
processArtifacts,
|
||||
});
|
||||
|
||||
expect(office).not.toBeNull();
|
||||
expect(office?.taskId).toBe('run-root');
|
||||
expect(office?.title).toBe('整理竞品研究并给出结论');
|
||||
expect(office?.sourceSessionLabel).toBe('需求讨论');
|
||||
expect(office?.members).toHaveLength(3);
|
||||
expect(office?.tasks).toHaveLength(3);
|
||||
expect(office?.assignments).toHaveLength(1);
|
||||
expect(office?.progress.label).toBe('已完成子任务 1 / 3');
|
||||
expect(office?.currentStageLabel).toBe('分析结果');
|
||||
expect(office?.stats.artifactCount).toBe(1);
|
||||
expect(office?.zones.find((zone) => zone.id === 'workspace')?.memberIds).toContain('main-agent');
|
||||
expect(office?.zones.find((zone) => zone.id === 'collab')?.memberIds).toContain('research-agent');
|
||||
expect(office?.zones.find((zone) => zone.id === 'research')?.memberIds).toContain('search-mcp');
|
||||
expect(runtime).not.toBeNull();
|
||||
expect(runtime?.taskId).toBe('run-root');
|
||||
expect(runtime?.title).toBe('整理竞品研究并给出结论');
|
||||
expect(runtime?.sourceSessionLabel).toBe('需求讨论');
|
||||
expect(runtime?.tasks).toHaveLength(3);
|
||||
expect(runtime?.progress.label).toBe('已完成子任务 1 / 3');
|
||||
expect(runtime?.stats.artifactCount).toBe(1);
|
||||
expect(runtime?.stats.alertCount).toBe(0);
|
||||
});
|
||||
|
||||
it('marks stale waiting tasks as blocked and emits alerts', () => {
|
||||
@ -155,109 +150,14 @@ describe('office view builders', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const office = buildOfficeView('run-blocked', {
|
||||
const runtime = buildTaskRuntimeView('run-blocked', {
|
||||
sessions: [],
|
||||
processRuns,
|
||||
processEvents: [],
|
||||
processArtifacts: [],
|
||||
});
|
||||
|
||||
expect(office?.status).toBe('blocked');
|
||||
expect(office?.alerts).toHaveLength(1);
|
||||
expect(office?.alerts[0].level).toBe('warn');
|
||||
expect(office?.members[0].zoneId).toBe('collab');
|
||||
});
|
||||
|
||||
it('builds a filtered task list and sorts active tasks ahead of finished ones', () => {
|
||||
const sessions: Session[] = [
|
||||
{ key: 'web:alpha', path: 'Alpha Session' },
|
||||
{ key: 'web:beta', path: 'Beta Session' },
|
||||
];
|
||||
|
||||
const processRuns: ProcessRun[] = [
|
||||
{
|
||||
run_id: 'run-active',
|
||||
parent_run_id: null,
|
||||
session_id: 'web:alpha',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-a',
|
||||
actor_name: 'Agent A',
|
||||
title: '执行活跃任务',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:20:00.000Z',
|
||||
},
|
||||
{
|
||||
run_id: 'run-done',
|
||||
parent_run_id: null,
|
||||
session_id: 'web:alpha',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-b',
|
||||
actor_name: 'Agent B',
|
||||
title: '已结束任务',
|
||||
status: 'done',
|
||||
started_at: '2026-03-24T10:00:00.000Z',
|
||||
finished_at: '2026-03-24T10:08:00.000Z',
|
||||
},
|
||||
{
|
||||
run_id: 'run-other-session',
|
||||
parent_run_id: null,
|
||||
session_id: 'web:beta',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-c',
|
||||
actor_name: 'Agent C',
|
||||
title: '其他会话任务',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const tasks = buildOfficeTaskList({
|
||||
sessionId: 'web:alpha',
|
||||
sessions,
|
||||
processRuns,
|
||||
processEvents: [],
|
||||
processArtifacts: [],
|
||||
});
|
||||
|
||||
expect(tasks).toHaveLength(2);
|
||||
expect(tasks[0].taskId).toBe('run-active');
|
||||
expect(tasks[1].taskId).toBe('run-done');
|
||||
expect(tasks[0].sessionLabel).toBe('Alpha Session');
|
||||
});
|
||||
|
||||
it('keeps office tasks visible when the root run inherits session from descendants', () => {
|
||||
const tasks = buildOfficeTaskList({
|
||||
sessionId: 'web:alpha',
|
||||
sessions: [{ key: 'web:alpha', path: 'Alpha Session' }],
|
||||
processRuns: [
|
||||
{
|
||||
run_id: 'run-root-no-session',
|
||||
parent_run_id: null,
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-a',
|
||||
actor_name: 'Agent A',
|
||||
title: '根任务缺少会话字段',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:20:00.000Z',
|
||||
},
|
||||
{
|
||||
run_id: 'run-child-with-session',
|
||||
parent_run_id: 'run-root-no-session',
|
||||
session_id: 'web:alpha',
|
||||
actor_type: 'agent',
|
||||
actor_id: 'agent-b',
|
||||
actor_name: 'Agent B',
|
||||
title: '子任务仍带着会话字段',
|
||||
status: 'running',
|
||||
started_at: '2026-03-24T11:21:00.000Z',
|
||||
},
|
||||
],
|
||||
processEvents: [],
|
||||
processArtifacts: [],
|
||||
});
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].taskId).toBe('run-root-no-session');
|
||||
expect(tasks[0].sessionId).toBe('web:alpha');
|
||||
expect(runtime?.status).toBe('blocked');
|
||||
expect(runtime?.stats.alertCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user