feat: add standalone web master-agent takeover page

This commit is contained in:
kris
2026-04-05 09:15:43 +08:00
parent 7cc33d391b
commit 35913f9d1d
4 changed files with 134 additions and 13 deletions

View File

@@ -0,0 +1,29 @@
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
import { MasterAgentTakeoverClient } from "@/components/master-agent-takeover-client";
import { requirePageSession } from "@/lib/boss-auth";
import { getProjectAgentControls } from "@/lib/boss-data";
import { formatTimestampLabel } from "@/lib/boss-projections";
export const dynamic = "force-dynamic";
export default async function MasterAgentTakeoverPage() {
const session = await requirePageSession();
const projectControls = await getProjectAgentControls("master-agent", session.account);
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="全局接管" backHref="/conversations/master-agent" />
<div className="px-[18px] pb-3">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
<span className="font-semibold text-[#111111]">{session.account}</span>
</div>
</div>
<MasterAgentTakeoverClient
enabled={projectControls?.globalTakeoverEnabled ?? false}
updatedAt={projectControls?.updatedAt ? formatTimestampLabel(projectControls.updatedAt) : null}
/>
</AppShell>
);
}

View File

@@ -0,0 +1,84 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
type Props = {
enabled: boolean;
updatedAt?: string | null;
};
export function MasterAgentTakeoverClient({ enabled, updatedAt }: Props) {
const router = useRouter();
const [takeoverEnabled, setTakeoverEnabled] = useState(enabled);
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState("");
async function save() {
setBusy(true);
setMessage("");
const response = await fetch("/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ globalTakeoverEnabled: takeoverEnabled }),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusy(false);
setMessage(
result.ok
? takeoverEnabled
? "已开启全局主 Agent 协同接管。"
: "已关闭全局主 Agent 协同接管。"
: result.message ?? "保存失败。",
);
if (result.ok) {
router.refresh();
}
}
return (
<div className="flex flex-col gap-4 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-2">
<div className="text-[16px] font-semibold text-[#111111]"> Agent </div>
<div className="text-[12px] leading-6 text-[#8C8C8C]">
线 Agent 线
</div>
<div className="text-[12px] leading-6 text-[#8C8C8C]">
线线
</div>
</div>
<label className="inline-flex shrink-0 items-center gap-2 rounded-full bg-[#F7F8FA] px-3 py-2 text-[13px] font-semibold text-[#111111]">
<input
type="checkbox"
checked={takeoverEnabled}
onChange={(event) => setTakeoverEnabled(event.target.checked)}
className="h-4 w-4 accent-[#07C160]"
/>
</label>
</div>
<div className="mt-4 flex flex-wrap items-center gap-3">
<button
type="button"
onClick={() => void save()}
disabled={busy}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busy ? "保存中" : "保存全局接管"}
</button>
{updatedAt ? <span className="text-[12px] text-[#8C8C8C]">{updatedAt}</span> : null}
</div>
</div>
{message ? (
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-3 text-[13px] text-[#57606A]">
{message}
</div>
) : null}
</div>
);
}

View File

@@ -5,10 +5,12 @@ export const MASTER_AGENT_CHAT_PAGE_ANCHORS = {
memory: "/me/master-agent#memory-section",
} as const;
export const MASTER_AGENT_TAKEOVER_PAGE_HREF = "/me/master-agent/takeover";
export type MasterAgentChatPageAnchors = typeof MASTER_AGENT_CHAT_PAGE_ANCHORS;
export type MasterAgentChatMenuItem = {
key: "prompt" | "model" | "reasoning_effort" | "memory" | "refresh";
key: "prompt" | "model" | "reasoning_effort" | "takeover" | "memory" | "refresh";
label: string;
href?: string;
action?: "refresh";
@@ -20,11 +22,6 @@ export function getMasterAgentChatMenuItems(projectId: string): MasterAgentChatM
}
return [
{
key: "prompt",
label: "提示词",
href: MASTER_AGENT_CHAT_PAGE_ANCHORS.prompt,
},
{
key: "model",
label: "模型",
@@ -35,6 +32,16 @@ export function getMasterAgentChatMenuItems(projectId: string): MasterAgentChatM
label: "推理强度",
href: MASTER_AGENT_CHAT_PAGE_ANCHORS.reasoningEffort,
},
{
key: "takeover",
label: "全局接管",
href: MASTER_AGENT_TAKEOVER_PAGE_HREF,
},
{
key: "prompt",
label: "提示词",
href: MASTER_AGENT_CHAT_PAGE_ANCHORS.prompt,
},
{
key: "memory",
label: "记忆",

View File

@@ -2,17 +2,18 @@ import test from "node:test";
import assert from "node:assert/strict";
import { getMasterAgentChatMenuItems } from "../src/lib/master-agent-chat-menu";
test("master-agent 聊天页菜单包含提示词、模型、推理强度、记忆和刷新", () => {
test("master-agent 聊天页菜单包含全局接管、提示词、模型、推理强度、记忆和刷新", () => {
const items = getMasterAgentChatMenuItems("master-agent");
assert.deepEqual(
items.map((item) => item.key),
["prompt", "model", "reasoning_effort", "memory", "refresh"],
["model", "reasoning_effort", "takeover", "prompt", "memory", "refresh"],
);
assert.equal(items[0]?.href, "/me/master-agent#prompt-section");
assert.equal(items[1]?.href, "/me/master-agent#model-section");
assert.equal(items[2]?.href, "/me/master-agent#reasoning-effort-section");
assert.equal(items[3]?.href, "/me/master-agent#memory-section");
assert.equal(items[4]?.action, "refresh");
assert.equal(items[0]?.href, "/me/master-agent#model-section");
assert.equal(items[1]?.href, "/me/master-agent#reasoning-effort-section");
assert.equal(items[2]?.href, "/me/master-agent/takeover");
assert.equal(items[3]?.href, "/me/master-agent#prompt-section");
assert.equal(items[4]?.href, "/me/master-agent#memory-section");
assert.equal(items[5]?.action, "refresh");
});
test("普通会话不返回主 Agent 专属菜单", () => {