Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 56 additions & 22 deletions frontend/src/adk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export interface AdkEvent {
error?: string;
errorMessage?: string;
error_message?: string;
// Error code accompanying the error message (snake_case and camelCase variants).
errorCode?: string;
error_code?: string;
content?: {
role?: string;
parts?: AdkPart[];
Expand Down Expand Up @@ -433,37 +436,58 @@ function formatErrorDetail(detail: unknown): string {
if (Array.isArray(detail)) {
return detail
.map((item) => {
if (item && typeof item === "object" && "msg" in item) {
if (item && typeof item === "object" && ("msg" in item || "message" in item)) {
const loc = Array.isArray((item as { loc?: unknown }).loc)
? (item as { loc?: unknown[] }).loc?.join(".")
: "";
const msg = String((item as { msg?: unknown }).msg ?? "");
const msg = String((item as { msg?: unknown; message?: unknown }).msg ?? (item as { msg?: unknown; message?: unknown }).message ?? "");
return loc ? `${loc}: ${msg}` : msg;
}
return String(item);
})
.filter(Boolean)
.join("\n");
}
if (detail && typeof detail === "object") return JSON.stringify(detail);
if (detail && typeof detail === "object") {
const msg = (detail as Record<string, unknown>).message;
if (typeof msg === "string" && msg) return msg;
return JSON.stringify(detail);
}
return "";
}

async function httpErrorMessage(res: Response, fallback: string): Promise<string> {
const text = await res.text().catch(() => "");
if (!text) return `${fallback} (${res.status})`;
try {
const data = JSON.parse(text) as { detail?: unknown; error?: unknown };
const data = JSON.parse(text) as {
detail?: unknown;
error?: unknown;
errorCode?: unknown;
error_code?: unknown;
code?: unknown;
};
// Extract a machine-readable error code when the server provides one.
const errorCode =
typeof data.errorCode === "string" && data.errorCode
? data.errorCode
: typeof data.error_code === "string" && data.error_code
? data.error_code
: typeof data.code === "string" && data.code
? data.code
: "";
const detail = formatErrorDetail(data.detail ?? data.error);
return detail || text || `${fallback} (${res.status})`;
if (!detail && !errorCode) return text || `${fallback} (${res.status})`;
const body = detail || `${fallback} (${res.status})`;
return errorCode ? `[${errorCode}] ${body}` : body;
} catch {
return text || `${fallback} (${res.status})`;
}
}

export async function listApps(): Promise<string[]> {
const res = await apiFetch(`/list-apps`);
if (!res.ok) throw new Error(`list-apps failed: ${res.status}`);
if (!res.ok) throw new Error(await httpErrorMessage(res, '读取 Agent 列表失败'));
return res.json();
}

Expand Down Expand Up @@ -643,7 +667,7 @@ export async function listSessions(
): Promise<AdkSession[]> {
const { app, ep } = resolve(appName);
const res = await apiFetch(`/apps/${app}/users/${encodeURIComponent(userId)}/sessions`, {}, ep);
if (!res.ok) throw new Error(`list sessions failed: ${res.status}`);
if (!res.ok) throw new Error(await httpErrorMessage(res, "读取会话列表失败"));
return res.json();
}

Expand All @@ -659,8 +683,7 @@ export async function getSession(
ep,
);
if (!res.ok) {
const detail = await httpErrorMessage(res, "读取会话失败");
throw new Error(`get session failed: ${res.status}:${detail}`);
throw new Error(await httpErrorMessage(res, "读取会话失败"));
}
const session = (await res.json()) as AdkSession;
if (ep.runtimeId) {
Expand Down Expand Up @@ -993,7 +1016,7 @@ export async function deleteSession(
{ method: "DELETE" },
ep,
);
if (!res.ok && res.status !== 404) throw new Error(`delete session failed: ${res.status}`);
if (!res.ok && res.status !== 404) throw new Error(await httpErrorMessage(res, '删除会话失败'));
}

function decodeArtifactData(value: string): Uint8Array {
Expand Down Expand Up @@ -1501,7 +1524,7 @@ async function fetchAgentInfo(
loadDraft = true,
): Promise<AgentInfo> {
const res = await apiFetch(`/web/agent-info/${app}`, {}, ep);
if (!res.ok) throw new Error(`agent-info failed: ${res.status}`);
if (!res.ok) throw new Error(await httpErrorMessage(res, '读取 Agent 信息失败'));
const info = (await res.json()) as Partial<AgentInfo>;
if (loadDraft && !info.draft) {
try {
Expand Down Expand Up @@ -1691,7 +1714,7 @@ export async function webSearch(
const res = await apiFetch(
`/web/search?source=web&app_name=${encodeURIComponent(app)}&q=${encodeURIComponent(query)}`,
);
if (!res.ok) throw new Error(`web search failed: ${res.status}`);
if (!res.ok) throw new Error(await httpErrorMessage(res, 'Agent 网络搜索失败'));
return res.json();
}

Expand Down Expand Up @@ -1794,12 +1817,25 @@ export async function* runSSE({
}
for await (const evt of parseSSE(res)) {
const event = evt as AdkEvent;
if (typeof event.error === "string") event.error = formatRunSseError(event.error);
// Normalise the error-code field (camelCase or snake_case) and prepend it
// as a bracketed token so callers always see "[CODE] message" when a code
// is present — rather than having the code silently dropped.
const errorCode =
typeof event.errorCode === "string" && event.errorCode
? event.errorCode
: typeof event.error_code === "string" && event.error_code
? event.error_code
: "";
const prefixCode = (msg: string) =>
errorCode && !msg.startsWith(`[${errorCode}]`) ? `[${errorCode}] ${msg}` : msg;
if (typeof event.error === "string") {
event.error = formatRunSseError(prefixCode(event.error));
}
if (typeof event.errorMessage === "string") {
event.errorMessage = formatRunSseError(event.errorMessage);
event.errorMessage = formatRunSseError(prefixCode(event.errorMessage));
}
if (typeof event.error_message === "string") {
event.error_message = formatRunSseError(event.error_message);
event.error_message = formatRunSseError(prefixCode(event.error_message));
}
yield event;
}
Expand Down Expand Up @@ -2039,8 +2075,7 @@ export async function cancelAgentkitDeployment(taskId: string): Promise<void> {
body: JSON.stringify({ taskId }),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text || `取消部署失败 (${res.status})`);
throw new Error(await httpErrorMessage(res, "取消部署失败"));
}
deploymentControllers.get(taskId)?.abort();
deploymentControllers.delete(taskId);
Expand All @@ -2062,7 +2097,7 @@ export async function getMyRuntimes(
region = VOLCENGINE_DEFAULT_REGION,
): Promise<ManagedRuntime[]> {
const res = await apiFetch(`/web/my-runtimes?region=${encodeURIComponent(region)}`);
if (!res.ok) throw new Error(`加载失败 (${res.status})`);
if (!res.ok) throw new Error(await httpErrorMessage(res, "加载失败"));
const d = (await res.json()) as { runtimes?: ManagedRuntime[] };
return d.runtimes ?? [];
}
Expand Down Expand Up @@ -2260,7 +2295,7 @@ export const DEFAULT_STUDIO_ACCESS: StudioAccess = {
/** Resolve the signed-in user's Studio role and capabilities. */
export async function getStudioAccess(): Promise<StudioAccess> {
const res = await apiFetch("/web/access");
if (!res.ok) throw new Error(`加载权限失败 (${res.status})`);
if (!res.ok) throw new Error(await httpErrorMessage(res, '加载权限失败'));
const access = (await res.json()) as StudioAccess;
if (
!["admin", "developer", "user"].includes(access.role) ||
Expand Down Expand Up @@ -2319,7 +2354,7 @@ export async function getStudioUpdateStatus(
if (startedAt) params.set("startedAt", String(startedAt));
const query = params.size ? `?${params.toString()}` : "";
const res = await apiFetch(`/web/studio-update${query}`);
if (!res.ok) throw new Error(`检查 Studio 更新失败 (${res.status})`);
if (!res.ok) throw new Error(await httpErrorMessage(res, '检查 Studio 更新失败'));
return (await res.json()) as StudioUpdateStatus;
}

Expand Down Expand Up @@ -2506,8 +2541,7 @@ export async function deleteRuntime(
body: JSON.stringify({ runtimeId, region }),
});
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(t || `删除失败 (${res.status})`);
throw new Error(await httpErrorMessage(res, "删除失败"));
}
}

Expand Down
9 changes: 5 additions & 4 deletions frontend/src/create/CustomCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2879,7 +2879,7 @@ export function CustomCreate({
setUsedAiGeneration(true);
} catch (error) {
setAiErrorDialog(
error instanceof Error ? error.message : String(error),
(error instanceof Error && error.message) ? error.message : String(error) || '未知错误',
);
} finally {
setAiGenerating(false);
Expand Down Expand Up @@ -3125,7 +3125,7 @@ export function CustomCreate({
setProject(generated);
setWorkspaceMode("publish");
} catch (error) {
setBuildErr(error instanceof Error ? error.message : String(error));
setBuildErr((error instanceof Error && error.message) ? error.message : String(error) || '未知错误');
} finally {
setBuilding(false);
}
Expand Down Expand Up @@ -3227,7 +3227,7 @@ export function CustomCreate({
...item,
phase: "error",
runtimeSnapshot: "",
error: err instanceof Error ? err.message : String(err),
error: (err instanceof Error && err.message) ? err.message : String(err) || '未知错误',
}
: item,
),
Expand Down Expand Up @@ -3283,7 +3283,7 @@ export function CustomCreate({
text,
})) {
const eventError =
event.error || event.errorMessage || event.error_message;
event.error ?? event.errorMessage ?? event.error_message;
if (!eventError) acc = applyEvent(acc, event);
setDebugVariants((current) =>
current.map((item) => {
Expand Down Expand Up @@ -3664,6 +3664,7 @@ export function CustomCreate({
className={`cw-input ${invalidClass(nameInvalid)}`}
value={node.name}
placeholder="assistant"
maxLength={64}
onChange={(e) => patch({ name: e.target.value })}
/>
{showErrors && nameProblem ? (
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/create/agentNameValidation.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { AgentDraft } from "./types";

const ADK_AGENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;

type AgentNameNode = Pick<AgentDraft, "name" | "subAgents">;

/** Return the Google ADK name validation error, or null when valid. */
export function agentNameProblem(name: string): string | null {
if (name.trim().length === 0) return "名称为必填项";
if (name.length > 64) return '名称不能超过 64 个字符';
if (name === "user") return "user 是 Google ADK 保留名称,请使用其他名称";
if (!ADK_AGENT_NAME_PATTERN.test(name)) {
return "名称须以英文字母或下划线开头,且只能包含英文字母、数字和下划线";
}
if (!/^[A-Za-z_]/.test(name)) return '名称须以英文字母或下划线开头';
const bad = name.match(/[^A-Za-z0-9_]/)?.[0];
if (bad) return `名称中不允许使用"${bad}",只能包含英文字母、数字和下划线`;
return null;
}

Expand Down
Loading
Loading