继续收敛活动回合读取错误

保留 Direct 活动回合快照 IPC 失败正文

补充窗口标题栏显示和回归测试
This commit is contained in:
kdletters
2026-10-05 21:24:12 +08:00
parent 7d73f225b5
commit 0850195d1d
8 changed files with 38 additions and 16 deletions
@@ -178,6 +178,7 @@ export function WindowChrome({ children }: WindowChromeProps) {
activeTurns={activeProjectRuns.activeTurns}
currentProjectPath={activeProjectRuns.currentProjectPath}
readFailed={activeProjectRuns.readFailed}
readError={activeProjectRuns.readError}
onOpenProject={activeProjectRuns.onOpenProject}
placement="titlebar"
/>
@@ -19,6 +19,7 @@ export type WindowChromeActiveProjectRuns = {
activeTurns: GameCreatorDirectActiveTurn[];
currentProjectPath?: string | null;
readFailed?: boolean;
readError?: string;
onOpenProject?: (projectPath: string) => void;
};
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { GameCreatorDirectActiveTurn, TauriInvoke } from '../../app/types';
import { visibleClientErrorMessage } from '../../services/errorReporting';
import {
canSubscribeTauriEvents,
subscribeTauriEvent,
@@ -35,6 +36,7 @@ export function useDirectActiveTurns({
[],
);
const [snapshotReadFailed, setSnapshotReadFailed] = useState(false);
const [snapshotReadError, setSnapshotReadError] = useState('');
const mountedRef = useRef(true);
const inFlightRef = useRef<Promise<void> | null>(null);
/**
@@ -93,10 +95,12 @@ export function useDirectActiveTurns({
setActiveTurns(nextTurns);
}
setSnapshotReadFailed(false);
setSnapshotReadError('');
inFlightRef.current = null;
return;
} catch {
} catch (error) {
if (!isCurrent()) return;
setSnapshotReadError(visibleClientErrorMessage(error));
if (attempt < DIRECT_ACTIVE_TURNS_READ_ATTEMPTS) {
await new Promise<void>((resolve) => {
retryTimerRef.current = window.setTimeout(() => {
@@ -132,6 +136,7 @@ export function useDirectActiveTurns({
// 空态也要保持引用稳定:已经空了就不要再换一个新数组。
setActiveTurns((current) => (current.length === 0 ? current : []));
setSnapshotReadFailed((current) => (current ? false : current));
setSnapshotReadError((current) => (current ? '' : current));
return;
}
let disposed = false;
@@ -172,5 +177,10 @@ export function useDirectActiveTurns({
};
}, [enabled, invoke, refreshActiveTurns]);
return { activeTurns, refreshActiveTurns, snapshotReadFailed };
return {
activeTurns,
refreshActiveTurns,
snapshotReadFailed,
snapshotReadError,
};
}
@@ -16,6 +16,7 @@ export type ActiveProjectRunsPanelProps = {
activeTurns: GameCreatorDirectActiveTurn[];
currentProjectPath?: string | null;
readFailed?: boolean;
readError?: string;
onOpenProject?: (projectPath: string) => void;
placement?: 'panel' | 'titlebar';
};
@@ -59,6 +60,7 @@ export function ActiveProjectRunsPanel({
activeTurns,
currentProjectPath = null,
readFailed = false,
readError = '',
onOpenProject,
placement = 'panel',
}: ActiveProjectRunsPanelProps) {
@@ -97,7 +99,7 @@ export function ActiveProjectRunsPanel({
className="launcher-runs-titlebar launcher-runs-titlebar--error"
role="status"
>
正在运行的项目读取失败
{readError || '正在运行的项目读取失败'}
</span>
);
}
@@ -105,7 +107,7 @@ export function ActiveProjectRunsPanel({
return (
<aside className="launcher-runs-panel" aria-label="正在运行的项目">
<span className="launcher-runs-panel-note" role="status">
未能读取正在运行的项目
{readError || '未能读取正在运行的项目'}
</span>
</aside>
);
@@ -166,10 +168,10 @@ export function ActiveProjectRunsPanel({
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
const isCurrent = Boolean(
currentProjectPath &&
projectPathsMatchForInvalidation(
turn.projectPath,
currentProjectPath,
),
projectPathsMatchForInvalidation(
turn.projectPath,
currentProjectPath,
),
);
return (
<li key={`${turn.projectPath}:${turn.turnId}`}>
@@ -215,10 +217,10 @@ export function ActiveProjectRunsPanel({
const elapsed = formatActiveTurnElapsed(turn.startedAt, now);
const isCurrent = Boolean(
currentProjectPath &&
projectPathsMatchForInvalidation(
turn.projectPath,
currentProjectPath,
),
projectPathsMatchForInvalidation(
turn.projectPath,
currentProjectPath,
),
);
return (
<li key={`${turn.projectPath}:${turn.turnId}`}>
@@ -174,10 +174,11 @@ export function WorkspaceLauncherShell({
: null,
);
const directInvoke = resolveTauriInvoke();
const { activeTurns, snapshotReadFailed } = useDirectActiveTurns({
invoke: directInvoke,
enabled: true,
});
const { activeTurns, snapshotReadFailed, snapshotReadError } =
useDirectActiveTurns({
invoke: directInvoke,
enabled: true,
});
const switchedToGameRuntime =
gameRuntimeSwitch !== null &&
currentProjectContext !== null &&
@@ -304,6 +305,7 @@ export function WorkspaceLauncherShell({
activeTurns,
currentProjectPath: currentProjectContext?.projectPath ?? null,
readFailed: snapshotReadFailed,
readError: snapshotReadError,
onOpenProject: (projectPath: string) =>
openActiveProjectRef.current(projectPath),
});
@@ -312,6 +314,7 @@ export function WorkspaceLauncherShell({
currentProjectContext?.projectPath,
setActiveProjectRuns,
snapshotReadFailed,
snapshotReadError,
]);
useEffect(() => () => setActiveProjectRuns(null), [setActiveProjectRuns]);
@@ -152,6 +152,9 @@ describe('useDirectActiveTurns', () => {
});
expect(result.current.activeTurns).toBe(firstSnapshot);
expect(result.current.snapshotReadFailed).toBe(true);
expect(result.current.snapshotReadError).toContain(
'snapshot unavailable',
);
} finally {
vi.useRealTimers();
}
@@ -135,6 +135,7 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
- 模板库权限探测和 Direct 线程订阅/consume/bootstrap 属于用户可见的前端状态边界:权限/IPC 失败可以关闭入口或结束订阅,但必须同时保留安全错误正文;禁止用空 catch、只 settle 锚点或清空状态来吞掉认证、网络和 IPC 原因。
- UI 编辑器剪贴板、文件选择、预览等原生 IPC 操作的失败提示也必须经过 `visibleClientErrorMessage`;操作指引可以保留,但不能覆盖原生错误正文。
- `agc_web_search` 的上游非 2xx 也属于用户可见的受控工具失败:MCP 结果必须保留 HTTP 状态与有界脱敏正文,不能只返回状态码。
- 跨项目活动回合快照读取失败即使保留旧快照,也必须把最近一次安全正文一并呈现;“读不到不等于没有在跑”不能变成“读不到所以没有原因”。
## 后果与边界
@@ -56,3 +56,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
- 前端继续发现两个静默出口:模板库权限检查失败会直接清空入口,Direct 线程订阅/consume/bootstrap 的 IPC 失败会只 settle anchor 而不显示正文;现分别保留权限检查错误,并把订阅链错误正文送入项目状态栏/运行错误出口。认证失败回归已验证显示 `authentication-required`,不再变成泛化“执行失败,请稍后重试”。
- UI 编辑器复制路径的原生剪贴板 IPC 失败也曾只显示“复制失败,请手动复制路径”,现保留脱敏后的剪贴板错误正文并同时给出手动复制指引。
- 受控联网搜索的上游非 2xx 响应原来只返回 HTTP 状态;现保留有界、脱敏的服务端正文,错误码/限流原因不再丢失。
- 窗口标题栏的 Direct 活动回合快照读取失败原来只给“读取失败”标志并保留旧快照;现额外保留脱敏后的 IPC/读取正文,面板不会把真实原因吞掉。