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

@ -53,10 +53,9 @@
| `/status` | 系统状态 |
| `/cron` | 定时任务 |
| `/skills` | 技能管理 |
| `/plugins` | 插件管理 |
| `/agents` | 智能体管理 |
| `/mcp` | MCP 服务管理 |
| `/marketplace` | 插件市场 |
| `/marketplace` | 技能市场 |
| `/files` | 工作区文件管理 |
| `/help` | 帮助说明 |
@ -240,15 +239,9 @@ docker build \
- 状态接口
- WebSocket 连接
### 2. 命令名和目录名未做品牌迁移
### 2. 技术标识
当前仓库的部分技术标识仍沿用旧命名,例如:
- `nanobot web`
- `~/.beaver/plugins/`
- 本地存储中的旧 token key
这些属于兼容性和后端约定的一部分,前端展示品牌已替换为 `Boardware Genius`,但技术标识没有在这个仓库里强制迁移。
当前前端使用 Beaver 技术命名,本地 token、语言和 handoff 状态都使用 `beaver_*` key。
### 3. 动态内容可能仍包含英文

View File

@ -149,7 +149,6 @@ export default function NotificationDetailPage() {
processArtifacts={[]}
selectedRunId={null}
onSelectRun={() => {}}
onCancelRun={() => {}}
onFeedback={() => {}}
/>
</div>

View File

@ -7,7 +7,6 @@ import { Brain, Plus, Send, Trash2, X } from 'lucide-react';
import { ChatWorkbench } from '@/components/chat-workbench/ChatWorkbench';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
cancelDelegation,
archiveSession,
createSession,
getActiveTask,
@ -464,18 +463,6 @@ export default function ChatPage() {
setSessionId(key);
};
const handleCancelRun = useCallback(async (runId: string) => {
try {
await cancelDelegation(runId);
} catch (err: any) {
addMessage({
role: 'assistant',
content: pickAppText(locale, `取消任务 ${runId} 失败:${err.message || '未知错误'}`, `Failed to cancel task ${runId}: ${err.message || 'Unknown error'}`),
timestamp: new Date().toISOString(),
});
}
}, [addMessage, locale]);
const removePendingFile = useCallback((file: File) => {
setPendingFiles((prev) => prev.filter((item) => item.file !== file));
}, []);
@ -566,7 +553,6 @@ export default function ChatPage() {
processArtifacts={sessionProcessArtifacts}
selectedRunId={selectedSessionRunId}
onSelectRun={(runId) => setSelectedRunId(selectedSessionRunId === runId ? null : runId)}
onCancelRun={handleCancelRun}
onFeedback={handleFeedback}
/>
</div>

View File

@ -40,7 +40,6 @@ import {
listSkillCandidates,
listSkillDrafts,
listSkills,
migrateSkills,
publishSkillDraft,
regenerateSkillDraft,
rejectSkillDraft,
@ -207,15 +206,6 @@ export default function SkillsPage() {
<Upload className="mr-2 h-4 w-4" />
{t('上传技能', 'Upload skill')}
</Button>
<Button
onClick={() => void runAction('migrate-skills', () => migrateSkills())}
variant="outline"
size="sm"
disabled={Boolean(actionId)}
>
{actionId === 'migrate-skills' ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Rocket className="mr-2 h-4 w-4" />}
{t('迁移旧技能', 'Migrate legacy skills')}
</Button>
</div>
</div>

View File

@ -15,17 +15,7 @@ import {
Settings2,
ScrollText,
} from 'lucide-react';
import { getStatus, restartSystem, updateProviderConfig } from '@/lib/api';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { getStatus, updateProviderConfig } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@ -57,9 +47,6 @@ export default function StatusPage() {
const [status, setStatus] = useState<SystemStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
const [restarting, setRestarting] = useState(false);
const [restartError, setRestartError] = useState<string | null>(null);
const [selectedProvider, setSelectedProvider] = useState<ProviderStatus | null>(null);
const [providerForm, setProviderForm] = useState<ProviderFormState>(() => ({
enabled: false,
@ -88,36 +75,6 @@ export default function StatusPage() {
loadStatus();
}, []);
useEffect(() => {
if (!restarting) {
return;
}
const intervalId = window.setInterval(async () => {
try {
await getStatus();
window.location.reload();
} catch {
// Ignore failures until the container is back.
}
}, 3000);
return () => {
window.clearInterval(intervalId);
};
}, [restarting]);
const handleRestart = async () => {
setRestartError(null);
try {
await restartSystem();
setRestartDialogOpen(false);
setRestarting(true);
} catch (err: any) {
setRestartError(err.message || pickAppText(locale, '重启失败', 'Restart failed'));
}
};
const openProviderDialog = (provider: ProviderStatus) => {
setSelectedProvider(provider);
setProviderError(null);
@ -204,7 +161,7 @@ export default function StatusPage() {
)}
</p>
</div>
<Button onClick={loadStatus} variant="outline" size="sm" disabled={restarting}>
<Button onClick={loadStatus} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
{pickAppText(locale, '刷新', 'Refresh')}
</Button>
@ -223,13 +180,8 @@ export default function StatusPage() {
<div className="space-y-1">
<p className="text-sm font-medium">{pickAppText(locale, '运行与调试', 'Runtime and debugging')}</p>
<p className="text-sm text-muted-foreground">
{restarting
? pickAppText(locale, '正在重启当前 docker服务恢复后页面会自动刷新。', 'Restarting the current Docker container. The page will refresh automatically once the service is back.')
: pickAppText(locale, '查看每次对话的运行日志,或重启当前 docker 容器。重启完成后需要重新登录。', 'Inspect per-chat runtime logs or restart the current Docker container. You will need to sign in again afterwards.')}
{pickAppText(locale, '查看每次对话的运行日志和当前实例运行状态。', 'Inspect per-chat runtime logs and current instance status.')}
</p>
{restartError ? (
<p className="text-sm text-destructive">{restartError}</p>
) : null}
</div>
<div className="flex flex-wrap justify-start gap-2 lg:justify-end">
<Button asChild variant="outline">
@ -238,34 +190,6 @@ export default function StatusPage() {
{pickAppText(locale, '运行日志', 'Runtime Logs')}
</Link>
</Button>
<AlertDialog open={restartDialogOpen} onOpenChange={setRestartDialogOpen}>
<Button
variant="destructive"
onClick={() => setRestartDialogOpen(true)}
disabled={restarting}
>
{restarting ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<RefreshCw className="w-4 h-4 mr-2" />
)}
Restart
</Button>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{pickAppText(locale, '确认重启当前实例?', 'Restart the current instance?')}</AlertDialogTitle>
<AlertDialogDescription>
{pickAppText(locale, '这会重启当前 docker 容器,页面会短暂不可用。由于当前登录态保存在内存里,重启完成后需要重新登录。', 'This restarts the current Docker container and the page will be temporarily unavailable. Because the current sign-in state is stored in memory, you will need to sign in again after the restart.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={restarting}>{pickAppText(locale, '取消', 'Cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={handleRestart} disabled={restarting}>
{restarting ? pickAppText(locale, '重启中...', 'Restarting...') : pickAppText(locale, '确认重启', 'Confirm restart')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="mt-5 grid gap-3 border-t pt-5 md:grid-cols-2">

View File

@ -3,21 +3,21 @@
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import React, { useMemo, useState } from 'react';
import { AlertCircle, ArrowLeft, Bot, CheckCircle2, Download, FileText, HelpCircle, MessageSquare, RefreshCw, RotateCcw, Trash2, User, XCircle } from 'lucide-react';
import { AlertCircle, ArrowLeft, Bot, CheckCircle2, Download, FileText, HelpCircle, MessageSquare, RefreshCw, Trash2, User, XCircle } from 'lucide-react';
import { OfficeStatusBadge, formatOfficeDuration, formatOfficeTime, progressPercent } from '@/components/office/OfficeShared';
import { TaskRuntimeStatusBadge, formatTaskRuntimeDuration, formatTaskRuntimeTime, progressPercent } from '@/components/task-runtime/TaskRuntimeShared';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
import { cancelDelegation, deleteBackendTask, getBackendTask, getFileUrl, retryDelegation, submitChatFeedback } from '@/lib/api';
import { deleteBackendTask, getBackendTask, getFileUrl, submitChatFeedback } from '@/lib/api';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import { buildOfficeView, isOfficeTaskTerminal, type OfficeTaskView } from '@/lib/office';
import { buildTaskRuntimeView, type TaskRuntimeNodeView } from '@/lib/task-runtime';
import { useChatStore } from '@/lib/store';
import type { BackendTask, BackendTaskRun, ProcessArtifact, ProcessEvent } from '@/types';
function taskVisibleStatus(task: OfficeTaskView, locale: 'zh-CN' | 'en-US') {
function taskVisibleStatus(task: TaskRuntimeNodeView, locale: 'zh-CN' | 'en-US') {
if (task.status === 'error') return pickAppText(locale, '任务失败', 'Task failed');
if (task.status === 'cancelled') return pickAppText(locale, '已取消', 'Cancelled');
return task.stageLabel || task.status;
@ -46,7 +46,7 @@ export default function TaskDetailPage() {
const updateMessageFeedback = useChatStore((state) => state.updateMessageFeedback);
const task = useMemo(
() => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale),
() => buildTaskRuntimeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale),
[locale, processArtifacts, processEvents, processRuns, sessions, taskId]
);
const [backendTask, setBackendTask] = useState<BackendTask | null>(null);
@ -105,7 +105,7 @@ export default function TaskDetailPage() {
return map;
}, [artifacts]);
const phaseGroups = useMemo(() => {
const groups = new Map<string, OfficeTaskView[]>();
const groups = new Map<string, TaskRuntimeNodeView[]>();
for (const item of task?.tasks ?? []) {
const label = item.stageLabel || taskVisibleStatus(item, locale);
groups.set(label, [...(groups.get(label) ?? []), item]);
@ -180,7 +180,7 @@ export default function TaskDetailPage() {
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span>{pickAppText(locale, '来源会话', 'Session')}: {backendTask.session_id}</span>
<span>{pickAppText(locale, '创建者', 'Creator')}: {backendTask.creator}</span>
<span>{pickAppText(locale, '更新', 'Updated')}: {formatOfficeTime(backendTask.updated_at, locale)}</span>
<span>{pickAppText(locale, '更新', 'Updated')}: {formatTaskRuntimeTime(backendTask.updated_at, locale)}</span>
</div>
</CardContent>
</Card>
@ -242,7 +242,7 @@ export default function TaskDetailPage() {
<div key={index} className="rounded-md border border-border p-3">
<div className="font-medium">{humanFeedback(String(item.feedback_type || ''), locale)}</div>
{item.comment ? <p className="mt-1 text-muted-foreground">{String(item.comment)}</p> : null}
{item.created_at ? <p className="mt-1 text-xs text-muted-foreground">{formatOfficeTime(String(item.created_at), locale)}</p> : null}
{item.created_at ? <p className="mt-1 text-xs text-muted-foreground">{formatTaskRuntimeTime(String(item.created_at), locale)}</p> : null}
</div>
))
)}
@ -295,25 +295,6 @@ export default function TaskDetailPage() {
</Link>
</Button>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={Boolean(actionBusy) || isOfficeTaskTerminal(task.status)}
onClick={() => void runAction('cancel', () => cancelDelegation(task.rootRunId))}
>
<XCircle className="mr-2 h-4 w-4" />
{pickAppText(locale, '取消任务', 'Cancel task')}
</Button>
<Button
size="sm"
disabled={Boolean(actionBusy) || !isOfficeTaskTerminal(task.status)}
onClick={() => void runAction('retry', () => retryDelegation(task.rootRunId))}
>
<RotateCcw className="mr-2 h-4 w-4" />
{pickAppText(locale, '重试任务', 'Retry task')}
</Button>
</div>
</div>
<Card>
@ -322,20 +303,20 @@ export default function TaskDetailPage() {
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-3">
<h1 className="truncate text-2xl font-semibold">{task.title}</h1>
<OfficeStatusBadge status={task.status} />
<TaskRuntimeStatusBadge status={task.status} />
</div>
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span>{pickAppText(locale, '来源会话', 'Session')}: {task.sourceSessionLabel}</span>
<span>{pickAppText(locale, '主 Agent', 'Lead agent')}: {task.rootActorName}</span>
<span>{pickAppText(locale, '开始', 'Started')}: {formatOfficeTime(task.createdAt, locale)}</span>
<span>{pickAppText(locale, '耗时', 'Duration')}: {formatOfficeDuration(task.durationMs, locale)}</span>
<span>{pickAppText(locale, '开始', 'Started')}: {formatTaskRuntimeTime(task.createdAt, locale)}</span>
<span>{pickAppText(locale, '耗时', 'Duration')}: {formatTaskRuntimeDuration(task.durationMs, locale)}</span>
</div>
</div>
<div className="grid w-full gap-3 sm:grid-cols-4 lg:w-[520px]">
<Metric label={pickAppText(locale, '节点', 'Nodes')} value={String(task.stats.totalRuns)} />
<Metric label={pickAppText(locale, '活跃', 'Active')} value={String(task.stats.activeRuns)} />
<Metric label={pickAppText(locale, '产物', 'Artifacts')} value={String(task.stats.artifactCount)} />
<Metric label={pickAppText(locale, '异常', 'Alerts')} value={String(task.alerts.length)} />
<Metric label={pickAppText(locale, '异常', 'Alerts')} value={String(task.stats.alertCount)} />
</div>
</div>
<div className="mt-5 space-y-2">
@ -398,7 +379,7 @@ export default function TaskDetailPage() {
<div className="truncate font-medium">{node.title}</div>
<div className="mt-1 text-xs text-muted-foreground">{node.actorName}</div>
</div>
<OfficeStatusBadge status={node.status} />
<TaskRuntimeStatusBadge status={node.status} />
</div>
<div className="mt-3 text-sm text-muted-foreground">
{node.summary || taskVisibleStatus(node, locale)}
@ -426,13 +407,13 @@ export default function TaskDetailPage() {
<div className="font-medium">{selectedNode.title}</div>
<div className="mt-1 text-xs text-muted-foreground">{selectedNode.runId}</div>
</div>
<OfficeStatusBadge status={selectedNode.status} />
<TaskRuntimeStatusBadge status={selectedNode.status} />
<p className="text-sm text-muted-foreground">{selectedNode.summary || '-'}</p>
<div className="space-y-2">
{(eventsByRun.get(selectedNode.runId) ?? []).slice(-5).map((event) => (
<div key={event.event_id} className="rounded-md border border-border bg-muted/30 p-2 text-xs">
<div className="font-medium">{event.kind}</div>
<div className="mt-1 text-muted-foreground">{event.text || formatOfficeTime(event.created_at, locale)}</div>
<div className="mt-1 text-muted-foreground">{event.text || formatTaskRuntimeTime(event.created_at, locale)}</div>
</div>
))}
</div>
@ -548,7 +529,7 @@ function BackendRunConversation({ run, index }: { run: BackendTaskRun; index: nu
<div>
<div className="font-medium">{run.title || pickAppText(locale, `Agent ${index + 1}`, `Agent ${index + 1}`)}</div>
<div className="mt-1 text-xs text-muted-foreground">
{run.started_at ? formatOfficeTime(run.started_at, locale) : pickAppText(locale, '时间未知', 'Unknown time')}
{run.started_at ? formatTaskRuntimeTime(run.started_at, locale) : pickAppText(locale, '时间未知', 'Unknown time')}
{run.finish_reason ? ` · ${humanFinishReason(run.finish_reason, locale)}` : ''}
</div>
</div>
@ -573,7 +554,7 @@ function BackendRunConversation({ run, index }: { run: BackendTaskRun; index: nu
<div className="min-w-0 flex-1">
<div className="mb-1 flex items-center gap-2 text-xs text-muted-foreground">
<span>{isAssistant ? run.title || pickAppText(locale, 'Agent 回复', 'Agent reply') : isTool ? message.tool_name || pickAppText(locale, '工具结果', 'Tool result') : pickAppText(locale, '用户要求', 'User request')}</span>
{message.created_at ? <span>{formatOfficeTime(message.created_at, locale)}</span> : null}
{message.created_at ? <span>{formatTaskRuntimeTime(message.created_at, locale)}</span> : null}
</div>
<div className="whitespace-pre-wrap rounded-md border border-border bg-muted/20 px-3 py-2 text-sm leading-6">
{message.content}

View File

@ -5,7 +5,7 @@ import { useSearchParams } from 'next/navigation';
import React, { useEffect, useMemo, useState } from 'react';
import { AlertCircle, ArrowRight, Clock3, FolderDown, ListTodo, Loader2, Play, Plus, RefreshCw, Trash2, X } from 'lucide-react';
import { formatOfficeTime } from '@/components/office/OfficeShared';
import { formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared';
import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@ -151,7 +151,7 @@ function OrdinaryTasks() {
</TableCell>
<TableCell className="text-sm text-muted-foreground">{task.run_ids.length}</TableCell>
<TableCell className="text-sm text-muted-foreground">{task.skill_names.length}</TableCell>
<TableCell className="text-xs text-muted-foreground">{formatOfficeTime(task.updated_at, locale)}</TableCell>
<TableCell className="text-xs text-muted-foreground">{formatTaskRuntimeTime(task.updated_at, locale)}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button asChild size="sm" variant="outline">

View File

@ -8,7 +8,7 @@ import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import { useChatStore } from '@/lib/store';
const HANDOFF_STATE_KEY = 'nanobot_handoff_state';
const HANDOFF_STATE_KEY = 'beaver_handoff_state';
type HandoffState = {
code?: string;

View File

@ -40,7 +40,7 @@ export function AppRuntimeBridge() {
const sessionId = useChatStore((state) => state.sessionId);
const setSessions = useChatStore((state) => state.setSessions);
const setWsStatus = useChatStore((state) => state.setWsStatus);
const setNanobotReady = useChatStore((state) => state.setNanobotReady);
const setBeaverReady = useChatStore((state) => state.setBeaverReady);
const resetProcessState = useChatStore((state) => state.resetProcessState);
const ingestProcessEvent = useChatStore((state) => state.ingestProcessEvent);
const statusCheckCleanupRef = React.useRef<(() => void) | null>(null);
@ -63,14 +63,14 @@ export function AppRuntimeBridge() {
statusCheckInFlightRef.current = true;
try {
await getStatus();
setNanobotReady(true);
setBeaverReady(true);
} catch {
setNanobotReady(false);
setBeaverReady(false);
} finally {
statusCheckInFlightRef.current = false;
}
});
}, [setNanobotReady]);
}, [setBeaverReady]);
React.useEffect(() => {
void loadSessions();
@ -89,7 +89,7 @@ export function AppRuntimeBridge() {
} else {
statusCheckCleanupRef.current?.();
statusCheckCleanupRef.current = null;
setNanobotReady(null);
setBeaverReady(null);
}
});
@ -98,7 +98,7 @@ export function AppRuntimeBridge() {
statusCheckCleanupRef.current = null;
unsubStatus();
};
}, [scheduleStatusCheck, setNanobotReady, setWsStatus]);
}, [scheduleStatusCheck, setBeaverReady, setWsStatus]);
React.useEffect(() => {
const unsubMessage = wsManager.onMessage((data) => {

View File

@ -3,7 +3,7 @@
import React from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Bell, Bot, ChevronDown, FolderOpen, ListTodo, LogOut, Mail, MessageSquare, PackageOpen, Puzzle, Settings, Store, Wrench } from 'lucide-react';
import { Bell, Bot, ChevronDown, FolderOpen, ListTodo, LogOut, Mail, MessageSquare, Puzzle, Settings, Store, Wrench } from 'lucide-react';
import { logout } from '@/lib/api';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
@ -15,7 +15,7 @@ import { useAppI18n } from '@/lib/i18n/provider';
import { useChatStore } from '@/lib/store';
type NavItem = {
key: 'chat' | 'tasks' | 'notifications' | 'skills' | 'files' | 'tools' | 'agents' | 'outlook' | 'marketplace' | 'plugins' | 'settings';
key: 'chat' | 'tasks' | 'notifications' | 'skills' | 'files' | 'tools' | 'agents' | 'outlook' | 'marketplace' | 'settings';
href: string;
icon: React.ComponentType<{ className?: string }>;
matchPrefixes?: string[];
@ -23,7 +23,7 @@ type NavItem = {
const NAV_ITEMS: NavItem[] = [
{ key: 'chat', href: '/', icon: MessageSquare },
{ key: 'tasks', href: '/tasks', icon: ListTodo, matchPrefixes: ['/tasks', '/office', '/cron'] },
{ key: 'tasks', href: '/tasks', icon: ListTodo, matchPrefixes: ['/tasks', '/cron'] },
{ key: 'notifications', href: '/notifications', icon: Bell, matchPrefixes: ['/notifications'] },
{ key: 'skills', href: '/skills', icon: Puzzle },
{ key: 'files', href: '/files', icon: FolderOpen, matchPrefixes: ['/files'] },
@ -31,7 +31,6 @@ const NAV_ITEMS: NavItem[] = [
{ key: 'agents', href: '/agents', icon: Bot, matchPrefixes: ['/agents'] },
{ key: 'outlook', href: '/outlook', icon: Mail, matchPrefixes: ['/outlook'] },
{ key: 'marketplace', href: '/marketplace', icon: Store, matchPrefixes: ['/marketplace'] },
{ key: 'plugins', href: '/plugins', icon: PackageOpen, matchPrefixes: ['/plugins'] },
{
key: 'settings',
href: '/settings',
@ -43,12 +42,12 @@ const NAV_ITEMS: NavItem[] = [
function ConnectionDot() {
const { locale } = useAppI18n();
const wsStatus = useChatStore((s) => s.wsStatus);
const nanobotReady = useChatStore((s) => s.nanobotReady);
const beaverReady = useChatStore((s) => s.beaverReady);
const isOnline = wsStatus === 'connected' && nanobotReady === true;
const isChecking = wsStatus === 'connected' && nanobotReady === null;
const isOnline = wsStatus === 'connected' && beaverReady === true;
const isChecking = wsStatus === 'connected' && beaverReady === null;
const isConnecting = wsStatus === 'connecting' || isChecking;
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && nanobotReady === false);
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && beaverReady === false);
const color = isOnline
? 'bg-[#869683]'
@ -56,7 +55,7 @@ function ConnectionDot() {
? 'bg-[#8B7E77]'
: 'bg-[#5F5550]';
const label = appConnectionStatusLabel(wsStatus, nanobotReady, locale);
const label = appConnectionStatusLabel(wsStatus, beaverReady, locale);
return (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
@ -84,7 +83,6 @@ const Header = () => {
if (key === 'agents') return pickAppText(locale, '智能体', 'Agents');
if (key === 'outlook') return 'Outlook';
if (key === 'marketplace') return pickAppText(locale, '市场', 'Marketplace');
if (key === 'plugins') return pickAppText(locale, '插件', 'Plugins');
return pickAppText(locale, '配置', 'Settings');
}, [locale]);

View File

@ -1,11 +1,10 @@
'use client';
import React from 'react';
import { CheckCircle2, Loader2, Sparkles, Square } from 'lucide-react';
import { CheckCircle2, Loader2, Sparkles } from 'lucide-react';
import type { ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { appArtifactPreview, appFeedRoleLabel, appStatusLabel } from '@/lib/i18n/common';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
@ -410,7 +409,6 @@ export function AgentTeamBlock({
artifacts,
selectedRunId,
onSelectRun,
onCancelRun,
}: {
rootRun: ProcessRun;
memberRuns: ProcessRun[];
@ -418,7 +416,6 @@ export function AgentTeamBlock({
artifacts: ProcessArtifact[];
selectedRunId: string | null;
onSelectRun: (runId: string) => void;
onCancelRun: (runId: string) => void;
}) {
const { locale } = useAppI18n();
const phases = useRunCardPhases(memberRuns);
@ -435,10 +432,6 @@ export function AgentTeamBlock({
const terminalRuns = sortedRuns.filter((run) => TERMINAL_STATUSES.has(run.status));
const collapsedRuns = sortedRuns.filter((run) => phases[run.run_id] === 'collapsed');
const liveCount = liveRuns.filter((run) => !TERMINAL_STATUSES.has(run.status)).length;
const canCancelRoot =
!rootRun.parent_run_id &&
(rootRun.status === 'running' || rootRun.status === 'waiting');
if (liveRuns.length === 0 && terminalRuns.length > 0) {
return (
<div className="inline-flex max-w-full flex-wrap items-start gap-2 rounded-2xl border border-border/60 bg-card/35 px-3 py-3 backdrop-blur-sm">
@ -486,12 +479,6 @@ export function AgentTeamBlock({
</p>
</div>
<div className="flex items-center gap-2">
{canCancelRoot && (
<Button variant="outline" size="sm" className="bg-background/60" onClick={() => onCancelRun(rootRun.run_id)}>
<Square className="mr-1.5 h-3.5 w-3.5" />
{pickAppText(locale, '取消', 'Cancel')}
</Button>
)}
<Badge variant="outline" className="border-border/70 bg-background/55 text-foreground/85">
{pickAppText(locale, `${memberRuns.length} 个子任务`, `${memberRuns.length} subtasks`)}
</Badge>

View File

@ -15,7 +15,6 @@ export function ChatWorkbench({
processArtifacts,
selectedRunId,
onSelectRun,
onCancelRun,
onFeedback,
}: {
messages: ChatMessage[];
@ -27,7 +26,6 @@ export function ChatWorkbench({
processArtifacts: ProcessArtifact[];
selectedRunId: string | null;
onSelectRun: (runId: string) => void;
onCancelRun: (runId: string) => void;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
}) {
return (
@ -42,7 +40,6 @@ export function ChatWorkbench({
processArtifacts={processArtifacts}
selectedRunId={selectedRunId}
onSelectRun={onSelectRun}
onCancelRun={onCancelRun}
onFeedback={onFeedback}
/>
</div>

View File

@ -329,7 +329,6 @@ export function MessageList({
processArtifacts,
selectedRunId,
onSelectRun,
onCancelRun,
onFeedback,
}: {
messages: ChatMessage[];
@ -341,7 +340,6 @@ export function MessageList({
processArtifacts: ProcessArtifact[];
selectedRunId: string | null;
onSelectRun: (runId: string) => void;
onCancelRun: (runId: string) => void;
onFeedback: (runId: string, feedbackType: 'satisfied' | 'revise' | 'abandon', comment?: string) => void;
}) {
const { locale } = useAppI18n();
@ -411,7 +409,6 @@ export function MessageList({
artifacts={processArtifacts}
selectedRunId={selectedRunId}
onSelectRun={onSelectRun}
onCancelRun={onCancelRun}
/>
)
)}

View File

@ -3,14 +3,14 @@
import { useAppI18n } from '@/lib/i18n/provider';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import type { OfficeTaskStatus, OfficeZoneView } from '@/lib/office';
import { officeTaskStatusLabel } from '@/lib/office';
import type { TaskRuntimeStatus } from '@/lib/task-runtime';
import { taskRuntimeStatusLabel } from '@/lib/task-runtime';
export function OfficeStatusBadge({
export function TaskRuntimeStatusBadge({
status,
className,
}: {
status: OfficeTaskStatus;
status: TaskRuntimeStatus;
className?: string;
}) {
const { locale } = useAppI18n();
@ -30,12 +30,12 @@ export function OfficeStatusBadge({
className
)}
>
{officeTaskStatusLabel(status, locale)}
{taskRuntimeStatusLabel(status, locale)}
</Badge>
);
}
export function formatOfficeTime(value?: string | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
export function formatTaskRuntimeTime(value?: string | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
@ -47,7 +47,7 @@ export function formatOfficeTime(value?: string | null, locale: 'zh-CN' | 'en-US
}).format(date);
}
export function formatOfficeDuration(durationMs: number | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
export function formatTaskRuntimeDuration(durationMs: number | null, locale: 'zh-CN' | 'en-US' = 'zh-CN'): string {
if (durationMs === null || durationMs < 0) return '-';
if (durationMs < 1000) return locale === 'en-US' ? '<1s' : '<1秒';
@ -65,15 +65,3 @@ export function progressPercent(value: number | null, max: number | null): numbe
if (value === null || max === null || max <= 0) return 0;
return Math.max(0, Math.min(100, Math.round((value / max) * 100)));
}
export function zonePanelClassName(zone: OfficeZoneView): string {
return cn(
'relative min-h-[220px] overflow-hidden rounded-2xl border p-4 shadow-sm',
'before:pointer-events-none before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.9),transparent_40%)]',
zone.tone === 'info' && 'border-[#BCC4CE] bg-[#E4E7EB]/70',
zone.tone === 'warn' && 'border-[#B8AEA8] bg-[#E7E2DE]/70',
zone.tone === 'danger' && 'border-[#B8AEA8] bg-[#E7E2DE]/80',
zone.tone === 'success' && 'border-[#B7C2B5] bg-[#E3E8E2]/75',
zone.tone === 'neutral' && 'border-border bg-card'
);
}

View File

@ -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)
// ---------------------------------------------------------------------------

View File

@ -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');

View File

@ -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;

View File

@ -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: [],

View File

@ -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);
});
});

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);
});
}

View File

@ -6,14 +6,6 @@ const nextConfig = {
ignoreDuringBuilds: true,
},
images: { unoptimized: true },
webpack: (config) => {
config.resolve = config.resolve || {};
config.resolve.alias = {
...(config.resolve.alias || {}),
phaser3spectorjs: false,
};
return config;
},
};
module.exports = nextConfig;

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "nextjs",
"name": "beaver-app-instance-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
@ -11,86 +11,41 @@
"test": "vitest run"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@next/swc-wasm-nodejs": "13.5.1",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@supabase/supabase-js": "^2.58.0",
"@tailwindcss/typography": "^0.5.19",
"@types/file-saver": "^2.0.7",
"@types/jspdf": "^1.3.3",
"@types/node": "20.6.2",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"docx": "^9.5.1",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"embla-carousel-react": "^8.3.0",
"eslint": "8.49.0",
"eslint-config-next": "13.5.1",
"file-saver": "^2.0.5",
"input-otp": "^1.2.4",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.446.0",
"next": "13.5.1",
"next-themes": "^0.3.0",
"pdfmake": "^0.2.20",
"pdfmake-with-chinese-fonts": "^1.0.16",
"phaser": "^3.90.0",
"postcss": "8.4.30",
"react": "18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-force-graph-2d": "^1.29.0",
"react-hook-form": "^7.53.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.3",
"react-resize-detector": "^12.3.0",
"recharts": "^2.12.7",
"remark-gfm": "^4.0.1",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.2.2",
"vaul": "^0.9.9",
"xlsx": "^0.18.5",
"zod": "^3.23.8",
"zustand": "^5.0.8"
},
"devDependencies": {
"@types/pdfmake": "^0.2.12",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.1"
}

View File

@ -1,4 +1,4 @@
// Nanobot frontend types
// Beaver frontend types
export interface AuthUser {
id: string;
@ -222,34 +222,6 @@ export interface SkillDetailResponse {
frontmatter?: Record<string, unknown>;
}
export interface SlashCommand {
name: string;
description: string;
argument_hint: string | null;
plugin_name: string;
}
export interface PluginAgent {
name: string;
description: string;
model: string | null;
}
export interface PluginCommand {
name: string;
description: string;
argument_hint: string | null;
}
export interface PluginInfo {
name: string;
description: string;
source: 'global' | 'workspace';
agents: PluginAgent[];
commands: PluginCommand[];
skills: string[];
}
export interface CronJob {
id: string;
name: string;
@ -380,19 +352,6 @@ export interface ActiveTask {
updated_at: string;
}
export interface Marketplace {
name: string;
source: string;
type: 'local' | 'git';
}
export interface MarketplacePlugin {
name: string;
description: string;
marketplace_name: string;
installed: boolean;
}
export interface SkillHubVersionRef {
id?: number;
version: string;
@ -557,14 +516,6 @@ export interface AuthzLocalBackendStatus {
registered: boolean;
}
export interface AuthzChannelSettings {
configured: boolean;
config?: Record<string, unknown>;
secrets_masked?: boolean;
secret_keys?: string[];
updated_at?: string;
}
export interface AuthzStatus {
enabled: boolean;
base_url: string;
@ -573,35 +524,10 @@ export interface AuthzStatus {
backend?: Record<string, unknown>;
permissions?: Record<string, unknown>;
outlook?: Record<string, unknown>;
channel_settings?: Record<string, AuthzChannelSettings>;
channel_settings?: Record<string, unknown>;
error?: string;
}
export interface AuthzBackendRecord {
backend_id: string;
name: string;
base_url: string;
frontend_base_url?: string | null;
status: string;
created_at: string;
updated_at: string;
is_local_backend?: boolean;
}
export interface AuthzRegisterBackendResponse {
backend_id: string;
client_id: string;
client_secret: string;
created_at: string;
saved_to_backend: boolean;
local_backend?: AuthzLocalBackendStatus & {
authz?: {
enabled: boolean;
base_url: string;
};
};
}
export interface OutlookMailboxAddress {
emailAddress?: {
name?: string | null;