console: 项目导航移到左侧栏,工作台不再承担项目模块

左侧栏 ProjectNav:项目完整列表(名 + 短 id + 我的角色,
选中高亮,点击切换)+ 创建项目(名/GitHub 仓库/repo_url)
+ 选中项目成员(邀请/改角色/新增移除);工作台删除
sec-projects 一节与任务列表里的项目下拉框,侧栏为唯一
项目选择器,自动选第一个项目逻辑不变。
薄模块补齐(均为已核实后端路由):成员移除 DELETE、
自动化详情(GET 单条 + 产出)与编辑 PATCH、分诊条目点进
任务/自动化详情、Workers 搜索与在线筛选、管理区设置配额
POST。主线新功能(Todos、Browser、taskMeta 等)原样保留,
未做任何回退。console 标记单测本地 53 项通过。
This commit is contained in:
host committed 2026-10-30 07:43:54 +00:00
1 parent d6369154c2
commit 08484302ac
16 files changed
+293 -100

No files matched your search

+30 -1
View File
@@ -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,
};
}
@@ -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) => {
@@ -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<Automation[]>([]);
const [autoMsg, setAutoMsg] = useState("");
const [autoDetail, setAutoDetail] = useState<AutomationDetail | null>(null);
const [autoDetailMsg, setAutoDetailMsg] = useState("");
const [triage, setTriage] = useState<Triage | null>(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<AutomationDetail>(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 };
}