From ac1542432812aeff6eb4b08e8ab5cee0a3080b6e Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Mon, 25 May 2026 01:13:08 +0800 Subject: [PATCH] refactor: externalize upload and conflict helpers --- ...0-browser-runtime-followup-checklist-v1.md | 8 ++++ .../document-conflict-panel-runtime.js | 28 +++++++++++++ .../mnote-web/browser/local-upload-runtime.js | 38 +++++++++++++++++ rust/crates/mnote-web/src/routes/mod.rs | 5 +++ rust/crates/mnote-web/src/routes/web_shell.rs | 22 ++++++++++ rust/crates/mnote-web/src/ssr/pages/layout.rs | 42 ++++++++++++------- 6 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 rust/crates/mnote-web/browser/document-conflict-panel-runtime.js 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 efd5fded..09a32880 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 @@ -71,3 +71,11 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - 修改:`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 mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_upload_runtime_contains_editor_upload_context_helpers -- --test-threads=1`。 - 未完成:`insertUploadedAssetIntoEditor`、`uploadFileToMediaAsset`、`uploadFilesWithResolvedTarget` 仍未迁出。 +- 2026-05-25:Codex 主控继续完成 local upload API 小切片:`local-upload-runtime.js` 新增并导出 `uploadLocalFolderAsset`,承接本地上传 FormData 构造、`/api/local-folder/assets/upload` 请求和响应解析;`layout.rs` 仍负责 editor 插入、sidebar snapshot refresh 和 `wolai:local-assets-changed` 事件。 + - 修改:`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 scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 + - 未完成:`insertUploadedAssetIntoEditor`、`uploadFileToMediaAsset` 编排壳、`uploadFilesWithResolvedTarget` 仍未迁出。 +- 2026-05-25:采纳并复核额外 Reasonix 候选 patch:新增 `document-conflict-panel-runtime.js`,把 document session conflict panel 的纯 DOM 清理 helper `clearSessionConflictSurface` 外置;inline adapter 保留 fallback。 + - 修改:`rust/crates/mnote-web/browser/document-conflict-panel-runtime.js`、`rust/crates/mnote-web/src/routes/web_shell.rs`、`rust/crates/mnote-web/src/routes/mod.rs`。 + - 已验证:`node --check rust/crates/mnote-web/browser/document-conflict-panel-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 mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`。 + - 未完成:conflict merge / accept / keep 业务逻辑、document session lifecycle、secondary pane conflict smoke 仍未迁出。 diff --git a/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js b/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js new file mode 100644 index 00000000..f2c1cb08 --- /dev/null +++ b/rust/crates/mnote-web/browser/document-conflict-panel-runtime.js @@ -0,0 +1,28 @@ +// MNote document conflict panel 运行时外置模块。 +// 第一刀:纯 DOM 清理 helper clearSessionConflictSurface。 +// 本文件以 type="module" 加载,执行时机可能晚于 inline script。 +// web_shell.rs 的 inline clearSessionConflictSurface 会优先委托到 +// window.__mnoteDocumentConflictPanelRuntime; +// 模块尚未加载或加载失败时,inline script 保留原地实现兜底。 + +/** + * 清除指定 session 所有视图中冲突面板 DOM。 + * 只做 DOM 清理,不处理冲突 merge/accept/keep 业务逻辑。 + * @param {Object} session - document session 对象 + * @param {Object} deps - 依赖注入,从 inline 上下文传入 + * @param {Function} deps.sessionViews - session.views 迭代器 + */ +function clearSessionConflictSurface(session, deps) { + var views = deps && typeof deps.sessionViews === 'function' + ? deps.sessionViews(session) + : []; + views.forEach(function(view) { + var host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root; + if (!(host instanceof HTMLElement)) return; + host.querySelectorAll('[data-testid="mnote-editor-conflict-panel"]').forEach(function(node) { node.remove(); }); + }); +} + +window.__mnoteDocumentConflictPanelRuntime = { + clearSessionConflictSurface: clearSessionConflictSurface +}; diff --git a/rust/crates/mnote-web/browser/local-upload-runtime.js b/rust/crates/mnote-web/browser/local-upload-runtime.js index 33b91be9..49b6e7f3 100644 --- a/rust/crates/mnote-web/browser/local-upload-runtime.js +++ b/rust/crates/mnote-web/browser/local-upload-runtime.js @@ -146,6 +146,43 @@ async function fetchWithTimeout(input, init, timeoutMs, label) { } } +async function uploadLocalFolderAsset(file, plan, context) { + var rootUri = String(context && context.rootUri || '').trim() || currentRootUri(); + var documentId = String( + plan && plan.targetDocumentId + || context && context.documentId + || '' + ).trim(); + var hasFolderTarget = plan && Object.prototype.hasOwnProperty.call(plan, 'targetRelativePath'); + var uploadIntent = String( + plan && plan.uploadIntent + || (hasFolderTarget ? 'filetree.folder.drop' : 'editor.markdown.attach') + ).trim(); + if (!rootUri || !uploadIntent) { + throw new Error('本地 Markdown 上传缺少 rootUri 或 documentId'); + } + var localForm = new FormData(); + localForm.append('file', file); + localForm.append('rootUri', rootUri); + localForm.append('uploadIntent', uploadIntent); + if (documentId) localForm.append('documentId', documentId); + if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || '')); + localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment'); + var localResponse = await fetchWithTimeout('/api/local-folder/assets/upload', { + method: 'POST', + credentials: 'include', + body: localForm + }, Number(context && context.timeoutMs) || 15000, '本地上传'); + var localPayload = await localResponse.json().catch(function() { return null; }); + if (!localResponse.ok || !localPayload || !localPayload.asset) { + throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败'); + } + return { + asset: localPayload.asset, + documentId: documentId + }; +} + function localAssetOpenUrl(asset, download, context) { if (!isLocalUploadedAsset(asset)) return ''; var rootUri = String(context && context.rootUri || asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri(); @@ -275,6 +312,7 @@ window.__mnoteLocalUploadRuntime = { editorRootFromUploadOptions: editorRootFromUploadOptions, openEditorUploadFilePicker: openEditorUploadFilePicker, fetchWithTimeout: fetchWithTimeout, + uploadLocalFolderAsset: uploadLocalFolderAsset, localAssetOpenUrl: localAssetOpenUrl, uploadedAssetType: uploadedAssetType, fileTreeIconKindForFileName: fileTreeIconKindForFileName, diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index ad7edcda..9f8de906 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -122,6 +122,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/tree-shell-runtime.js", get(web_shell::tree_shell_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/document-conflict-panel-runtime.js", + get(web_shell::document_conflict_panel_runtime_asset), + ) .route("/api/search/documents", post(search::documents)) .route( "/api/search/local-index/refresh", @@ -530,6 +534,7 @@ mod tests { "/api/mnote-browser-runtime/filetree-context-menu-runtime.js", "/api/mnote-browser-runtime/tree-live-controller.js", "/api/mnote-browser-runtime/tree-shell-runtime.js", + "/api/mnote-browser-runtime/document-conflict-panel-runtime.js", ] { let response = app(false) .oneshot( diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 4577cf17..eb87c473 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -246,6 +246,7 @@ pub async fn document_page_shell( {} {} {} + {} "#, escape_html(title), @@ -270,6 +271,7 @@ pub async fn document_page_shell( .unwrap_or_default(), render_document_title_controller_script(), render_editor_island_adapter_script(), + r#""#.to_string(), ); let mut response = Html(html).into_response(); stamp_shell_headers(response.headers_mut(), "document"); @@ -1896,6 +1898,11 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { }; const clearSessionConflictSurface = (session) => { + var _rt_ = window.__mnoteDocumentConflictPanelRuntime; + if (_rt_ && typeof _rt_.clearSessionConflictSurface === 'function') { + _rt_.clearSessionConflictSurface(session, { sessionViews: sessionViews }); + return; + } sessionViews(session).forEach((view) => { const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root; if (!(host instanceof HTMLElement)) return; @@ -4465,6 +4472,21 @@ pub async fn tree_shell_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn document_conflict_panel_runtime_asset() -> Response { + // include_str! 路径相对于当前源文件 (src/routes/web_shell.rs -> ../../browser/) + const JS: &str = include_str!("../../browser/document-conflict-panel-runtime.js"); + Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + ) + .header(header::CACHE_CONTROL, "no-store") + .header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .body(Body::from(JS)) + .unwrap_or_else(|_| Response::new(Body::empty())) +} + pub async fn leptos_tiptap_manifest() -> Response { let manifest = json!({ "entryAssetPath": "mnote-leptos-tiptap-spike-island.js", diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 5eef2295..eedc6ff4 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -4003,21 +4003,32 @@ const SIDEBAR_TREE_JS: &str = r##" if (!rootUri || !uploadIntent) { throw new Error('本地 Markdown 上传缺少 rootUri 或 documentId'); } - var localForm = new FormData(); - localForm.append('file', file); - localForm.append('rootUri', rootUri); - localForm.append('uploadIntent', uploadIntent); - if (documentId) localForm.append('documentId', documentId); - if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || '')); - localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment'); - var localResponse = await fetchWithTimeout('/api/local-folder/assets/upload', { - method: 'POST', - credentials: 'include', - body: localForm - }, 15000, '本地上传'); - var localPayload = await localResponse.json().catch(function() { return null; }); - if (!localResponse.ok || !localPayload || !localPayload.asset) { - throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败'); + var localUploadRuntime = localUploadRuntimeFunction('uploadLocalFolderAsset'); + var localPayload = null; + if (localUploadRuntime) { + var localResult = await localUploadRuntime(file, plan, { + rootUri: rootUri, + documentId: documentId, + timeoutMs: 15000 + }); + localPayload = localResult && localResult.asset ? { asset: localResult.asset } : null; + } else { + var localForm = new FormData(); + localForm.append('file', file); + localForm.append('rootUri', rootUri); + localForm.append('uploadIntent', uploadIntent); + if (documentId) localForm.append('documentId', documentId); + if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || '')); + localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment'); + var localResponse = await fetchWithTimeout('/api/local-folder/assets/upload', { + method: 'POST', + credentials: 'include', + body: localForm + }, 15000, '本地上传'); + localPayload = await localResponse.json().catch(function() { return null; }); + if (!localResponse.ok || !localPayload || !localPayload.asset) { + throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败'); + } } if (options && options.insertIntoEditor) { await insertUploadedAssetIntoEditor(localPayload.asset, editorRootFromUploadOptions(options)); @@ -10935,6 +10946,7 @@ mod tests { assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function editorRootFromUploadOptions")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function openEditorUploadFilePicker")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function fetchWithTimeout")); + assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function uploadLocalFolderAsset")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFilesWithResolvedTarget")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot")); }