Expose Canvas project read errors and recover existing records

This commit is contained in:
鲨鱼辣椒 committed 2026-10-14 15:17:28 +08:00
1 parent ea5d47787e
commit 0d10e72b27
5 files changed
+157 -4

No files matched your search

@@ -12,7 +12,7 @@
## Scope
- Remove the exact create-dialog description, change the Canvas rail from pale lavender to pale yellow, remove its Gallery entry while preserving Code Gallery, diagnose actual Canvas project creation timeouts, and implement the smallest evidenced safe recovery/fix.
- Remove the exact create-dialog description, change the Canvas rail from pale lavender to pale yellow, remove its Gallery entry while preserving Code Gallery, diagnose actual Canvas project creation timeouts and apparently missing records, and implement the smallest evidenced safe recovery/fix.
## Intent And Constraints
@@ -89,3 +89,18 @@ Gate result:
- Same-task narrow steering completed: Canvas Gallery entry is absent; Code chat/settings Gallery remains. Standard typecheck, scoped Sidebar/E2E lint, default full build and independent diff review pass.
- Final real isolated Electron pair passed: Canvas V2 desktop/compact creation/dialog/draft/plan/history (7.1s) and Code remembered-project/shared-settings chrome (5.1s), total 2 passed/12.7s. Inspected final creation-dialog screenshot with pale-yellow rail, no original description and no Canvas Gallery. Logs/PNG updated in the external task evidence stage. Earlier 81 focused unit tests remain applicable to the unchanged retry/store/dialog implementation.
- The final continuation source HEAD supersedes ca546c9; both are in this branch. No new actual runtime update or Main restart was performed by this continuation. Authorized coordinator will preserve f1dab live WeChat source and active user work during unified activation.
## Same-task Record Visibility Investigation
- User reports no Canvas works/history in the current temporary app. Official same-task start/status again match Identity. Prior positioning/ADR007/cloud authority/runtime/auth/data-preservation context and scoped peer assessment remain applicable; refreshed all readable peer Scope/Intent/Promotion records. Main35142 is unchanged, pointer still436, actual immutable Renderer f1dab. Primary remains occupied and untouched; ea5d477 changes have not been activated, so they cannot be assumed to cause the current empty display.
- Native readonly observation shows the original five project names and account alice but central region remains Loading AI Design. Source ImageCanvas prioritizes status-ready/nonempty-list/null-workspace loading over its error state; selectProject catch retains status ready with error/null workspace. This is an evidenced presentation path, not proof that canonical records were removed.
- Planning Gate Passed for this related diagnosis. Read real authenticated bootstrap and project details without mutation or prompt/media body output; compare server counts with visible selected project and request failures. Repair the smallest evidenced loading/error recovery path if necessary, preserving all actual data/drafts and previous source changes. No delete, recreate, model/generation call or server deployment.
## Record Visibility Outcome And Verification
- Real current Main35142 authenticated readonly bootstrap again failed504/30023ms. A second fresh pointer/Main/Renderer/PPID/profile guard terminated only its NetworkService71263, rebuilt as98782. Main35142 and Renderer35176 were preserved; no real command replay, creation, generation, removal or auth binding. current.json bytes later changed as the peer navigation-icon overlay finalized Renderer d3b0d411 based on WeChat f1dab; this task did not edit the pointer or stable alias and does not claim final pointer byte equality. Main source436 is unchanged.
- After transport recovery, actual readonly bootstrap200/92ms returned five projects. All canonical details200/106–172ms: 愚公移山1turn/0task/0asset; 精卫填海4/2/2; 秋意浓5/1/1; test3/0/0; alice trip2/0/0. Total15 turns,3 tasks,3 assets. These counts verify records still exist; no prompt or media body was printed. External record-read-receipt.json and record-network-recovery-receipt.json retain minimal evidence.
- ImageCanvas now renders a selected-project detail error before the ready/null-workspace loading state. Its reconnect calls selectProject for the existing active identity, without mutation; missing bootstrap/selection and explicit bootstrap errors reload bootstrap. Normal loading keeps the shell. Later errors with a loaded Workspace preserve its conversation, works and drafts. README synchronized. This presentation/retry change does not pretend to fix the unproven recurring transport trigger.
- 89 focused unit tests across4 files passed; standard typecheck, scoped index/page-test/E2E ESLint, complete default build:vite and diff check passed. Updated existing isolated Electron V2 acceptance passed1test/6.9s (test6.5s), including simulated detail504 after project switch, visible error/no misleading empty/loading state, reconnect and canonical conversation/completed-history recovery, plus prior desktop/compact/creation/color/no-Gallery checks. Inspected its error screenshot; logs and screenshots retained in the external task evidence stage. Native app was observed on the user's active Agent editor after recovery, so this task did not navigate away or force reload their work. Source activation and actual Canvas UI check remain the authorized coordinator's unified update step.
- Sole safe conclusion for the live timeout is that recreating the app-owned Chromium network utility restored authenticated reads twice. The30s Main budget spans token acquisition/refresh, Design fetch and body parsing; no evidence proves a fixed20min expiry/background/HTTP2 cause. Future phase-only diagnostics may distinguish these paths, but no timeout extension, unsafe POST fallback or backend fix was introduced.
- Independent readonly final index/page-unit/E2E/README diff review passed with no blocker. Existing refreshWorkspace rejection does not itself publish an error; this patch preserves loaded data when the store already has an error and specifically fixes selection-read error visibility. The authoritative merged, pushed and actually activated runtime result will be recorded by the separate authorized coordinator, preserving user work and all peer updates.
+1 -1
View File
@@ -56,7 +56,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
- AI 绘画:一个 Workspace 固定对应一个设计方向、一条 Agent Session 和一份持久 Living Form。中央使用与 AI 编程一致的低干扰对话布局:助手回复不套独立卡片,底部输入器是唯一强调层级;当前制作方案以对话内轻量编辑稿呈现,而不是独立硬模块。聊天提取、用户修改创作提示词和制作参数都提交到同一个服务端 reducer,并由方向版本、字段来源和决策状态形成权威投影。创作提示词是公开规格 `content.concept` 的可编辑投影;媒介、画幅、时长和数量始终以紧凑控件直接显示,不用折叠面板隐藏。
- AI 绘画生成:服务端从当前规格编译专业图片或视频指令并返回不可变 Quote;客户端只展示创作提示词、参考图绑定、媒介、画幅、数量、格式、时长、警告和设计点,不展示或改写供应商 Prompt、模型、价格原子或存储地址。修改方案会先提交规格并重新核价,确认时只提交 Quote 身份;确认后的方案不再常驻,而是进入可展开的历史作品记录,桌面端位于右侧全高栏,紧凑窗口保留在时间线中。参考图行只负责缩略图、`@图片N` 状态、替换和删除,保留什么、改变什么等语义统一由创作提示词表达。任务与资产始终属于 Workspace。
- 视频制作:类型、画幅、时长和数量依据服务端真实能力显示。尚未保存时长时明确显示“请选择时长”,不把默认数字当作已选值;参数保存失败后恢复服务端已接受的值。用户可以上传图片或从历史作品选择视频开始画面,绑定状态不依赖提示词中是否出现 `@图片N`。制作前先由 Agent 整理动作与可执行方案,再按最新方案核价;缺少内部分镜时引导点击“准备制作方案”,不要求用户填写分镜字段。整理失败不继续报价,也不创建任务。只有用户确认 Quote 后才开始制作。历史视频提供播放、放大预览和下载;切回图片会清理隐藏的视频参数,不能使用的参考用途须由用户显式移除,图片素材不被删除。
- AI 绘画健壮性:Renderer 为每次命令生成稳定 operation id,网络结果未知时保留原命令供原样重试,不把未知写入当失败或创建第二次生成。Main 负责 Token 刷新、Agent Gateway REST 提交和有界 Run 查询,并将可恢复事件流投影为 Host API SSE;断线后按事件游标续接并重新读取权威 Workspace。Canvas 只使用 Works Square 云端 V2 契约,没有本地语义适配器或降级路径,上游不可用时明确报错。
- AI 绘画健壮性:Renderer 为每次命令生成稳定 operation id,网络结果未知时保留原命令供原样重试,不把未知写入当失败或创建第二次生成。Main 负责 Token 刷新、Agent Gateway REST 提交和有界 Run 查询,并将可恢复事件流投影为 Host API SSE;断线后按事件游标续接并重新读取权威 Workspace。切换项目时读取失败会显示错误与“重新连接”,重试读取当前项目;已载入的作品、记录与草稿在后续读取失败时保留。Canvas 只使用 Works Square 云端 V2 契约,没有本地语义适配器或降级路径,上游不可用时明确报错。
- AI 绘画左侧项目栏只展示 Workspace,桌面设计模式下固定为 256px 且不提供收纳入口,不再在项目下创建独立设计会话;右侧全高历史作品栏同为 256px,展示当前 Workspace 的任务与生成资产。紧凑窗口则把项目列表放进左侧抽屉并在中央时间线保留历史记录。删除时必须完整输入项目名称;删除后项目、Living Form、任务、参考图和生成作品会从账户中隐藏且无法访问,不影响用户已另存到磁盘的副本。删除当前项目后自动打开最近更新的剩余项目,删除最后一个项目后进入空状态。
- 项目资源设置:项目配置页顶部以一横排三张资源卡提供模型、知识与插件入口,不再提供独立的本地 Skill 入口。模型列表完整显示具体模型名称,以及服务端或已验证模型档案声明的可选思考强度;未知能力不做猜测。插件抽屉把插件与 Skill 统一为“可选 / 已安装”两个页签:可选页展示云端目录及仍待安装的账号能力,已安装页同时展示官方 Release、随应用提供的插件和用户自行安装的 loose `SKILL.md` / Pi 包。每项提供“查看详情”和当前真实生命周期对应的添加、安装、添加到项目、启用或状态按钮。详情只展示介绍、主要能力、使用方式和必要设置,不暴露版本、包、权限标识、运行方式或计费实现字段。深链仍使用 `/project-config/plugins`;旧 `/plugins`、Marketplace、My Plugins、Project Plugins URL 会确定性落入对应页签。来源身份保持 `official:`、`local:`、`retained:` 分离,账号获取、官方设备交付和项目启用仍分别写入既有 authority,不会自动推进下一步;本机 Skill/Pi 插件仍只能通过对话安装,页面只管理已安装包。
- 官方插件运行架构:Renderer 只调用 Main-owned Marketplace facade;Main 负责账号、请求 deadline、签名/摘要校验、不可变 Release、current selection 与原子回滚。Pi parent worker 使用冻结的 effective snapshot,将每个有效 Skill 与已验证 Package Store root 成对传给 resource loader、Extension Host 和 CLI;普通 `skill_only` 下载包不依赖运行时 Policy,也不得执行分发包中的任意代码。Project Scaffold 的 `.mjs` 是代码所有的 bundled 例外,只能从签名客户端固定资源根通过应用自带 Node 执行,不能扩展为 Marketplace 下载 artifact 的脚本权限。正式激活仍等待官方 Ed25519 公钥(production key activation HOLD);生产私钥只能来自部署 secret,测试使用注入的临时密钥。
+12 -1
View File
@@ -109,10 +109,12 @@ export function ImageCanvas() {
const authenticated = useAuthStore((state) => state.isAuthenticated());
const status = useImageWorkspaceStore((state) => state.status);
const bootstrap = useImageWorkspaceStore((state) => state.bootstrap);
const activeWorkspaceId = useImageWorkspaceStore((state) => state.activeWorkspaceId);
const workspace = useImageWorkspaceStore((state) => state.workspace);
const error = useImageWorkspaceStore((state) => state.error);
const quoteBlockers = useImageWorkspaceStore((state) => state.quoteBlockers);
const load = useImageWorkspaceStore((state) => state.load);
const selectProject = useImageWorkspaceStore((state) => state.selectProject);
const refreshWorkspace = useImageWorkspaceStore((state) => state.refreshWorkspace);
const connectEvents = useImageWorkspaceStore((state) => state.connectEvents);
const disconnectEvents = useImageWorkspaceStore((state) => state.disconnectEvents);
@@ -138,7 +140,7 @@ export function ImageCanvas() {
if (pendingPrompt) setChatDraft(pendingPrompt.prompt);
}, [consumePendingPrompt, setChatDraft, workspaceId]);
if (status === 'loading' || (status === 'ready' && bootstrap?.workspaces.length && !workspace)) {
if (status === 'loading') {
return <LoadingWorkspace />;
}
@@ -146,6 +148,15 @@ export function ImageCanvas() {
return <ErrorWorkspace message={error ?? '请检查网络后重试'} retry={() => void load()} />;
}
if (!workspace && error) {
return <ErrorWorkspace message={error} retry={() => {
if (bootstrap && activeWorkspaceId) void selectProject(activeWorkspaceId);
else void load();
}} />;
}
if (status === 'ready' && bootstrap?.workspaces.length && !workspace) return <LoadingWorkspace />;
if (!workspace && status !== 'ready') return <LoadingWorkspace />;
const generationAvailable = bootstrap?.capabilities.generation !== false;
+39 -1
View File
@@ -188,10 +188,12 @@ test.describe('AI Design V2 workspace', () => {
__designE2eCompleteTask?: () => void;
__designE2eOpenIncompleteVideo?: () => void;
__designE2eDownloadCount?: number;
__designE2eFailProjectRead?: boolean;
};
e2eGlobal.__designE2eAuthReady = false;
e2eGlobal.__designE2eProfileReady = false;
e2eGlobal.__designE2eDownloadCount = 0;
e2eGlobal.__designE2eFailProjectRead = false;
e2eGlobal.__designE2eOpenIncompleteVideo = () => {
workspace.form.specification.values.intent.media = 'video';
workspace.form.specification.values.video.total_duration_seconds = null;
@@ -282,12 +284,28 @@ test.describe('AI Design V2 workspace', () => {
},
},
},
workspaces: [workspace.workspace],
workspaces: [workspace.workspace, {
...workspace.workspace, workspaceId: 'workspace-2', title: '历史作品项目',
}],
});
}
if (path === '/api/works/image-workspace/workspaces/workspace-1' && request.method === 'GET') {
return envelope(workspace);
}
if (path === '/api/works/image-workspace/workspaces/workspace-2' && request.method === 'GET') {
if (e2eGlobal.__designE2eFailProjectRead) {
e2eGlobal.__designE2eFailProjectRead = false;
return { ok: true, data: { status: 504, ok: false, json: {
success: false, code: 'DESIGN_WORKSPACE_REQUEST_TIMEOUT', error: 'AI 设计服务响应超时,请重试',
} } };
}
return envelope({
...workspace,
workspace: { ...workspace.workspace, workspaceId: 'workspace-2', title: '历史作品项目' },
form: { ...workspace.form, workspaceId: 'workspace-2' },
assets: workspace.assets.map((asset) => ({ ...asset, workspaceId: 'workspace-2' })),
});
}
if (path === '/api/works/image-workspace/workspaces/workspace-1/commands') {
const command = JSON.parse(request.body ?? '{}') as {
kind?: string;
@@ -755,6 +773,26 @@ test.describe('AI Design V2 workspace', () => {
expect(await app.evaluate(() => (
globalThis as typeof globalThis & { __designE2eDownloadCount?: number }
).__designE2eDownloadCount)).toBe(0);
// A failed project read must expose recovery, then restore its canonical records.
await page.setViewportSize({ width: 1440, height: 900 });
await app.evaluate(() => {
(globalThis as typeof globalThis & { __designE2eFailProjectRead?: boolean }).__designE2eFailProjectRead = true;
});
await page.getByTestId('sidebar-image-project-workspace-2').click();
await expect(page.getByTestId('image-workspace-unavailable')).toContainText('AI 设计服务响应超时,请重试');
await expect(page.getByLabel('正在加载 AI 设计')).toHaveCount(0);
await expect(page.getByTestId('image-workspace-empty-state')).toHaveCount(0);
await page.screenshot({ path: testInfo.outputPath('canvas-project-read-error.png'), animations: 'disabled' });
await page.getByRole('button', { name: '重新连接', exact: true }).click();
await expect(page.getByTestId('image-workspace-header')).toContainText('历史作品项目');
await expect(conversation).toContainText(followUp);
await historyTab.click();
await expect(worksPane.getByTestId('design-plan-history-task-1')).toContainText('已完成');
await expect(page.getByTestId('image-workspace-unavailable')).toHaveCount(0);
await page.getByTestId('sidebar-image-project-workspace-1').click();
await page.setViewportSize({ width: 375, height: 800 });
await app.evaluate(() => {
(globalThis as typeof globalThis & { __designE2eOpenIncompleteVideo?: () => void })
.__designE2eOpenIncompleteVideo?.();
+89
View File
@@ -111,6 +111,95 @@ describe('AI Design Canvas page', () => {
expect(load).toHaveBeenCalledOnce();
});
it('shows a failed selected Workspace read and retries that project instead of hiding it behind loading', async () => {
const { workspace, actions } = prepareWorkspace();
useImageWorkspaceStore.setState({
workspace: null,
error: 'AI 设计服务响应超时,请重试',
eventState: 'degraded',
});
actions.selectProject.mockImplementationOnce(async () => {
useImageWorkspaceStore.setState({ workspace, error: null });
});
render(<ImageCanvas />);
expect(screen.getByTestId('image-workspace-unavailable')).toHaveTextContent('AI 设计服务响应超时,请重试');
expect(screen.queryByLabelText('正在加载 AI 设计')).not.toBeInTheDocument();
expect(screen.queryByTestId('image-workspace-empty-state')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '重新连接' }));
expect(actions.selectProject).toHaveBeenCalledExactlyOnceWith(workspace.workspace.workspaceId);
expect(actions.load).not.toHaveBeenCalled();
expect(actions.refreshWorkspace).not.toHaveBeenCalled();
await waitFor(() => expect(screen.getByTestId('image-workspace-conversation')).toBeInTheDocument());
expect(screen.queryByTestId('image-workspace-unavailable')).not.toBeInTheDocument();
});
it.each([
{ status: 'loading' as const, error: '上一次读取失败' },
{ status: 'ready' as const, error: null },
])('keeps the loading shell while the selected Workspace is being read ($status)', ({ status, error }) => {
prepareWorkspace();
useImageWorkspaceStore.setState({ status, workspace: null, error });
render(<ImageCanvas />);
expect(screen.getByLabelText('正在加载 AI 设计')).toBeInTheDocument();
expect(screen.queryByTestId('image-workspace-unavailable')).not.toBeInTheDocument();
expect(screen.queryByTestId('image-workspace-empty-state')).not.toBeInTheDocument();
});
it.each(['activeWorkspaceId', 'bootstrap'] as const)(
'reloads bootstrap after a failed read without %s', (missing) => {
const { actions } = prepareWorkspace();
useImageWorkspaceStore.setState({ workspace: null, error: '项目读取失败', [missing]: null });
render(<ImageCanvas />);
fireEvent.click(screen.getByRole('button', { name: '重新连接' }));
expect(actions.load).toHaveBeenCalledOnce();
expect(actions.selectProject).not.toHaveBeenCalled();
},
);
it.each(['error', 'unavailable'] as const)('reloads failed bootstrap for %s status', (status) => {
const { actions } = prepareWorkspace();
useImageWorkspaceStore.setState({ status, workspace: null, error: 'AI 设计暂时无法读取' });
render(<ImageCanvas />);
fireEvent.click(screen.getByRole('button', { name: '重新连接' }));
expect(actions.load).toHaveBeenCalledOnce();
expect(actions.selectProject).not.toHaveBeenCalled();
});
it('keeps a loaded Workspace, its drafts and works pane visible when a later read fails', () => {
const { workspace, actions } = prepareWorkspace();
useImageWorkspaceStore.setState({
chatDraft: '保留我的创作想法',
fieldDrafts: { 'content.concept': '保留我的制作方案' },
});
render(<ImageCanvas />);
fireEvent.change(screen.getByRole('textbox', { name: '创作提示词' }), {
target: { value: '保留我的制作方案' },
});
act(() => useImageWorkspaceStore.setState({ error: '这次刷新失败' }));
expect(screen.getByRole('alert')).toHaveTextContent('这次刷新失败');
expect(screen.getByTestId('image-workspace-conversation')).toBeInTheDocument();
expect(screen.getByTestId('image-workspace-history-rail')).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: '告诉 AI 你想创作什么' })).toHaveValue('保留我的创作想法');
expect(screen.getByRole('textbox', { name: '创作提示词' })).toHaveValue('保留我的制作方案');
expect(screen.queryByTestId('image-workspace-unavailable')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '刷新', exact: true }));
expect(actions.refreshWorkspace).toHaveBeenCalledOnce();
expect(actions.selectProject).not.toHaveBeenCalled();
expect(actions.load).not.toHaveBeenCalled();
expect(useImageWorkspaceStore.getState()).toMatchObject({
workspace,
chatDraft: '保留我的创作想法',
fieldDrafts: { 'content.concept': '保留我的制作方案' },
});
});
it('keeps one active editable plan in the works pane beside the conversation', () => {
prepareWorkspace();
render(<ImageCanvas />);