继续收敛预览与生成队列错误正文

保留预览复用 IPC 拒绝与重启回退的具体原因

保留素材生成事件订阅失败并补充预览与队列回归测试
This commit is contained in:
kdletters
2026-10-05 23:31:41 +08:00
parent f938ac6bf4
commit 196a5979ce
5 changed files with 59 additions and 34 deletions
+27 -22
View File
@@ -1513,24 +1513,19 @@ export function App({
invoke: TauriInvoke,
nextProjectPath: string,
): Promise<LocalPreviewResult | null> {
try {
const status = await invoke<LocalPreviewStatus>(
'activate_local_game_preview',
{ projectPath: nextProjectPath },
);
if (
status.status !== 'running' ||
!status.url ||
!status.port ||
!status.root
) {
return null;
}
return { url: status.url, port: status.port, root: status.root };
} catch {
// 没在跑、不属于这个项目,或权限位要求确认:都不是错误,回落到重新启动预览。
const status = await invoke<LocalPreviewStatus>(
'activate_local_game_preview',
{ projectPath: nextProjectPath },
);
if (
status.status !== 'running' ||
!status.url ||
!status.port ||
!status.root
) {
return null;
}
return { url: status.url, port: status.port, root: status.root };
}
async function executeRunLocal() {
@@ -1548,11 +1543,14 @@ export function App({
return;
}
let previewActivationError: string | null = null;
try {
const activePreview = await activateRunningPreview(
invoke,
nextProjectPath,
);
let activePreview: LocalPreviewResult | null = null;
try {
activePreview = await activateRunningPreview(invoke, nextProjectPath);
} catch (error) {
previewActivationError = visibleClientErrorMessage(error);
}
if (activePreview) {
updateClientPreview(activePreview);
// 运行成功的反馈走工作台壳的 toast(对话区只保留对话内容),因此不再往聊天里
@@ -1571,11 +1569,18 @@ export function App({
* `announceToChat` 约束(它是旧的「聊天播报」开关)。当前唯一调用点由播放请求驱动、
* 恒为 true(见 `executeRunLocalRef.current(true)`)。
*/
onRunNotice?.({ message: '运行通过,已载入客户端运行视图' });
onRunNotice?.({
message: previewActivationError
? `已有预览复用失败,已重新启动运行视图:${previewActivationError}`
: '运行通过,已载入客户端运行视图',
});
} catch (error) {
const startError = visibleClientErrorMessage(error);
onRunNotice?.({
tone: 'error',
message: `运行游戏失败:${visibleClientErrorMessage(error)}`,
message: previewActivationError
? `已有预览复用失败:${previewActivationError};重新启动运行也失败:${startError}`
: `运行游戏失败:${startError}`,
});
}
}
@@ -153,13 +153,13 @@ export function createResourceCanvasAssetGenerationQueue(
function replaceFromRecord(
task: ResourceCanvasAssetGenerationTask,
record: LocalProjectAssetGenerationTaskRecord,
subscriptionWarning?: string | null,
) {
deps.replaceTask(
mergeLocalProjectAssetGenerationRecord(
task,
safeGenerationRecord(record),
),
);
const safeRecord = safeGenerationRecord(record);
if (subscriptionWarning) {
safeRecord.phaseDetail = `${subscriptionWarning};${safeRecord.phaseDetail}`;
}
deps.replaceTask(mergeLocalProjectAssetGenerationRecord(task, safeRecord));
}
async function dispatch(
@@ -167,6 +167,7 @@ export function createResourceCanvasAssetGenerationQueue(
): Promise<ResourceCanvasAssetGenerationSettlement> {
const projectPath = deps.projectPath();
let releaseTaskSubscription: (() => void) | null = null;
let subscriptionWarning: string | null = null;
let taskEventPending = false;
let resolveTaskEvent: (() => void) | null = null;
const waitForTaskEvent = () => {
@@ -190,8 +191,10 @@ export function createResourceCanvasAssetGenerationQueue(
resolve?.();
},
);
} catch {
// 事件桥不可用时仅回退到测试/浏览器预览的注入等待器。
} catch (error) {
// 事件桥不可用时回退到轮询,但不能把 IPC 失败伪装成“没有事件”;把安全正文附到
// 任务阶段,用户仍能看到本轮为何降级以及后续账本读取是否成功。
subscriptionWarning = `任务事件订阅失败,已回退轮询:${errorMessage(error)}`;
releaseTaskSubscription = null;
}
}
@@ -239,7 +242,7 @@ export function createResourceCanvasAssetGenerationQueue(
error: errorMessage(error),
};
}
replaceFromRecord(task, started);
replaceFromRecord(task, started, subscriptionWarning);
let missingRecordPolls = 0;
let readFailed = false;
let lastReadError = RESOURCE_CANVAS_ASSET_GENERATION_MALFORMED_READ_ERROR;
@@ -266,7 +269,7 @@ export function createResourceCanvasAssetGenerationQueue(
missingRecordPolls = 0;
readFailed = false;
const safeRecord = safeGenerationRecord(record);
replaceFromRecord(task, safeRecord);
replaceFromRecord(task, safeRecord, subscriptionWarning);
if (isTerminalStatus(safeRecord.status)) {
return {
taskId: task.taskId,
@@ -154,7 +154,7 @@ describe('运行入口切到已经在跑的客户端预览', () => {
);
});
it('权限位要求确认时不把拒绝当错误,直接回落到重新启动预览', async () => {
it('预览复用 IPC 被拒绝时回落重启但保留具体正文', async () => {
const { invokeCounts } = installTauri(() => {
throw new Error('需要先确认预览权限');
});
@@ -168,7 +168,7 @@ describe('运行入口切到已经在跑的客户端预览', () => {
);
await expectRunNoticeWithoutChatTip(
onRunNotice,
'运行通过,已载入客户端运行视图',
'已有预览复用失败,已重新启动运行视图:需要先确认预览权限',
);
});
@@ -348,6 +348,22 @@ describe('本地排队驱动器', () => {
expect(releaseCount).toBe(1);
});
test('事件桥 IPC 失败回退轮询时保留安全错误正文', async () => {
const harness = createHarness((pollCount, api) => {
if (pollCount === 1) api.advance('task-subscribe-failed', 'completed');
});
harness.deps.subscribe = async () => {
throw new Error('IPC EPIPE:事件订阅失败');
};
const queue = createResourceCanvasAssetGenerationQueue(harness.deps);
await queue.submit(localTask('task-subscribe-failed', 10));
expect(harness.tasks()[0]?.phaseDetail).toContain('任务事件订阅失败');
expect(harness.tasks()[0]?.phaseDetail).toContain('IPC EPIPE');
expect(harness.settlements[0]?.status).toBe('completed');
});
test('第一条未完成时提交第二条:第二条仍是排队中且生成提交 IPC 只发了一次', async () => {
let assertedMidFlight = false;
const harness = createHarness((pollCount, api) => {
@@ -72,3 +72,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
- 继续真实客户端启动和旁路扫描又发现三处仍会损失事实:旧历史通用中断文案遇到换行/多空格时未命中“正文缺失”标记,非 2xx 响应正文读取失败被 `unwrap_or_default()` 抹掉,CC sidecar 的 stderr 排空超时/读取任务失败也被当成“没有 stderr”。现分别改为折叠空白识别、保留“响应正文读取失败:底层原因”和保留 stderr 排空阶段/底层原因;不改变状态码、重试和脱敏边界。
- 第二轮 Agent/CC 旁路扫描继续发现执行许可/执行会话/MCP 本地读取/编辑器 worker/Cocos worker 的 JoinError,以及 MCP future panic,会被固定成“任务未返回/异常/需要核对”;现把 join/panic 正文并入既有 ToolFailure / MCP 错误载荷,保留 `needs-reconciliation` 的不可自动重放语义。Native 定向测试:Direct MCP 33 项、Direct Tool Bridge 41 项、CC 14 项均通过。
- 最新漏扫又补出 Direct Tools MCP 启动/运行退出码、Skill `resources/read` 索引与资源读取、交付 `finish_sealing` 错误三处固定句;现在分别写入 stderr、MCP 错误正文和交付复核底层原因,避免只剩退出码或“暂不可用/复核失败”。
- 前端继续实测发现预览活体复用的 IPC / 权限拒绝会被当成“没有活体”并静默重启,资源生成事件订阅失败也只回退轮询;现在两条回退仍保留,但分别把复用失败正文附到重启结果、把订阅失败正文附到任务阶段。预览与生成队列定向测试 25 项通过。