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 e994ece9..fc38ed29 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 @@ -83,3 +83,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据 - `reasonix-2026-05-24T17-07-59-638Z-29274a46` 产生 handoff,但内容串到 Worker B 的 block menu 任务,worktree 无 diff,拒绝采纳。 - `reasonix-2026-05-24T17-12-41-044Z-25b9d5d7` 仅产生 `prompt.md` / `memory-recall.json` / `reasonix-transcript.jsonl`,无 `final.md`、`result.json`、`process-handoff.md/json`,worktree 无 diff,拒绝采纳。 - 该项保持未完成;下一刀应进一步缩窄为只迁 editor 插入 DOM 拼接中的一个纯 helper,或由 Codex 本地实现后再交 Reasonix 做浏览器 smoke。 +- 2026-05-25:Codex 主控完成 `insertUploadedAssetIntoEditor` 外置切片:`local-upload-runtime.js` 新增并导出 `insertUploadedAssetIntoEditor(asset, targetRoot, deps)`,`layout.rs` 优先委托 runtime,保留完整 inline 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 scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`。 + - 未完成:`uploadFileToMediaAsset` 编排壳、`uploadFilesWithResolvedTarget`、conflict merge / accept / keep 业务逻辑、secondary pane conflict smoke 仍未迁出。 diff --git a/rust/crates/mnote-web/browser/local-upload-runtime.js b/rust/crates/mnote-web/browser/local-upload-runtime.js index 49b6e7f3..543c4fc2 100644 --- a/rust/crates/mnote-web/browser/local-upload-runtime.js +++ b/rust/crates/mnote-web/browser/local-upload-runtime.js @@ -183,6 +183,101 @@ async function uploadLocalFolderAsset(file, plan, context) { }; } +async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) { + deps = deps || {}; + var editorRoot = targetRoot instanceof HTMLElement + ? targetRoot.querySelector('.editor-surface .ProseMirror') + : document.querySelector('.editor-surface .ProseMirror'); + if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot; + var editor = editorRoot && editorRoot.editor; + if (!editor || !editor.chain) { + document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false'); + document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'editor_unavailable'); + return false; + } + var title = typeof deps.uploadedAssetTitle === 'function' ? deps.uploadedAssetTitle(asset) : uploadedAssetTitle(asset); + var localOpenUrl = typeof deps.localAssetOpenUrl === 'function' ? deps.localAssetOpenUrl(asset, false) : localAssetOpenUrl(asset, false); + var fallbackUrl = typeof deps.uploadedAssetUrl === 'function' ? deps.uploadedAssetUrl(asset) : uploadedAssetUrl(asset); + var url = localOpenUrl || fallbackUrl; + var type = typeof deps.uploadedAssetType === 'function' ? deps.uploadedAssetType(asset) : uploadedAssetType(asset); + var assetId = String(asset && asset.id || '').trim(); + var sizeLabel = typeof deps.uploadedFileSize === 'function' ? deps.uploadedFileSize(asset) : uploadedFileSize(asset); + try { + if (type === 'image' && url) { + return editor.chain().focus().setImage({ src: url, alt: title, title: title }).run() === true; + } + var isLocalAsset = typeof deps.isLocalUploadedAsset === 'function' ? deps.isLocalUploadedAsset(asset) : isLocalUploadedAsset(asset); + var userId = ''; + var buildOnlyOfficeAssetOpenUrl = typeof deps.buildOnlyOfficeAssetOpenUrl === 'function' ? deps.buildOnlyOfficeAssetOpenUrl : function() { return ''; }; + var onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId); + if (onlyOfficeUrl && assetId) { + userId = typeof deps.fetchCurrentOnlyOfficeUserId === 'function' ? await deps.fetchCurrentOnlyOfficeUserId() : ''; + onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId); + } + var href = onlyOfficeUrl || url; + if (href) { + var storedHref = onlyOfficeUrl + ? (isLocalAsset ? onlyOfficeUrl : deps.buildOnlyOfficeOpenPath({ + fileUrl: '', + fileName: title, + fileType: deps.inferOnlyOfficeFileType(title, asset && asset.mime_type) || 'docx', + assetId: assetId, + documentId: String(asset && (asset.document_id || asset.documentId) || deps.currentDocumentId() || '').trim(), + userId: userId || '', + mode: 'view' + })) + : href; + var inserted = editor.chain().focus().insertContent([ + { + type: 'paragraph', + content: [{ + type: 'text', + text: title, + marks: [{ + type: 'link', + attrs: { + href: storedHref, + target: '_blank', + rel: 'noopener noreferrer nofollow', + class: typeof deps.uploadedAttachmentClass === 'function' ? deps.uploadedAttachmentClass(asset) : uploadedAttachmentClass(asset) + } + }] + }] + }, + { type: 'paragraph' } + ]).focus('end').run() === true; + if (inserted) { + document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'true'); + document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId || ''); + document.documentElement.removeAttribute('data-mnote-last-upload-insert-error'); + } else { + document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false'); + document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'insert_content_failed'); + } + window.setTimeout(function() { + if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot; + if (typeof deps.enhanceEditorAttachmentLinks === 'function') deps.enhanceEditorAttachmentLinks(); + var cssEscape = typeof deps.cssEscape === 'function' ? deps.cssEscape : function(value) { return String(value || '').replace(/"/g, '\\"'); }; + var selector = assetId + ? '.editor-surface .ProseMirror a[href*="' + cssEscape(assetId) + '"]' + : '.editor-surface .ProseMirror a'; + var link = targetRoot instanceof HTMLElement + ? targetRoot.querySelector(selector) + : document.querySelector(selector); + if (link instanceof HTMLElement) { + link.setAttribute('data-mnote-attachment-link', 'true'); + if (assetId) link.setAttribute('data-asset-id', assetId); + if (sizeLabel) link.setAttribute('data-file-size', sizeLabel); + } + }, 0); + return inserted; + } + } catch (error) { + console.warn('[mnote upload] insert uploaded asset failed', error); + } + return false; +} + function localAssetOpenUrl(asset, download, context) { if (!isLocalUploadedAsset(asset)) return ''; var rootUri = String(context && context.rootUri || asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri(); @@ -313,6 +408,7 @@ window.__mnoteLocalUploadRuntime = { openEditorUploadFilePicker: openEditorUploadFilePicker, fetchWithTimeout: fetchWithTimeout, uploadLocalFolderAsset: uploadLocalFolderAsset, + insertUploadedAssetIntoEditor: insertUploadedAssetIntoEditor, localAssetOpenUrl: localAssetOpenUrl, uploadedAssetType: uploadedAssetType, fileTreeIconKindForFileName: fileTreeIconKindForFileName, diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index eedc6ff4..8b8f5e1f 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -3905,6 +3905,25 @@ const SIDEBAR_TREE_JS: &str = r##" } async function insertUploadedAssetIntoEditor(asset, targetRoot) { + var runtimeFn = localUploadRuntimeFunction('insertUploadedAssetIntoEditor'); + if (runtimeFn) { + return await runtimeFn(asset, targetRoot, { + uploadedAssetTitle: uploadedAssetTitle, + localAssetOpenUrl: localAssetOpenUrl, + uploadedAssetUrl: uploadedAssetUrl, + uploadedAssetType: uploadedAssetType, + isLocalUploadedAsset: isLocalUploadedAsset, + buildOnlyOfficeAssetOpenUrl: buildOnlyOfficeAssetOpenUrl, + fetchCurrentOnlyOfficeUserId: fetchCurrentOnlyOfficeUserId, + buildOnlyOfficeOpenPath: buildOnlyOfficeOpenPath, + inferOnlyOfficeFileType: inferOnlyOfficeFileType, + currentDocumentId: currentDocumentId, + uploadedAttachmentClass: uploadedAttachmentClass, + uploadedFileSize: uploadedFileSize, + cssEscape: cssEscape, + enhanceEditorAttachmentLinks: enhanceEditorAttachmentLinks + }); + } var editorRoot = targetRoot instanceof HTMLElement ? targetRoot.querySelector('.editor-surface .ProseMirror') : document.querySelector('.editor-surface .ProseMirror'); @@ -10932,6 +10951,9 @@ mod tests { assert!( SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('openEditorUploadFilePicker')") ); + assert!( + SIDEBAR_TREE_JS.contains("localUploadRuntimeFunction('insertUploadedAssetIntoEditor')") + ); assert!(SIDEBAR_TREE_JS.contains("__mnoteLastEditorUploadRoot")); assert!(SIDEBAR_TREE_JS.contains("data-pane-role=\"primary\"")); assert!(SIDEBAR_TREE_JS.contains("insertUploadedAssetIntoEditor(localPayload.asset, editorRootFromUploadOptions(options))")); @@ -10947,6 +10969,8 @@ mod tests { 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("function insertUploadedAssetIntoEditor")); + assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("data-mnote-last-upload-inserted")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("uploadFilesWithResolvedTarget")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot")); }