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

103 lines
5.1 KiB
TypeScript

import { useCallback, useState } from "react";
import { api } from "../api/client";
import { paths } from "../api/paths";
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("");
const refreshAutomations = useCallback(async (pid?: string) => {
const id = pid !== undefined ? pid : getProjectId();
if (!id) { setAutomations([]); return; }
try {
const d = await api<{ automations: Automation[] }>(paths.projectAutomations(id));
setAutomations(d.automations || []);
} catch (e: any) { setAutoMsg("自动化加载失败:" + e.message); }
}, [getProjectId]);
const createAutomation = useCallback(async (name: string, prompt: string, interval: string, repo: string) => {
const pid = getProjectId();
if (!pid) { setAutoMsg("先选项目"); return; }
if (!name.trim()) { setAutoMsg("自动化名称必填"); return; }
if (!prompt.trim()) { setAutoMsg("定时任务内容必填"); return; }
const body: { name: string; prompt: string; interval_seconds?: number; repo_url?: string } = { name: name.trim(), prompt: prompt.trim() };
if (interval.trim()) body.interval_seconds = parseInt(interval.trim(), 10);
if (repo.trim()) body.repo_url = repo.trim();
try {
await api(paths.projectAutomations(pid), "POST", body);
setAutoMsg("自动化已创建(登记定义,到期后由执行器认领)");
await refreshAutomations(pid);
} catch (e: any) { setAutoMsg("创建失败:" + e.message); }
}, [getProjectId, refreshAutomations]);
const updateAutomation = useCallback(async (a: Automation, name: string, prompt: string, interval: string, repo: string) => {
const pid = getProjectId();
if (!pid) { setAutoMsg("先选项目"); return false; }
const n = name.trim();
const p = prompt.trim();
if (!n) { setAutoMsg("自动化名称必填"); return false; }
if (!p) { setAutoMsg("定时任务内容必填"); return false; }
const body: { name?: string; prompt?: string; interval_seconds?: number; repo_url?: string | null } = {};
if (n !== (a.name || "")) body.name = n;
if (p !== (a.prompt || "")) body.prompt = p;
if (interval.trim()) {
const v = parseInt(interval.trim(), 10);
if (Number.isNaN(v)) { setAutoMsg("间隔秒数必须是数字"); return false; }
if (v < 60) { setAutoMsg("间隔秒数最小 60"); return false; }
if (v !== a.interval_seconds) body.interval_seconds = v;
}
const r = repo.trim();
if (r !== (a.repo_url || "")) body.repo_url = r === "" ? null : r;
if (Object.keys(body).length === 0) { setAutoMsg("没有改动"); return false; }
try {
await api(paths.projectAutomation(pid, a.id), "PATCH", body);
setAutoMsg("自动化已更新");
await refreshAutomations(pid);
return true;
} catch (e: any) { setAutoMsg("更新失败:" + e.message); return false; }
}, [getProjectId, refreshAutomations]);
const cancelAutomation = useCallback((id: string) => {
const pid = getProjectId();
if (!pid) return;
void api(paths.projectAutomationCancel(pid, id), "POST", {})
.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; }
try {
const d = await api<Triage>(paths.projectTriage(id));
setTriage(d);
setTriageError("");
} catch (e: any) { setTriageError("分诊加载失败:" + e.message); }
}, [getProjectId]);
return { automations, autoMsg, autoDetail, autoDetailMsg, triage, triageError, refreshAutomations, createAutomation, updateAutomation, cancelAutomation, loadAutomationDetail, closeAutomationDetail, refreshTriage };
}