diff --git a/web/hwlab-cloud-web/app-conversation.ts b/web/hwlab-cloud-web/app-conversation.ts index 8d6d5367..93cdf429 100644 --- a/web/hwlab-cloud-web/app-conversation.ts +++ b/web/hwlab-cloud-web/app-conversation.ts @@ -1,3 +1,5 @@ +import type { WorkbenchAugmentedError } from "./web-types.ts"; + function renderConversation() { const renderVersion = ++state.conversationRenderVersion; captureConversationScrollPosition(); @@ -53,7 +55,7 @@ function captureConversationScrollPosition(): void { }; } -function restoreConversationScrollPosition(options = {}) { +function restoreConversationScrollPosition(options: { deferred?: boolean } = {}): void { if (options.deferred === true && Date.now() < state.conversationScrollUserActiveUntil) return; const position = state.conversationScrollPosition; if (!position) return; @@ -61,7 +63,7 @@ function restoreConversationScrollPosition(options = {}) { el.conversationList.scrollLeft = Math.min(position.left, Math.max(0, el.conversationList.scrollWidth - el.conversationList.clientWidth)); } -function captureTraceScrollPositions(root = el.conversationList) { +function captureTraceScrollPositions(root: HTMLElement = el.conversationList): void { for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { rememberTraceScrollPosition(list.dataset.traceUiKey, list); } @@ -77,7 +79,7 @@ function rememberTraceScrollPosition(traceUiKey, list) { state.traceScrollPinnedToBottom.set(traceUiKey, isScrolledToBottom(list)); } -function restoreTraceScrollPositions(root = el.conversationList, options = {}) { +function restoreTraceScrollPositions(root: HTMLElement = el.conversationList, options: { deferred?: boolean } = {}): void { for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { const traceUiKey = list.dataset.traceUiKey; if (options.deferred === true && isTraceScrollUserActive(traceUiKey)) continue; @@ -92,7 +94,7 @@ function restoreTraceScrollPositions(root = el.conversationList, options = {}) { } } -function captureTraceBodyScrollPositions(root = el.conversationList) { +function captureTraceBodyScrollPositions(root: HTMLElement = el.conversationList): void { for (const body of root.querySelectorAll(".message-trace-body")) { rememberTraceBodyScrollPosition(body); } @@ -160,7 +162,7 @@ function scrollBottomTop(element) { return Math.max(0, element.scrollHeight - element.clientHeight); } -function scrollTopForPosition(element, position) { +function scrollTopForPosition(element: HTMLElement, position: { top: number; left: number; bottomGap?: number }): number { const maxTop = scrollBottomTop(element); if (Number(position.bottomGap) <= SCROLL_BOTTOM_PIN_PX) return maxTop; return Math.min(position.top, maxTop); @@ -177,7 +179,7 @@ function shouldFollowTraceBottom(list) { return pinned === true || (pinned !== false && shouldDefaultTracePinToBottom(list)); } -function scrollTraceToBottom(list, options = {}) { +function scrollTraceToBottom(list: HTMLElement, options: { deferred?: boolean; force?: boolean; behavior?: ScrollBehavior } = {}): void { const traceUiKey = list?.dataset?.traceUiKey; if (!traceUiKey || !list) return; const run = () => { @@ -537,7 +539,7 @@ function textFallbackTitle(result) { return `Code Agent 文本 fallback 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`; } -function classifyCodeAgentCompletion(result, { blockedError = null } = {}) { +function classifyCodeAgentCompletion(result: unknown, { blockedError = null }: { blockedError?: unknown } = {}): string { if (result?.status === "canceled") { return { status: "canceled", @@ -664,7 +666,7 @@ function failureMessage(result) { return `Code Agent 调用失败:${result.error?.message ?? "未知错误"}。${suffix}请稍后重试或联系维护者补齐后端 provider。`; } -function agentErrorFromHttpResponse(response, traceId) { +function agentErrorFromHttpResponse(response: { timeout?: boolean; timeoutMs?: number; idleMs?: number; lastActivityAt?: number; waitingFor?: string; data?: { traceId?: string; code?: string }; error?: string; status?: string }, traceId: string): WorkbenchAugmentedError { const payloadError = response.data?.error; const code = response.timeout ? "client_timeout" @@ -674,29 +676,29 @@ function agentErrorFromHttpResponse(response, traceId) { response.data?.code ?? "request_failed" ); - const error = new Error(response.timeout + const error: WorkbenchAugmentedError = new Error(response.timeout ? `Code Agent 超过 ${response.timeoutMs}ms 无新事件;已保留输入,可稍后重试。` : payloadError?.userMessage || response.error || "Code Agent 请求失败"); error.traceId = response.data?.traceId || traceId; error.code = code; - error.layer = payloadError && typeof payloadError === "object" ? payloadError.layer : undefined; - error.blocker = payloadError && typeof payloadError === "object" ? payloadError.blocker : undefined; - error.retryable = payloadError && typeof payloadError === "object" ? payloadError.retryable : response.timeout ? true : undefined; - error.userMessage = payloadError && typeof payloadError === "object" ? payloadError.userMessage : undefined; - error.missingConfig = payloadError && typeof payloadError === "object" ? payloadError.missingConfig : undefined; - error.route = payloadError && typeof payloadError === "object" ? payloadError.route : undefined; - error.toolName = payloadError && typeof payloadError === "object" ? payloadError.toolName : undefined; - error.timeoutMs = response.timeoutMs ?? (payloadError && typeof payloadError === "object" ? payloadError.timeoutMs : undefined); - error.hardTimeoutMs = payloadError && typeof payloadError === "object" ? payloadError.hardTimeoutMs : undefined; - error.idleMs = response.idleMs ?? (payloadError && typeof payloadError === "object" ? payloadError.idleMs : undefined); - error.lastActivityAt = response.lastActivityAt ?? (payloadError && typeof payloadError === "object" ? payloadError.lastActivityAt : undefined); - error.waitingFor = response.waitingFor ?? (payloadError && typeof payloadError === "object" ? payloadError.waitingFor : undefined); - error.providerStatus = payloadError && typeof payloadError === "object" ? payloadError.providerStatus : response.status; - error.category = payloadError && typeof payloadError === "object" ? payloadError.category : undefined; + error.layer = payloadError && typeof payloadError === "object" ? (payloadError as { layer?: string }).layer : undefined; + error.blocker = payloadError && typeof payloadError === "object" ? (payloadError as { blocker?: string }).blocker : undefined; + error.retryable = payloadError && typeof payloadError === "object" ? (payloadError as { retryable?: boolean }).retryable : response.timeout ? true : undefined; + error.userMessage = payloadError && typeof payloadError === "object" ? (payloadError as { userMessage?: string }).userMessage : undefined; + error.missingConfig = payloadError && typeof payloadError === "object" ? (payloadError as { missingConfig?: string[] }).missingConfig : undefined; + error.route = payloadError && typeof payloadError === "object" ? (payloadError as { route?: string }).route : undefined; + error.toolName = payloadError && typeof payloadError === "object" ? (payloadError as { toolName?: string }).toolName : undefined; + error.timeoutMs = response.timeoutMs ?? (payloadError && typeof payloadError === "object" ? (payloadError as { timeoutMs?: number }).timeoutMs : undefined); + error.hardTimeoutMs = payloadError && typeof payloadError === "object" ? (payloadError as { hardTimeoutMs?: number }).hardTimeoutMs : undefined; + error.idleMs = response.idleMs ?? (payloadError && typeof payloadError === "object" ? (payloadError as { idleMs?: number }).idleMs : undefined); + error.lastActivityAt = response.lastActivityAt ?? (payloadError && typeof payloadError === "object" ? (payloadError as { lastActivityAt?: number }).lastActivityAt : undefined); + error.waitingFor = response.waitingFor ?? (payloadError && typeof payloadError === "object" ? (payloadError as { waitingFor?: string }).waitingFor : undefined); + error.providerStatus = payloadError && typeof payloadError === "object" ? (payloadError as { providerStatus?: string }).providerStatus : response.status; + error.category = payloadError && typeof payloadError === "object" ? (payloadError as { category?: string }).category : undefined; return error; } -function agentFailurePresentation(error, { result = null, traceId = null } = {}) { +function agentFailurePresentation(error: unknown, { result = null, traceId = null }: { result?: unknown; traceId?: string | null } = {}): WorkbenchAugmentedError { const code = normalizeErrorCode(error?.code); const message = String(error?.message ?? result?.error?.message ?? "").trim(); const structuredBlocker = error?.blocker ?? result?.error?.blocker ?? result?.blocker ?? null; diff --git a/web/hwlab-cloud-web/tsconfig.json b/web/hwlab-cloud-web/tsconfig.json index d70c3c8b..bbb555b0 100644 --- a/web/hwlab-cloud-web/tsconfig.json +++ b/web/hwlab-cloud-web/tsconfig.json @@ -3,7 +3,7 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", - "lib": ["DOM", "ES2022"], + "lib": ["DOM", "DOM.Iterable", "ES2022"], "allowJs": false, "noEmit": true, "allowImportingTsExtensions": true, diff --git a/web/hwlab-cloud-web/web-types.d.ts b/web/hwlab-cloud-web/web-types.d.ts index 9003ecc6..f863ca91 100644 --- a/web/hwlab-cloud-web/web-types.d.ts +++ b/web/hwlab-cloud-web/web-types.d.ts @@ -76,7 +76,7 @@ export interface ElMap { codeAgentProviderProfile: HTMLSelectElement codeAgentTimeout: HTMLSelectElement gatewayShellTimeout: HTMLSelectElement - commandSend: HTMLElement + commandSend: HTMLButtonElement commandClear: HTMLElement devicePodSelect: HTMLSelectElement devicePodStatusTag: HTMLElement @@ -114,11 +114,11 @@ export interface WorkbenchState { traceDetailsOpen: Map; conversationRenderVersion: number; conversationScrollUserActiveUntil: number; - conversationScrollPosition: { top: number; left: number }; - traceScrollPositions: Map; + conversationScrollPosition: { top: number; left: number; bottomGap?: number }; + traceScrollPositions: Map; traceScrollPinnedToBottom: Map; traceScrollUserActiveUntil: Map; - traceBodyScrollPositions: Map; + traceBodyScrollPositions: Map; traceBodyScrollUserActiveUntil: Map; traceProgrammaticScrollWrites: number; fullTraceReplayInFlight: Set; @@ -369,7 +369,7 @@ declare global { function nonEmptyString(value: WorkbenchUnknown, fallback?: string | null): string function textOf(value: WorkbenchUnknown): string; function shortTime(iso: string): string; - function textSpan(text: string, className?: string): HTMLElement; + function textSpan(text: string, className?: string): HTMLButtonElement; function replaceChildren(parent: Node | null, ...children: Node[]): void; function firstNonEmptyString(...values: WorkbenchUnknown[]): string function toneClass(tone: string | null | undefined): string; @@ -540,7 +540,7 @@ declare global { function setTraceScrollTop(top: number): void; function setConversationScrollTop(top: number): void; function resetProgrammaticScrollWriteCount(): number; - function recordField(name: string, value: WorkbenchUnknown): HTMLElement; + function recordField(name: string, value: WorkbenchUnknown): HTMLButtonElement; const CODEX_RUNNER_CAPABLE_PROVIDERS: readonly string[]; const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN: RegExp; function openWorkbenchDialog(body: HTMLElement, title?: string): HTMLDialogElement; @@ -555,4 +555,85 @@ declare global { function updateGatewayShellTimeout(value: number): void; } +// ---- Workbench 内部对 Error 的扩展字段集合,由 agentErrorFromHttpResponse 等 +// 在 throw 之前直接挂到 Error 实例上。runtime-side 使用 Object.assign / 直接 +// 赋值;tsc 看到 Error 类型后无法访问自定义字段,所以走 WorkbenchAugmentedError +// 把这些字段明示出来。---- +export interface WorkbenchAugmentedErrorFields { + traceId?: string; + code?: string; + layer?: string; + blocker?: string; + retryable?: boolean; + userMessage?: string; + missingConfig?: string[]; + route?: string; + toolName?: string; + timeoutMs?: number; + hardTimeoutMs?: number; + idleMs?: number; + lastActivityAt?: number; + waitingFor?: string; + providerStatus?: string; + category?: string; + [k: string]: WorkbenchUnknown | undefined; +} + +export interface WorkbenchAugmentedError extends Error, WorkbenchAugmentedErrorFields {} + +// 常见 options / 配置 / 控制器参数的轻量接口 +export interface WorkbenchScrollOptions { + deferred?: boolean; + force?: boolean; + behavior?: ScrollBehavior; + silent?: boolean; +} + +export interface WorkbenchLoadOptions { + quiet?: boolean; +} + +export interface WorkbenchActionResultOptions { + result?: unknown; + traceId?: string; + blockedError?: unknown; +} + +// 一些返回 summary 对象的 function 的统一形态 +export interface WorkbenchSummaryShape { + tone: string; + kind: string; + icon: string; + label: string; + capabilityLevel: string; + lastTraceId: string; + codeAgentStatus: string; + rows: Array<{ label: string; value: string; tone: string }>; + summary: string; + missingFields: string[]; +} + +export interface WorkbenchComposerShape { + disabled: boolean; + submitMode: "send" | "steer" | "retry" | "cancel"; + targetTraceId: string | null; + traceId: string | null; + conversationId: string | null; + sessionId: string | null; + blocked: boolean; + tone: string; + [k: string]: WorkbenchUnknown | undefined; +} + +// ---- ambient global aliases(让未 import 的 source 也能用) ---- +declare global { + type WorkbenchAugmentedErrorG = WorkbenchAugmentedError; + type WorkbenchAugmentedErrorFieldsG = WorkbenchAugmentedErrorFields; + type WorkbenchScrollOptionsG = WorkbenchScrollOptions; + type WorkbenchLoadOptionsG = WorkbenchLoadOptions; + type WorkbenchActionResultOptionsG = WorkbenchActionResultOptions; + type WorkbenchSummaryShapeG = WorkbenchSummaryShape; + type WorkbenchComposerShapeG = WorkbenchComposerShape; +} + export {};