From 08484302ac2dd4d5ee1d1b7c8e3863fb755637a8 Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 15:38:50 +0800 Subject: [PATCH] =?UTF-8?q?console:=20=E9=A1=B9=E7=9B=AE=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E7=A7=BB=E5=88=B0=E5=B7=A6=E4=BE=A7=E6=A0=8F=EF=BC=8C=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E4=B8=8D=E5=86=8D=E6=89=BF=E6=8B=85=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 左侧栏 ProjectNav:项目完整列表(名 + 短 id + 我的角色, 选中高亮,点击切换)+ 创建项目(名/GitHub 仓库/repo_url) + 选中项目成员(邀请/改角色/新增移除);工作台删除 sec-projects 一节与任务列表里的项目下拉框,侧栏为唯一 项目选择器,自动选第一个项目逻辑不变。 薄模块补齐(均为已核实后端路由):成员移除 DELETE、 自动化详情(GET 单条 + 产出)与编辑 PATCH、分诊条目点进 任务/自动化详情、Workers 搜索与在线筛选、管理区设置配额 POST。主线新功能(Todos、Browser、taskMeta 等)原样保留, 未做任何回退。console 标记单测本地 53 项通过。 --- cloud/console-react/src/App.tsx | 15 ++-- cloud/console-react/src/api/client.ts | 1 + cloud/console-react/src/api/types.ts | 6 +- cloud/console-react/src/context.ts | 8 +- cloud/console-react/src/hooks/useCatalog.ts | 31 ++++++- .../src/hooks/useConsoleStore.ts | 6 +- .../src/hooks/useProjectTools.ts | 24 +++++- .../src/sections/AdminSection.tsx | 8 ++ .../src/sections/MembersPanel.tsx | 1 + .../console-react/src/sections/ProjectNav.tsx | 51 ++++++++++++ .../src/sections/ProjectsSection.tsx | 32 -------- .../src/sections/TaskListSection.tsx | 15 ++-- .../src/sections/WorkersSection.tsx | 36 ++++++++- .../sections/detail/ProjectToolsPanels.tsx | 80 +++++++++++++++++-- cloud/console-react/src/tokens.css | 17 +++- cloud/console.html | 62 +++++++------- 16 files changed, 293 insertions(+), 100 deletions(-) create mode 100644 cloud/console-react/src/sections/ProjectNav.tsx delete mode 100644 cloud/console-react/src/sections/ProjectsSection.tsx diff --git a/cloud/console-react/src/App.tsx b/cloud/console-react/src/App.tsx index 8d26c16..5f1ca79 100644 --- a/cloud/console-react/src/App.tsx +++ b/cloud/console-react/src/App.tsx @@ -9,8 +9,7 @@ import ConversationSection from "./sections/ConversationSection"; import TaskDetailPanel from "./sections/TaskDetailPanel"; import ApiKeySection from "./sections/ApiKeySection"; import AdminSection from "./sections/AdminSection"; -import ProjectsSection from "./sections/ProjectsSection"; -import MembersPanel from "./sections/MembersPanel"; +import ProjectNav from "./sections/ProjectNav"; import EnvSection from "./sections/EnvSection"; import EnvContextPanel from "./sections/EnvContextPanel"; import WorkersSection from "./sections/WorkersSection"; @@ -25,7 +24,10 @@ import TodosSection from "./sections/TodosSection"; // data-cards row on top of a long stacked page, no detail stack // hanging under the conversation. The new shell is an IDE-style // workspace that never scrolls as one page: -// 左侧栏:导航(按工作流分组)+ 数据卡片(紧凑版,始终可见) +// 左侧栏:导航(按工作流分组)+ 项目模块(ProjectNav:项目列表 + +// 创建项目 + 成员,id nav-projects,是全站唯一的项目选择器; +// 因此不再有独立的“项目与成员”视图,旧 ProjectsSection 已删, +// 工作台也不再带项目选择器)+ 数据卡片(紧凑版,始终可见) // 任务工作台:左 = 任务列表轨(搜索/筛选/分页),中 = 派发 + 对话, // 右 = 任务详情检查器(概览/协作/评审与审批/线程四个内页签, // 见 TaskDetailPanel) @@ -36,11 +38,10 @@ import TodosSection from "./sections/TodosSection"; // unmounted with a view. The view is synced to location.hash // (#view-tasks etc.) so a reload or a shared link lands on the // same view; unknown hashes fall back to the task workspace. -type ViewId = "tasks" | "todos" | "projects" | "resources" | "automation" | "admin"; +type ViewId = "tasks" | "todos" | "resources" | "automation" | "admin"; const VIEWS: Array<[ViewId, string, string]> = [ ["tasks", "任务工作台", "任务"], ["todos", "项目待办", "任务"], - ["projects", "项目与成员", "管理"], ["resources", "环境与 Workers", "管理"], ["automation", "技能与自动化", "管理"], ["admin", "系统与 Key", "管理"], @@ -67,6 +68,9 @@ function SideNav({ view, setView }: { view: ViewId; setView: (v: ViewId) => void ))} +
概览
@@ -138,7 +142,6 @@ export default function App() { ) : (
{view === "todos" ? (<>) - : view === "projects" ? (<>) : view === "resources" ? (<>) : view === "automation" ? (<>) : (<>)} diff --git a/cloud/console-react/src/api/client.ts b/cloud/console-react/src/api/client.ts index 47a9d49..a4a73f0 100644 --- a/cloud/console-react/src/api/client.ts +++ b/cloud/console-react/src/api/client.ts @@ -93,6 +93,7 @@ export function memberErr(e: Error): string { if (m.includes("forbidden")) return "没有权限:只有项目 owner 或全局管理员可以管理成员"; if (m.includes("already_member")) return "该用户已是成员,改角色请用成员行的下拉框"; if (m.includes("cannot demote the only owner")) return "不能把唯一 owner 降为非 owner:请先把另一位成员升为 owner"; + if (m.includes("cannot remove the only owner")) return "不能移除唯一 owner:请先把另一位成员升为 owner"; if (m.includes("user not found")) return "找不到该用户:请核对 user_id 或用户名是否正确"; if (m.includes("member not found")) return "该用户不是本项目成员"; if (m.includes("unknown role")) return "角色无效:只能是 viewer / contributor / maintainer / owner"; diff --git a/cloud/console-react/src/api/types.ts b/cloud/console-react/src/api/types.ts index 44228fc..046048f 100644 --- a/cloud/console-react/src/api/types.ts +++ b/cloud/console-react/src/api/types.ts @@ -36,7 +36,9 @@ export interface Followup { mode?: string; status?: string; sender_task_id?: str export interface ChatHistoryMessage { id: string; role: "user" | "assistant" | "tool" | "status"; source?: string; kind?: string; label?: string; text: string; ts_ms?: number | null } export interface ChatHistory { task_id?: string; status?: string; messages: ChatHistoryMessage[]; continue_messages_stored?: number; legacy_continue_count?: number | null; trace_note?: string } export interface PeerMessage { task_id?: string; mode?: string; status?: string; body?: string } -export interface Automation { id: string; name?: string; prompt?: string; repo_url?: string; status?: string; due?: boolean; interval_seconds?: number; next_run_at?: number; last_run_at?: number; created_at?: number; cancelled_at?: number; run_once?: boolean } +export interface Automation { id: string; name?: string; prompt?: string; repo_url?: string; status?: string; due?: boolean; interval_seconds?: number; next_run_at?: number; last_run_at?: number; last_claimed_at?: number; created_by?: string; created_at?: number; cancelled_at?: number; first_run_at?: number; run_once?: boolean; project_id?: string } +export interface AutomationRun { id?: string; automation_id?: string; project_id?: string; task_id?: string; status?: string; detail?: string; created_at?: number } +export interface AutomationDetail extends Automation { runs?: AutomationRun[]; runs_note?: string; heartbeat_binding?: unknown } export interface Member { user_id: string; username?: string; role?: string } export interface SkillItem { name?: string; id?: string; description?: string; detail?: string } export interface McpServer { name?: string; id?: string; status?: string; detail?: string; details?: string; state?: string } @@ -45,7 +47,7 @@ export interface Agent { kind?: string; name?: string; status?: string; prompt?: export interface ThreadInfo { id?: string; name?: string; thread_name?: string; archived?: boolean | null; cwd?: string; turns?: number; turn_count?: number; turns_count?: number } export interface Triage { counts?: { automations_due?: number; failed_tasks?: number; pending_followups?: number; recent_runs?: number }; - automations_due?: Array<{ name?: string; interval_seconds?: number }>; + automations_due?: Array<{ id?: string; name?: string; status?: string; due?: boolean; interval_seconds?: number; next_run_at?: number }>; failed_tasks?: Array<{ id?: string; status?: string; prompt?: string }>; pending_followups?: Array<{ task_id?: string; body?: string }>; recent_runs?: Array<{ automation_id?: string; status?: string; task_id?: string; task_status?: string }>; diff --git a/cloud/console-react/src/context.ts b/cloud/console-react/src/context.ts index 5118256..295d21a 100644 --- a/cloud/console-react/src/context.ts +++ b/cloud/console-react/src/context.ts @@ -1,5 +1,5 @@ import { createContext, useContext } from "react"; -import type { Automation, Env, Member, Project, Review, Approval, Followup, PeerMessage, Task, TaskCost, ThreadInfo, Triage, Worker, SkillItem, McpServer, Agent, DispatchBody, ReviewBody, TraceEntry, ChatHistory } from "./api/types"; +import type { Automation, AutomationDetail, Env, Member, Project, Review, Approval, Followup, PeerMessage, Task, TaskCost, ThreadInfo, Triage, Worker, SkillItem, McpServer, Agent, DispatchBody, ReviewBody, TraceEntry, ChatHistory } from "./api/types"; export interface ConsoleCtx { keySaved: boolean; keyState: string; @@ -7,13 +7,13 @@ export interface ConsoleCtx { quotaText: string; healthText: string; refreshHealth: () => void; adminGate: string; adminBody: boolean; adminResult: string; adminUserId: string; setAdminUserId: (v: string) => void; - adminQuota: () => void; adminSweep: () => void; adminBackup: () => void; + adminQuota: () => void; adminSetQuota: (uid: string, usdText: string) => void; adminSweep: () => void; adminBackup: () => void; projects: Project[]; projectId: string; projectMsg: string; selectProject: (id: string) => void; refreshProjects: () => void; createProject: (name: string, repo: string) => Promise; githubRepos: Array<{ clone_url: string; repo_full_name: string }>; workers: Worker[] | null; workersSummary: string; workersError: string; refreshWorkers: () => void; - members: Member[]; memberMsg: string; memberError: string; refreshMembers: () => void; inviteMember: (raw: string, role: string) => Promise; changeMemberRole: (uid: string, role: string) => Promise; + members: Member[]; memberMsg: string; memberError: string; refreshMembers: () => void; inviteMember: (raw: string, role: string) => Promise; changeMemberRole: (uid: string, role: string) => Promise; removeMember: (uid: string) => void; skills: SkillItem[]; apps: SkillItem[]; mcpServers: McpServer[]; skillMsg: string; skillError: string; mcpMsg: string; mcpOauthMsg: string; mcpOauthBox: string; refreshSkills: () => void; reloadMcp: () => void; mcpOauth: (name: string) => Promise; envs: Env[]; envCount: string; envMsg: string; envRepoLine: string; envRepoBar: boolean; envRepoMsg: string; envSelect: string; setEnvSelect: (v: string) => void; @@ -35,7 +35,7 @@ export interface ConsoleCtx { followups: Followup[]; approvals: Approval[]; approvalMsg: string; refreshApprovals: () => void; decideApproval: (id: string, decision: string) => void; peerMessages: PeerMessage[]; peerMsg: string; postPeer: (mode: string, target: string, text: string) => Promise; - automations: Automation[]; autoMsg: string; refreshAutomations: () => void; createAutomation: (name: string, prompt: string, interval: string, repo: string) => Promise; updateAutomation: (a: Automation, name: string, prompt: string, interval: string, repo: string) => Promise; cancelAutomation: (id: string) => void; + automations: Automation[]; autoMsg: string; autoDetail: AutomationDetail | null; autoDetailMsg: string; refreshAutomations: () => void; createAutomation: (name: string, prompt: string, interval: string, repo: string) => Promise; updateAutomation: (a: Automation, name: string, prompt: string, interval: string, repo: string) => Promise; cancelAutomation: (id: string) => void; loadAutomationDetail: (id: string) => void; closeAutomationDetail: () => void; triage: Triage | null; triageError: string; refreshTriage: () => void; threadText: string; threadGateNote: string; threadMsg: string; threadList: ThreadInfo[]; threadAction: (kind: string, name: string) => void; dispatch: (body: DispatchBody, atts: Array<{ path: string; file: File }>) => Promise; dispatchMsg: string; diff --git a/cloud/console-react/src/hooks/useCatalog.ts b/cloud/console-react/src/hooks/useCatalog.ts index 6e99ea9..84a3054 100644 --- a/cloud/console-react/src/hooks/useCatalog.ts +++ b/cloud/console-react/src/hooks/useCatalog.ts @@ -253,6 +253,17 @@ export function useCatalog() { } catch (e: any) { setMemberMsg(memberErr(e)); } }, []); + const removeMember = useCallback(async (uid: string) => { + const pid = projectIdRef.current; + if (!pid) return; + if (!window.confirm("确定移除该成员吗?移除后其项目权限立即失效(唯一 owner 不可移除,服务端会拒绝)。")) return; + try { + await api(paths.projectMember(pid, uid), "DELETE"); + setMemberMsg("成员已移除"); + await refreshMembers(pid); + } catch (e: any) { setMemberMsg(memberErr(e)); } + }, [refreshMembers]); + const reloadMcp = useCallback(async () => { const pid = projectIdRef.current; if (!pid) { setMcpMsg("先选项目"); return; } @@ -289,6 +300,24 @@ export function useCatalog() { } catch (e: any) { setAdminResult(adminErr(e)); } }, [adminUserId]); + const adminSetQuota = useCallback(async (uid: string, usdText: string) => { + const target = uid.trim(); + if (!target) { setAdminResult("请填要设置配额的用户 id"); return; } + // POST /admin/quotas {user_id, quota_usd_micros}: integer micros or + // null for unlimited, >= 0. The console takes dollars and converts. + let micros: number | null; + if (!usdText.trim()) micros = null; + else { + const usd = Number(usdText.trim()); + if (!Number.isFinite(usd) || usd < 0) { setAdminResult("配额金额无效:请输入 >= 0 的美元数,留空表示不限额"); return; } + micros = Math.round(usd * 1000000); + } + try { + const d = await api<{ used_usd_micros?: number; quota_usd_micros?: number | null; remaining_usd_micros?: number | null; month?: string }>(paths.adminQuotas, "POST", { user_id: target, quota_usd_micros: micros }); + setAdminResult("配额已设置:用户 " + target.slice(0, 8) + ":本月用量 " + fmtUsd(d.used_usd_micros) + " / 配额 " + fmtUsd(d.quota_usd_micros) + (d.remaining_usd_micros !== null && d.remaining_usd_micros !== undefined ? "(剩余 " + fmtUsd(d.remaining_usd_micros) + ")" : "") + " | 月份 " + (d.month || "")); + } catch (e: any) { setAdminResult(adminErr(e)); } + }, []); + const adminSweep = useCallback(async () => { if (!window.confirm("确定手动触发 sweep 吗?超阈值的陈旧 running 任务会被标为 interrupted。")) return; try { @@ -314,6 +343,6 @@ export function useCatalog() { models, modelCustomVisible, refreshHealth, loadQuota, probeAdmin, refreshProjects, loadGithubRepos, loadRelayModels, refreshWorkers, refreshEnvs, refreshMembers, refreshSkills, saveKeyInput, clearKeyInput, createProject, createEnv, - archiveEnv, saveEnvRepo, inviteMember, changeMemberRole, reloadMcp, mcpOauth, adminQuota, adminSweep, adminBackup, + archiveEnv, saveEnvRepo, inviteMember, changeMemberRole, removeMember, reloadMcp, mcpOauth, adminQuota, adminSetQuota, adminSweep, adminBackup, }; } diff --git a/cloud/console-react/src/hooks/useConsoleStore.ts b/cloud/console-react/src/hooks/useConsoleStore.ts index 3a1efef..4200972 100644 --- a/cloud/console-react/src/hooks/useConsoleStore.ts +++ b/cloud/console-react/src/hooks/useConsoleStore.ts @@ -108,14 +108,14 @@ export function useConsoleStore(): ConsoleCtx { healthText: catalog.healthText, refreshHealth: catalog.refreshHealth, adminGate: catalog.adminGate, adminBody: catalog.adminBody, adminResult: catalog.adminResult, adminUserId: catalog.adminUserId, setAdminUserId: catalog.setAdminUserId, - adminQuota: catalog.adminQuota, adminSweep: catalog.adminSweep, adminBackup: catalog.adminBackup, + adminQuota: catalog.adminQuota, adminSetQuota: catalog.adminSetQuota, adminSweep: catalog.adminSweep, adminBackup: catalog.adminBackup, projects: catalog.projects, projectId: catalog.projectId, projectMsg: catalog.projectMsg, selectProject, refreshProjects: catalog.refreshProjects, createProject: catalog.createProject, githubRepos: catalog.githubRepos, workers: catalog.workers, workersSummary: catalog.workersSummary, workersError: catalog.workersError, refreshWorkers: catalog.refreshWorkers, members: catalog.members, memberMsg: catalog.memberMsg, memberError: catalog.memberError, - refreshMembers: () => void catalog.refreshMembers(), inviteMember: catalog.inviteMember, changeMemberRole: catalog.changeMemberRole, + refreshMembers: () => void catalog.refreshMembers(), inviteMember: catalog.inviteMember, changeMemberRole: catalog.changeMemberRole, removeMember: catalog.removeMember, skills: catalog.skills, apps: catalog.apps, mcpServers: catalog.mcpServers, skillMsg: catalog.skillMsg, skillError: catalog.skillError, mcpMsg: catalog.mcpMsg, mcpOauthMsg: catalog.mcpOauthMsg, mcpOauthBox: catalog.mcpOauthBox, refreshSkills: () => void catalog.refreshSkills(), reloadMcp: catalog.reloadMcp, mcpOauth: catalog.mcpOauth, @@ -144,7 +144,7 @@ export function useConsoleStore(): ConsoleCtx { followups: detail.followups, approvals: detail.approvals, approvalMsg: detail.approvalMsg, refreshApprovals: detail.refreshApprovals, decideApproval: detail.decideApproval, peerMessages: detail.peerMessages, peerMsg: detail.peerMsg, postPeer: detail.postPeer, - automations: tools.automations, autoMsg: tools.autoMsg, refreshAutomations: () => void tools.refreshAutomations(), createAutomation: tools.createAutomation, updateAutomation: tools.updateAutomation, cancelAutomation: tools.cancelAutomation, + automations: tools.automations, autoMsg: tools.autoMsg, autoDetail: tools.autoDetail, autoDetailMsg: tools.autoDetailMsg, refreshAutomations: () => void tools.refreshAutomations(), createAutomation: tools.createAutomation, updateAutomation: tools.updateAutomation, cancelAutomation: tools.cancelAutomation, loadAutomationDetail: tools.loadAutomationDetail, closeAutomationDetail: tools.closeAutomationDetail, triage: tools.triage, triageError: tools.triageError, refreshTriage: () => void tools.refreshTriage(), threadText: detail.threadText, threadGateNote: detail.threadGateNote, threadMsg: detail.threadMsg, threadList: detail.threadList, threadAction: detail.threadAction, dispatch: async (body, atts) => { diff --git a/cloud/console-react/src/hooks/useProjectTools.ts b/cloud/console-react/src/hooks/useProjectTools.ts index 6d2a3e5..06fa09f 100644 --- a/cloud/console-react/src/hooks/useProjectTools.ts +++ b/cloud/console-react/src/hooks/useProjectTools.ts @@ -1,13 +1,15 @@ import { useCallback, useState } from "react"; import { api } from "../api/client"; import { paths } from "../api/paths"; -import type { Automation, Triage } from "../api/types"; +import type { Automation, AutomationDetail, Triage } from "../api/types"; // Project-scoped tools shown in the centre for their own views: // automations (create/list/cancel) and the triage inbox (4 groups). export function useProjectTools(getProjectId: () => string) { const [automations, setAutomations] = useState([]); const [autoMsg, setAutoMsg] = useState(""); + const [autoDetail, setAutoDetail] = useState(null); + const [autoDetailMsg, setAutoDetailMsg] = useState(""); const [triage, setTriage] = useState(null); const [triageError, setTriageError] = useState(""); @@ -66,10 +68,26 @@ export function useProjectTools(getProjectId: () => string) { const pid = getProjectId(); if (!pid) return; void api(paths.projectAutomationCancel(pid, id), "POST", {}) - .then(() => { setAutoMsg("自动化已取消"); void refreshAutomations(pid); }) + .then(() => { setAutoMsg("自动化已取消"); setAutoDetail((prev) => (prev && prev.id === id ? null : prev)); void refreshAutomations(pid); }) .catch((e: any) => setAutoMsg("取消失败:" + e.message)); }, [getProjectId, refreshAutomations]); + // GET /projects/{pid}/automations/{aid}: full definition + recent + // runs (+ runs_note, heartbeat_binding). Verified against origin/main + // cloud/server.py _automation_get / cloud/automations.py get_automation. + const loadAutomationDetail = useCallback(async (id: string) => { + const pid = getProjectId(); + if (!pid) { setAutoDetailMsg("先选项目"); return; } + try { + const d = await api(paths.projectAutomation(pid, id)); + setAutoDetail(d); + setAutoDetailMsg(""); + } catch (e: any) { setAutoDetailMsg("详情加载失败:" + e.message); } + }, [getProjectId]); + + const closeAutomationDetail = useCallback(() => { setAutoDetail(null); setAutoDetailMsg(""); }, []); + + const refreshTriage = useCallback(async (pid?: string) => { const id = pid !== undefined ? pid : getProjectId(); if (!id) { setTriage(null); return; } @@ -80,5 +98,5 @@ export function useProjectTools(getProjectId: () => string) { } catch (e: any) { setTriageError("分诊加载失败:" + e.message); } }, [getProjectId]); - return { automations, autoMsg, triage, triageError, refreshAutomations, createAutomation, updateAutomation, cancelAutomation, refreshTriage }; + return { automations, autoMsg, autoDetail, autoDetailMsg, triage, triageError, refreshAutomations, createAutomation, updateAutomation, cancelAutomation, loadAutomationDetail, closeAutomationDetail, refreshTriage }; } diff --git a/cloud/console-react/src/sections/AdminSection.tsx b/cloud/console-react/src/sections/AdminSection.tsx index 50c2669..8ffc563 100644 --- a/cloud/console-react/src/sections/AdminSection.tsx +++ b/cloud/console-react/src/sections/AdminSection.tsx @@ -1,8 +1,11 @@ +import { useState } from "react"; import { useConsole } from "../context"; import { Card, Bar } from "../components/ui"; export default function AdminSection() { const ctx = useConsole(); + const [setUser, setSetUser] = useState(""); + const [setUsd, setSetUsd] = useState(""); return ( 管理区 (仅全局管理员)}>
{ctx.adminGate}
@@ -12,6 +15,11 @@ export default function AdminSection() { ctx.setAdminUserId(e.target.value)} /> + + setSetUser(e.target.value)} /> + setSetUsd(e.target.value)} /> + + diff --git a/cloud/console-react/src/sections/MembersPanel.tsx b/cloud/console-react/src/sections/MembersPanel.tsx index ef3f1df..6e5182d 100644 --- a/cloud/console-react/src/sections/MembersPanel.tsx +++ b/cloud/console-react/src/sections/MembersPanel.tsx @@ -37,6 +37,7 @@ export default function MembersPanel() { + ); })} diff --git a/cloud/console-react/src/sections/ProjectNav.tsx b/cloud/console-react/src/sections/ProjectNav.tsx new file mode 100644 index 0000000..180be1b --- /dev/null +++ b/cloud/console-react/src/sections/ProjectNav.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { useConsole } from "../context"; +import { Card, Bar } from "../components/ui"; +import { shortId } from "../api/client"; +import MembersPanel from "./MembersPanel"; + +// Left sidebar: the one project selector for the whole console. +// Lists every project (name + short id + my role), click selects it; +// below the list is the create-project form (same fields as the old +// workbench ProjectsSection: name, GitHub repo select, repo_url) and +// the selected project's members. The workbench no longer carries any +// project-module UI; it only consumes ctx.projectId. +export default function ProjectNav() { + const ctx = useConsole(); + const [name, setName] = useState(""); + const [repoSelect, setRepoSelect] = useState(""); + const [repo, setRepo] = useState(""); + return ( +
+ + + {ctx.projects.length === 0 ?
暂无项目
: ( +
    + {ctx.projects.map((p) => ( +
  • ctx.selectProject(p.id)} title={p.id}> + {p.name} + {shortId(p.id)}{p.my_role ? " · " + p.my_role : ""} +
  • + ))} +
+ )} +

创建项目

+ + setName(e.target.value)} /> + + + + + + setRepo(e.target.value)} /> + + + {ctx.projectMsg} +
+ +
+ ); +} diff --git a/cloud/console-react/src/sections/ProjectsSection.tsx b/cloud/console-react/src/sections/ProjectsSection.tsx deleted file mode 100644 index 4449787..0000000 --- a/cloud/console-react/src/sections/ProjectsSection.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useState } from "react"; -import { useConsole } from "../context"; -import { Card, Bar } from "../components/ui"; -import { shortId } from "../api/client"; - -export default function ProjectsSection() { - const ctx = useConsole(); - const [name, setName] = useState(""); - const [repoSelect, setRepoSelect] = useState(""); - const [repo, setRepo] = useState(""); - return ( - - - - - - - setName(e.target.value)} /> - - setRepo(e.target.value)} /> - - - {ctx.projectMsg} - - ); -} diff --git a/cloud/console-react/src/sections/TaskListSection.tsx b/cloud/console-react/src/sections/TaskListSection.tsx index d6aca91..3cb5a51 100644 --- a/cloud/console-react/src/sections/TaskListSection.tsx +++ b/cloud/console-react/src/sections/TaskListSection.tsx @@ -5,9 +5,12 @@ import { firstChar, durationOf, tokenOf } from "../api/taskMeta"; // 任务列表行 meta:首字 / 总耗时 / token,计算见 api/taskMeta.ts, // 与任务详情共用同一口径。 -// GitHub issues-style task list: search on top, the project selector -// (the auto-project fix — do not remove it), a 主要聊天 header with a -// + button, then hairline rows with a Primer status dot at the left, +// GitHub issues-style task list: search on top, a 主要聊天 header with +// a + button, then hairline rows with a Primer status dot at the +// left. There is deliberately NO project selector here any more: +// the left sidebar (ProjectNav) is the one project selector, and the +// auto-select-first-project fix in the store keeps the list from +// ever starting empty. // a 14px title and a 12px muted meta line. The search input IS the // existing task search (id/prompt); status and executor filters stay // as a compact row below it. The selected row gets the light @@ -22,12 +25,6 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) { ctx.setSearchText(e.target.value)} />
-
- -
主要聊天 diff --git a/cloud/console-react/src/sections/WorkersSection.tsx b/cloud/console-react/src/sections/WorkersSection.tsx index cd07cac..3d78a15 100644 --- a/cloud/console-react/src/sections/WorkersSection.tsx +++ b/cloud/console-react/src/sections/WorkersSection.tsx @@ -1,22 +1,52 @@ +import { useState } from "react"; import { useConsole } from "../context"; import { Card, Bar, DataListRaw } from "../components/ui"; import { workerAgeText, shortId } from "../api/client"; +// Workers are read-only by design (GET /workers + worker-side +// heartbeat only — no start/stop/kill actions exist to surface), so +// production-grade here is list + status + refresh + frontend-only +// filtering of the already-fetched list. Non-admin users only get +// the online/offline aggregate from the backend (see _workers_list), +// so search/filter apply to the full list only and say so. export default function WorkersSection() { const ctx = useConsole(); + const [query, setQuery] = useState(""); + const [onlineFilter, setOnlineFilter] = useState<"all" | "online" | "offline">("all"); + const isAlive = (w: { alive?: boolean | null; last_seen?: number; last_seen_seconds_ago?: number }) => { + const info = workerAgeText(w); + return w.alive !== undefined && w.alive !== null ? !!w.alive : (info.age !== null && info.age <= 120); + }; + const q = query.trim().toLowerCase(); + const filtered = (ctx.workers || []).filter((w) => { + if (q && !(((w.worker_id || w.id || "") + " " + (w.executor || "")).toLowerCase().includes(q))) return false; + if (onlineFilter === "online" && !isAlive(w)) return false; + if (onlineFilter === "offline" && isAlive(w)) return false; + return true; + }); return ( + + setQuery(e.target.value)} /> + + {ctx.workers ? 显示 {filtered.length} / 共 {ctx.workers.length} 个 : null} + {ctx.workersError ?
{ctx.workersError}
- : ctx.workers === null ?
{ctx.workersSummary || "未加载"}
+ : ctx.workers === null ?
{ctx.workersSummary || "未加载"}(完整清单仅全局管理员可见,搜索/筛选仅对完整清单生效)
: ctx.workers.length === 0 ?
无 worker 注册
+ : filtered.length === 0 ?
没有符合筛选条件的 worker
: ( - {ctx.workers.map((w, i) => { + {filtered.map((w, i) => { const run = w.running === undefined || w.running === null ? "?" : w.running; const cap = w.capacity === undefined || w.capacity === null ? "?" : w.capacity; const info = workerAgeText(w); - const alive = w.alive !== undefined && w.alive !== null ? !!w.alive : (info.age !== null && info.age <= 120); + const alive = isAlive(w); const live = alive ? " / 在线" : " / 离线(占用为上次上报值)"; return (
  • diff --git a/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx b/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx index 04eb972..f95c565 100644 --- a/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx +++ b/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { useConsole } from "../../context"; import { Bar, DataList } from "../../components/ui"; import { fmt, shortId } from "../../api/client"; -import type { Automation } from "../../api/types"; +import type { Automation, AutomationDetail } from "../../api/types"; const PROMPT_PREVIEW = 160; @@ -14,6 +14,49 @@ function humanInterval(s?: number): string { return ""; } +// Automation detail (read-only display): backed by GET +// /projects/{pid}/automations/{aid} — full definition + runs[] + +// runs_note + heartbeat_binding. Editing is NOT duplicated here: +// the single edit form is main's inline edit in the list below +// (updateAutomation with the (a, name, prompt, interval, repo) +// signature). Detail display + runs + task buttons are our side's +// addition; cross-view jumps use location.hash because the IDE +// shell switches views on hashchange (no sec-conv / sec-automations +// element to scroll to). +function AutomationDetailBox({ detail }: { detail: AutomationDetail }) { + const ctx = useConsole(); + const openTask = (id?: string) => { + if (!id) return; + ctx.showDetail(id); + try { window.location.hash = "#view-tasks"; } catch { /* noop */ } + }; + return ( +
    +

    自动化详情:{detail.name}

    +
    + 状态 {detail.status}{detail.due ? "|到期" : ""} | 每 {detail.interval_seconds} 秒 | 下次 {fmt(detail.next_run_at)} + | 上次执行 {fmt(detail.last_run_at)} | 创建人 {detail.created_by ? shortId(detail.created_by) : ""} | 创建于 {fmt(detail.created_at)} + {detail.cancelled_at ? " | 已取消于 " + fmt(detail.cancelled_at) : ""} + {detail.heartbeat_binding ? " | 心跳绑定:已绑定" : ""} +
    +
    {detail.prompt || ""}
    + {detail.repo_url ?
    仓库 {detail.repo_url}
    : null} + + + 编辑请在下面列表里点“编辑”。{ctx.autoDetailMsg} + +
    最近产出({(detail.runs || []).length}) + ( + + {r.status}{r.created_at ? "|" + fmt(r.created_at) : ""}{r.task_id ? <> | 任务 : ""}{r.detail ? "|" + String(r.detail).slice(0, 120) : ""} + + ))} /> +
    + {detail.runs_note ?
    {detail.runs_note}
    : null} +
    + ); +} + export function AutomationPanel() { const ctx = useConsole(); const [name, setName] = useState(""); @@ -53,6 +96,7 @@ export function AutomationPanel() {
    [{a.status}{a.due ? "|到期" : ""}{a.run_once ? "|一次性" : ""}] {a.name}|每 {a.interval_seconds} 秒{humanInterval(a.interval_seconds)}|上次 {fmt(a.last_run_at) || "未运行"}|下次 {fmt(a.next_run_at)} + {a.status === "active" ? <> : null}
    {a.repo_url ?
    仓库:{a.repo_url}
    : null} @@ -80,6 +124,8 @@ export function AutomationPanel() {
    ); })} /> + {ctx.autoDetailMsg && !ctx.autoDetail ?
    {ctx.autoDetailMsg}
    : null} + {ctx.autoDetail ? : null} ); } @@ -87,21 +133,45 @@ export function AutomationPanel() { export function TriagePanel() { const ctx = useConsole(); const t = ctx.triage; + // Clicking an actionable item opens that task's detail (tasks view) + // or the automation's detail (automation view). Field names are + // exactly the triage payload's: failed_tasks[].id, pending_followups + // [].task_id, recent_runs[].task_id (nullable), automations_due[].id. + // Jumps use location.hash because the IDE shell switches views on + // hashchange; there is no sec-conv / sec-automations to scroll to. + const openTask = (id?: string) => { + if (!id) return; + ctx.showDetail(id); + try { window.location.hash = "#view-tasks"; } catch { /* noop */ } + }; + const openAutomation = (id?: string) => { + if (!id) return; + void ctx.loadAutomationDetail(id); + try { window.location.hash = "#view-automation"; } catch { /* noop */ } + }; if (ctx.triageError) return
    {ctx.triageError}
    ; if (!t) return
    (先选项目)
    ; return ( <>
    到期自动化({t.counts?.automations_due || 0}) - {a.name}|每 {a.interval_seconds} 秒)} /> + ( + {a.id ? : <>{a.name}|每 {a.interval_seconds} 秒} + ))} />
    失败/中断任务({t.counts?.failed_tasks || 0}) - {shortId(x.id)}|{x.status}|{String(x.prompt || "").slice(0, 80)})} /> + ( + + ))} />
    待处理后续消息({t.counts?.pending_followups || 0}) - 任务 {shortId(f.task_id)}|{String(f.body || "").slice(0, 80)})} /> + ( + + ))} />
    最近自动化产出({t.counts?.recent_runs || 0}) - {shortId(r.automation_id)}|{r.status}{r.task_id ? "|任务 " + shortId(r.task_id) : ""}{r.task_status ? "|" + r.task_status : ""})} /> + ( + {shortId(r.automation_id)}|{r.status}{r.task_id ? <>|任务 : ""}{r.task_status ? "|" + r.task_status : ""} + ))} />
    ); diff --git a/cloud/console-react/src/tokens.css b/cloud/console-react/src/tokens.css index d746070..e311701 100644 --- a/cloud/console-react/src/tokens.css +++ b/cloud/console-react/src/tokens.css @@ -254,7 +254,7 @@ pre{background:var(--inset-bg);color:var(--text);border:1px solid var(--line);bo which is still tight — just in a different order. */ .dash-col{display:contents} #sec-dispatch{order:1} #sec-tasks{order:2} #sec-conv{order:3} - #sec-projects{order:4} #sec-env{order:5} #sec-workers{order:6} + #sec-env{order:5} #sec-workers{order:6} #sec-skills{order:7} #sec-automations{order:8} #sec-triage{order:9} #sec-admin{order:10} #sec-key{order:11} .dash-cards{grid-template-columns:repeat(2,1fr)} @@ -285,6 +285,20 @@ pre{background:var(--inset-bg);color:var(--text);border:1px solid var(--line);bo .shell-nav button{text-align:left;border:0;box-shadow:none;background:transparent;padding:8px;border-radius:var(--radius);font-weight:500} .shell-nav button:hover{background:#fff} .shell-nav button.active{background:var(--text);color:#fff} +/* Project sidebar (ProjectNav): the one project selector for the whole + console — project list + create + members — living in the IDE + shell's left sidebar. Same Primer tokens as the rest of the shell; + the sidebar is 260px wide, so its bars stack full-width. */ +.shell-project-nav{padding:0 12px 16px;min-width:0} +.shell-project-nav .project-nav{display:flex;flex-direction:column;gap:16px;min-width:0} +.shell-project-nav .project-nav-list{list-style:none;margin:0;padding:0;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden} +.shell-project-nav .project-nav-item{padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--line);background:#fff} +.shell-project-nav .project-nav-item:last-child{border-bottom:0} +.shell-project-nav .project-nav-item:hover{background:var(--subtle-bg)} +.shell-project-nav .project-nav-item.selected{background:var(--selected-bg)} +.shell-project-nav .project-nav-name{display:block;font-size:14px;font-weight:600;overflow-wrap:anywhere} +.shell-project-nav .project-nav-meta{display:block;font-size:12px;color:var(--muted)} +.shell-project-nav .bar > input[type="text"],.shell-project-nav .bar > select{flex:1 1 100%} .shell-stats{margin-top:auto;border-top:1px solid var(--line);padding:16px 12px} .shell-stats-label{font-size:12px;font-weight:600;color:var(--muted);padding:0 4px 8px} .shell-stats .dash-cards{display:flex;flex-direction:column;gap:8px} @@ -316,6 +330,7 @@ pre{background:var(--inset-bg);color:var(--text);border:1px solid var(--line);bo .shell-side{flex-direction:row;flex-wrap:wrap;align-items:flex-start;border-right:0;border-bottom:1px solid var(--line);overflow:visible} .shell-nav{flex-direction:row;flex-wrap:wrap} .shell-nav-group{flex-direction:row;align-items:center} + .shell-project-nav{flex:1 1 100%} .shell-stats{display:none} .shell-hide-sm{display:none} .workspace{grid-template-columns:1fr} diff --git a/cloud/console.html b/cloud/console.html index 11c70d6..dcd1e42 100644 --- a/cloud/console.html +++ b/cloud/console.html @@ -4,7 +4,7 @@ Codex 命令台 - - +`)}}S("流已断开")}catch(et){if(et.name==="AbortError")return;O(et instanceof Error?et.message:String(et)),S("未开始")}finally{x(!1)}},[r]),J=d.useCallback(()=>{var D;(D=H.current)==null||D.abort(),x(!1),S("已停止")},[]),gt=d.useCallback(async D=>{if(r)try{const X=await W(F.taskBrowserControl(r.id),"POST",{holder:D});C(X),O("")}catch(X){O(X instanceof Error?X.message:String(X))}},[r]),_t=d.useCallback(async D=>{if(!r||(y==null?void 0:y.holder)!=="user"||!L.current)return;const X=L.current.getBoundingClientRect(),et=Math.round((D.clientX-X.left)/X.width*1280),xt=Math.round((D.clientY-X.top)/X.height*800);try{await W(F.taskBrowserInput(r.id),"POST",{type:"mouse_click",x:et,y:xt})}catch(Mt){O(Mt instanceof Error?Mt.message:String(Mt))}},[r,y]),Dt=d.useCallback(async D=>{if(!(!r||(y==null?void 0:y.holder)!=="user")){D.preventDefault();try{D.key.length===1?await W(F.taskBrowserInput(r.id),"POST",{type:"type",text:D.key}):await W(F.taskBrowserInput(r.id),"POST",{type:"key",key:D.key})}catch(X){O(X instanceof Error?X.message:String(X))}}},[r,y]);if(!r)return c.jsx("div",{className:"muted",children:"点任务行查看详情"});const Q=(y==null?void 0:y.holder)||"—",ft=y!==null;return c.jsxs("div",{children:[c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",alignItems:"center"},children:[c.jsxs("span",{children:["状态 ",c.jsx("b",{children:M})]}),c.jsxs("span",{className:"muted",children:["会话 ",ft?"活跃":"未激活(轮次结束即消失)"]}),c.jsxs("span",{className:"muted",children:["控制权 ",Q]}),y!=null&&y.url?c.jsx("span",{className:"muted",title:y.url,children:y.url.slice(0,80)}):null]}),c.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginTop:8},children:[Y?c.jsx("button",{type:"button",onClick:J,children:"停止观看"}):c.jsx("button",{type:"button",onClick:()=>void dt(),children:"开始观看"}),c.jsx("button",{type:"button",onClick:()=>void tt(),children:"刷新状态"}),Q!=="user"?c.jsx("button",{type:"button",onClick:()=>void gt("user"),disabled:!ft,children:"接管"}):c.jsx("button",{type:"button",onClick:()=>void gt("agent"),children:"交还给 Agent"})]}),_?c.jsx("div",{className:"muted",style:{marginTop:8},children:_}):null,Q==="user"?c.jsx("div",{className:"muted",style:{marginTop:8},children:"你已接管:点画面即点击,键盘输入直达页面。交还后 Agent 续跑前会先快照确认你的改动。"}):null,c.jsx("div",{style:{marginTop:8},children:h?c.jsx("img",{ref:L,src:h,alt:"浏览器实时画面",tabIndex:0,onClick:D=>void _t(D),onKeyDown:D=>void Dt(D),style:{width:"100%",maxWidth:1280,cursor:Q==="user"?"crosshair":"default",border:"1px solid var(--border, #ccc)"}}):c.jsx("div",{className:"muted",children:ft?"点“开始观看”接入画面流(接管后点画面即点击)":"浏览器会话未激活:任务本轮未开浏览器,或轮次已结束。"})})]})}function jg(){const i=Lt(),[r,h]=d.useState("uncommittedChanges"),[f,y]=d.useState(""),[C,M]=d.useState(""),[S,_]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("h3",{children:"评审队列"}),c.jsxs("select",{"aria-label":"评审目标",value:r,onChange:O=>h(O.target.value),children:[c.jsx("option",{value:"uncommittedChanges",children:"未提交改动(工作区)"}),c.jsx("option",{value:"baseBranch",children:"与分支的差异"}),c.jsx("option",{value:"commit",children:"指定提交"}),c.jsx("option",{value:"custom",children:"自定义评审指令"})]}),c.jsx("input",{className:"stacked",placeholder:"与分支的差异用:分支名(如 main)",style:{width:"100%"},value:f,onChange:O=>y(O.target.value)}),c.jsx("input",{className:"stacked",placeholder:"指定提交用:提交 sha",style:{width:"100%"},value:C,onChange:O=>M(O.target.value)}),c.jsx("textarea",{placeholder:"自定义评审指令用:评审指令正文",value:S,onChange:O=>_(O.target.value)}),c.jsxs(At,{children:[c.jsx("button",{className:"primary",onClick:()=>void i.postReview({target_type:r,branch:f.trim()||void 0,sha:C.trim()||void 0,instructions:S.trim()||void 0}),children:"登记评审需求"}),c.jsx("button",{onClick:i.refreshReviews,children:"刷新队列"}),c.jsx("span",{className:"muted",children:i.reviewMsg})]}),c.jsx("div",{className:"muted",children:"本基线只登记评审需求(requested),不自动跑评审;评审是否执行、结果如何,以评审队列里的记录为准。"}),i.reviewError?c.jsx("div",{className:"muted",children:i.reviewError}):i.reviews.length===0?c.jsx("div",{className:"muted",children:"点任务行查看详情后,点“刷新队列”查看评审记录"}):c.jsx(ou,{children:i.reviews.map(O=>{let Y=O.target_type||"";return O.branch&&(Y+=":"+O.branch),O.sha&&(Y+=":"+String(O.sha).slice(0,12)),O.instructions&&(Y+=":"+String(O.instructions).slice(0,80)),c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:"评审"})," ",pe(O.id)," | ",Y," | ",c.jsx("span",{className:"st-"+(O.status||""),children:O.status}),O.created_at?" | "+Le(O.created_at):"",c.jsx("br",{}),O.findings?String(O.findings).slice(0,1e3):c.jsx("span",{className:"muted",children:"(暂无 findings:评审尚未执行或尚未写回)"})]}),c.jsx("button",{onClick:()=>i.submitFindings(O.id),children:"写 findings"})]},O.id)})})]})}function Tg(){const i=Lt();return i.currentTask?i.currentTask.coordinator_parts?i.coordChildren.length===0?c.jsx("div",{className:"muted",children:"(子任务尚未创建)"}):c.jsx(ou,{children:i.coordChildren.map(r=>c.jsx("li",{children:c.jsxs("span",{className:"row-main",children:["分工 ",c.jsx("b",{children:r.coordinator_part||""})," | 状态 ",c.jsx("span",{className:"st-"+r.status,children:r.status})," | ",pe(r.id)," | ",String(r.prompt||"").slice(0,60)]})},r.id))}):c.jsx("div",{className:"muted",children:"(非协调员任务)"}):c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示协调员的子任务"})}function _g(){const i=Lt();if(!i.currentTask)return c.jsx("div",{className:"muted",children:"点任务行查看详情后,这里显示子代理链路"});if(i.agents.length===0)return c.jsxs("div",{className:"muted",children:["(暂无子代理)",i.agentsNote?c.jsx("span",{className:"err",children:i.agentsNote}):null]});const r={main:"主任务",delegate:"委派子任务",model:"模型子代理"},h={running:"进行中",completed:"已完成",failed:"失败",interrupted:"已中断",unknown:"未知"};return c.jsx(ou,{children:i.agents.map((f,y)=>c.jsx("li",{children:c.jsxs("span",{className:"row-main",children:[c.jsxs("b",{children:[r[f.kind||""]||f.kind||"",":"]}),f.name||""," ",c.jsx("span",{className:"st-"+(f.status||""),children:h[f.status||""]||f.status||"?"})," | 过程调用 "+(f.calls?f.calls.length:0)+" 条",f.updated_ts?" | 最近活动 "+Le(f.updated_ts/1e3):"",f.prompt?c.jsxs(c.Fragment,{children:[c.jsx("br",{}),c.jsxs("span",{className:"muted",children:["任务:",f.prompt]})]}):null,f.calls&&f.calls.length?c.jsxs(c.Fragment,{children:[c.jsx("br",{}),f.calls.slice(-50).map((C,M)=>c.jsx("div",{children:yo(C)},M))]}):c.jsxs(c.Fragment,{children:[c.jsx("br",{}),c.jsx("span",{className:"muted",children:"(暂无过程调用)"})]})]})},y))})}function Eg(){const i=Lt();return c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("button",{onClick:i.refreshApprovals,children:"刷新审批队列"}),c.jsx("span",{className:"muted",children:i.approvalMsg})]}),c.jsx("div",{className:"muted",children:"待审批项需人工点记录允许或记录拒绝;超时默认拒绝。当前版本只留回执,不拦截实时行为(实时请求的事前拦截待 host 接入后生效,在此之前实时行为不变)。"}),c.jsx(ol,{empty:"(暂无审批项)",items:i.approvals.map(r=>c.jsxs("span",{className:"row-main",children:["[",r.kind,"|",r.status,"] ",r.summary,["approved","denied","expired"].includes(r.status||"")?"|回执:"+(r.decision||r.status)+" by "+String(r.decided_by||"").slice(0,8)+" "+Le(r.decided_at):"",r.status==="pending"?c.jsxs(c.Fragment,{children:[" ",c.jsx("button",{onClick:()=>i.decideApproval(r.id,"approved"),children:"记录允许"})," ",c.jsx("button",{onClick:()=>i.decideApproval(r.id,"denied"),children:"记录拒绝"})]}):null]},r.id))})]})}function Ng(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("");return c.jsxs(c.Fragment,{children:[c.jsx("input",{placeholder:"目标任务 id(与本任务同项目;对方运行中排队执行、已终态则续跑复活,绝不打断当前轮)",style:{width:"100%"},value:r,onChange:C=>h(C.target.value)}),c.jsx("textarea",{placeholder:"消息正文(以本任务名义发出;对方 agent 会看到发送方任务 id,可照此回消息)",value:f,onChange:C=>y(C.target.value)}),c.jsxs(At,{children:[c.jsx("button",{onClick:async()=>{await i.postPeer("queued",r,f),y("")},children:"排队发给对方"}),c.jsx("button",{className:"primary",onClick:async()=>{await i.postPeer("direct",r,f),y("")},children:"直接发送"}),c.jsx("span",{className:"muted",children:i.peerMsg})]}),c.jsx(ol,{empty:"(本任务尚未发出任务间消息)",items:i.peerMessages.map((C,M)=>c.jsxs("span",{className:"row-main",children:["[发给 ",String(C.task_id).slice(0,8),"|",C.mode,"|",C.status,"] ",String(C.body||"").slice(0,120)]},M))})]})}function Cg(){const i=Lt(),[r,h]=d.useState(""),f=i.currentTask?["completed","failed","interrupted"].includes(i.currentTask.status):!1;return c.jsxs(c.Fragment,{children:[c.jsx("div",{className:"muted",children:i.threadText||"点任务行查看详情后,这里显示线程元数据"}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"新线程名",value:r,onChange:y=>h(y.target.value)}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("rename",r),children:"重命名"}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("archive",""),children:"归档"}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("unarchive",""),children:"取消归档"}),c.jsx("button",{disabled:!f,onClick:()=>i.threadAction("compact",""),children:"压缩"}),c.jsx("span",{className:"muted",children:i.threadGateNote}),c.jsx("span",{className:"muted",children:i.threadMsg})]}),c.jsx(ol,{empty:"暂无其他线程",items:i.threadList.map((y,C)=>c.jsxs("span",{className:"row-main",children:[y.name||y.thread_name||y.id||""," [",y.id||"","]",y.archived?" [已归档]":"",y.cwd?" "+y.cwd:""]},C))}),c.jsx("div",{className:"muted",children:"限制:Docker/异机任务线程操作一律 409;merged/merge-failed 不可改;turn/steer 与 review/start 不在本批。"})]})}function Ag(i){return/503|Service Unavailable/.test(i)?"上游模型服务暂时不可用(503):模型一轮都没跑出来,不是任务逻辑失败;等上游恢复或换模型重试":/429|rate limit/i.test(i)?"上游限流(429):模型拒绝了本次调用,可稍后重试":/timeout|timed out/i.test(i)?"上游调用超时:模型响应未在限时内返回":/401|403|Invalid API key|unauthorized/i.test(i)?"上游鉴权失败:网关 key 或权限有问题":/ECONNREFUSED|ENOTFOUND|network|fetch failed/i.test(i)?"连不上上游模型服务:网络或网关地址有问题":"任务失败(原因见原始错误文本)"}function Mg(i){return i===null||i<0?"未知":i<60?Math.floor(i)+" 秒前":i<3600?Math.floor(i/60)+" 分钟前":(i/3600).toFixed(1)+" 小时前"}const Og=[["overview","概览"],["collab","协作"],["review","评审与审批"],["threads","线程"],["browser","浏览器"]];function zg(){const i=Lt(),r=i.currentTask,[h,f]=d.useState("overview");if(!r)return c.jsx(Ft,{id:"nav-detail",title:"任务详情",children:c.jsx("div",{className:"muted",children:"点任务行查看详情"})});const[y,C]=$h(r),[M,S]=Ih(r),_=Wh(r),O=i.costSummary,Y=r.plan?"计划模式(只读)":r.goal_mode?r.no_time_limit?"goal 模式无敌版(不限时)":"goal 模式(不限轮次,模型宣告完成)":r.verify_command?"goal 模式(旧语义,验收命令)":null;return c.jsxs("div",{className:"detail-stack",id:"nav-detail",children:[c.jsx("nav",{className:"detail-tabs","aria-label":"任务详情页签",children:Og.map(([x,H])=>c.jsx("button",{type:"button",className:h===x?"active":"",onClick:()=>f(x),children:H},x))}),h==="overview"?c.jsxs(c.Fragment,{children:[c.jsxs(Ft,{title:"任务详情",children:[c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[c.jsx("span",{className:"issue-first",title:"任务首字:"+_,"aria-hidden":"true",children:_}),c.jsxs("span",{children:["状态 ",c.jsx("b",{className:"st-"+r.status,children:r.status}),"(",Wy[r.status]||"未知状态",")"]}),c.jsxs("span",{className:"muted",children:["更新 ",Le(r.updated_at)]})]}),c.jsxs("div",{className:"detail-cards",children:[c.jsxs("div",{className:"detail-card",title:C,children:[c.jsx("div",{className:"detail-card-label",children:"耗时"}),c.jsx("div",{className:"detail-card-value",children:y})]}),c.jsxs("div",{className:"detail-card",title:S,children:[c.jsx("div",{className:"detail-card-label",children:"token"}),c.jsx("div",{className:"detail-card-value",children:M})]}),c.jsxs("div",{className:"detail-card",title:"LLM 请求数 = GET /tasks/{id}/cost 的 llm_requests,即 cost_records 的 completions 之和:host.py 每收到一次 thread/tokenUsage/updated(一次上游补全)计 1,worker 每轮终局记账时累加;估算行(无真实用量)计 0;部署前已记账的老任务没有 completions 列,显示 0 而非回填数",children:[c.jsx("div",{className:"detail-card-label",children:"LLM 请求数"}),c.jsx("div",{className:"detail-card-value",children:O===void 0?"加载中…":O===null?"—":(O.llm_requests??0).toLocaleString()}),O===void 0?c.jsx("div",{className:"muted",children:"成本加载中…"}):O===null?c.jsx("div",{className:"muted",children:"成本明细加载失败,LLM 请求数不可用"}):c.jsxs("div",{className:"muted",children:["上游补全次数之和| 轮数 ",O.turns??0," | fallback ",O.fallback_count??0]})]}),c.jsxs("div",{className:"detail-card",title:"成本明细:GET /tasks/{id}/cost 聚合;美元只计有价行,无价 token 单列;缓存是输入 token 的子集,不另计",children:[c.jsx("div",{className:"detail-card-label",children:"成本"}),c.jsx("div",{className:"detail-card-value",children:O===void 0?"加载中…":O===null?"—":dg(O.usd_micros)}),O===void 0?c.jsx("div",{className:"muted",children:"成本加载中…"}):O===null?c.jsx("div",{className:"muted",children:"成本明细加载失败(“查成本”按钮仍可手动拉取原始 JSON)"}):c.jsxs("div",{className:"muted",children:["token ",(O.total_tokens??0).toLocaleString(),"(实际 ",(O.actual_tokens??0).toLocaleString()," / 估算 ",(O.estimated_tokens??0).toLocaleString(),",缓存 ",(O.cached_input_tokens??0).toLocaleString(),",无价 ",(O.unpriced_actual_tokens??0).toLocaleString(),")| fallback ",O.fallback_count??0]})]})]})]}),r.status==="running"||r.status==="queued"?(()=>{const x=Date.now()/1e3,H=r.last_heartbeat||r.updated_at||null,L=H?x-H:null;return c.jsxs(Ft,{title:"LLM 进度",children:[c.jsxs("div",{children:["状态 ",r.status==="running"?"进行中":"排队中"," · 已用 ",y,L!==null?" · 上次活动 "+Mg(L):""]}),L!==null&&L>180?c.jsx("div",{className:"muted",children:"超过 3 分钟无活动:可能在等上游模型响应(上游 503 时 worker 会重试后失败,失败后这里会显示失败原因;无活动不等于已断连,服务端没有“LLM 已连接”字段,不作此断言)"}):c.jsx("div",{className:"muted",children:"首轮无输出时属正常:模型第一轮回复到达前 trace 里没有消息;回复到达后会在会话区出现,成本里的 LLM 请求数随之增加"})]})})():null,r.error_text?c.jsxs(Ft,{title:"失败原因",children:[c.jsx("div",{children:Ag(r.error_text)}),c.jsx("pre",{style:{whiteSpace:"pre-wrap",overflowWrap:"anywhere"},children:r.error_text.slice(0,1e3)}),c.jsxs("div",{className:"muted",children:["完整错误文本仍在下方“操作与结果”里;模型 ",r.model||"默认"," · executor ",r.executor||""]})]}):null,c.jsx(Ft,{title:"基本信息",children:c.jsxs("dl",{className:"detail-kv",children:[c.jsxs("div",{children:[c.jsx("dt",{children:"任务"}),c.jsx("dd",{children:r.id})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"turn"}),c.jsx("dd",{children:pe(r.turn_id)})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"root turn"}),c.jsx("dd",{children:pe(r.root_turn_id)})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"executor"}),c.jsx("dd",{children:r.executor||""})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"主机"}),c.jsx("dd",{children:r.worktree_host||""})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"模型"}),c.jsx("dd",{children:r.model||"默认"})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"强度"}),c.jsx("dd",{children:r.effort||"默认"})]}),r.child_model||r.child_effort?c.jsxs("div",{children:[c.jsx("dt",{children:"子任务默认"}),c.jsx("dd",{children:(r.child_model||"跟随本任务")+" / "+(r.child_effort||"跟随本任务")})]}):null,c.jsxs("div",{children:[c.jsx("dt",{children:"权限"}),c.jsx("dd",{children:r.approval||"默认"})]}),c.jsxs("div",{children:[c.jsx("dt",{children:"项目"}),c.jsx("dd",{children:pe(r.project_id)})]}),Y?c.jsxs("div",{children:[c.jsx("dt",{children:"模式"}),c.jsx("dd",{children:Y})]}):null,r.forked_from?c.jsxs("div",{children:[c.jsx("dt",{children:"分支自"}),c.jsx("dd",{children:pe(r.forked_from)})]}):null,r.coordinator_parts?c.jsxs("div",{children:[c.jsx("dt",{children:"协调员"}),c.jsxs("dd",{children:[r.coordinator_parts.length," 个子任务(",r.coordinator_parts.map(x=>x.name).join("、"),")"]})]}):null,r.parent_task_id?c.jsxs("div",{children:[c.jsx("dt",{children:"协调子任务"}),c.jsxs("dd",{children:["父任务 ",pe(r.parent_task_id),r.coordinator_part?"("+r.coordinator_part+")":""]})]}):null,r.attachments&&r.attachments.length?c.jsxs("div",{children:[c.jsx("dt",{children:"附件"}),c.jsx("dd",{children:r.attachments.join("、")})]}):null,r.pr_url?c.jsxs("div",{children:[c.jsx("dt",{children:"PR"}),c.jsx("dd",{children:c.jsxs("a",{href:r.pr_url,target:"_blank",rel:"noopener noreferrer",children:["PR #",r.pr_number]})})]}):null]})}),c.jsx(Ft,{title:"prompt",children:c.jsx("div",{style:{whiteSpace:"pre-wrap",overflowWrap:"anywhere"},children:r.prompt||"(无 prompt)"})}),c.jsxs(Ft,{title:"操作与结果",children:[c.jsx(xg,{}),c.jsx("pre",{children:i.detailBody||"(暂无 result/error 输出)"}),i.costBox?c.jsx("pre",{children:i.costBox}):null]})]}):h==="collab"?c.jsxs(c.Fragment,{children:[c.jsx(Ft,{title:"协调子任务",children:c.jsx(Tg,{})}),c.jsx(Ft,{title:"子代理链路",children:c.jsx(_g,{})}),c.jsx(Ft,{title:"给其他任务发消息",children:c.jsx(Ng,{})})]}):h==="review"?c.jsxs(c.Fragment,{children:[c.jsx(Ft,{title:"评审队列",children:c.jsx(jg,{})}),c.jsx(Ft,{title:"运行时审批队列",children:c.jsx(Eg,{})})]}):h==="browser"?c.jsx(c.Fragment,{children:c.jsx(Ft,{title:"浏览器远控",children:c.jsx(bg,{})})}):c.jsx(c.Fragment,{children:c.jsx(Ft,{title:"线程",children:c.jsx(Cg,{})})})]})}function Rg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState([]),[S,_]=d.useState(null),[O,Y]=d.useState(!1),[x,H]=d.useState(""),[L,tt]=d.useState(""),dt=d.useCallback(async()=>{if(!i.keySaved){M([]);return}try{const Q=await W(F.userKeys);M(Q.keys||[]),tt("")}catch(Q){tt(Q instanceof Error?Q.message:"加载 key 列表失败")}},[i.keySaved]);d.useEffect(()=>{dt()},[dt]);const J=async()=>{H(""),tt(""),Y(!1);try{const Q=await W(F.userKeys,"POST",{label:f.trim()||"api-key"});_(Q),y(""),H("新 key 已生成,只显示一次:复制后请妥善保存。"),await dt()}catch(Q){tt(Q instanceof Error?Q.message:"生成 key 失败")}},gt=async()=>{if(S)try{await navigator.clipboard.writeText(S.api_key),Y(!0)}catch{tt("复制失败:请手动选中上方 key 文本复制")}},_t=()=>{S&&(i.saveKeyInput(S.api_key),_(null),H("已存为当前浏览器 key(原有 key 未被撤销,需要时可单独撤销)。"))},Dt=async Q=>{const ft=(()=>{try{return rl().slice(-4)}catch{return""}})(),D=Q.id==="legacy"?"(主 key:撤销后当前浏览器保存的 key 也可能随之失效,需重新登录拿新 key)":Q.hint&&Q.hint===ft?"(这就是本浏览器当前在用的 key,撤销后本页会立刻未连接)":"";if(window.confirm(`撤销 key「${Q.label}」(后 4 位 ${Q.hint||"无"})吗?${D}撤销不可恢复。`)){H(""),tt("");try{await W(F.userKey(Q.id),"DELETE"),H(`已撤销 key「${Q.label}」。`),await dt()}catch(X){tt(X instanceof Error?X.message:"撤销失败")}}};return c.jsxs(Ft,{id:"nav-key",title:"API Key",children:[c.jsxs(At,{children:[c.jsx("input",{type:"password",placeholder:"粘贴你的 X-API-Key(与调 API 用的同一个)",autoComplete:"off",value:r,onChange:Q=>h(Q.target.value)}),c.jsx("button",{className:"primary",onClick:()=>{i.saveKeyInput(r),h("")},children:"保存到本浏览器"}),c.jsx("button",{onClick:i.clearKeyInput,children:"清除"}),c.jsx("span",{className:"muted",children:i.keyState})]}),c.jsx("div",{children:c.jsx("span",{className:"muted",children:i.quotaText})}),!i.keySaved&&c.jsx("div",{children:c.jsx("span",{className:"muted",children:"先粘贴并保存已有 key,再生成或管理 key(生成接口本身也要用已有 key 鉴权)。"})}),i.keySaved&&c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("input",{placeholder:"新 key 备注(可选,默认 api-key,如 harness2-temp)",autoComplete:"off",value:f,onChange:Q=>y(Q.target.value)}),c.jsx("button",{className:"primary",onClick:()=>void J(),children:"生成新 key"}),c.jsx("button",{onClick:()=>void dt(),children:"刷新列表"})]}),S&&c.jsxs("div",{children:[c.jsx("div",{children:c.jsxs("span",{className:"muted",children:["新 key(",S.label,",后 4 位 ",S.hint,"):只显示一次,关闭后无法再查看。"]})}),c.jsxs(At,{children:[c.jsx("input",{type:"text",readOnly:!0,value:S.api_key,onFocus:Q=>Q.target.select()}),c.jsx("button",{className:"primary",onClick:()=>void gt(),children:O?"已复制":"复制"}),c.jsx("button",{onClick:_t,children:"存为当前浏览器 key"}),c.jsx("button",{onClick:()=>_(null),children:"关闭(不再显示)"})]})]}),x&&c.jsx("div",{children:c.jsx("span",{className:"muted",children:x})}),L&&c.jsx("div",{children:c.jsx("span",{children:L})}),c.jsxs("div",{className:"api-key-list",children:[C.length===0&&c.jsx("span",{className:"muted",children:"暂无 key(或列表加载失败,见上方报错)。"}),C.map(Q=>c.jsxs("div",{children:[c.jsx("span",{children:Q.label})," ",c.jsxs("span",{className:"muted",children:["后 4 位 ",Q.hint||"无"," · 创建 ",Le(Q.created_at)," · 最近使用 ",Q.last_used_at?Le(Q.last_used_at):"未使用过"]})," ",Q.revoked_at?c.jsxs("span",{className:"muted",children:["已撤销(",Le(Q.revoked_at),")"]}):c.jsx("button",{onClick:()=>void Dt(Q),children:"撤销"})]},Q.id))]})]})]})}function Dg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("");return c.jsxs(Ft,{id:"adminCard",title:c.jsxs(c.Fragment,{children:["管理区 ",c.jsx("span",{className:"muted",children:"(仅全局管理员)"})]}),children:[c.jsx("div",{className:"muted",children:i.adminGate}),i.adminBody?c.jsxs(c.Fragment,{children:[c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"要查配额的用户 id",value:i.adminUserId,onChange:C=>i.setAdminUserId(C.target.value)}),c.jsx("button",{onClick:i.adminQuota,children:"查该用户配额"})]}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"要设置配额的用户 id",value:r,onChange:C=>h(C.target.value)}),c.jsx("input",{type:"text",placeholder:"美元配额(留空 = 不限额)",value:f,onChange:C=>y(C.target.value)}),c.jsx("button",{className:"primary",onClick:()=>i.adminSetQuota(r,f),children:"设置配额"})]}),c.jsxs(At,{children:[c.jsx("button",{onClick:i.adminSweep,children:"手动触发 sweep(清理陈旧 running 任务)"}),c.jsx("button",{onClick:i.adminBackup,children:"手动触发备份"})]}),c.jsx("div",{className:"muted",children:i.adminResult})]}):null]})}function Ug(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("contributor"),C=i.projects.find(M=>M.id===i.projectId);return c.jsxs(Ft,{title:"项目成员",children:[C?c.jsxs("div",{className:"muted",children:[C.name," | ",C.id," | 仓库 ",C.repo_url||"未绑定"," | 我的角色 ",C.my_role||"未知"]}):c.jsx("div",{className:"muted",children:"先选项目"}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"成员 user_id 或用户名",value:r,onChange:M=>h(M.target.value)}),c.jsxs("select",{"aria-label":"邀请角色",value:f,onChange:M=>y(M.target.value),children:[c.jsx("option",{value:"viewer",children:"viewer(只读)"}),c.jsx("option",{value:"contributor",children:"contributor(可建任务)"}),c.jsx("option",{value:"maintainer",children:"maintainer(可批合并)"}),c.jsx("option",{value:"owner",children:"owner(可管项目)"})]}),c.jsx("button",{onClick:async()=>{await i.inviteMember(r,f),h("")},children:"邀请成员"}),c.jsx("button",{onClick:i.refreshMembers,children:"刷新成员"})]}),i.memberError?c.jsx("div",{className:"muted",children:i.memberError}):i.members.length===0?c.jsx("div",{className:"muted",children:"先选项目"}):c.jsx(ou,{children:i.members.map(M=>{const S=(M.username||"")+(M.username?"("+String(M.user_id).slice(0,8)+")":String(M.user_id).slice(0,8));return c.jsxs("li",{children:[c.jsx("span",{className:"row-main",children:S}),c.jsx("select",{"aria-label":"成员角色",value:M.role||"viewer",onChange:_=>void i.changeMemberRole(M.user_id,_.target.value),children:$y.map(_=>c.jsx("option",{value:_,children:_},_))}),c.jsx("button",{onClick:()=>void i.removeMember(M.user_id),children:"移除"})]},M.user_id)})}),c.jsx("span",{className:"muted",children:i.memberMsg})]})}function wg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState("");return c.jsxs("div",{className:"project-nav",children:[c.jsxs(Ft,{title:"项目",children:[c.jsx(At,{children:c.jsx("button",{onClick:i.refreshProjects,children:"刷新项目"})}),i.projects.length===0?c.jsx("div",{className:"muted",children:"暂无项目"}):c.jsx("ul",{className:"project-nav-list",children:i.projects.map(S=>c.jsxs("li",{className:"project-nav-item"+(S.id===i.projectId?" selected":""),onClick:()=>i.selectProject(S.id),title:S.id,children:[c.jsx("span",{className:"project-nav-name",children:S.name}),c.jsxs("span",{className:"project-nav-meta",children:[pe(S.id),S.my_role?" · "+S.my_role:""]})]},S.id))}),c.jsx("h3",{children:"创建项目"}),c.jsx(At,{children:c.jsx("input",{type:"text",placeholder:"新项目名",value:r,onChange:S=>h(S.target.value)})}),c.jsx(At,{children:c.jsxs("select",{"aria-label":"从 GitHub 仓库选",value:f,onChange:S=>{y(S.target.value),S.target.value&&M(S.target.value)},children:[c.jsx("option",{value:"",children:"(从 GitHub App 安装仓库里选,或手动输入)"}),i.githubRepos.map(S=>c.jsx("option",{value:S.clone_url,children:S.repo_full_name},S.clone_url))]})}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"repo_url(可选)",value:C,onChange:S=>M(S.target.value)}),c.jsx("button",{className:"primary",onClick:async()=>{await i.createProject(r,C),h(""),M(""),y("")},children:"创建项目"})]}),c.jsx("span",{className:"muted",children:i.projectMsg})]}),c.jsx(Ug,{})]})}function kg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState("");return c.jsxs(Ft,{id:"nav-env",title:c.jsxs(c.Fragment,{children:["环境 ",c.jsx("span",{className:"muted",children:"(选、建、管:当前项目的基准检出)"})]}),children:[c.jsxs(At,{children:[c.jsx("button",{onClick:i.refreshEnvs,children:"刷新"}),c.jsx("span",{className:"muted",children:i.envCount})]}),i.envs.length===0?c.jsx("div",{className:"muted",children:"先选项目"}):c.jsx(ou,{children:i.envs.map(S=>c.jsxs("li",{children:[c.jsxs("span",{className:"row-main",children:[S.name,"(",pe(S.id),") / 分支 ",S.branch||"默认分支"," / 执行器 ",S.executor||"不限"," / 状态 ",S.status,S.status==="failed"&&S.error_text?" / 失败原因:"+S.error_text:""]}),S.status==="archived"?null:c.jsx("button",{onClick:()=>void i.archiveEnv(S.id),children:"归档"})]},S.id))}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"新环境名",value:r,onChange:S=>h(S.target.value)}),c.jsx("input",{type:"text",placeholder:"分支(可选,空=默认分支)",value:f,onChange:S=>y(S.target.value)}),c.jsxs("select",{"aria-label":"新环境执行器",value:C,onChange:S=>M(S.target.value),children:[c.jsx("option",{value:"",children:"执行器:不限"}),c.jsx("option",{value:"local",children:"local"}),c.jsx("option",{value:"docker",children:"docker"})]}),c.jsx("button",{onClick:async()=>{await i.createEnv(r,f,C),h(""),y("")},children:"新建环境"})]}),c.jsx("span",{className:"muted",children:i.envMsg})]})}function Hg(){const i=Lt(),[r,h]=d.useState("");return c.jsxs(Ft,{title:"仓库与环境上下文",children:[c.jsx("div",{className:"muted",children:i.envRepoLine||"先选项目"}),c.jsx("div",{className:"muted",children:i.envCount}),i.envRepoBar?c.jsxs(At,{children:[c.jsxs("select",{"aria-label":"补仓库",value:r,onChange:f=>h(f.target.value),children:[c.jsx("option",{value:"",children:"(从 GitHub App 安装仓库里选)"}),i.githubRepos.map(f=>c.jsx("option",{value:f.clone_url,children:f.repo_full_name},f.clone_url))]}),c.jsx("button",{onClick:()=>void i.saveEnvRepo(r),children:"保存仓库"})]}):null,c.jsx("span",{className:"muted",children:i.envRepoMsg})]})}function Bg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState("all"),C=_=>{const O=vo(_);return _.alive!==void 0&&_.alive!==null?!!_.alive:O.age!==null&&O.age<=120},M=r.trim().toLowerCase(),S=(i.workers||[]).filter(_=>!(M&&!((_.worker_id||_.id||"")+" "+(_.executor||"")).toLowerCase().includes(M)||f==="online"&&!C(_)||f==="offline"&&C(_)));return c.jsxs(Ft,{id:"nav-workers",title:"Workers",children:[c.jsx(At,{children:c.jsx("button",{onClick:i.refreshWorkers,children:"刷新"})}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"搜索 worker_id / 执行器","aria-label":"搜索 Worker",value:r,onChange:_=>h(_.target.value)}),c.jsxs("select",{"aria-label":"在线状态筛选",value:f,onChange:_=>y(_.target.value),children:[c.jsx("option",{value:"all",children:"在线状态:全部"}),c.jsx("option",{value:"online",children:"仅在线"}),c.jsx("option",{value:"offline",children:"仅离线"})]}),i.workers?c.jsxs("span",{className:"muted",children:["显示 ",S.length," / 共 ",i.workers.length," 个"]}):null]}),i.workersError?c.jsx("div",{className:"muted",children:i.workersError}):i.workers===null?c.jsxs("div",{className:"muted",children:[i.workersSummary||"未加载","(完整清单仅全局管理员可见,搜索/筛选仅对完整清单生效)"]}):i.workers.length===0?c.jsx("div",{className:"muted",children:"无 worker 注册"}):S.length===0?c.jsx("div",{className:"muted",children:"没有符合筛选条件的 worker"}):c.jsx(ou,{children:S.map((_,O)=>{const Y=_.running===void 0||_.running===null?"?":_.running,x=_.capacity===void 0||_.capacity===null?"?":_.capacity,H=vo(_),L=C(_),tt=L?" / 在线":" / 离线(占用为上次上报值)";return c.jsx("li",{className:L?"is-online":"is-offline",children:c.jsxs("span",{className:"row-main",children:[_.worker_id||_.id||"?"," / ",_.executor||""," / 占用 ",Y,"/",x,tt," / 最后心跳 ",H.text]})},O)})})]})}function qg(){const i=Lt();return c.jsxs(Ft,{title:"健康与配额",children:[c.jsxs(At,{children:[c.jsx("span",{className:"muted",children:i.healthText}),c.jsx("button",{onClick:i.refreshHealth,children:"刷新"})]}),c.jsx("div",{children:c.jsx("span",{className:"muted",children:i.quotaText})}),c.jsx("div",{className:"muted",children:i.workersSummary})]})}function Yg(){const i=Lt();return c.jsxs(Ft,{id:"nav-skills",title:"技能与 MCP",children:[c.jsx("div",{className:"muted",children:"反映宿主租户 home,容器内 home 可能不同;重载只在 schema 枚举里,当前 pin 的二进制拒绝时原样 502;OAuth 只发起登录,不代表登录完成。"}),c.jsxs(At,{children:[c.jsx("button",{onClick:i.refreshSkills,children:"刷新"}),c.jsx("span",{className:"muted",children:i.skillMsg})]}),c.jsx("h3",{children:"技能"}),i.skillError?c.jsx("div",{className:"muted",children:i.skillError}):c.jsx(ol,{empty:"暂无技能",items:i.skills.map((r,h)=>c.jsxs("span",{className:"row-main",children:[r.name||r.id||"",r.description?" - "+r.description:"",r.detail?" - "+r.detail:""]},h))}),c.jsx("h3",{children:"应用"}),c.jsx(ol,{empty:"暂无应用",items:i.apps.map((r,h)=>c.jsxs("span",{className:"row-main",children:[r.name||r.id||"",r.description?" - "+r.description:""]},h))}),c.jsx("h3",{children:"MCP 服务"}),c.jsx(ol,{empty:"暂无 MCP 服务",items:i.mcpServers.map((r,h)=>{const f=r.detail||r.details||r.state||"";return c.jsxs("span",{className:"row-main",children:[r.name||r.id||""," | 状态 ",r.status||"",f?" | "+f:""]},h)})})]})}function Gg(){const i=Lt(),[r,h]=d.useState("");return c.jsxs(Ft,{title:"MCP 操作",children:[c.jsxs(At,{children:[c.jsx("button",{onClick:i.reloadMcp,children:"重载配置"}),c.jsx("span",{className:"muted",children:i.mcpMsg})]}),c.jsxs(At,{children:[c.jsx("input",{type:"text",placeholder:"MCP 服务名",value:r,onChange:f=>h(f.target.value)}),c.jsx("button",{onClick:()=>void i.mcpOauth(r),children:"发起 OAuth 登录"}),c.jsx("span",{className:"muted",children:i.mcpOauthMsg})]}),i.mcpOauthBox?c.jsx("pre",{children:i.mcpOauthBox}):null]})}const Vh=160;function Qg(i){return i?i%86400===0?"(每天)":i%3600===0?"(每小时)":i%60===0?"(每 "+i/60+" 分钟)":"":""}function Xg({detail:i}){const r=Lt(),h=f=>{if(f){r.showDetail(f);try{window.location.hash="#view-tasks"}catch{}}};return c.jsxs("div",{className:"card",children:[c.jsxs("h2",{children:["自动化详情:",i.name]}),c.jsxs("div",{className:"muted",children:["状态 ",i.status,i.due?"|到期":""," | 每 ",i.interval_seconds," 秒 | 下次 ",Le(i.next_run_at),"| 上次执行 ",Le(i.last_run_at)," | 创建人 ",i.created_by?pe(i.created_by):""," | 创建于 ",Le(i.created_at),i.cancelled_at?" | 已取消于 "+Le(i.cancelled_at):"",i.heartbeat_binding?" | 心跳绑定:已绑定":""]}),c.jsx("div",{className:"muted",style:{whiteSpace:"pre-wrap",marginTop:8},children:i.prompt||""}),i.repo_url?c.jsxs("div",{className:"muted",children:["仓库 ",i.repo_url]}):null,c.jsxs(At,{children:[c.jsx("button",{onClick:r.closeAutomationDetail,children:"关闭详情"}),c.jsxs("span",{className:"muted",children:["编辑请在下面列表里点“编辑”。",r.autoDetailMsg]})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["最近产出(",(i.runs||[]).length,")"]}),c.jsx(ol,{empty:"(无)",items:(i.runs||[]).map((f,y)=>c.jsxs("span",{className:"row-main",children:[f.status,f.created_at?"|"+Le(f.created_at):"",f.task_id?c.jsxs(c.Fragment,{children:[" | 任务 ",c.jsx("button",{onClick:()=>h(f.task_id),children:pe(f.task_id)})]}):"",f.detail?"|"+String(f.detail).slice(0,120):""]},y))})]}),i.runs_note?c.jsx("div",{className:"muted",children:i.runs_note}):null]})}function Lg(){const i=Lt(),[r,h]=d.useState(""),[f,y]=d.useState(""),[C,M]=d.useState(""),[S,_]=d.useState(""),[O,Y]=d.useState(""),[x,H]=d.useState(""),[L,tt]=d.useState(""),[dt,J]=d.useState(""),[gt,_t]=d.useState(""),[Dt,Q]=d.useState({}),ft=D=>{Y(D.id),H(D.name||""),tt(D.prompt||""),J(D.interval_seconds?String(D.interval_seconds):""),_t(D.repo_url||"")};return c.jsxs(c.Fragment,{children:[c.jsx("input",{placeholder:"自动化名称(同项目内启用中的名称唯一)",style:{width:"100%"},value:r,onChange:D=>h(D.target.value)}),c.jsx("textarea",{placeholder:"定时任务内容(创建只登记定义与到期状态;执行由后续执行器经 claim 抢单后建任务,不另起调度器)",value:f,onChange:D=>y(D.target.value)}),c.jsxs(At,{children:[c.jsx("input",{placeholder:"间隔秒数(默认 86400,最小 60)",style:{width:"40%"},value:C,onChange:D=>M(D.target.value)}),c.jsx("input",{placeholder:"仓库(可选)",style:{width:"40%"},value:S,onChange:D=>_(D.target.value)}),c.jsx("button",{className:"primary",onClick:async()=>{await i.createAutomation(r,f,C,S),h(""),y(""),M(""),_("")},children:"创建自动化"}),c.jsx("button",{onClick:i.refreshAutomations,children:"刷新自动化"}),c.jsx("span",{className:"muted",children:i.autoMsg})]}),c.jsx(ol,{empty:"(暂无自动化)",items:i.automations.map(D=>{const X=D.prompt||"",et=!!Dt[D.id],xt=X.length>Vh,Mt=X?et||!xt?X:X.slice(0,Vh)+"…":"(无内容)";return c.jsxs("div",{children:[c.jsxs("div",{className:"row-main",children:["[",D.status,D.due?"|到期":"",D.run_once?"|一次性":"","] ",D.name,"|每 ",D.interval_seconds," 秒",Qg(D.interval_seconds),"|上次 ",Le(D.last_run_at)||"未运行","|下次 ",Le(D.next_run_at),c.jsx("button",{onClick:()=>void i.loadAutomationDetail(D.id),children:"详情"}),D.status==="active"?c.jsxs(c.Fragment,{children:[" ",c.jsx("button",{onClick:()=>ft(D),children:"编辑"})," ",c.jsx("button",{onClick:()=>i.cancelAutomation(D.id),children:"取消"})]}):null]}),D.repo_url?c.jsxs("div",{className:"muted",children:["仓库:",D.repo_url]}):null,D.status==="cancelled"&&D.cancelled_at?c.jsxs("div",{className:"muted",children:["已取消于 ",Le(D.cancelled_at)]}):null,c.jsxs("div",{className:"muted",style:{whiteSpace:"pre-wrap",overflowWrap:"anywhere"},children:[Mt,xt?c.jsx("button",{onClick:()=>Q(mt=>({...mt,[D.id]:!mt[D.id]})),children:et?"收起":"展开"}):null]}),O===D.id?c.jsxs("div",{children:[c.jsx("input",{placeholder:"自动化名称",style:{width:"100%"},value:x,onChange:mt=>H(mt.target.value)}),c.jsx("textarea",{placeholder:"定时任务内容",value:L,onChange:mt=>tt(mt.target.value)}),c.jsxs(At,{children:[c.jsx("input",{placeholder:"间隔秒数(最小 60,留空保持原值)",style:{width:"40%"},value:dt,onChange:mt=>J(mt.target.value)}),c.jsx("input",{placeholder:"仓库(可选,清空则清除)",style:{width:"40%"},value:gt,onChange:mt=>_t(mt.target.value)}),c.jsx("button",{className:"primary",onClick:async()=>{const mt=i.automations.find(B=>B.id===O)||D;await i.updateAutomation(mt,x,L,dt,gt)&&Y("")},children:"保存"}),c.jsx("button",{onClick:()=>Y(""),children:"取消"})]})]}):null]},D.id)})}),i.autoDetailMsg&&!i.autoDetail?c.jsx("div",{className:"muted",children:i.autoDetailMsg}):null,i.autoDetail?c.jsx(Xg,{detail:i.autoDetail},i.autoDetail.id):null]})}function Vg(){var y,C,M,S;const i=Lt(),r=i.triage,h=_=>{if(_){i.showDetail(_);try{window.location.hash="#view-tasks"}catch{}}},f=_=>{if(_){i.loadAutomationDetail(_);try{window.location.hash="#view-automation"}catch{}}};return i.triageError?c.jsx("div",{className:"muted",children:i.triageError}):r?c.jsxs(c.Fragment,{children:[c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["到期自动化(",((y=r.counts)==null?void 0:y.automations_due)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.automations_due||[]).map((_,O)=>c.jsx("span",{className:"row-main",children:_.id?c.jsxs("button",{onClick:()=>f(_.id),children:[_.name,"|每 ",_.interval_seconds," 秒"]}):c.jsxs(c.Fragment,{children:[_.name,"|每 ",_.interval_seconds," 秒"]})},O))})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["失败/中断任务(",((C=r.counts)==null?void 0:C.failed_tasks)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.failed_tasks||[]).map((_,O)=>c.jsx("span",{className:"row-main",children:c.jsxs("button",{onClick:()=>h(_.id),children:[pe(_.id),"|",_.status,"|",String(_.prompt||"").slice(0,80)]})},O))})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["待处理后续消息(",((M=r.counts)==null?void 0:M.pending_followups)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.pending_followups||[]).map((_,O)=>c.jsx("span",{className:"row-main",children:c.jsxs("button",{onClick:()=>h(_.task_id),children:["任务 ",pe(_.task_id),"|",String(_.body||"").slice(0,80)]})},O))})]}),c.jsxs("div",{className:"data-group",children:[c.jsxs("b",{children:["最近自动化产出(",((S=r.counts)==null?void 0:S.recent_runs)||0,")"]}),c.jsx(ol,{empty:"(无)",items:(r.recent_runs||[]).map((_,O)=>c.jsxs("span",{className:"row-main",children:[pe(_.automation_id),"|",_.status,_.task_id?c.jsxs(c.Fragment,{children:["|任务 ",c.jsx("button",{onClick:()=>h(_.task_id),children:pe(_.task_id)})]}):"",_.task_status?"|"+_.task_status:""]},O))})]})]}):c.jsx("div",{className:"muted",children:"(先选项目)"})}function Zg(){return c.jsx(Ft,{id:"nav-automations",title:"自动化",children:c.jsx(Lg,{})})}function Kg(){const i=Lt();return c.jsxs(Ft,{id:"nav-triage",title:"分诊收件箱",children:[c.jsx(At,{children:c.jsx("button",{onClick:i.refreshTriage,children:"刷新分诊"})}),c.jsx(Vg,{})]})}const ho=["pending","in_progress","blocked","completed","cancelled"],wc={pending:"待办",in_progress:"进行中",blocked:"受阻",completed:"已完成",cancelled:"已取消"},Jg=["low","medium","high"],Zh={low:"低",medium:"中",high:"高"};function Fg(i){return i?new Date(i*1e3).toLocaleString():""}function Wg(){const i=Lt(),[r,h]=d.useState([]),[f,y]=d.useState({}),[C,M]=d.useState(0),[S,_]=d.useState(""),[O,Y]=d.useState(""),[x,H]=d.useState(""),[L,tt]=d.useState(""),[dt,J]=d.useState(""),[gt,_t]=d.useState(""),[Dt,Q]=d.useState("medium"),[ft,D]=d.useState(""),[X,et]=d.useState(""),xt=d.useCallback(async()=>{if(!i.projectId){h([]),y({}),M(0);return}try{let B=F.projectTodos(i.projectId);const nt=new URLSearchParams;x&&nt.set("status",x),L.trim()&&nt.set("q",L.trim());const rt=nt.toString();rt&&(B+="?"+rt);const G=await W(B);h(G.todos||[]),y(G.counts||{}),M(G.total||0),Y("")}catch(B){Y("待办加载失败:"+((B==null?void 0:B.message)||String(B)))}},[i.projectId,x,L]);d.useEffect(()=>{xt()},[xt]);const Mt=async()=>{if(!i.projectId){_("先选项目");return}if(!dt.trim()){_("标题必填");return}_("");try{const B={title:dt.trim(),description:gt,priority:Dt};ft.trim()&&(B.task_id=ft.trim()),X&&(B.due_at=Math.floor(new Date(X).getTime()/1e3)),await W(F.projectTodos(i.projectId),"POST",B),J(""),_t(""),D(""),et(""),_("已创建"),await xt()}catch(B){_("创建失败:"+((B==null?void 0:B.message)||String(B)))}},mt=async(B,nt)=>{if(i.projectId)try{await W(F.projectTodo(i.projectId,B.id),"PATCH",nt),await xt()}catch(rt){Y("待办更新失败:"+((rt==null?void 0:rt.message)||String(rt)))}},Nt=async B=>{if(i.projectId&&window.confirm("删除待办“"+B.title+"”?删除后不可恢复(取消请改状态为已取消)。"))try{await W(F.projectTodo(i.projectId,B.id),"DELETE"),await xt()}catch(nt){Y("待办删除失败:"+((nt==null?void 0:nt.message)||String(nt)))}};return c.jsxs(Ft,{id:"nav-todos",title:"项目待办",children:[c.jsx("div",{className:"muted",children:i.projectId?"共 "+C+" 项 | "+ho.map(B=>wc[B]+" "+(f[B]||0)).join(" | "):"先选项目"}),O?c.jsx("div",{className:"err",children:O}):null,c.jsxs(At,{children:[c.jsxs("select",{"aria-label":"状态筛选",value:x,onChange:B=>H(B.target.value),children:[c.jsx("option",{value:"",children:"状态:全部"}),ho.map(B=>c.jsx("option",{value:B,children:wc[B]},B))]}),c.jsx("input",{type:"text",placeholder:"搜索标题/说明",value:L,onChange:B=>tt(B.target.value)}),c.jsx("button",{onClick:()=>void xt(),children:"刷新"})]}),c.jsx("input",{type:"text",placeholder:"待办标题(必填)",style:{width:"100%"},value:dt,onChange:B=>J(B.target.value)}),c.jsx("textarea",{placeholder:"说明(可选)",value:gt,onChange:B=>_t(B.target.value)}),c.jsxs(At,{children:[c.jsx("select",{"aria-label":"优先级",value:Dt,onChange:B=>Q(B.target.value),children:Jg.map(B=>c.jsxs("option",{value:B,children:["优先级:",Zh[B]]},B))}),c.jsx("input",{type:"text",placeholder:"关联任务 id(可选)",value:ft,onChange:B=>D(B.target.value)}),c.jsx("input",{type:"datetime-local","aria-label":"截止时间",value:X,onChange:B=>et(B.target.value)}),c.jsx("button",{className:"primary",onClick:()=>void Mt(),children:"新建待办"}),c.jsx("span",{className:"muted",children:S})]}),c.jsx(ol,{empty:i.projectId?"(暂无待办)":"(先选项目)",items:r.map(B=>c.jsxs("span",{className:"row-main",children:[c.jsx("b",{children:B.title})," | ",c.jsx("span",{className:"st-"+B.status,children:wc[B.status]||B.status})," | 优先级 "+(Zh[B.priority]||B.priority),B.task_id?" | 任务 "+pe(B.task_id):"",B.due_at?" | 截止 "+Fg(B.due_at):"",B.completed_at?" | 完成于 "+new Date(B.completed_at*1e3).toLocaleString():"",B.description?c.jsxs(c.Fragment,{children:[c.jsx("br",{}),c.jsx("span",{className:"muted",children:B.description})]}):null,c.jsx("br",{}),c.jsx("select",{"aria-label":"改状态",value:B.status,onChange:nt=>void mt(B,{status:nt.target.value}),children:ho.map(nt=>c.jsx("option",{value:nt,children:wc[nt]},nt))})," ",c.jsx("button",{onClick:()=>void mt(B,{status:"completed"}),children:"完成"})," ",c.jsx("button",{onClick:()=>void Nt(B),children:"删除"})]},B.id))}),c.jsx("div",{className:"muted",children:"状态与 Muse 待办同款五态;完成时服务端记完成时间,改回其它状态时清掉;已取消是软关闭,删除才是硬删除。"})]})}const So=[["tasks","任务工作台","任务"],["todos","项目待办","任务"],["resources","环境与 Workers","管理"],["automation","技能与自动化","管理"],["admin","系统与 Key","管理"]],$g=Object.fromEntries(So.map(([i,r])=>[i,r]));function Kh(){const i=window.location.hash.replace("#view-","");return So.some(([r])=>r===i)?i:"tasks"}function Ig({view:i,setView:r}){return c.jsxs("aside",{className:"shell-side",children:[c.jsx("div",{className:"shell-brand",children:"Codex 命令台"}),c.jsx("nav",{className:"shell-nav","aria-label":"导航",children:["任务","管理"].map(h=>c.jsxs("div",{className:"shell-nav-group",children:[c.jsx("div",{className:"shell-nav-group-label",children:h}),So.filter(([,,f])=>f===h).map(([f,y])=>c.jsx("button",{type:"button",className:i===f?"active":"",onClick:()=>r(f),children:y},f))]},h))}),c.jsx("div",{id:"nav-projects",className:"shell-project-nav",children:c.jsx(wg,{})}),c.jsxs("div",{className:"shell-stats",children:[c.jsx("div",{className:"shell-stats-label",children:"概览"}),c.jsx(og,{})]})]})}function Pg(){const i=Lt();return c.jsx("span",{className:i.keySaved?"dash-status ok":"dash-status off",children:i.keySaved?"● 已连接":"○ 未连接"})}function tp(){const i=rg();ig(S=>i.refreshAll(S),1e4),cg(()=>i.refreshSlow(),6e4);const[r,h]=d.useState(()=>Kh()),[f,y]=d.useState(!0),C=S=>{h(S);try{window.history.replaceState(null,"","#view-"+S)}catch{}};d.useEffect(()=>{const S=()=>h(Kh());return window.addEventListener("hashchange",S),()=>window.removeEventListener("hashchange",S)},[]);const M=()=>{C("tasks"),y(!0),setTimeout(()=>{var S;return(S=document.getElementById("sec-dispatch"))==null?void 0:S.scrollIntoView({behavior:"smooth"})},0)};return c.jsx(Fh.Provider,{value:i,children:c.jsxs("div",{className:"shell",children:[c.jsx(Ig,{view:r,setView:C}),c.jsxs("div",{className:"shell-main",children:[c.jsxs("header",{className:"shell-header",children:[c.jsx("h1",{children:$g[r]}),c.jsx(Pg,{}),c.jsx("span",{className:"muted shell-hide-sm",children:i.healthText}),c.jsx("span",{className:"muted shell-hide-sm",children:i.quotaText}),c.jsxs("span",{className:"muted shell-stamp",children:["自动刷新 · 每 10 秒 · 已刷新 ",i.refreshStamp]})]}),r==="tasks"?c.jsxs("div",{className:"workspace",children:[c.jsxs("section",{className:"workspace-rail","aria-label":"任务列表",children:[c.jsx("h2",{children:"任务列表"}),c.jsx(mg,{onNew:M})]}),c.jsxs("section",{className:"workspace-center",children:[c.jsx("div",{className:"composer-toggle",children:c.jsx("button",{type:"button",onClick:()=>y(!f),children:f?"收起新建任务":"新建任务"})}),f?c.jsx("section",{id:"sec-dispatch",children:c.jsx(fg,{})}):null,c.jsxs("section",{className:"card workspace-conv",children:[c.jsx("h2",{children:"对话"}),c.jsx(Sg,{})]})]}),c.jsx("section",{className:"workspace-inspector","aria-label":"任务详情",children:c.jsx(zg,{})})]}):c.jsx("main",{className:"page",children:r==="todos"?c.jsx(c.Fragment,{children:c.jsx(Wg,{})}):r==="resources"?c.jsxs(c.Fragment,{children:[c.jsx(kg,{}),c.jsx(Hg,{}),c.jsx(Bg,{}),c.jsx(qg,{})]}):r==="automation"?c.jsxs(c.Fragment,{children:[c.jsx(Yg,{}),c.jsx(Gg,{}),c.jsx(Zg,{}),c.jsx(Kg,{})]}):c.jsxs(c.Fragment,{children:[c.jsx(Dg,{}),c.jsx(Rg,{})]})})]})]})})}Jy.createRoot(document.getElementById("root")).render(c.jsx(Yy.StrictMode,{children:c.jsx(tp,{})})); +