From d22c45dfa5f41b7c7539f67f1870597e3f7f106b Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Mon, 25 May 2026 02:24:48 +0800 Subject: [PATCH] refactor: delegate media asset upload runtime --- ...0-browser-runtime-followup-checklist-v1.md | 7 +++ ...29-editor-runtime-followup-checklist-v1.md | 4 +- .../mnote-web/browser/local-upload-runtime.js | 61 +++++++++++++++++++ rust/crates/mnote-web/src/ssr/pages/layout.rs | 22 +++++++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md index 3d175f55..0dddc1c5 100644 --- a/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md +++ b/design/03-rust-web/process/3-20-browser-runtime-followup-checklist-v1.md @@ -106,3 +106,10 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - Codex 已读取 Hindsight recall、`process-handoff.md/json`、`result.json`,并核对 worktree `git status --short` / `git diff --stat` 为空;runner 已产出 completed handoff 但进程未自行退出,主控已终止残留 runner。 - 审计结论:`uploadFileToMediaAsset` 的 API 调用、editor 插入、多文件循环等纯 helper 已外置;剩余主要是 source 判定、sidebar refresh / append、事件分发等 shell 编排职责,不适合继续整体迁入 runtime。 - 下一刀建议:不要盲迁整个 `uploadFileToMediaAsset`;若继续 3-20,应转向 filetree DOM runtime 的 `refreshLocalFolderSidebarSnapshot` / `appendUploadedAssetRow` 依赖拆分,或只做 fallback 精简但需重新评估是否仍要保留 inline fallback 策略。 +- 2026-05-25:Codex 独立复核并采纳 3-20 候选 patch:`local-upload-runtime.js` 新增并导出 `uploadFileToMediaAsset(file, plan, options, deps)`,`layout.rs` 的同名 wrapper 优先委托 runtime,仍保留完整 inline fallback;`resolveFileTreeUploadTarget` 未迁出,资源归属、上传 API、editor 插入状态和事件名保持不变。 + - Reasonix 协作证据:只读审计 run `reasonix-2026-05-24T18-04-44-852Z-2c2d5ce9` 已有 Hindsight recall 与 `process-handoff.md/json`,指出剩余职责主要是 shell 编排;实现 run `reasonix-2026-05-24T18-07-08-227Z-1358f36b` 仅产出 `prompt.md` / `memory-recall.json` / `reasonix-transcript.jsonl`,无 `final.md`、`result.json`、`process-handoff.md/json`,因此不作为完整 Reasonix handoff 采纳。 + - 候选 worktree:`/mnt/Data1T/mnote-worktrees/0525-d-worker-b-3-20-upload-file-orchestrator`;Codex 已核对 `git diff --stat`、`git diff --check` 和修改范围,并按“runtime 委托 + shell deps 注入 + fallback 保留”的较窄边界采纳。 + - 修改:`rust/crates/mnote-web/browser/local-upload-runtime.js`、`rust/crates/mnote-web/src/ssr/pages/layout.rs`。 + - 已验证:`node --check rust/crates/mnote-web/browser/local-upload-runtime.js`、`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/Cargo.toml -p mnote-web`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_upload_runtime_contains_editor_upload_context_helpers -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder -- --test-threads=1`、`NODE_PATH=/mnt/Data1T/mnote/node_modules node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 + - 过程说明:Worker B 在独立 worktree 中复跑 `task479` 时曾出现 `broken-link-after-real-file-delete` 等待超时且 JSON `ok:false`;Codex 在主工作区移植后复跑同一 smoke,结果 `ok:true`,截图证据位于该 smoke 输出的临时目录。 + - 未完成:`resolveFileTreeUploadTarget`、filetree DOM runtime 的 `refreshLocalFolderSidebarSnapshot` / `appendUploadedAssetRow` 更细拆分、conflict merge / accept / keep 业务逻辑、secondary pane conflict smoke 仍未迁出。 diff --git a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md index cbf1b1f7..8dcc1cdb 100644 --- a/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md +++ b/design/05-editor-mainline/process/5-29-editor-runtime-followup-checklist-v1.md @@ -67,7 +67,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 已迁函数:`node_is_within_root`、`active_editor_text_selection`、`has_active_text_selection`、`selection_summary`、`selection_has_rich_marks`。 - 未迁:`SelectionPayload`、`selection_payload`、`send_selection_state*`、`selection_event_payload`,原因是仍依赖 bridge event、`HoveredBlockState` 和 runtime block id 边界。 - 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check`、`cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`、wasm release build、`wasm-bindgen`、`task488`、`task489`、CodeGraph pending 0。 -- 2026-05-25:执行口径修正:继续遵循当前 goal 的“每批最多 2 个 worker”;Reasonix 后续必须使用独立 worktree 或明确只读任务,完成后由 Codex 读取 handoff 并复核。 +- 2026-05-25:执行口径修正:继续遵循当前 goal 的“每批最多 4 个 worker”;Reasonix 后续必须使用独立 worktree 或明确只读任务,完成后由 Codex 读取 handoff 并复核。 - 2026-05-25:Batch D 额外 runner 曾自动启动 Worker A / Worker B,未经过本轮主控计划确认;主控已终止相关 runner / ACP 进程,不采纳这些未受控 run。遗留 task 草稿暂不删除,后续如继续使用必须重新生成受控任务书。 - 2026-05-25:Reasonix runner 异常:后续实现 run 未写 `result.json`、`process-handoff.md/json`,部分 transcript 停在工具调用阶段;Codex 不采纳其文字结论,只复核实际 diff。 - 2026-05-25:Reasonix overlay / block menu 只读审计尝试未产生 `result.json` / `process-handoff`,且运行期间出现额外 Batch A2 runner,主控已终止进程;该结果不作为正式 handoff 采纳。 @@ -92,7 +92,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 未覆盖:`cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml block_menu_document -- --test-threads=1` 被既有 `STYLE` 测试编译错误阻断,未进入本次新增 helper 测试。 - 2026-05-25:根据用户纠正,后续 B/C/D 等实现 worker 不再只读或在主工作区直接修改;Batch C 统一采用独立 worktree 候选 patch,Codex 读取 Hindsight recall、`process-handoff.md/json`、`result.json`、diff 与验证证据后再选择性合入。 - 2026-05-25:根据用户再次纠正,后续 B/C/D 等实现 worker 必须统一在独立 worktree 中产出候选 patch,再由 Codex 复核 diff / handoff / 验证证据后选择性合入;不得在主工作区直接推进实现 worker。 -- 2026-05-25:Batch D Reasonix runner 再次异常:Worker A run `reasonix-2026-05-24T18-07-08-220Z-ccc4102a`、Worker B run `reasonix-2026-05-24T18-07-08-227Z-1358f36b` 未经过本轮主控计划确认且已被终止;该结果不采纳。 +- 2026-05-25:Batch D Reasonix runner 再次异常:Worker A run `reasonix-2026-05-24T18-07-08-220Z-ccc4102a`、Worker B run `reasonix-2026-05-24T18-07-08-227Z-1358f36b` 仅产出 `prompt.md` / `memory-recall.json` / `reasonix-transcript.jsonl`,无 `final.md`、`result.json`、`process-handoff.md/json`,对应 worktree diff 均为空;该结果拒绝采纳。为保持推进速度,本批改用独立 worktree worker 继续产出候选 patch,Codex 仍按同一采纳规则复核。 - 2026-05-25:Batch C Worker B(`reasonix-2026-05-24T17-41-07-634Z-a39f8963`)在独立 worktree `/mnt/Data1T/mnote-worktrees/0525-c-worker-b-block-delete-undo` 未产出 `final.md`、`result.json` 或 `process-handoff.md/json`,worktree diff 为空,拒绝采纳;Codex 随后在同一 worktree 独立定位根因、产出候选 patch、通过 smoke 后移植到主工作区。 - 2026-05-25:Reasonix 只读审计 `sync_editor_overlay_state` / `try_sync_editor_overlay_state` 下一刀,使用独立 worktree `/mnt/Data1T/mnote-worktrees/0525-d-5-29-overlay-readonly`,run id `reasonix-2026-05-24T17-53-20-962Z-d3077308`。 - Codex 已读取 Hindsight recall、`process-handoff.md/json`、`result.json`,并核对 worktree `git status --short` / `git diff --stat` 为空;runner 已产出 completed handoff 但进程未自行退出,主控已终止残留 runner。 diff --git a/rust/crates/mnote-web/browser/local-upload-runtime.js b/rust/crates/mnote-web/browser/local-upload-runtime.js index fe663948..1df63d33 100644 --- a/rust/crates/mnote-web/browser/local-upload-runtime.js +++ b/rust/crates/mnote-web/browser/local-upload-runtime.js @@ -298,6 +298,66 @@ async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) { return false; } +async function uploadFileToMediaAsset(file, plan, options, deps) { + deps = deps || {}; + options = options || {}; + var getCurrentSourceKind = typeof deps.currentSourceKind === 'function' ? deps.currentSourceKind : function() { return ''; }; + var getCurrentRootUri = typeof deps.currentRootUri === 'function' ? deps.currentRootUri : currentRootUri; + var getCurrentDocumentId = typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId : function() { return ''; }; + var resolveEditorRoot = typeof deps.editorRootFromUploadOptions === 'function' + ? deps.editorRootFromUploadOptions + : function(nextOptions) { return editorRootFromUploadOptions(nextOptions, deps); }; + var insertIntoEditor = typeof deps.insertUploadedAssetIntoEditor === 'function' + ? deps.insertUploadedAssetIntoEditor + : function(asset, targetRoot) { return insertUploadedAssetIntoEditor(asset, targetRoot, deps); }; + var dispatch = typeof deps.dispatchEvent === 'function' + ? deps.dispatchEvent + : function(event) { return window.dispatchEvent(event); }; + var EventCtor = typeof deps.CustomEvent === 'function' ? deps.CustomEvent : window.CustomEvent; + if (getCurrentSourceKind() === 'local_folder') { + var rootUri = getCurrentRootUri(); + var documentId = String(plan && plan.targetDocumentId || getCurrentDocumentId() || '').trim(); + var localResult = await uploadLocalFolderAsset(file, plan, { + rootUri: rootUri, + documentId: documentId, + timeoutMs: 15000 + }); + var localAsset = localResult && localResult.asset ? localResult.asset : null; + if (!localAsset) { + throw new Error('上传失败'); + } + if (options.insertIntoEditor) { + await insertIntoEditor(localAsset, resolveEditorRoot(options)); + } + if (typeof deps.refreshLocalFolderSidebarSnapshot === 'function') { + void deps.refreshLocalFolderSidebarSnapshot(); + } + dispatch(new EventCtor('wolai:local-assets-changed', { + detail: { docId: documentId, asset: localAsset, assetIds: [localAsset.id] } + })); + return localAsset; + } + var mediaResult = await uploadMediaAsset(file, plan, { timeoutMs: 15000 }); + var mediaAsset = mediaResult && mediaResult.asset ? mediaResult.asset : null; + if (!mediaAsset) { + throw new Error('上传失败'); + } + if (typeof deps.appendUploadedAssetRow === 'function') { + deps.appendUploadedAssetRow(mediaAsset, plan && plan.targetDocumentId); + } + if (options.insertIntoEditor) { + await insertIntoEditor(mediaAsset, resolveEditorRoot(options)); + } + dispatch(new EventCtor('wolai:assets-changed', { + detail: { + docId: plan && plan.targetDocumentId, + asset: mediaAsset, + assetIds: [mediaAsset.id] + } + })); + return mediaAsset; +} + async function uploadFilesWithResolvedTarget(files, detail, options, deps) { deps = deps || {}; var resolveFileTreeUploadTarget = typeof deps.resolveFileTreeUploadTarget === 'function' ? deps.resolveFileTreeUploadTarget : null; @@ -460,6 +520,7 @@ window.__mnoteLocalUploadRuntime = { uploadLocalFolderAsset: uploadLocalFolderAsset, uploadMediaAsset: uploadMediaAsset, insertUploadedAssetIntoEditor: insertUploadedAssetIntoEditor, + uploadFileToMediaAsset: uploadFileToMediaAsset, uploadFilesWithResolvedTarget: uploadFilesWithResolvedTarget, localAssetOpenUrl: localAssetOpenUrl, uploadedAssetType: uploadedAssetType, diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index f3badb37..483fa86f 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -4014,6 +4014,25 @@ const SIDEBAR_TREE_JS: &str = r##" } async function uploadFileToMediaAsset(file, plan, options) { + var runtimeFn = localUploadRuntimeFunction('uploadFileToMediaAsset'); + if (runtimeFn) { + return await runtimeFn(file, plan, options || {}, { + currentSourceKind: currentSourceKind, + currentRootUri: currentRootUri, + currentDocumentId: currentDocumentId, + editorRootFromUploadOptions: editorRootFromUploadOptions, + appendUploadedAssetRow: appendUploadedAssetRow, + refreshLocalFolderSidebarSnapshot: refreshLocalFolderSidebarSnapshot, + dispatchEvent: function(event) { return window.dispatchEvent(event); }, + CustomEvent: CustomEvent, + buildOnlyOfficeAssetOpenUrl: buildOnlyOfficeAssetOpenUrl, + fetchCurrentOnlyOfficeUserId: fetchCurrentOnlyOfficeUserId, + buildOnlyOfficeOpenPath: buildOnlyOfficeOpenPath, + inferOnlyOfficeFileType: inferOnlyOfficeFileType, + enhanceEditorAttachmentLinks: enhanceEditorAttachmentLinks, + cssEscape: cssEscape + }); + } if (currentSourceKind() === 'local_folder') { var rootUri = currentRootUri(); var documentId = String(plan && plan.targetDocumentId || currentDocumentId() || '').trim(); @@ -10972,6 +10991,7 @@ mod tests { assert!( SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('insertUploadedAssetIntoEditor')") ); + assert!(SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('uploadFileToMediaAsset')")); assert!(SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('uploadMediaAsset')")); assert!( SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('uploadFilesWithResolvedTarget')") @@ -10993,8 +11013,10 @@ mod tests { assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadLocalFolderAsset")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadMediaAsset")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function insertUploadedAssetIntoEditor")); + assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFileToMediaAsset")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadFilesWithResolvedTarget")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("data-mnote-last-upload-inserted")); + assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFileToMediaAsset: uploadFileToMediaAsset")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFilesWithResolvedTarget")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot")); }