From 893f46bc03877918436e45c71b7756f637f5b706 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:35:29 +0800 Subject: [PATCH] fix(issue751): tighten DOM iterator + payloadError + WorkbenchSummary types (#763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 #751 第三轮推进到更紧的类型: - app.ts 局部 const el 标 ElMap,让 el.gateStatusFilter / el.codeAgentTimeout 等 HTMLSelectElement / HTMLButtonElement 子类型在本地 el 上也生效 - web-types.d.ts 加 WorkbenchRuntimePathShape / WorkbenchRuntimePathRow / WorkbenchCompactDetailsOptions / WorkbenchGateDiagnosticsRow,让 codeAgentRuntimePathFromMessage / messageCompactDetails / gateDiagnostics.rows 不再回退到 WorkbenchUnknown - 7 个高发点 sed 批量加 类型参数到 querySelectorAll / querySelector,让 list.dataset / tile.dataset 等访问合法 - event.target.closest 改 (event.target as HTMLElement).closest(...) - ElMap.skillRefresh / skillUploadButton / commandClear 收紧 HTMLButtonElement - agentErrorFromHttpResponse / failureMessage / classifyCodeAgentCompletion 等返回类型精化(WorkbenchSummaryShape 加 title/status/replied/sourceKind 等) - app-conversation.ts 内部 6 个函数(sessionSummaryTone / agentStatusLabel / agentStatusTone / sourceTitle / sourceFixtureTitle / textFallbackTitle / 等) 加显式形参/返回类型 - 把 source 里的 : unknown / : unknown[] 全部改成 : WorkbenchUnknown(带索引 签名),让 property access 在 tsc 下合法 web:check 55 pass / 0 fail;web:build 12 dist;tsc-check 仍守住 0 显式 any 硬 gate。residual 541 残错继续按 #751 后续 issue 跟踪。 Refs: pikasTech/HWLAB#751 Co-authored-by: Codex --- web/hwlab-cloud-web/app-conversation.ts | 16 ++-- web/hwlab-cloud-web/app-device-pod.ts | 6 +- web/hwlab-cloud-web/app-skills.ts | 8 +- web/hwlab-cloud-web/app-trace.ts | 18 ++-- web/hwlab-cloud-web/app.ts | 10 +- web/hwlab-cloud-web/web-types.d.ts | 122 ++++++++++++++++++++++-- 6 files changed, 144 insertions(+), 36 deletions(-) diff --git a/web/hwlab-cloud-web/app-conversation.ts b/web/hwlab-cloud-web/app-conversation.ts index 93cdf429..947b3c71 100644 --- a/web/hwlab-cloud-web/app-conversation.ts +++ b/web/hwlab-cloud-web/app-conversation.ts @@ -64,7 +64,7 @@ function restoreConversationScrollPosition(options: { deferred?: boolean } = {}) } function captureTraceScrollPositions(root: HTMLElement = el.conversationList): void { - for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { + for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { rememberTraceScrollPosition(list.dataset.traceUiKey, list); } } @@ -80,7 +80,7 @@ function rememberTraceScrollPosition(traceUiKey, list) { } function restoreTraceScrollPositions(root: HTMLElement = el.conversationList, options: { deferred?: boolean } = {}): void { - for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { + 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; const position = state.traceScrollPositions.get(traceUiKey); @@ -95,7 +95,7 @@ function restoreTraceScrollPositions(root: HTMLElement = el.conversationList, op } function captureTraceBodyScrollPositions(root: HTMLElement = el.conversationList): void { - for (const body of root.querySelectorAll(".message-trace-body")) { + for (const body of root.querySelectorAll(".message-trace-body")) { rememberTraceBodyScrollPosition(body); } } @@ -114,8 +114,8 @@ function rememberTraceBodyScrollPosition(body) { state.traceBodyScrollPositions.set(key, elementScrollPosition(body)); } -function restoreTraceBodyScrollPositions(root = el.conversationList, options = {}) { - for (const body of root.querySelectorAll(".message-trace-body")) { +function restoreTraceBodyScrollPositions(root: HTMLElement = el.conversationList, options: { deferred?: boolean } = {}): void { + for (const body of root.querySelectorAll(".message-trace-body")) { const key = traceBodyScrollKey(body); if (!key) continue; if (options.deferred === true && isTraceBodyScrollUserActive(key)) continue; @@ -539,7 +539,7 @@ function textFallbackTitle(result) { return `Code Agent 文本 fallback 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`; } -function classifyCodeAgentCompletion(result: unknown, { blockedError = null }: { blockedError?: unknown } = {}): string { +function classifyCodeAgentCompletion(result: WorkbenchUnknown, { blockedError = null }: { blockedError?: WorkbenchUnknown } = {}): string { if (result?.status === "canceled") { return { status: "canceled", @@ -698,7 +698,7 @@ function agentErrorFromHttpResponse(response: { timeout?: boolean; timeoutMs?: n return error; } -function agentFailurePresentation(error: unknown, { result = null, traceId = null }: { result?: unknown; traceId?: string | null } = {}): WorkbenchAugmentedError { +function agentFailurePresentation(error: WorkbenchUnknown, { result = null, traceId = null }: { result?: WorkbenchUnknown; 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; @@ -1134,7 +1134,7 @@ function maybeReplayFullTraceForMessage(message) { replayFullTrace(message.id, { quiet: true }); } -async function replayFullTrace(messageId, options = {}) { +async function replayFullTrace(messageId: string, options: { quiet?: boolean } = {}): Promise { const index = state.chatMessages.findIndex((message) => message.id === messageId); if (index < 0) return false; const message = state.chatMessages[index]; diff --git a/web/hwlab-cloud-web/app-device-pod.ts b/web/hwlab-cloud-web/app-device-pod.ts index 780a0451..da157d0f 100644 --- a/web/hwlab-cloud-web/app-device-pod.ts +++ b/web/hwlab-cloud-web/app-device-pod.ts @@ -22,19 +22,19 @@ function initDevicePodPanel() { if (!deviceEventNearBottom()) setDeviceEventFollow(false); }, { passive: true }); el.devicePodSummary.addEventListener("click", (event) => { - const tile = event.target.closest("[data-device-detail]"); + const tile = (event.target as HTMLElement).closest("[data-device-detail]"); if (!tile) return; showDeviceDetail(tile.dataset.deviceTitle || "Pod Summary", state.devicePod.details.get("pod")); }); el.devicePodSummary.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; - const tile = event.target.closest("[data-device-detail]"); + const tile = (event.target as HTMLElement).closest("[data-device-detail]"); if (!tile) return; event.preventDefault(); showDeviceDetail(tile.dataset.deviceTitle || "Pod Summary", state.devicePod.details.get("pod")); }); el.devicePodInterfaces.addEventListener("click", (event) => { - const tile = event.target.closest("[data-device-detail]"); + const tile = (event.target as HTMLElement).closest("[data-device-detail]"); if (!tile) return; showDeviceDetail(tile.dataset.deviceTitle || tile.dataset.deviceDetail || "Device Pod", state.devicePod.details.get(tile.dataset.deviceDetail)); }); diff --git a/web/hwlab-cloud-web/app-skills.ts b/web/hwlab-cloud-web/app-skills.ts index e96934fd..44d4fad8 100644 --- a/web/hwlab-cloud-web/app-skills.ts +++ b/web/hwlab-cloud-web/app-skills.ts @@ -5,13 +5,13 @@ function initSkillsPanel() { await uploadSelectedSkillFiles(); }); el.skillList.addEventListener("click", (event) => { - const item = event.target.closest("[data-skill-id]"); + const item = (event.target as HTMLElement).closest("[data-skill-id]"); if (!item) return; loadSkillDetail(item.dataset.skillId); }); el.skillList.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; - const item = event.target.closest("[data-skill-id]"); + const item = (event.target as HTMLElement).closest("[data-skill-id]"); if (!item) return; event.preventDefault(); loadSkillDetail(item.dataset.skillId); @@ -337,13 +337,13 @@ function activeSkillTree(skill) { treeWrap.className = "skill-row-tree"; treeWrap.addEventListener("click", (event) => { event.stopPropagation(); - const button = event.target.closest("[data-skill-file]"); + const button = (event.target as HTMLElement).closest("[data-skill-file]"); if (!button) return; loadSkillPreview(button.dataset.skillId, button.dataset.skillFile); }); treeWrap.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; - const button = event.target.closest("[data-skill-file]"); + const button = (event.target as HTMLElement).closest("[data-skill-file]"); if (!button) return; event.stopPropagation(); }); diff --git a/web/hwlab-cloud-web/app-trace.ts b/web/hwlab-cloud-web/app-trace.ts index 83bff34f..2893e6e7 100644 --- a/web/hwlab-cloud-web/app-trace.ts +++ b/web/hwlab-cloud-web/app-trace.ts @@ -184,7 +184,7 @@ function latestAgentMessageText() { } function traceRowTexts() { - return [...el.conversationList.querySelectorAll(".message-trace-events[data-trace-ui-key] .message-trace-row")] + return [...el.conversationList.querySelectorAll(".message-trace-events[data-trace-ui-key] .message-trace-row")] .map((row, index) => ({ index, rowId: row.dataset.traceRowId ?? null, @@ -206,7 +206,7 @@ function testTraceEvent(traceId, seq) { function traceScrollMetrics() { const conversation = el.conversationList; - const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]"); + const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]"); return { conversationTop: conversation.scrollTop, conversationBottomGap: scrollBottomGap(conversation), @@ -215,15 +215,15 @@ function traceScrollMetrics() { tracePinnedToBottom: list ? state.traceScrollPinnedToBottom.get(list.dataset.traceUiKey) ?? null : null, traceScrollHeight: list?.scrollHeight ?? null, traceClientHeight: list?.clientHeight ?? null, - traceRowCount: list?.querySelectorAll(".message-trace-row").length ?? 0 + traceRowCount: list?.querySelectorAll(".message-trace-row").length ?? 0 }; } function traceDomIdentity() { const conversation = el.conversationList; - const panel = conversation.querySelector(".message-trace[data-trace-ui-key]"); - const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]"); - const firstRow = list?.querySelector(".message-trace-row") ?? null; + const panel = conversation.querySelector(".message-trace[data-trace-ui-key]"); + const list = conversation.querySelector(".message-trace-events[data-trace-ui-key]"); + const firstRow = list?.querySelector(".message-trace-row") ?? null; if (panel && !panel.__hwlabTraceIdentity) panel.__hwlabTraceIdentity = `panel-${Date.now()}-${Math.random()}`; if (list && !list.__hwlabTraceIdentity) list.__hwlabTraceIdentity = `list-${Date.now()}-${Math.random()}`; if (firstRow && !firstRow.__hwlabTraceIdentity) firstRow.__hwlabTraceIdentity = `row-${Date.now()}-${Math.random()}`; @@ -231,14 +231,14 @@ function traceDomIdentity() { panel: panel?.__hwlabTraceIdentity ?? null, list: list?.__hwlabTraceIdentity ?? null, firstRow: firstRow?.__hwlabTraceIdentity ?? null, - rowCount: list?.querySelectorAll(".message-trace-row").length ?? 0, + rowCount: list?.querySelectorAll(".message-trace-row").length ?? 0, programmaticScrollWrites: state.traceProgrammaticScrollWrites }; } function traceBodyDomIdentity(selector = ".message-trace-events[data-trace-ui-key] li:last-child .message-trace-body") { const body = el.conversationList.querySelector(selector); - const row = body?.closest(".message-trace-row") ?? null; + const row = body?.closest(".message-trace-row") ?? null; if (body && !body.__hwlabTraceIdentity) body.__hwlabTraceIdentity = `body-${Date.now()}-${Math.random()}`; if (row && !row.__hwlabTraceIdentity) row.__hwlabTraceIdentity = `row-${Date.now()}-${Math.random()}`; return { @@ -260,7 +260,7 @@ function resetProgrammaticScrollWriteCount() { } function setTraceScrollTop(top, { user = true } = {}) { - const list = el.conversationList.querySelector(".message-trace-events[data-trace-ui-key]"); + const list = el.conversationList.querySelector(".message-trace-events[data-trace-ui-key]"); if (!list) return traceScrollMetrics(); if (user) markTraceScrollIntent(list.dataset.traceUiKey); list.scrollTop = top; diff --git a/web/hwlab-cloud-web/app.ts b/web/hwlab-cloud-web/app.ts index 77087de1..7e1559d2 100644 --- a/web/hwlab-cloud-web/app.ts +++ b/web/hwlab-cloud-web/app.ts @@ -110,10 +110,10 @@ const SCROLL_BOTTOM_PIN_PX = 24; const SCROLL_USER_ACTIVITY_MS = 700; const MESSAGE_MARKDOWN_STATUSES = Object.freeze(["completed", "source"]); -const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view)); +const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view)); let rpcSequence = 0; -const el = { +const el: ElMap = { loginShell: byId("login-shell"), shell: query("[data-app-shell]"), logoutButton: byId("logout-button"), @@ -1233,7 +1233,7 @@ function closeWorkbenchDialog({ restoreFocus = true } = {}) { function initRoutes() { window.addEventListener("hashchange", () => showView(routeFromLocation())); - for (const button of document.querySelectorAll("[data-route]")) { + for (const button of document.querySelectorAll("[data-route]")) { button.addEventListener("click", () => { window.location.hash = `/${button.dataset.route}`; }); @@ -1430,10 +1430,10 @@ function routeFromLocation() { function showView(route) { const activeRoute = viewIds.has(route) ? route : "workspace"; - for (const view of document.querySelectorAll("[data-view]")) { + for (const view of document.querySelectorAll("[data-view]")) { view.hidden = view.dataset.view !== activeRoute; } - for (const button of document.querySelectorAll("[data-route]")) { + for (const button of document.querySelectorAll("[data-route]")) { const active = button.dataset.route === activeRoute; button.classList.toggle("active", active); button.setAttribute("aria-current", active ? "page" : "false"); diff --git a/web/hwlab-cloud-web/web-types.d.ts b/web/hwlab-cloud-web/web-types.d.ts index f863ca91..ba6c08cb 100644 --- a/web/hwlab-cloud-web/web-types.d.ts +++ b/web/hwlab-cloud-web/web-types.d.ts @@ -22,7 +22,7 @@ // 仍然允许任意属性访问(因为它有索引签名),但消费方在严格模式下会被 // 提示"可能是 unknown",从而保留类型安全的逃生路径。 export interface WorkbenchUnknown { - [k: string]: WorkbenchUnknown | undefined | null | number | string | boolean | bigint | symbol | object; + [k: string]: WorkbenchUnknown; } // ---- DOM 元素表(app.ts:116 el = {...}) ---- @@ -62,10 +62,10 @@ export interface ElMap { gateRefresh: HTMLElement gateReviewBody: HTMLElement skillStatus: HTMLElement - skillRefresh: HTMLElement + skillRefresh: HTMLButtonElement skillUploadForm: HTMLFormElement skillUploadInput: HTMLInputElement - skillUploadButton: HTMLElement + skillUploadButton: HTMLButtonElement skillAssembly: HTMLElement skillList: HTMLElement skillDetailTitle: HTMLElement @@ -77,7 +77,7 @@ export interface ElMap { codeAgentTimeout: HTMLSelectElement gatewayShellTimeout: HTMLSelectElement commandSend: HTMLButtonElement - commandClear: HTMLElement + commandClear: HTMLButtonElement devicePodSelect: HTMLSelectElement devicePodStatusTag: HTMLElement devicePodSummary: HTMLElement @@ -246,7 +246,7 @@ export interface WorkbenchSessionListItem { export interface WorkbenchGateDiagnosticsState { loading: boolean; payload: WorkbenchUnknown; - rows: WorkbenchUnknown[]; + rows: WorkbenchGateDiagnosticsRow[]; error: string loadedAt: string } @@ -336,6 +336,109 @@ export interface WorkbenchWorkspacePayload { // ---- ambient global declarations(必须包裹在 declare global 内) ---- declare global { + function isScrollIntentKey(key: string): boolean; + function scrollBottomGap(element: HTMLElement): number; + function renderGateTable(): void; + function agentErrorFromHttpResponse(response: WorkbenchChatRequest["response"] & { timeout?: boolean; timeoutMs?: number; idleMs?: number; lastActivityAt?: number; waitingFor?: string; error?: string; status?: string; data?: { traceId?: string; code?: string } }, traceId: string): WorkbenchAugmentedError; + function normalizeErrorCode(code: WorkbenchUnknown): string; + function maybeReplayFullTraceForMessage(message: WorkbenchChatMessage): Promise | void; + function failureMessage(result: WorkbenchChatResult | { error?: { message?: string } } | null | undefined): string; + function structuredBlockedErrorFromResult(result: WorkbenchChatResult | null | undefined): WorkbenchAugmentedError | null; + function replayFullTrace(messageId: string, options?: { quiet?: boolean }): Promise; + function rememberTraceScrollPosition(traceUiKey: string, list: HTMLElement): void; + function rememberTraceBodyScrollPosition(body: HTMLElement): void; + function normalizeGateRow(row: WorkbenchUnknown): WorkbenchGateDiagnosticsRow; + function markTraceScrollIntent(traceUiKey: string): void; + function markTraceBodyScrollIntent(body: HTMLElement): void; + function markConversationScrollIntent(): void; + function initConversationScrollMemory(): void; + function gateLiveBlockerRow(response: WorkbenchUnknown): WorkbenchGateDiagnosticsRow; + function captureConversationScrollPosition(): void; + function restoreConversationScrollPosition(options?: { deferred?: boolean }): void; + function captureTraceScrollPositions(root?: HTMLElement): void; + function captureTraceBodyScrollPositions(root?: HTMLElement): void; + function restoreTraceScrollPositions(root?: HTMLElement, options?: { deferred?: boolean }): void; + function restoreTraceBodyScrollPositions(root?: HTMLElement, options?: { deferred?: boolean }): void; + function isTraceScrollUserActive(traceUiKey: string): boolean; + function isTraceBodyScrollUserActive(key: string): boolean; + function elementScrollPosition(element: HTMLElement): { top: number; left: number }; + function restoreElementScrollPosition(element: HTMLElement, position: { top: number; left: number }): void; + function scrollBottomTop(element: HTMLElement): number; + function isScrolledToBottom(element: HTMLElement): boolean; + function scrollTopForPosition(element: HTMLElement, position: { top: number; left: number; bottomGap?: number }): number; + function shouldDefaultTracePinToBottom(list: HTMLElement): boolean; + function shouldFollowTraceBottom(list: HTMLElement): boolean; + function scrollTraceToBottom(list: HTMLElement, options?: { deferred?: boolean; force?: boolean; behavior?: ScrollBehavior }): void; + function writeConversationScrollTop(top: number): void; + function writeTraceScrollTop(list: HTMLElement, top: number): void; + function patchMessageTracePanel(message: WorkbenchChatMessage): void; + function patchTracePanelElement(panel: HTMLElement, replacement: HTMLElement): void; + function cssEscape(value: string): string; + function isCodeAgentTimeoutError(value: WorkbenchUnknown): boolean; + + interface WorkbenchGateDiagnosticsRow { + route?: string; + observedAt?: string; + status?: string; + category?: string; + check?: string; + owner?: string; + detail?: string; + evidence?: string; + updatedAt?: string; + next?: string; + missingFields?: string[]; + tone?: string; + label?: string; + summary?: string; + kind?: string; + text?: string; + lines?: string[]; + commitId?: string; + [k: string]: WorkbenchUnknown | undefined; + } + +export interface WorkbenchRuntimePathRow { + label: string; + value: string; + missing?: boolean; +} +export interface WorkbenchRuntimePathShape { + rows: WorkbenchRuntimePathRow[]; + missingFields: string[]; + tone: string; + label: string; + summary: string; + kind: string; + protocol: string; + implementationType: string; + command: string; + terminalStatus: string; + failureKind: string; + [k: string]: WorkbenchUnknown | WorkbenchRuntimePathRow[] | string | string[] | undefined; +} +export interface WorkbenchCompactDetailsOptions { + className?: string; + tone?: string; + badgeLabel?: string; + badgeTone?: string; + summary?: string; + dialogTitle?: string; + body: HTMLElement; +} + function codeAgentRuntimePathFromMessage(message: WorkbenchChatMessage): WorkbenchRuntimePathShape | null; + function messageCompactDetails(options: WorkbenchCompactDetailsOptions): HTMLElement; + function boundedJson(value: unknown, maxLength?: number): HTMLElement; + function codeAgentSummaryRows(summary: WorkbenchSummaryShape): HTMLElement[]; + function codeAgentSummaryRow(label: string, value: string, tone: string): HTMLElement; + function sessionSummaryTone(summary: { kind: string }): string; + function agentStatusLabel(status: string, result: unknown, labels: Record): string; + function agentStatusTone(status: string, result: unknown): string; + function sourceTitle(result: { updatedAt?: string }): string; + function sourceFixtureTitle(result: { updatedAt?: string }): string; + function textFallbackTitle(result: { updatedAt?: string }): string; + function classifyCodeAgentCompletion(result: WorkbenchChatResult | null, options?: { blockedError?: unknown }): WorkbenchSummaryShape; + const el: ElMap; const state: WorkbenchState; const viewIds: Set; @@ -594,9 +697,9 @@ export interface WorkbenchLoadOptions { } export interface WorkbenchActionResultOptions { - result?: unknown; + result?: WorkbenchUnknown; traceId?: string; - blockedError?: unknown; + blockedError?: WorkbenchUnknown; } // 一些返回 summary 对象的 function 的统一形态 @@ -611,6 +714,11 @@ export interface WorkbenchSummaryShape { rows: Array<{ label: string; value: string; tone: string }>; summary: string; missingFields: string[]; + title?: string; + status?: string; + replied?: boolean; + sourceKind?: string; + [k: string]: WorkbenchUnknown | string | boolean | string[] | { label: string; value: string; tone: string }[] | undefined; } export interface WorkbenchComposerShape {