diff --git a/docs/content/docs/framework/troubleshooting.en.mdx b/docs/content/docs/framework/troubleshooting.en.mdx index 1a24d6a5..f7c37254 100644 --- a/docs/content/docs/framework/troubleshooting.en.mdx +++ b/docs/content/docs/framework/troubleshooting.en.mdx @@ -31,6 +31,17 @@ This page collects common issues you may hit when using VeADK, grouped by setup, ## Development + + Your local development environment may include the test dependencies but not the static-checking tools. Install them into the current project virtual environment: + + ```bash + uv pip install ruff pyright + uv run ruff --version + uv run pyright --version + ``` + + After installation, use `uv run ruff check ...` and `uv run pyright ...`. If checking historical files reports many existing issues, separate problems introduced by the current change from the repository's existing baseline so a feature branch does not mix in a large unrelated cleanup. + VeADK does not automatically create a Volcengine Ark platform API Key for you. Please create your API Key on the Volcengine Ark platform. For detailed steps, see the [documentation](https://www.volcengine.com/docs/82379/1361424), or go directly to the [console entry](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D). diff --git a/docs/content/docs/framework/troubleshooting.mdx b/docs/content/docs/framework/troubleshooting.mdx index 6b88419a..f9976684 100644 --- a/docs/content/docs/framework/troubleshooting.mdx +++ b/docs/content/docs/framework/troubleshooting.mdx @@ -31,6 +31,17 @@ title: "常见问题" ## 开发环节 + + 当前开发环境可能只安装了测试依赖,未安装静态检查工具。可将工具安装到当前项目虚拟环境: + + ```bash + uv pip install ruff pyright + uv run ruff --version + uv run pyright --version + ``` + + 安装后即可使用 `uv run ruff check ...` 和 `uv run pyright ...`。如果检查历史文件时出现大量既有问题,请优先区分当前改动引入的问题和仓库已有基线问题,避免在功能分支中混入大规模无关修复。 + VeADK 不会帮助您自动创建火山引擎方舟平台 API Key,请您在火山方舟平台创建您的 API Key。具体步骤可见[文档](https://www.volcengine.com/docs/82379/1361424),或直接进入[控制台入口](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D)。 diff --git a/frontend/src/adk/client.ts b/frontend/src/adk/client.ts index 2bc05451..eaad1c4f 100644 --- a/frontend/src/adk/client.ts +++ b/frontend/src/adk/client.ts @@ -1240,6 +1240,24 @@ export interface DeployAgentkitResult { }; } +export interface DeployBuildLogSnapshot { + source: "code-pipeline"; + status: "running" | "complete" | "error"; + text: string; + lineCount: number; + truncated: boolean; + omittedEarly?: boolean; + snapshotTruncated?: boolean; + updatedAt: number; + pipelineId?: string; + pipelineName?: string; + pipelineRunId?: string; + workspaceId?: string; + workspaceName?: string; + error?: string; + pendingMessage?: string; +} + /** One live progress frame streamed during a deployment. */ export interface DeployStage { level: "info" | "success" | "warning" | "error"; @@ -1247,6 +1265,7 @@ export interface DeployStage { message: string; pct?: number; runtimeName?: string; + buildLog?: DeployBuildLogSnapshot; } interface DeployFrame extends Partial { diff --git a/frontend/src/ui/AgentWorkspace.css b/frontend/src/ui/AgentWorkspace.css index 2431a870..c4d92ddb 100644 --- a/frontend/src/ui/AgentWorkspace.css +++ b/frontend/src/ui/AgentWorkspace.css @@ -1047,6 +1047,126 @@ color: hsl(var(--foreground) / 0.78); } +.aw-deploy-step-log { + min-width: 0; + margin-top: 10px; +} + +.aw-deploy-log { + min-width: 0; + border: 1px solid hsl(var(--border)); + border-radius: 10px; + background: hsl(var(--canvas)); + overflow: hidden; +} + +.aw-deploy-log header, +.aw-deploy-log header > div, +.aw-deploy-log-actions, +.aw-deploy-log-actions button { + display: flex; + align-items: center; +} + +.aw-deploy-log header { + min-width: 0; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--panel)); +} + +.aw-deploy-log.is-collapsed header { + border-bottom: 0; +} + +.aw-deploy-log header > div:first-child { + min-width: 0; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} + +.aw-deploy-log strong { + color: hsl(var(--foreground)); + font-size: 12.5px; + font-weight: 640; + line-height: 1.35; +} + +.aw-deploy-log span { + min-width: 0; + color: hsl(var(--muted-foreground)); + font-size: 11px; + line-height: 1.35; +} + +.aw-deploy-log-actions { + flex: 0 0 auto; + gap: 6px; +} + +.aw-deploy-log-actions button { + min-height: 28px; + gap: 5px; + padding: 0 8px; + border: 1px solid hsl(var(--border)); + border-radius: 6px; + background: hsl(var(--panel)); + color: hsl(var(--foreground)); + font-size: 11.5px; + font-weight: 560; + cursor: pointer; +} + +.aw-deploy-log-actions button:hover { + background: hsl(var(--muted)); +} + +.aw-deploy-log-actions button span { + color: inherit; + font-size: inherit; + line-height: inherit; +} + +.aw-deploy-log-actions button:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 1px; +} + +.aw-deploy-log-actions svg { + width: 13px; + height: 13px; + flex: 0 0 auto; +} + +.aw-deploy-log pre { + max-height: 260px; + min-width: 0; + margin: 0; + padding: 12px; + overflow: auto; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + color: hsl(var(--foreground) / 0.86); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 11.5px; + line-height: 1.55; +} + +.aw-deploy-log-empty { + padding: 12px; + color: hsl(var(--muted-foreground)); + font-size: 12px; + line-height: 1.5; +} + +.aw-deploy-log.is-error { + border-color: hsl(6 32% 80%); +} + .aw-card-head { justify-content: space-between; gap: 12px; diff --git a/frontend/src/ui/AgentWorkspace.tsx b/frontend/src/ui/AgentWorkspace.tsx index 127d6727..9a8e8b4e 100644 --- a/frontend/src/ui/AgentWorkspace.tsx +++ b/frontend/src/ui/AgentWorkspace.tsx @@ -5,6 +5,7 @@ import { CircleAlert, CircleCheck, CircleX, + Copy, FlaskConical, Loader2, MessageCircle, @@ -279,6 +280,7 @@ const DEPLOYMENT_STEPS = [ { phase: "publish", label: "发布服务", description: "等待服务就绪并生成访问地址" }, { phase: "complete", label: "部署完成", description: "智能体已可以正常使用" }, ] as const; +const BUILD_STEP_INDEX = DEPLOYMENT_STEPS.findIndex((step) => step.phase === "build"); function deploymentStepIndex(task: DeploymentTaskUpdate): number { if (task.status === "success") return DEPLOYMENT_STEPS.length - 1; @@ -293,6 +295,121 @@ function deploymentStepIndex(task: DeploymentTaskUpdate): number { return index < 0 ? 0 : index; } +function formatBuildLogTime(updatedAt: number): string { + if (!updatedAt) return ""; + try { + return new Intl.DateTimeFormat("zh-CN", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).format(new Date(updatedAt)); + } catch { + return ""; + } +} + +function DeploymentBuildLog({ task }: { task: DeploymentTaskUpdate }) { + const log = task.buildLog; + const logTextRef = useRef(null); + const shouldAutoExpand = Boolean( + log?.status !== "complete" + && (task.status === "running" || task.status === "error") + && deploymentStepIndex(task) === BUILD_STEP_INDEX, + ); + const [expanded, setExpanded] = useState(shouldAutoExpand); + const [copied, setCopied] = useState(false); + const hasLogText = Boolean(log?.text || log?.error); + const text = log?.text || log?.error || ""; + const lines = text.split("\n"); + const visibleText = expanded ? text : lines.slice(-36).join("\n"); + const pendingMessage = log?.pendingMessage || "正在等待构建日志…"; + + useEffect(() => { + if (!log) return; + setExpanded(shouldAutoExpand); + }, [task.id, log?.status, shouldAutoExpand]); + + useEffect(() => { + if (!expanded || !hasLogText) return; + const node = logTextRef.current; + if (!node) return; + node.scrollTop = node.scrollHeight; + }, [expanded, hasLogText, visibleText]); + + if (!log || (!log.text && log.status !== "error" && !log.pendingMessage)) return null; + + const updatedAt = formatBuildLogTime(log.updatedAt); + const statusLabel = log.status === "complete" + ? "已同步" + : log.status === "error" + ? "读取失败" + : "同步中"; + const truncationLabel = log.omittedEarly + ? "已省略早期日志" + : log.snapshotTruncated + ? "仅显示最近的构建日志" + : log.truncated + ? "已省略部分日志" + : ""; + const meta = [ + statusLabel, + log.lineCount ? `${log.lineCount} 行` : "", + truncationLabel, + updatedAt, + ].filter(Boolean).join(" · "); + + async function copyLog() { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + window.setTimeout(() => setCopied(false), 1500); + } catch { + setCopied(false); + } + } + + return ( +
+
+
+ 构建日志 + {meta} +
+
+ {hasLogText && ( + + )} + {hasLogText && ( + + )} +
+
+ {expanded && ( + hasLogText + ?
{visibleText}
+ :
{pendingMessage}
+ )} +
+ ); +} + function DeploymentProgressCard({ task }: { task: DeploymentTaskUpdate }) { const currentIndex = deploymentStepIndex(task); const progress = task.status === "success" @@ -367,6 +484,11 @@ function DeploymentProgressCard({ task }: { task: DeploymentTaskUpdate }) {
{step.label}

{message || step.description}

+ {step.phase === "build" && task.buildLog && ( +
+ +
+ )}
); @@ -440,7 +562,6 @@ export function AgentWorkspace({ const [section, setSection] = useState("basic"); const [activeAgentId, setActiveAgentId] = useState(""); const [activeDraftId, setActiveDraftId] = useState(""); - const [activeDeploymentTaskId, setActiveDeploymentTaskId] = useState(""); const [runtimeDetail, setRuntimeDetail] = useState(null); const [detailAgentInfo, setDetailAgentInfo] = useState(null); const [detailAgentInfoResolved, setDetailAgentInfoResolved] = useState(false); @@ -559,9 +680,9 @@ export function AgentWorkspace({ const selectedAgent = agents.find((agent) => agent.id === activeAgentId); const selectedDraft = drafts.find((item) => item.id === activeDraftId); - const selectedPendingTask = deploymentTasks.find( - (task) => task.id === activeDeploymentTaskId, - ); + const selectedPendingTask = focusedDeploymentTaskId + ? deploymentTasks.find((task) => task.id === focusedDeploymentTaskId) + : undefined; const selectedAgentUpdateDraft = selectedAgent?.runtimeId ? updateDraftByRuntimeId.get(selectedAgent.runtimeId) : undefined; @@ -669,6 +790,17 @@ export function AgentWorkspace({ ) .sort((left, right) => right.startedAt - left.startedAt)[0]; }, [deploymentTasks, selectedAgent, selectedDraft, selectedPendingTask]); + const focusedDeploymentTaskActive = Boolean( + focusedDeploymentTaskId + && deploymentTask + && deploymentTask.id === focusedDeploymentTaskId, + ); + const shouldShowDeploymentTask = Boolean( + deploymentTask && ( + deploymentTask.status !== "success" + || focusedDeploymentTaskActive + ), + ); const draftFlowKey = useMemo(() => canvasDraftKey(draft), [draft]); const displayCurrentVersion = selectedAgent?.currentVersion ?? runtimeDetail?.currentVersion ?? null; @@ -689,7 +821,6 @@ export function AgentWorkspace({ ? agentByRuntimeId.get(focusedTask.runtimeId) : undefined; if (matchingAgent) { - setActiveDeploymentTaskId(""); setActiveDraftId(""); setActiveAgentId(matchingAgent.id); setSection("basic"); @@ -697,7 +828,6 @@ export function AgentWorkspace({ } setActiveAgentId(""); setActiveDraftId(""); - setActiveDeploymentTaskId(focusedDeploymentTaskId); setSection("basic"); }, [agentByRuntimeId, deploymentTasks, focusedDeploymentTaskId]); @@ -710,7 +840,6 @@ export function AgentWorkspace({ if (appliedFocusKeyRef.current === focusKey) return; if (!agents.some((agent) => agent.id === focusedAgentId)) return; appliedFocusKeyRef.current = focusKey; - setActiveDeploymentTaskId(""); setActiveDraftId(""); setActiveAgentId(focusedAgentId); setSection(focusedAgentSection); @@ -1326,7 +1455,6 @@ export function AgentWorkspace({ return; } setActiveAgentId(""); - setActiveDeploymentTaskId(""); setActiveDraftId(item.id); setSection("basic"); }} @@ -1458,7 +1586,6 @@ export function AgentWorkspace({ suppressAgentClickRef.current = false; return; } - setActiveDeploymentTaskId(""); setActiveDraftId(""); setActiveAgentId(agent.id); setSection("basic"); @@ -1578,7 +1705,7 @@ export function AgentWorkspace({ )} - {deploymentTask && (deploymentTask.status !== "success" || deploymentTask.message) && ( + {deploymentTask && shouldShowDeploymentTask && (
diff --git a/frontend/src/ui/ProjectPreview.tsx b/frontend/src/ui/ProjectPreview.tsx index 74a390b0..afcf9a98 100644 --- a/frontend/src/ui/ProjectPreview.tsx +++ b/frontend/src/ui/ProjectPreview.tsx @@ -66,11 +66,16 @@ import { runtimeEnvDisplayRows, runtimeEnvVars, } from "../create/deploymentEnv"; -import { RuntimeProbeError, type DeployStage } from "../adk/client"; +import { + RuntimeProbeError, + type DeployBuildLogSnapshot, + type DeployStage, +} from "../adk/client"; import feishuLogo from "../assets/feishu-logo.svg"; import { buildZip } from "./zip"; import { ProjectCodeBrowser } from "./CodeBrowserDialog"; import { DeploymentErrorMessage } from "./DeploymentErrorMessage"; +import { mergeDeployBuildLog } from "./deployBuildLog"; import "./ProjectPreview.css"; const CodeEditor = lazy(() => import("./CodeEditor")); @@ -294,6 +299,7 @@ export interface DeploymentTaskUpdate { label: string; message?: string; pct?: number; + buildLog?: DeployBuildLogSnapshot; /** Draft used to render the Agent detail while its Runtime is still publishing. */ agentDraft?: AgentDraft; /** Re-runs the same project/config as a new deployment task. */ @@ -783,11 +789,58 @@ export function ProjectPreview({ }; onDeploymentTaskChange?.(initialTask); onDeploymentStarted?.(initialTask); + let latestBuildLog: DeployBuildLogSnapshot | undefined; + let latestPhase = initialTask.phase ?? "prepare"; + const terminalBuildLog = ( + status: DeployBuildLogSnapshot["status"], + ): DeployBuildLogSnapshot | undefined => ( + latestBuildLog + ? { ...latestBuildLog, status, updatedAt: Date.now() } + : undefined + ); + const terminalBuildLogUpdate = ( + status: DeployBuildLogSnapshot["status"], + ): { buildLog?: DeployBuildLogSnapshot } => { + const buildLog = terminalBuildLog(status); + return buildLog ? { buildLog } : {}; + }; + const pendingBuildLog = (): DeployBuildLogSnapshot => ({ + source: "code-pipeline", + status: "running", + text: "", + lineCount: 0, + truncated: false, + updatedAt: Date.now(), + pendingMessage: "正在等待构建日志…", + }); + const mergeBuildFailureLog = (message: string): DeployBuildLogSnapshot | undefined => { + if (latestPhase !== "build") return undefined; + const failureText = [ + "", + "----- 构建失败 -----", + message, + ].join("\n"); + latestBuildLog = mergeDeployBuildLog(latestBuildLog, { + source: "code-pipeline", + status: "error", + text: failureText, + lineCount: failureText.split("\n").length, + truncated: false, + updatedAt: Date.now(), + }); + return latestBuildLog; + }; try { const result = await onDeploy( project, (s) => { if (s.runtimeName) taskRuntimeName = s.runtimeName; + latestPhase = s.phase; + if (s.buildLog) { + latestBuildLog = mergeDeployBuildLog(latestBuildLog, s.buildLog); + } else if (s.phase === "build" && !latestBuildLog) { + latestBuildLog = pendingBuildLog(); + } if (mountedRef.current) { setStageMap((prev) => ({ ...prev, [s.phase]: s })); setActivePhase(s.phase); @@ -805,6 +858,7 @@ export function ProjectPreview({ s.phase, message: s.message, pct: s.pct, + ...(latestBuildLog ? { buildLog: latestBuildLog } : {}), }); }, feishuEnabled @@ -832,6 +886,7 @@ export function ProjectPreview({ status: "success", phase: "complete", label: "部署完成", + ...terminalBuildLogUpdate("complete"), }); try { await onDeploymentComplete?.(result); @@ -847,6 +902,7 @@ export function ProjectPreview({ phase: "complete", label: "部署完成,暂未连接", message: error.message, + ...terminalBuildLogUpdate("complete"), }); } } catch (err) { @@ -865,10 +921,13 @@ export function ProjectPreview({ status: "cancelled", label: "已取消", message: "部署已取消,相关 Runtime 资源已请求销毁。", + ...terminalBuildLogUpdate("complete"), }); return; } if (mountedRef.current) setDeployError(message); + const buildLog = mergeBuildFailureLog(message); + const failedInBuild = Boolean(buildLog); onDeploymentTaskChange?.({ id: taskId, runtimeName: taskRuntimeName, @@ -876,8 +935,10 @@ export function ProjectPreview({ region: deployRegion, startedAt: taskStartedAt, status: "error", + phase: latestPhase, label: "部署失败", - message, + message: failedInBuild ? "构建镜像失败,详见构建日志。" : message, + ...(buildLog ? { buildLog } : terminalBuildLogUpdate("complete")), retry: requestDeploymentConfirmation, }); } finally { diff --git a/frontend/src/ui/deployBuildLog.ts b/frontend/src/ui/deployBuildLog.ts new file mode 100644 index 00000000..e6696e81 --- /dev/null +++ b/frontend/src/ui/deployBuildLog.ts @@ -0,0 +1,53 @@ +import type { DeployBuildLogSnapshot } from "../adk/client"; + +export const DEPLOY_BUILD_LOG_MAX_CHARS = 50000; + +function appendSnapshotText(previous: string, next: string): string { + if (!previous) return next; + if (!next) return previous; + if (previous.endsWith(next)) return previous; + if (next.startsWith(previous)) return next; + + const previousLines = previous.split("\n"); + const nextLines = next.split("\n"); + const maxOverlap = Math.min(previousLines.length, nextLines.length, 260); + for (let count = maxOverlap; count > 0; count -= 1) { + const previousTail = previousLines.slice(-count).join("\n"); + const nextHead = nextLines.slice(0, count).join("\n"); + if (previousTail === nextHead) { + const remaining = nextLines.slice(count).join("\n"); + return remaining ? `${previous}\n${remaining}` : previous; + } + } + + return `${previous}\n${next}`; +} + +function trimEarlyLogText(text: string, maxChars: number): { text: string; omitted: boolean } { + if (text.length <= maxChars) return { text, omitted: false }; + let trimmed = text.slice(-maxChars); + const firstNewline = trimmed.indexOf("\n"); + if (firstNewline >= 0) trimmed = trimmed.slice(firstNewline + 1); + return { text: trimmed, omitted: true }; +} + +export function mergeDeployBuildLog( + previous: DeployBuildLogSnapshot | undefined, + snapshot: DeployBuildLogSnapshot, + maxChars = DEPLOY_BUILD_LOG_MAX_CHARS, +): DeployBuildLogSnapshot { + const mergedText = appendSnapshotText(previous?.text ?? "", snapshot.text ?? ""); + const trimmed = trimEarlyLogText(mergedText, maxChars); + const lineCount = trimmed.text ? trimmed.text.split("\n").length : 0; + const snapshotTruncated = Boolean(snapshot.snapshotTruncated || snapshot.truncated); + const omittedEarly = Boolean(previous?.omittedEarly || trimmed.omitted); + + return { + ...snapshot, + text: trimmed.text, + lineCount, + truncated: Boolean(previous?.truncated || snapshot.truncated || omittedEarly), + omittedEarly, + snapshotTruncated: Boolean(previous?.snapshotTruncated || snapshotTruncated), + }; +} diff --git a/frontend/tests/agentWorkspace.test.mjs b/frontend/tests/agentWorkspace.test.mjs index c8c4e23b..d99c2947 100644 --- a/frontend/tests/agentWorkspace.test.mjs +++ b/frontend/tests/agentWorkspace.test.mjs @@ -145,6 +145,15 @@ test("workspace publish flow restores PR 748 deployment lifecycle hooks", () => assert.match(projectPreviewSource, /const isRuntimeUpdate = deploymentActionLabel\.includes\("更新"\)/); assert.match(projectPreviewSource, /onDeploymentStarted\?\.\(initialTask\)/); assert.match(projectPreviewSource, /RuntimeProbeError/); + assert.match(projectPreviewSource, /import \{ mergeDeployBuildLog \} from "\.\/deployBuildLog"/); + assert.match(projectPreviewSource, /latestBuildLog = mergeDeployBuildLog\(latestBuildLog, s\.buildLog\)/); + assert.match(projectPreviewSource, /const pendingBuildLog = \(\): DeployBuildLogSnapshot/); + assert.match(projectPreviewSource, /s\.phase === "build" && !latestBuildLog[\s\S]*?latestBuildLog = pendingBuildLog\(\)/); + assert.match(projectPreviewSource, /let latestPhase = initialTask\.phase \?\? "prepare"/); + assert.match(projectPreviewSource, /const mergeBuildFailureLog = \(message: string\): DeployBuildLogSnapshot \| undefined =>/); + assert.match(projectPreviewSource, /"----- 构建失败 -----"[\s\S]*?latestBuildLog = mergeDeployBuildLog\(latestBuildLog/); + assert.match(projectPreviewSource, /latestPhase = s\.phase/); + assert.match(projectPreviewSource, /message: failedInBuild \? "构建镜像失败,详见构建日志。" : message/); assert.match( projectPreviewSource, /await onDeploymentComplete\?\.\(result\)[\s\S]*?catch \(error\)[\s\S]*?error instanceof RuntimeProbeError[\s\S]*?status: "success"[\s\S]*?label: "部署完成,暂未连接"[\s\S]*?message: error\.message/, @@ -152,8 +161,37 @@ test("workspace publish flow restores PR 748 deployment lifecycle hooks", () => assert.doesNotMatch(workspaceSource, /aw-deployment-focus/); assert.match( workspaceSource, - /className="aw-agent-head"[\s\S]*?deploymentTask\.status !== "success" \|\| deploymentTask\.message[\s\S]*?className="aw-detail-deployment"[\s\S]*?[\s\S]*?