refactor: externalize upload and conflict helpers

This commit is contained in:
lix-2026
2026-05-25 01:13:08 +08:00
parent 7975ce0997
commit ac15424328
6 changed files with 128 additions and 15 deletions
@@ -71,3 +71,11 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
- 修改:`rust/crates/mnote-web/browser/local-upload-runtime.js``rust/crates/mnote-web/src/ssr/pages/layout.rs` - 修改:`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` - 已验证:`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` 仍未迁出。 - 未完成:`insertUploadedAssetIntoEditor``uploadFileToMediaAsset``uploadFilesWithResolvedTarget` 仍未迁出。
- 2026-05-25Codex 主控继续完成 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 仍未迁出。
@@ -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
};
@@ -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) { function localAssetOpenUrl(asset, download, context) {
if (!isLocalUploadedAsset(asset)) return ''; if (!isLocalUploadedAsset(asset)) return '';
var rootUri = String(context && context.rootUri || asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri(); var rootUri = String(context && context.rootUri || asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri();
@@ -275,6 +312,7 @@ window.__mnoteLocalUploadRuntime = {
editorRootFromUploadOptions: editorRootFromUploadOptions, editorRootFromUploadOptions: editorRootFromUploadOptions,
openEditorUploadFilePicker: openEditorUploadFilePicker, openEditorUploadFilePicker: openEditorUploadFilePicker,
fetchWithTimeout: fetchWithTimeout, fetchWithTimeout: fetchWithTimeout,
uploadLocalFolderAsset: uploadLocalFolderAsset,
localAssetOpenUrl: localAssetOpenUrl, localAssetOpenUrl: localAssetOpenUrl,
uploadedAssetType: uploadedAssetType, uploadedAssetType: uploadedAssetType,
fileTreeIconKindForFileName: fileTreeIconKindForFileName, fileTreeIconKindForFileName: fileTreeIconKindForFileName,
+5
View File
@@ -122,6 +122,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/mnote-browser-runtime/tree-shell-runtime.js", "/api/mnote-browser-runtime/tree-shell-runtime.js",
get(web_shell::tree_shell_runtime_asset), 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/documents", post(search::documents))
.route( .route(
"/api/search/local-index/refresh", "/api/search/local-index/refresh",
@@ -530,6 +534,7 @@ mod tests {
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js", "/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
"/api/mnote-browser-runtime/tree-live-controller.js", "/api/mnote-browser-runtime/tree-live-controller.js",
"/api/mnote-browser-runtime/tree-shell-runtime.js", "/api/mnote-browser-runtime/tree-shell-runtime.js",
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
] { ] {
let response = app(false) let response = app(false)
.oneshot( .oneshot(
@@ -246,6 +246,7 @@ pub async fn document_page_shell(
{} {}
{} {}
{} {}
{}
</body> </body>
</html>"#, </html>"#,
escape_html(title), escape_html(title),
@@ -270,6 +271,7 @@ pub async fn document_page_shell(
.unwrap_or_default(), .unwrap_or_default(),
render_document_title_controller_script(), render_document_title_controller_script(),
render_editor_island_adapter_script(), render_editor_island_adapter_script(),
r#"<script type="module" src="/api/mnote-browser-runtime/document-conflict-panel-runtime.js"></script>"#.to_string(),
); );
let mut response = Html(html).into_response(); let mut response = Html(html).into_response();
stamp_shell_headers(response.headers_mut(), "document"); stamp_shell_headers(response.headers_mut(), "document");
@@ -1896,6 +1898,11 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
}; };
const clearSessionConflictSurface = (session) => { const clearSessionConflictSurface = (session) => {
var _rt_ = window.__mnoteDocumentConflictPanelRuntime;
if (_rt_ && typeof _rt_.clearSessionConflictSurface === 'function') {
_rt_.clearSessionConflictSurface(session, { sessionViews: sessionViews });
return;
}
sessionViews(session).forEach((view) => { sessionViews(session).forEach((view) => {
const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root; const host = view.runtimeDescriptor.root.closest('.document-pane') || view.runtimeDescriptor.root;
if (!(host instanceof HTMLElement)) return; if (!(host instanceof HTMLElement)) return;
@@ -4465,6 +4472,21 @@ pub async fn tree_shell_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty())) .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 { pub async fn leptos_tiptap_manifest() -> Response {
let manifest = json!({ let manifest = json!({
"entryAssetPath": "mnote-leptos-tiptap-spike-island.js", "entryAssetPath": "mnote-leptos-tiptap-spike-island.js",
+27 -15
View File
@@ -4003,21 +4003,32 @@ const SIDEBAR_TREE_JS: &str = r##"
if (!rootUri || !uploadIntent) { if (!rootUri || !uploadIntent) {
throw new Error(' Markdown rootUri documentId'); throw new Error(' Markdown rootUri documentId');
} }
var localForm = new FormData(); var localUploadRuntime = localUploadRuntimeFunction('uploadLocalFolderAsset');
localForm.append('file', file); var localPayload = null;
localForm.append('rootUri', rootUri); if (localUploadRuntime) {
localForm.append('uploadIntent', uploadIntent); var localResult = await localUploadRuntime(file, plan, {
if (documentId) localForm.append('documentId', documentId); rootUri: rootUri,
if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || '')); documentId: documentId,
localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment'); timeoutMs: 15000
var localResponse = await fetchWithTimeout('/api/local-folder/assets/upload', { });
method: 'POST', localPayload = localResult && localResult.asset ? { asset: localResult.asset } : null;
credentials: 'include', } else {
body: localForm var localForm = new FormData();
}, 15000, ''); localForm.append('file', file);
var localPayload = await localResponse.json().catch(function() { return null; }); localForm.append('rootUri', rootUri);
if (!localResponse.ok || !localPayload || !localPayload.asset) { localForm.append('uploadIntent', uploadIntent);
throw new Error(localPayload && localPayload.error ? localPayload.error : ''); 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) { if (options && options.insertIntoEditor) {
await insertUploadedAssetIntoEditor(localPayload.asset, editorRootFromUploadOptions(options)); 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 editorRootFromUploadOptions"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function openEditorUploadFilePicker")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function openEditorUploadFilePicker"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("function fetchWithTimeout")); 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("uploadFilesWithResolvedTarget"));
assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot")); assert!(LOCAL_UPLOAD_RUNTIME_JS.contains("__mnoteLastEditorUploadRoot"));
} }