继续收敛预览与生成队列错误正文
保留预览复用 IPC 拒绝与重启回退的具体原因 保留素材生成事件订阅失败并补充预览与队列回归测试
This commit is contained in:
@@ -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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+13
-10
@@ -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 项通过。
|
||||
|
||||
Reference in New Issue
Block a user