From 1b53bc572be461f5150d32e84612553a8884882e Mon Sep 17 00:00:00 2001 From: zhengchuyi Date: Thu, 16 Jul 2026 14:46:12 +0800 Subject: [PATCH 1/3] feat: add secure generated agent debug run --- frontend/src/App.tsx | 1 + frontend/src/adk/client.ts | 81 +++ frontend/src/create/CustomCreate.css | 365 ++++++++++- frontend/src/create/CustomCreate.tsx | 447 ++++++++++++- veadk/cli/cli_frontend.py | 378 ++++++++++- veadk/cli/generated_agent_test_runner.py | 43 ++ veadk/webui/assets/index-DA5jSmfq.css | 10 + veadk/webui/assets/index-ee5kzfk6.js | 762 +++++++++++++++++++++++ veadk/webui/index.html | 4 +- 9 files changed, 2065 insertions(+), 26 deletions(-) create mode 100644 veadk/cli/generated_agent_test_runner.py create mode 100644 veadk/webui/assets/index-DA5jSmfq.css create mode 100644 veadk/webui/assets/index-ee5kzfk6.js diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6d544aa5..8bea11dc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1167,6 +1167,7 @@ export default function App() { onCreate={onCreate} onAgentAdded={onAgentAdded} author={String(userInfo?.email ?? userId ?? "")} + features={features} /> ) : createView === "template" ? ( setCreateView("menu")} onCreate={onCreate} /> diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index 24a016c5..f2ee4ee5 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -469,6 +469,7 @@ export interface UiFeatures { addAgent: boolean; manageAgents: boolean; addAgentkit: boolean; + generatedAgentTestRun?: boolean; } export interface UiConfig { @@ -490,6 +491,7 @@ const DEFAULT_UI_CONFIG: UiConfig = { addAgent: true, manageAgents: true, addAgentkit: true, + generatedAgentTestRun: true, }, defaultView: "chat", agentsSource: "local", @@ -627,3 +629,82 @@ export async function getRuntimeDetail( } return res.json(); } + +export interface GeneratedAgentTestRun { + runId: string; + appName: string; + expiresAt: number; +} + +export async function createGeneratedAgentTestRun( + name: string, + files: { path: string; content: string }[], +): Promise { + const res = await apiFetch("/web/generated-agent-test-runs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, files }), + }); + if (!res.ok) { + const t = await res.text().catch(() => ""); + throw new Error(t || `创建调试运行失败 (${res.status})`); + } + return res.json(); +} + +export async function createGeneratedAgentTestSession( + runId: string, + userId: string, +): Promise { + const res = await apiFetch(`/web/generated-agent-test-runs/${runId}/sessions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId }), + }); + if (!res.ok) { + const t = await res.text().catch(() => ""); + throw new Error(t || `创建调试会话失败 (${res.status})`); + } + const session = await res.json(); + return session.id; +} + +export async function* runGeneratedAgentTestSSE({ + runId, + userId, + sessionId, + text, + signal, +}: { + runId: string; + userId: string; + sessionId: string; + text: string; + signal?: AbortSignal; +}): AsyncGenerator { + const parts: Record[] = text.trim() ? [{ text }] : []; + const res = await apiFetch(`/web/generated-agent-test-runs/${runId}/run_sse`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + user_id: userId, + session_id: sessionId, + new_message: { role: "user", parts }, + streaming: true, + }), + signal, + }); + if (!res.ok) throw new Error(`调试运行失败: ${res.status}`); + for await (const evt of parseSSE(res)) { + yield evt as AdkEvent; + } +} + +export async function deleteGeneratedAgentTestRun(runId: string): Promise { + const res = await apiFetch(`/web/generated-agent-test-runs/${runId}`, { + method: "DELETE", + }); + if (!res.ok && res.status !== 404) { + throw new Error(`清理调试运行失败 (${res.status})`); + } +} diff --git a/frontend/src/create/CustomCreate.css b/frontend/src/create/CustomCreate.css index 04af966b..120ca65b 100644 --- a/frontend/src/create/CustomCreate.css +++ b/frontend/src/create/CustomCreate.css @@ -22,7 +22,7 @@ /* Left: the Agent structure tree. */ .cw-tree { flex-shrink: 0; - width: 320px; + width: 280px; overflow-y: auto; padding: 16px 14px; border-right: 1px solid hsl(var(--border)); @@ -150,7 +150,7 @@ flex-shrink: 0; background: hsl(var(--panel)); border-bottom: 1px solid hsl(var(--border)); - padding: 20px 44px 14px 16px; + padding: 18px 16px 12px; } .cw-typebar-inner { max-width: 1080px; @@ -172,7 +172,7 @@ } .cw-lower { display: flex; - gap: 36px; + gap: 24px; align-items: flex-start; } .cw-detail .cw-form-col { @@ -181,6 +181,260 @@ margin: 0; } +/* Right: generated-agent debug panel. */ +.cw-debug { + flex-shrink: 0; + width: 300px; + min-height: 0; + display: flex; + flex-direction: column; + border-left: 1px solid hsl(var(--border)); + background: hsl(var(--panel)); +} +.cw-debug-head { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 16px 16px 10px; +} +.cw-debug-title { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 14px; + font-weight: 650; + color: hsl(var(--foreground)); +} +.cw-debug-badge { + flex-shrink: 0; + padding: 3px 8px; + border-radius: 999px; + background: hsl(var(--secondary)); + color: hsl(var(--secondary-foreground)); + font-size: 11px; + font-weight: 600; +} +.cw-debug-badge-ready { + background: hsl(var(--primary) / 0.1); + color: hsl(var(--foreground)); +} +.cw-debug-badge-error { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} +.cw-debug-badge-stale { + background: hsl(38 92% 50% / 0.14); + color: hsl(32 95% 38%); +} +.cw-debug-sub { + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 3px; + padding: 0 16px 14px; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.45; +} +.cw-debug-body { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 10px 16px 18px; +} +.cw-debug-empty { + min-height: 120px; + display: flex; + align-items: center; + justify-content: center; + padding: 14px; + border: 1px dashed hsl(var(--border)); + border-radius: 10px; + color: hsl(var(--muted-foreground)); + font-size: 13px; + line-height: 1.6; + text-align: center; +} +.cw-debug-progress { + display: flex; + flex-direction: column; + gap: 8px; +} +.cw-debug-logline { + display: flex; + align-items: center; + gap: 8px; + min-height: 28px; + padding: 7px 9px; + border-radius: 9px; + background: hsl(var(--foreground) / 0.04); + color: hsl(var(--muted-foreground)); + font-size: 12.5px; +} +.cw-debug-logline .cw-i { + color: hsl(var(--foreground)); +} +.cw-debug-error { + display: flex; + flex-direction: column; + gap: 12px; + color: hsl(var(--destructive)); + font-size: 13px; + line-height: 1.5; +} +.cw-debug-stale { + flex-shrink: 0; + display: flex; + align-items: flex-start; + gap: 10px; + margin: 0 14px 12px; + padding: 11px 12px; + border: 1px solid hsl(38 92% 50% / 0.26); + border-radius: 10px; + background: hsl(38 92% 50% / 0.08); +} +.cw-debug-stale-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; + color: hsl(var(--foreground)); + font-size: 12.5px; + line-height: 1.45; +} +.cw-debug-stale-text span { + color: hsl(var(--muted-foreground)); +} +.cw-debug-stale-btn { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 30px; + padding: 6px 10px; + border: none; + border-radius: 8px; + background: hsl(var(--foreground)); + color: hsl(var(--background)); + font: inherit; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.12s, transform 0.1s; +} +.cw-debug-stale-btn:hover:not(:disabled) { + opacity: 0.86; +} +.cw-debug-stale-btn:active:not(:disabled) { + transform: scale(0.96); +} +.cw-debug-stale-btn:disabled { + opacity: 0.45; + cursor: default; +} +.cw-debug-chat { + display: flex; + flex-direction: column; + gap: 18px; +} +.cw-debug-msg { + display: flex; + flex-direction: column; + gap: 6px; + max-width: 100%; +} +.cw-debug-msg-user { + align-items: flex-end; +} +.cw-debug-msg-assistant { + align-items: flex-start; +} +.cw-debug-role { + display: none; +} +.cw-debug-content { + max-width: 100%; + color: hsl(var(--foreground)); + font-size: 14px; + line-height: 1.65; + word-break: break-word; +} +.cw-debug-msg-user .cw-debug-content { + max-width: 88%; + padding: 9px 14px; + border-radius: 18px; + background: hsl(var(--secondary)); +} +.cw-debug-msg-assistant .cw-debug-content { + width: 100%; +} +.cw-debug-msg-error { + color: hsl(var(--destructive)); +} +.cw-debug-composer { + flex-shrink: 0; + padding: 10px 14px 14px; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--panel)); +} +.cw-debug-composerbox { + display: flex; + align-items: flex-end; + gap: 6px; + padding: 6px 6px 6px 10px; + border: 1px solid hsl(var(--border)); + border-radius: 24px; + background: hsl(var(--background)); +} +.cw-debug-input { + flex: 1; + min-width: 0; + max-height: 120px; + padding: 8px 4px; + border: none; + outline: none; + resize: none; + overflow-y: auto; + background: transparent; + color: hsl(var(--foreground)); + font: inherit; + font-size: 14px; + line-height: 1.5; +} +.cw-debug-input::placeholder { + color: hsl(var(--muted-foreground)); +} +.cw-debug-send { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + cursor: pointer; + transition: opacity 0.15s, transform 0.1s, background 0.12s, color 0.12s; +} +.cw-debug-send { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} +.cw-debug-send:hover:not(:disabled) { + opacity: 0.85; +} +.cw-debug-send:active:not(:disabled) { + transform: scale(0.94); +} +.cw-debug-send:disabled { + opacity: 0.3; + cursor: default; +} + /* ---------- bottom action bar (validation + generate) ---------- */ .cw-footer-bar { flex-shrink: 0; @@ -391,7 +645,7 @@ top: 0; align-self: flex-start; flex-shrink: 0; - width: 208px; + width: 176px; padding: 4px 0; } .cw-steps { @@ -1078,17 +1332,32 @@ flex-direction: column; gap: 8px; } -/* Horizontal variant for the top bar: compact chips (icon + title) in a row. */ +/* Horizontal variant for the top bar: main-style compact chips. */ .cw-typeradio--row { flex-direction: row; - flex-wrap: wrap; + flex-wrap: nowrap; + align-items: center; gap: 8px; + overflow-x: hidden; + width: 100%; + max-width: 100%; + padding: 1px 0; + border: none; + background: transparent; + scrollbar-width: thin; } .cw-typeradio--row .cw-typeradio-item { flex: 1 1 0; min-width: 0; + min-height: 44px; align-items: center; - padding: 10px 12px; + justify-content: center; + gap: 0; + padding: 10px 10px; + border: 1px solid hsl(var(--border)); + border-radius: 10px; + background: hsl(var(--background)); + color: hsl(var(--muted-foreground)); } .cw-typeradio-item { display: flex; @@ -1107,9 +1376,23 @@ border-color: hsl(var(--foreground) / 0.2); background: hsl(var(--foreground) / 0.03); } +.cw-typeradio--row .cw-typeradio-item:hover { + border-color: hsl(var(--foreground) / 0.18); + background: hsl(var(--foreground) / 0.03); + color: hsl(var(--foreground)); +} .cw-typeradio-item.is-on { - border-color: hsl(var(--primary)); - background: hsl(var(--primary) / 0.05); + border-color: transparent; + background: hsl(var(--background)); + box-shadow: + 0 0 0 1px hsl(var(--foreground) / 0.12), + 0 1px 2px hsl(var(--foreground) / 0.08); +} +.cw-typeradio--row .cw-typeradio-item.is-on { + border-color: hsl(var(--foreground) / 0.26); + background: hsl(var(--foreground) / 0.055); + color: hsl(var(--foreground)); + box-shadow: none; } /* Visually-hidden native input keeps keyboard/a11y behavior. */ .cw-typeradio-input { @@ -1127,6 +1410,9 @@ border: 2px solid hsl(var(--border)); transition: border-color 0.12s; } +.cw-typeradio--row .cw-typeradio-dot { + display: none; +} .cw-typeradio-item.is-on .cw-typeradio-dot { border-color: hsl(var(--primary)); background: radial-gradient( @@ -1144,25 +1430,55 @@ flex-direction: column; gap: 5px; } +.cw-typeradio--row .cw-typeradio-main { + flex: 0 1 auto; + min-width: 0; + max-width: 100%; +} .cw-typeradio-head { display: flex; align-items: center; gap: 7px; } +.cw-typeradio--row .cw-typeradio-head { + flex-wrap: nowrap; + justify-content: center; + min-width: 0; + max-width: 100%; + gap: 6px; +} .cw-typeradio-icon { flex-shrink: 0; width: 16px; height: 16px; color: hsl(var(--muted-foreground)); } +.cw-typeradio--row .cw-typeradio-icon { + width: 16px; + height: 16px; + color: currentColor; +} .cw-typeradio-item.is-on .cw-typeradio-icon { color: hsl(var(--foreground)); } +.cw-typeradio--row .cw-typeradio-item.is-on .cw-typeradio-icon { + color: currentColor; +} .cw-typeradio-title { font-size: 13.5px; font-weight: 600; color: hsl(var(--foreground)); } +.cw-typeradio--row .cw-typeradio-title { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: inherit; + font-size: 13.5px; + font-weight: 650; + line-height: 1.25; +} .cw-typeradio-desc { font-size: 11.5px; line-height: 1.5; @@ -1562,6 +1878,37 @@ } /* ---------- responsive ---------- */ +@media (max-width: 1280px) { + .cw-debug { + width: 280px; + } + .cw-tree { + width: 220px; + } + .cw-rail { + width: 160px; + } +} + +@media (max-width: 1080px) { + .cw-editor { + flex-wrap: wrap; + overflow-y: auto; + } + .cw-tree { + height: 260px; + } + .cw-detail { + min-height: 560px; + } + .cw-debug { + width: 100%; + min-height: 360px; + border-left: none; + border-top: 1px solid hsl(var(--border)); + } +} + @media (max-width: 860px) { /* Drop the rail; the form is the single source of navigation by scrolling. */ .cw-rail { diff --git a/frontend/src/create/CustomCreate.tsx b/frontend/src/create/CustomCreate.tsx index 0d0a4ec2..5bf20ea0 100644 --- a/frontend/src/create/CustomCreate.tsx +++ b/frontend/src/create/CustomCreate.tsx @@ -3,6 +3,7 @@ import { AnimatePresence, motion } from "motion/react"; import { ArrowLeft, Bot, + Bug, Boxes, Check, Cloud, @@ -21,6 +22,8 @@ import { Plus, Repeat, Rocket, + Search, + Send, Shapes, Sparkles, Split, @@ -53,8 +56,16 @@ import { SkillHubPicker } from "./SkillHubPicker"; import { LocalPicker } from "./LocalPicker"; import { SkillSpacePicker } from "./SkillSpacePicker"; import { ProjectPreview } from "../ui/ProjectPreview"; -import { deployAgentkitProject } from "../adk/client"; -import type { DeployStage } from "../adk/client"; +import { Blocks } from "../ui/Blocks"; +import { + createGeneratedAgentTestRun, + createGeneratedAgentTestSession, + deleteGeneratedAgentTestRun, + deployAgentkitProject, + runGeneratedAgentTestSSE, +} from "../adk/client"; +import type { DeployStage, GeneratedAgentTestRun, UiFeatures } from "../adk/client"; +import { applyEvent, emptyAcc, type Block } from "../blocks"; import "./CustomCreate.css"; /** Trigger a browser download of a text file. */ @@ -815,6 +826,229 @@ function TreeNode({ ); } +type DebugPhase = "idle" | "building" | "starting" | "ready" | "sending" | "error"; + +interface DebugMessage { + role: "user" | "assistant"; + content: string; + blocks?: Block[]; + error?: string; +} + +function fmtDebugExpiry(expiresAt?: number): string { + if (!expiresAt) return ""; + const ms = expiresAt * 1000 - Date.now(); + if (ms <= 0) return "即将清理"; + const minutes = Math.floor(ms / 60000); + const seconds = Math.floor((ms % 60000) / 1000); + return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")} 后自动清理`; +} + +function debugSnapshotKey(draft: AgentDraft): string { + return JSON.stringify(draft); +} + +function DebugPanel({ + enabled, + phase, + stale, + run, + projectName, + logs, + messages, + input, + error, + onInput, + onSend, + onRestart, +}: { + enabled: boolean; + phase: DebugPhase; + stale: boolean; + run: GeneratedAgentTestRun | null; + projectName: string; + logs: string[]; + messages: DebugMessage[]; + input: string; + error: string | null; + onInput: (v: string) => void; + onSend: () => void; + onRestart: () => void; +}) { + const [, refreshExpiry] = useState(0); + const ready = phase === "ready" || phase === "sending"; + const busy = phase === "building" || phase === "starting" || phase === "sending"; + const badgeState = stale ? "stale" : phase; + const statusText = stale + ? "配置已变更" + : !enabled + ? "未开启" + : phase === "idle" + ? "等待调试" + : phase === "building" + ? "生成代码中" + : phase === "starting" + ? "启动环境中" + : phase === "sending" + ? "运行中" + : phase === "error" + ? "调试失败" + : "可对话"; + + useEffect(() => { + if (!run) return; + const timer = window.setInterval(() => refreshExpiry((v) => v + 1), 1000); + return () => window.clearInterval(timer); + }, [run]); + + return ( +