d04fe83f10
- App.tsx:`conversation.write` 策略确认后的重跑改为整体复用首轮入参(`directCodexPolicyRetryInput`),不再手写字段,修掉漏传 `references` 导致 @ 引用被静默丢弃的缺陷 - resourceReferences.ts:运行画面引用的判别指纹补上 `resourceIds`(排序集合口径)/`versionId`/`elementRole`/`width`/`height`,避免不同选点撞 key 被 `dedupeChatReferences` 去重丢掉 - ResourceReferenceInput.tsx:程序化重建编辑器内容改为把 chip 内联嵌在文本里的同名 token 位置(不再另起一段堆在末尾),并把 `lastEmittedDraftRef` 记为「编辑器实际读回来的草稿」而不是 props,消除每轮多一段 `@显示名` 的渲染循环 - ResourceReferenceInput.tsx:候选浮层(候选菜单 / 素材选择器)打开时 Enter 不再提交表单,把按键让给 `LexicalTypeaheadMenuPlugin` - ResourceReferenceInput.tsx:`assets` / `currentVersionAssetReferences` 的 memo 依赖改用内容签名;预览请求在换素材 / 卸载时主动作废上一个 scope;空态文案的嵌套三元抽成 `pickerEmptyMessage` - usePromptPolish.ts:`polish()` 补 `catch`,注入式 `requestPolish` 拒绝时给出可见失败提示而不是未处理拒绝;`reset()` 递增请求代次作废在飞请求 - ChatPromptPolishReminder.tsx:章节级 Esc 分支补 `busy` 判据,与关闭按钮 / 遮罩同一口径(在飞不许关面板) - LocalGamePreviewFrame.tsx:资源 id 改用专用净化(允许 `:` 与可打印非 ASCII),修掉 `local-asset:<id>`、`persisted-角色草图.png` 被整条过滤导致引用丢素材关联 - sessionPreview.ts:`get_local_game_preview_status` 读失败不再当作「确认没有在跑」去发停止命令,避免停掉真在跑的活体预览 - useHomeProjectCreation.ts:进项目流程加代次令牌,慢请求后到不再覆盖新项目 - WorkspaceLauncher.tsx:拒收恢复的阶段回写按 `projectId`/`heldRevision`/`snapshotRevision` 关联,不再把恢复结果盖到后续新拒收的提示上 - 用例:`directCodexPolicyRetryInput` 透传、引用指纹区分度、重建不循环、浮层打开时 Enter 不提交、润色拒绝 / reset 作废、预览读失败不停预览
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
import type { GameCreationAppPreviewState } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||
|
||
/**
|
||
* 进入项目时的预览活体核验。
|
||
*
|
||
* 为什么不能直接信项目 manifest 里的 `preview`:那是"上次留下的一条记录",不是"现在
|
||
* 有没有在跑"。本地预览服务器是**进程内线程**(`src-tauri/src/preview.rs` 的
|
||
* `thread::spawn` + loopback listener),非 Linux 平台端口还是**随机临时端口**
|
||
* (`bind_loopback_listener_with_linux_fallback` 里的 `TcpListener::bind(..., 0)`),
|
||
* 客户端一退 URL 就永久失效;而落盘记录会一直写着 `running`。照它渲染,用户就会被直接
|
||
* 丢进一个打不开的运行界面——这正是"有时候进项目看到的是运行界面"的成因。
|
||
*
|
||
* 所以进项目时问一次内存 registry(`get_local_game_preview_status`):
|
||
* - 确实在跑 → 作为**本次会话的预览**交给工作台(工作台据此自动进运行视图);
|
||
* - 没在跑但记录写着 running → 记录陈旧:把落盘记录对齐成 stopped(复用现役
|
||
* `stop_local_game_preview`,它现在会把陈旧记录一并收尾),本次会话不带预览。
|
||
*/
|
||
export type SessionPreviewResolution = {
|
||
/** 本次会话可用的预览:只有内存 registry 里真的在跑才有值。 */
|
||
sessionPreview: GameCreationAppPreviewState | null;
|
||
/**
|
||
* 会话 manifest 里该投影成的预览状态;`null` 表示落盘记录与真相一致,不用改。
|
||
*
|
||
* 判定陈旧的同一刻就按真相投影(而不是等落盘写成功):否则一份写不进去的陈旧记录
|
||
* (例如项目策略把 `preview.stop` 设成需确认)会让运行入口继续拿着一个打不开的 URL
|
||
* 渲染运行画面。落盘那份由这次写入、退出收尾或下次进入时的核对负责收敛。
|
||
*/
|
||
manifestPreviewPatch: GameCreationAppPreviewState | null;
|
||
};
|
||
|
||
/** 预览命令的最小 invoke 形状(与 App 里 `window.__TAURI__.core.invoke` 一致)。 */
|
||
export type PreviewCommandInvoke = <T>(
|
||
command: string,
|
||
args?: Record<string, unknown>,
|
||
) => Promise<T>;
|
||
|
||
type LocalPreviewStatusLike = {
|
||
status?: string | null;
|
||
url?: string | null;
|
||
port?: number | null;
|
||
};
|
||
|
||
function livePreviewFromStatus(
|
||
status: LocalPreviewStatusLike | null,
|
||
): GameCreationAppPreviewState | null {
|
||
const url = typeof status?.url === 'string' ? status.url.trim() : '';
|
||
if (status?.status !== 'running' || !url) {
|
||
return null;
|
||
}
|
||
return {
|
||
status: 'running',
|
||
url,
|
||
port: typeof status.port === 'number' ? status.port : null,
|
||
};
|
||
}
|
||
|
||
export async function resolveSessionPreviewOnProjectOpen({
|
||
invoke,
|
||
projectPath,
|
||
recordedPreview,
|
||
}: {
|
||
invoke: PreviewCommandInvoke | null | undefined;
|
||
projectPath: string;
|
||
recordedPreview: GameCreationAppPreviewState | null;
|
||
}): Promise<SessionPreviewResolution> {
|
||
const trimmedProjectPath = projectPath.trim();
|
||
if (!invoke || !trimmedProjectPath) {
|
||
return { sessionPreview: null, manifestPreviewPatch: null };
|
||
}
|
||
|
||
let status: LocalPreviewStatusLike | null = null;
|
||
try {
|
||
status = await invoke<LocalPreviewStatusLike>(
|
||
'get_local_game_preview_status',
|
||
{ projectPath: trimmedProjectPath },
|
||
);
|
||
} catch {
|
||
// 读不到 registry 不等于"确认没有在跑":`stop_local_game_preview` 会停掉
|
||
// 真在跑的活体预览,读失败(权限拒绝 / 瞬时 IPC 错误)时不能顺手当清理动作。
|
||
// 只按"不带预览进入"处理,让用户自己点「运行」。
|
||
return { sessionPreview: null, manifestPreviewPatch: null };
|
||
}
|
||
const sessionPreview = livePreviewFromStatus(status);
|
||
if (sessionPreview) {
|
||
return { sessionPreview, manifestPreviewPatch: null };
|
||
}
|
||
|
||
if (recordedPreview?.status !== 'running') {
|
||
return { sessionPreview: null, manifestPreviewPatch: null };
|
||
}
|
||
|
||
try {
|
||
await invoke('stop_local_game_preview', {
|
||
projectPath: trimmedProjectPath,
|
||
});
|
||
} catch {
|
||
// 对齐落盘记录失败不改判:本次会话照样不把陈旧记录当活体用。
|
||
}
|
||
return {
|
||
sessionPreview: null,
|
||
manifestPreviewPatch: { status: 'stopped' },
|
||
};
|
||
}
|