fix(issue751): tighten DOM iterator + payloadError + WorkbenchSummary types (#763)

#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 批量加 <HTMLElement> 类型参数到 querySelectorAll /
  querySelector<HTMLElement>,让 list.dataset / tile.dataset 等访问合法
- event.target.closest 改 (event.target as HTMLElement).closest<HTMLElement>(...)
- 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 <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-03 16:35:29 +08:00
committed by GitHub
parent cae2f1e71b
commit 893f46bc03
6 changed files with 144 additions and 36 deletions
+8 -8
View File
@@ -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<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>(".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<void> {
const index = state.chatMessages.findIndex((message) => message.id === messageId);
if (index < 0) return false;
const message = state.chatMessages[index];
+3 -3
View File
@@ -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<HTMLElement>("[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<HTMLElement>("[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<HTMLElement>("[data-device-detail]");
if (!tile) return;
showDeviceDetail(tile.dataset.deviceTitle || tile.dataset.deviceDetail || "Device Pod", state.devicePod.details.get(tile.dataset.deviceDetail));
});
+4 -4
View File
@@ -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<HTMLElement>("[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<HTMLElement>("[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();
});
+9 -9
View File
@@ -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<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>(".message-trace[data-trace-ui-key]");
const list = conversation.querySelector<HTMLElement>(".message-trace-events[data-trace-ui-key]");
const firstRow = list?.querySelector<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>(".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<HTMLElement>(".message-trace-events[data-trace-ui-key]");
if (!list) return traceScrollMetrics();
if (user) markTraceScrollIntent(list.dataset.traceUiKey);
list.scrollTop = top;
+5 -5
View File
@@ -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<HTMLElement>("[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<HTMLButtonElement>("[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<HTMLElement>("[data-view]")) {
view.hidden = view.dataset.view !== activeRoute;
}
for (const button of document.querySelectorAll("[data-route]")) {
for (const button of document.querySelectorAll<HTMLButtonElement>("[data-route]")) {
const active = button.dataset.route === activeRoute;
button.classList.toggle("active", active);
button.setAttribute("aria-current", active ? "page" : "false");
+115 -7
View File
@@ -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> | 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<void>;
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, string>): 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<string>;
@@ -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 {