From 2d644f901b05e012ac12ccaced4fe865763fa12d Mon Sep 17 00:00:00 2001 From: host Date: Fri, 30 Oct 2026 15:22:41 +0800 Subject: [PATCH] feat(console): automation panel edit and richer list - AutomationPanel rows now show title, prompt content (160-char preview with expand/collapse), repo_url, last/next run, run_once badge and cancelled time. - Active automations get an inline edit form (name/prompt/interval/repo) that PATCHes only changed fields via the existing automation_update endpoint; backend semantics unchanged (no last_run_at reset, cancelled stays terminal). - Rebuilt cloud/console.html with tsc + vite (build green); automation python tests 18/18 OK. --- cloud/console-react/src/api/paths.ts | 1 + cloud/console-react/src/api/types.ts | 2 +- cloud/console-react/src/context.ts | 2 +- .../src/hooks/useConsoleStore.ts | 2 +- .../src/hooks/useProjectTools.ts | 29 +++++++- .../sections/detail/ProjectToolsPanels.tsx | 66 +++++++++++++++++-- cloud/console.html | 50 +++++++------- 7 files changed, 117 insertions(+), 35 deletions(-) diff --git a/cloud/console-react/src/api/paths.ts b/cloud/console-react/src/api/paths.ts index e66cba5..232515d 100644 --- a/cloud/console-react/src/api/paths.ts +++ b/cloud/console-react/src/api/paths.ts @@ -19,6 +19,7 @@ export const paths = { projectMcpReload: (pid: string) => "/projects/" + enc(pid) + "/mcp-servers/reload", projectMcpOauth: (pid: string, name: string) => "/projects/" + enc(pid) + "/mcp-servers/" + enc(name) + "/oauth-login", projectAutomations: (pid: string) => "/projects/" + enc(pid) + "/automations", + projectAutomation: (pid: string, id: string) => "/projects/" + enc(pid) + "/automations/" + enc(id), projectAutomationCancel: (pid: string, id: string) => "/projects/" + enc(pid) + "/automations/" + enc(id) + "/cancel", projectTriage: (pid: string) => "/projects/" + enc(pid) + "/triage", projectTodos: (pid: string) => "/projects/" + enc(pid) + "/todos", diff --git a/cloud/console-react/src/api/types.ts b/cloud/console-react/src/api/types.ts index d8dc130..44228fc 100644 --- a/cloud/console-react/src/api/types.ts +++ b/cloud/console-react/src/api/types.ts @@ -36,7 +36,7 @@ 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; status?: string; due?: boolean; interval_seconds?: number; next_run_at?: number } +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 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 } diff --git a/cloud/console-react/src/context.ts b/cloud/console-react/src/context.ts index 58a41a2..5118256 100644 --- a/cloud/console-react/src/context.ts +++ b/cloud/console-react/src/context.ts @@ -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; cancelAutomation: (id: string) => void; + 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; 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/useConsoleStore.ts b/cloud/console-react/src/hooks/useConsoleStore.ts index bb6de92..3a1efef 100644 --- a/cloud/console-react/src/hooks/useConsoleStore.ts +++ b/cloud/console-react/src/hooks/useConsoleStore.ts @@ -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, cancelAutomation: tools.cancelAutomation, + automations: tools.automations, autoMsg: tools.autoMsg, refreshAutomations: () => void tools.refreshAutomations(), createAutomation: tools.createAutomation, updateAutomation: tools.updateAutomation, cancelAutomation: tools.cancelAutomation, 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 e8c5cdf..6d2a3e5 100644 --- a/cloud/console-react/src/hooks/useProjectTools.ts +++ b/cloud/console-react/src/hooks/useProjectTools.ts @@ -35,6 +35,33 @@ export function useProjectTools(getProjectId: () => string) { } 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; @@ -53,5 +80,5 @@ export function useProjectTools(getProjectId: () => string) { } catch (e: any) { setTriageError("分诊加载失败:" + e.message); } }, [getProjectId]); - return { automations, autoMsg, triage, triageError, refreshAutomations, createAutomation, cancelAutomation, refreshTriage }; + return { automations, autoMsg, triage, triageError, refreshAutomations, createAutomation, updateAutomation, cancelAutomation, refreshTriage }; } diff --git a/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx b/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx index 53fe2bf..04eb972 100644 --- a/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx +++ b/cloud/console-react/src/sections/detail/ProjectToolsPanels.tsx @@ -2,6 +2,17 @@ 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"; + +const PROMPT_PREVIEW = 160; + +function humanInterval(s?: number): string { + if (!s) return ""; + if (s % 86400 === 0) return "(每天)"; + if (s % 3600 === 0) return "(每小时)"; + if (s % 60 === 0) return "(每 " + s / 60 + " 分钟)"; + return ""; +} export function AutomationPanel() { const ctx = useConsole(); @@ -9,6 +20,19 @@ export function AutomationPanel() { const [prompt, setPrompt] = useState(""); const [interval, setInterval] = useState(""); const [repo, setRepo] = useState(""); + const [editingId, setEditingId] = useState(""); + const [editName, setEditName] = useState(""); + const [editPrompt, setEditPrompt] = useState(""); + const [editInterval, setEditInterval] = useState(""); + const [editRepo, setEditRepo] = useState(""); + const [expanded, setExpanded] = useState>({}); + const startEdit = (a: Automation) => { + setEditingId(a.id); + setEditName(a.name || ""); + setEditPrompt(a.prompt || ""); + setEditInterval(a.interval_seconds ? String(a.interval_seconds) : ""); + setEditRepo(a.repo_url || ""); + }; return ( <> setName(e.target.value)} /> @@ -20,12 +44,42 @@ export function AutomationPanel() { {ctx.autoMsg} - ( - - [{a.status}{a.due ? "|到期" : ""}] {a.name}|每 {a.interval_seconds} 秒|下次 {fmt(a.next_run_at)} - {a.status === "active" ? <> : null} - - ))} /> + { + const full = a.prompt || ""; + const showFull = !!expanded[a.id]; + const tooLong = full.length > PROMPT_PREVIEW; + const shown = !full ? "(无内容)" : (showFull || !tooLong ? full : full.slice(0, PROMPT_PREVIEW) + "…"); + return ( +
+
+ [{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} + {a.status === "cancelled" && a.cancelled_at ?
已取消于 {fmt(a.cancelled_at)}
: null} +
+ {shown} + {tooLong ? : null} +
+ {editingId === a.id ? ( +
+ setEditName(e.target.value)} /> +