feat: 修复审计落差 #1/#2/#3/#6 + 管理面板移动端适配
#1 工具布尔标记生效(agent-106, 安全红线): loop toolSchemas/runToolCalls 加 allow-set 安全门 restrictSchemas/applyToolSecurityGate——群聊/陌生人只暴露允许工具(默认关), 被拒not_allowed软提示+AuditEvent, owner全工具不变; 身份靠 ActorEnv(非提示词) #2 checkpoint 恢复(agent-109): run.go LoadLatest + resumableStopReason门控(只恢复in-flight工具循环,不出答案不续跑), 幂等不重放副作用, 新会话不误恢复; resume_test 4用例 #3 tool_result(agent-108, 协议层): Anthropic 原生tool_result块(tool_use_id/is_error)非user纯文本 + OpenAI tool消息补tool_call_id; Content加ToolCallID/IsError等; (loop接线层2处留待,见备注) #6 capacity_awaken推送(agent-107): onCapacity从只Debug改推WS hub(BroadcastCapacityAwaken, sessionId定向+payload对齐前端AwakenPayload) 管理面板移动端(agent-110): AdminApp 汉堡菜单+抽屉侧栏+标题truncate防换行, RBAC守卫不受影响 全仓 build/vet/test绿 + 前端build绿; 前端无涉差异已对齐 备注: #3 loop接线层2处(assistant工具轮append历史 + Message.ToolCallID/IsError)留给后续; #4代审/#5子代理触发/#7拆文件后置(主人说后三不急)
This commit is contained in:
1 parent
aedae2190b
commit
7e39153a69
24 files changed
+1286
-56
No files matched your search
@@ -42,16 +42,22 @@ function renderSection(section: AdminSectionId): React.ReactNode {
|
||||
}
|
||||
}
|
||||
|
||||
function AdminSidebar({ active }: { active: AdminSectionId }): React.ReactNode {
|
||||
/**
|
||||
* Sidebar content shared by the desktop rail and the mobile drawer (design/10).
|
||||
* Both render ADMIN_GROUPS with the same `onNavigate` hook — the drawer passes
|
||||
* its close callback (tap an item → close), the desktop rail passes a no-op — so
|
||||
* desktop and mobile never diverge.
|
||||
*/
|
||||
function adminNavContent({ active, onNavigate }: { active: AdminSectionId; onNavigate: () => void }): React.ReactNode {
|
||||
return (
|
||||
<nav className="glass hidden h-full w-[var(--mu-sidebar-w)] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--mu-glass-border)] p-3 md:flex">
|
||||
<>
|
||||
{ADMIN_GROUPS.map((group) => (
|
||||
<div key={group} className="mb-1">
|
||||
<div className="px-2 py-2 text-xs font-semibold uppercase tracking-wider text-[var(--mu-text-lo)]">{group}</div>
|
||||
{ADMIN_SECTIONS.filter((s) => s.group === group).map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => navigateAdmin(s.id)}
|
||||
onClick={() => { navigateAdmin(s.id); onNavigate(); }}
|
||||
className={`flex w-full items-center gap-2.5 rounded-[var(--mu-radius)] px-3 py-2 text-sm text-left transition-all duration-200 ease-[var(--mu-spring)] active:scale-[0.98] ${
|
||||
s.id === active
|
||||
? "bg-[var(--mu-accent)]/15 font-medium text-[var(--mu-accent-hi)]"
|
||||
@@ -64,11 +70,50 @@ function AdminSidebar({ active }: { active: AdminSectionId }): React.ReactNode {
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Desktop left rail — fixed, `hidden md:flex`. */
|
||||
function AdminSidebar({ active }: { active: AdminSectionId }): React.ReactNode {
|
||||
return (
|
||||
<nav className="glass hidden h-full w-[var(--mu-sidebar-w)] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--mu-glass-border)] p-3 md:flex">
|
||||
{adminNavContent({ active, onNavigate: () => {} })}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminTopBar({ section }: { section: AdminSectionId }): React.ReactNode {
|
||||
/** Mobile drawer — scrim + spring slide-in panel (reuses mu-drawer-in). */
|
||||
function AdminMobileNav({ open, active, onClose }: { open: boolean; active: AdminSectionId; onClose: () => void }): React.ReactNode {
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 md:hidden" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-[2px]"
|
||||
onClick={onClose}
|
||||
aria-label="关闭侧边栏"
|
||||
/>
|
||||
<nav
|
||||
className="glass absolute inset-y-0 left-0 flex w-[var(--mu-sidebar-w)] max-w-[80vw] flex-col gap-1 overflow-y-auto border-r border-[var(--mu-glass-border)] p-3"
|
||||
style={{ animation: "mu-drawer-in 0.3s var(--mu-spring) both" }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-2 pb-1">
|
||||
<span className="text-xs font-medium text-[var(--mu-text-lo)]">菜单</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-[var(--mu-radius)] text-[var(--mu-text-mid)] transition-all duration-200 hover:bg-[var(--mu-surface-4)] active:scale-95"
|
||||
>
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{adminNavContent({ active, onNavigate: onClose })}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminTopBar({ section, onMenu }: { section: AdminSectionId; onMenu?: () => void }): React.ReactNode {
|
||||
const role = getStoredRole();
|
||||
const [theme, setTheme] = useState<ThemeMode>(readTheme);
|
||||
const meta = ADMIN_SECTIONS.find((s) => s.id === section);
|
||||
@@ -79,24 +124,38 @@ function AdminTopBar({ section }: { section: AdminSectionId }): React.ReactNode
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="glass sticky top-0 z-30 flex h-[var(--mu-topbar-h)] items-center gap-3 border-b border-[var(--mu-glass-border)] px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/icon.svg" alt="Cyrene" className="h-7 w-7" />
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold">Cyrene · 管理面板</div>
|
||||
<div className="text-[11px] text-[var(--mu-text-lo)]">{meta?.label ?? "仪表盘"}</div>
|
||||
<header className="glass sticky top-0 z-30 flex h-[var(--mu-topbar-h)] items-center gap-2 border-b border-[var(--mu-glass-border)] px-3 sm:px-4">
|
||||
{onMenu ? (
|
||||
<button
|
||||
onClick={onMenu}
|
||||
aria-label="打开菜单"
|
||||
title="菜单"
|
||||
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-[var(--mu-radius)] text-[var(--mu-text-mid)] transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-4)] active:scale-95 md:hidden"
|
||||
>
|
||||
<Icon name="menu" size={20} />
|
||||
</button>
|
||||
) : null}
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<img src="/icon.svg" alt="Cyrene" className="h-7 w-7 shrink-0" />
|
||||
<div className="min-w-0 leading-tight">
|
||||
<div className="truncate whitespace-nowrap text-sm font-semibold">Cyrene · 管理面板</div>
|
||||
<div className="truncate whitespace-nowrap text-[11px] text-[var(--mu-text-lo)]">{meta?.label ?? "仪表盘"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
{role ? <Tag tone={role === "owner" ? "accent" : role === "admin" ? "ok" : "neutral"}>{role}</Tag> : null}
|
||||
{role ? (
|
||||
<Tag tone={role === "owner" ? "accent" : role === "admin" ? "ok" : "neutral"} className="hidden shrink-0 sm:inline-flex">
|
||||
{role}
|
||||
</Tag>
|
||||
) : null}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
aria-label="切换主题"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-[var(--mu-radius)] bg-[var(--mu-surface-4)] text-sm transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-5)] active:scale-95"
|
||||
className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-[var(--mu-radius)] bg-[var(--mu-surface-4)] text-sm transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-5)] active:scale-95"
|
||||
>
|
||||
<Icon name={theme === "dark" ? "sun" : "moon"} size={18} />
|
||||
</button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { window.location.hash = ""; }}>
|
||||
<Button size="sm" variant="ghost" className="shrink-0" onClick={() => { window.location.hash = ""; }}>
|
||||
返回用户端
|
||||
</Button>
|
||||
</header>
|
||||
@@ -137,6 +196,10 @@ function Forbidden403(): React.ReactNode {
|
||||
}
|
||||
|
||||
export function AdminApp({ section }: { section: AdminSectionId }): React.ReactNode {
|
||||
// Mobile nav drawer — only surfaced on the admin shell (md+ keeps the fixed
|
||||
// sidebar); the login/403 guards never render it but still hold the hook.
|
||||
const [adminMenuOpen, setAdminMenuOpen] = useState(false);
|
||||
|
||||
// RBAC guard (design/10 §2 + design/11 §7). A logged-in viewer or an
|
||||
// unauthenticated user never sees the admin module; the gateway would also
|
||||
// reject them with 401/403.
|
||||
@@ -159,13 +222,14 @@ export function AdminApp({ section }: { section: AdminSectionId }): React.ReactN
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<AdminTopBar section={section} />
|
||||
<AdminTopBar section={section} onMenu={() => setAdminMenuOpen(true)} />
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<AdminSidebar active={section} />
|
||||
<main className="relative flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<div className="mx-auto max-w-6xl">{renderSection(section)}</div>
|
||||
</main>
|
||||
</div>
|
||||
<AdminMobileNav open={adminMenuOpen} active={section} onClose={() => setAdminMenuOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user