Files
beaver_project/app-instance/frontend/components/Header.tsx
steven_li 0c180f48f2 feat(delegation): 添加直连模式下的委托公告回调机制
- 引入 DirectAnnouncementCallback 类型用于处理直连模式下的公告
- 在 DelegationManager 中添加 _direct_announcement_callback 属性和设置方法
- 实现 _notify_direct_announcement 方法用于在非总线模式下将公告回写到本地会话
- 在委托取消、完成和分组完成时添加对直连公告的通知逻辑

feat(web): 增加 WebSocket 广播器支持实时会话更新通知

- 创建 WebSocketBroadcaster 类用于跟踪认证的 WebSocket 连接并广播 JSON 事件
- 在应用启动时初始化 websocket_broadcaster 实例
- 实现连接注册、注销和消息广播功能
- 添加过期连接清理机制

feat(agent): 新增系统公告处理方法支持本地处理

- 在 AgentLoop 中添加 process_system_announcement 方法用于在无常驻 run() 场景下处理系统公告
- 创建 InboundMessage 并通过 _process_message 进行处理

feat(cron): 改进定时任务的会话路由解析和实时更新

- 添加 _resolve_cron_session_key 和 _infer_cron_route_from_session_key 辅助函数
- 在 cron 任务执行完成后通过 WebSocket 广播会话更新事件
- 在添加定时任务时自动推断目标会话的渠道和聊天 ID

refactor: 项目名称从 Boardware Genius 统一改为 Boardware Agent Sandbox

- 更新前端页面标题和描述文本中的产品名称
- 添加新的品牌 Logo 图片资源
- 在前端布局中使用新的 Logo 显示
- 更新授权门户中的品牌信息和 Logo 显示

feat(frontend): 添加会话更新事件监听实现消息自动刷新

- 定义 SessionUpdatedEvent 类型接口
- 在 ChatPage 中添加会话更新事件的处理逻辑
- 当收到会话更新事件时自动重新加载会话列表和当前会话消息

feat(api): 扩展定时任务 API 支持会话键参数

- 在 addCronJob API 参数中添加 session_key 字段
- 更新前端 Cron 页面的表单处理以传递当前会话键
2026-03-18 14:34:25 +08:00

168 lines
6.2 KiB
TypeScript

'use client';
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { usePathname, useRouter } from 'next/navigation';
import { MessageSquare, Activity, Clock, Puzzle, Blocks, HelpCircle, FolderOpen, Store, LogIn, UserPlus, Bot, ServerCog, Mail, LogOut, UserCircle2 } from 'lucide-react';
import { logout } from '@/lib/api';
import { useChatStore } from '@/lib/store';
const NAV_ITEMS = [
{ name: '对话', href: '/', icon: MessageSquare },
{ name: '状态', href: '/status', icon: Activity },
{ name: '定时任务', href: '/cron', icon: Clock },
{ name: '技能', href: '/skills', icon: Puzzle },
{ name: '插件', href: '/plugins', icon: Blocks },
{ name: '智能体', href: '/agents', icon: Bot },
{ name: 'MCP', href: '/mcp', icon: ServerCog },
{ name: 'Outlook', href: '/outlook', icon: Mail },
{ name: '市场', href: '/marketplace', icon: Store },
{ name: '文件', href: '/files', icon: FolderOpen },
{ name: '帮助', href: '/help', icon: HelpCircle },
];
const AUTH_ITEMS = [
{ name: '登录', href: '/login', icon: LogIn },
{ name: '注册', href: '/register', icon: UserPlus },
];
function ConnectionDot() {
const wsStatus = useChatStore((s) => s.wsStatus);
const nanobotReady = useChatStore((s) => s.nanobotReady);
const isOnline = wsStatus === 'connected' && nanobotReady === true;
const isChecking = wsStatus === 'connected' && nanobotReady === null;
const isConnecting = wsStatus === 'connecting' || isChecking;
const isOffline = wsStatus === 'disconnected' || (wsStatus === 'connected' && nanobotReady === false);
const color = isOnline
? 'bg-green-500'
: isConnecting
? 'bg-yellow-500'
: 'bg-red-500';
const label = isOnline
? '已连接'
: isChecking
? '检查中'
: wsStatus === 'connecting'
? '连接中'
: isOffline && wsStatus === 'connected'
? '服务离线'
: '未连接';
return (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className={`w-2 h-2 rounded-full ${color}`} />
<span>{label}</span>
</div>
);
}
const Header = () => {
const pathname = usePathname();
const router = useRouter();
const user = useChatStore((s) => s.user);
const isAuthLoading = useChatStore((s) => s.isAuthLoading);
const setUser = useChatStore((s) => s.setUser);
const handleLogout = async () => {
await logout();
setUser(null);
router.replace('/login');
router.refresh();
};
return (
<header className="fixed top-0 left-0 right-0 bg-background border-b border-border z-50">
<div className="max-w-[1720px] mx-auto px-5 sm:px-6 lg:px-8 xl:px-10">
<div className="flex items-center h-16 gap-6">
<Link href="/" className="flex shrink-0 items-center gap-3 pr-2">
<Image
src="/boardware-logo.svg"
alt="Boardware logo"
width={40}
height={32}
className="h-8 w-10 shrink-0 rounded-sm bg-white object-contain p-0.5"
/>
<span className="whitespace-nowrap text-[1.05rem] font-semibold leading-none tracking-tight sm:text-[1.15rem]">
Boardware Agent Sandbox
</span>
</Link>
<div className="flex min-w-0 flex-1 items-center justify-end gap-3">
<nav className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto whitespace-nowrap [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{NAV_ITEMS.map((item) => {
const isActive =
item.href === '/'
? pathname === '/'
: pathname.startsWith(item.href);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`flex shrink-0 items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
<Icon className="w-4 h-4" />
{item.name}
</Link>
);
})}
</nav>
<div className="flex shrink-0 items-center gap-1.5 border-l border-border pl-4">
{user ? (
<>
<div className="flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium text-foreground">
<UserCircle2 className="w-4 h-4" />
<span className="max-w-32 truncate">{user.username}</span>
</div>
<button
type="button"
onClick={handleLogout}
className="flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<LogOut className="w-4 h-4" />
退
</button>
</>
) : !isAuthLoading ? (
AUTH_ITEMS.map((item) => {
const isActive = pathname.startsWith(item.href);
const Icon = item.icon;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
}`}
>
<Icon className="w-4 h-4" />
{item.name}
</Link>
);
})
) : null}
</div>
<div className="shrink-0 border-l border-border pl-4">
<ConnectionDot />
</div>
</div>
</div>
</div>
</header>
);
};
export default Header;