2026-05-24 22:42:53 +08:00
|
|
|
|
// MNote 本地上传运行时外置模块。
|
|
|
|
|
|
// 当前先承接 SIDEBAR_TREE_JS 中的纯辅助函数。
|
|
|
|
|
|
// SIDEBAR_TREE_JS 会优先委托到 window.__mnoteLocalUploadRuntime;
|
|
|
|
|
|
// 模块未加载时,inline fallback 保持旧行为。
|
2026-05-26 09:44:35 +08:00
|
|
|
|
import {
|
|
|
|
|
|
editorDocumentFromTiptapDocument,
|
|
|
|
|
|
legacyBlocksFromEditorDocument,
|
|
|
|
|
|
} from './document-tiptap-conversion-runtime.js';
|
2026-05-24 22:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
function uploadedAssetTitle(asset) {
|
|
|
|
|
|
return String(asset && (asset.file_name || asset.title || asset.name) || '未命名附件').trim() || '未命名附件';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 17:04:27 +08:00
|
|
|
|
/** Reject path escape segments and null bytes after light decode. */
|
|
|
|
|
|
function hasPathEscape(value) {
|
|
|
|
|
|
var s = String(value || '').replace(/\\/g, '/');
|
|
|
|
|
|
try {
|
|
|
|
|
|
s = decodeURIComponent(s);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// keep raw
|
|
|
|
|
|
}
|
|
|
|
|
|
s = s.replace(/\\/g, '/');
|
|
|
|
|
|
if (s.indexOf('\0') >= 0) return true;
|
|
|
|
|
|
var parts = s.split('/');
|
|
|
|
|
|
for (var i = 0; i < parts.length; i += 1) {
|
|
|
|
|
|
if (parts[i] === '..') return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Allow only safe URL schemes for editor img/src or link href. */
|
|
|
|
|
|
function isSafeAssetUrl(url) {
|
|
|
|
|
|
var value = String(url || '').trim();
|
|
|
|
|
|
if (!value) return false;
|
|
|
|
|
|
if (value.charAt(0) === '#' || value.charAt(0) === '/' || value.indexOf('./') === 0) {
|
|
|
|
|
|
return !hasPathEscape(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
// scheme-relative
|
|
|
|
|
|
if (value.indexOf('//') === 0) return false;
|
|
|
|
|
|
var colon = value.indexOf(':');
|
|
|
|
|
|
if (colon < 0) {
|
|
|
|
|
|
// relative path without scheme
|
|
|
|
|
|
return !hasPathEscape(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
var scheme = value.slice(0, colon).toLowerCase();
|
|
|
|
|
|
if (scheme === 'http' || scheme === 'https' || scheme === 'blob' || scheme === 'data') {
|
|
|
|
|
|
// data: raster images only. Reject svg+xml (can embed script even when base64 hides "script").
|
|
|
|
|
|
if (scheme === 'data') {
|
|
|
|
|
|
if (!/^data:image\//i.test(value)) return false;
|
|
|
|
|
|
if (/^data:image\/svg\+xml/i.test(value)) return false;
|
|
|
|
|
|
// deny explicit script markers in non-svg image payloads
|
|
|
|
|
|
if (value.toLowerCase().indexOf('script') >= 0) return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:42:53 +08:00
|
|
|
|
function uploadedAssetUrl(asset) {
|
2026-07-28 17:04:27 +08:00
|
|
|
|
var raw = String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
|
|
|
|
|
|
return isSafeAssetUrl(raw) ? raw : '';
|
2026-05-24 22:42:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 11:13:05 +08:00
|
|
|
|
function uploadedAssetMarkdownHref(asset) {
|
|
|
|
|
|
var href = String(asset && (asset.markdownHref || asset.markdown_href) || '').trim();
|
|
|
|
|
|
if (href) {
|
2026-07-28 17:04:27 +08:00
|
|
|
|
if (hasPathEscape(href) || !isSafeAssetUrl(href)) return '';
|
2026-05-29 11:13:05 +08:00
|
|
|
|
return href;
|
|
|
|
|
|
}
|
|
|
|
|
|
var relativePath = String(asset && (asset.markdownRelativePath || asset.markdown_relative_path) || '').trim().replace(/\\/g, '/');
|
2026-07-28 17:04:27 +08:00
|
|
|
|
if (!relativePath || hasPathEscape(relativePath)) return '';
|
2026-05-29 11:13:05 +08:00
|
|
|
|
if (relativePath.indexOf('./') === 0) return relativePath;
|
|
|
|
|
|
return './' + relativePath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 09:44:35 +08:00
|
|
|
|
function dispatchUploadedEditorChange(editorRoot, editor, deps) {
|
|
|
|
|
|
deps = deps || {};
|
|
|
|
|
|
if (!(editorRoot instanceof HTMLElement) || !editor || typeof editor.getJSON !== 'function') return;
|
|
|
|
|
|
var runtimeRoot = editorRoot.closest('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|
|
|
|
|
if (!(runtimeRoot instanceof HTMLElement) || typeof CustomEvent !== 'function') return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
editorRoot.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
var currentDocumentId = typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId() : '';
|
|
|
|
|
|
var payload = {
|
|
|
|
|
|
protocol: 'mnote.leptos_tiptap.bridge.v1',
|
|
|
|
|
|
runtime: '8123-leptos-tiptap-runtime',
|
|
|
|
|
|
version: '1.1.0',
|
|
|
|
|
|
source: 'mnote:leptos-tiptap-spike',
|
|
|
|
|
|
event: 'mnote:leptos-tiptap-spike:change',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
documentId: String(currentDocumentId || '').trim() || null,
|
|
|
|
|
|
workspaceId: null,
|
|
|
|
|
|
title: '',
|
|
|
|
|
|
content: editor.getJSON(),
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
dirtyCount: Date.now(),
|
|
|
|
|
|
editorFocused: document.activeElement === editorRoot || editorRoot.contains(document.activeElement),
|
|
|
|
|
|
slashOpen: false,
|
|
|
|
|
|
toolbarOpen: false,
|
|
|
|
|
|
selectedBlockIndex: null,
|
|
|
|
|
|
revision: null,
|
|
|
|
|
|
conflictDetectionKey: null,
|
|
|
|
|
|
readOnly: false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
runtimeRoot.dispatchEvent(new CustomEvent('mnote:leptos-tiptap-spike:change', {
|
|
|
|
|
|
detail: payload,
|
|
|
|
|
|
bubbles: true
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 22:01:44 +08:00
|
|
|
|
function firstNonEmptyString(values) {
|
|
|
|
|
|
for (var i = 0; i < values.length; i += 1) {
|
|
|
|
|
|
var value = String(values[i] || '').trim();
|
|
|
|
|
|
if (value) return value;
|
2026-05-26 09:44:35 +08:00
|
|
|
|
}
|
2026-05-28 22:01:44 +08:00
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function makeSaveOperationToken(prefix, documentId) {
|
|
|
|
|
|
var doc = String(documentId || 'unknown').replace(/[^a-zA-Z0-9._:-]+/g, '-').slice(0, 80);
|
|
|
|
|
|
var random = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
|
|
|
|
? crypto.randomUUID()
|
|
|
|
|
|
: Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);
|
|
|
|
|
|
return prefix + ':' + doc + ':' + random;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentConflictDetectionKey(documentId, rootUri) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (typeof window.__mnoteDebugDocumentSessions?.snapshot === 'function') {
|
|
|
|
|
|
var snapshot = window.__mnoteDebugDocumentSessions.snapshot();
|
|
|
|
|
|
var sessions = Array.isArray(snapshot && snapshot.sessions) ? snapshot.sessions : [];
|
|
|
|
|
|
var targetDocumentId = String(documentId || '').trim();
|
|
|
|
|
|
var targetRootUri = String(rootUri || '').trim();
|
|
|
|
|
|
for (var i = 0; i < sessions.length; i += 1) {
|
|
|
|
|
|
var session = sessions[i] || {};
|
|
|
|
|
|
if (targetDocumentId && String(session.documentId || '').trim() !== targetDocumentId) continue;
|
|
|
|
|
|
if (targetRootUri && String(session.rootUri || '').trim() !== targetRootUri) continue;
|
|
|
|
|
|
var sessionKey = firstNonEmptyString([
|
|
|
|
|
|
session.lastExternalConflictDetectionKey,
|
|
|
|
|
|
session.conflictDetectionKey,
|
|
|
|
|
|
session.fileVersion
|
|
|
|
|
|
]);
|
|
|
|
|
|
if (sessionKey) return sessionKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// 继续尝试从 page aggregate script 读取版本。
|
|
|
|
|
|
}
|
|
|
|
|
|
var script = document.getElementById('__MNOTE_PAGE_AGGREGATE__');
|
|
|
|
|
|
if (script) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var aggregate = JSON.parse(script.textContent || 'null');
|
|
|
|
|
|
var body = aggregate && typeof aggregate.body === 'object' ? aggregate.body : null;
|
|
|
|
|
|
var key = firstNonEmptyString([
|
|
|
|
|
|
body && body.conflictDetectionKey,
|
|
|
|
|
|
body && body.conflict_detection_key,
|
|
|
|
|
|
body && body.fileVersion,
|
|
|
|
|
|
body && body.file_version,
|
|
|
|
|
|
aggregate && aggregate.conflictDetectionKey,
|
|
|
|
|
|
aggregate && aggregate.conflict_detection_key,
|
|
|
|
|
|
aggregate && aggregate.fileVersion,
|
|
|
|
|
|
aggregate && aggregate.file_version
|
|
|
|
|
|
]);
|
|
|
|
|
|
if (key) return key;
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function localUploadSessionConflict(documentId, rootUri) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (typeof window.__mnoteDebugDocumentSessions?.snapshot !== 'function') return null;
|
|
|
|
|
|
var snapshot = window.__mnoteDebugDocumentSessions.snapshot();
|
|
|
|
|
|
var sessions = Array.isArray(snapshot && snapshot.sessions) ? snapshot.sessions : [];
|
|
|
|
|
|
var targetDocumentId = String(documentId || '').trim();
|
|
|
|
|
|
var targetRootUri = String(rootUri || '').trim();
|
|
|
|
|
|
for (var i = 0; i < sessions.length; i += 1) {
|
|
|
|
|
|
var session = sessions[i] || {};
|
|
|
|
|
|
if (targetDocumentId && String(session.documentId || '').trim() !== targetDocumentId) continue;
|
|
|
|
|
|
if (targetRootUri && String(session.rootUri || '').trim() !== targetRootUri) continue;
|
|
|
|
|
|
var status = String(session.status || '').trim();
|
|
|
|
|
|
if (status === 'external-change-conflict' || session.hasExternalConflict === true || session.lastExternalConflictEnvelope) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: status || 'external-change-conflict',
|
|
|
|
|
|
dirtyState: String(session.dirtyState || ''),
|
|
|
|
|
|
message: String(session.lastExternalConflictEnvelope?.message || '当前文档存在文件冲突,请先处理冲突后再上传附件')
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
2026-05-26 09:44:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function persistLocalFolderSelfChangeSuppression(documentId, expiresAt) {
|
|
|
|
|
|
var doc = String(documentId || '').trim();
|
|
|
|
|
|
if (!doc) return;
|
|
|
|
|
|
window.__mnoteLocalFolderSelfChangeSuppressions = window.__mnoteLocalFolderSelfChangeSuppressions || new Map();
|
|
|
|
|
|
window.__mnoteLocalFolderSelfChangeSuppressions.set(doc, expiresAt);
|
|
|
|
|
|
try {
|
|
|
|
|
|
var key = 'mnote.localFolder.selfChangeSuppressions.v1';
|
|
|
|
|
|
var existing = JSON.parse(window.sessionStorage.getItem(key) || '{}');
|
|
|
|
|
|
existing[doc] = expiresAt;
|
|
|
|
|
|
window.sessionStorage.setItem(key, JSON.stringify(existing));
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function persistUploadedEditorChange(editor, asset, deps) {
|
|
|
|
|
|
deps = deps || {};
|
|
|
|
|
|
var isLocalAsset = typeof deps.isLocalUploadedAsset === 'function' ? deps.isLocalUploadedAsset(asset) : isLocalUploadedAsset(asset);
|
|
|
|
|
|
if (!isLocalAsset || !editor || typeof editor.getJSON !== 'function') return;
|
|
|
|
|
|
var documentId = String(
|
|
|
|
|
|
asset && (asset.document_id || asset.documentId)
|
|
|
|
|
|
|| (typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId() : '')
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
).trim();
|
|
|
|
|
|
var rootUri = String(
|
|
|
|
|
|
asset && (asset.root_uri || asset.rootUri)
|
|
|
|
|
|
|| (typeof deps.currentRootUri === 'function' ? deps.currentRootUri() : currentRootUri())
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
).trim();
|
|
|
|
|
|
if (!documentId || !rootUri) return;
|
|
|
|
|
|
var tiptapDocument = editor.getJSON();
|
|
|
|
|
|
var editorDocument = editorDocumentFromTiptapDocument({ documentId: documentId }, tiptapDocument);
|
|
|
|
|
|
var content = legacyBlocksFromEditorDocument(editorDocument);
|
|
|
|
|
|
var savePayload = {
|
|
|
|
|
|
documentId: documentId,
|
|
|
|
|
|
sourceKind: 'local_folder',
|
|
|
|
|
|
rootUri: rootUri,
|
2026-05-28 22:01:44 +08:00
|
|
|
|
expectedFileVersion: currentConflictDetectionKey(documentId, rootUri),
|
|
|
|
|
|
writeIntentId: makeSaveOperationToken('intent:local-upload', documentId),
|
|
|
|
|
|
saveOperationId: makeSaveOperationToken('save:local-upload', documentId),
|
2026-05-26 09:44:35 +08:00
|
|
|
|
contentFormat: 'editorBlocks',
|
|
|
|
|
|
editorSource: 'local-upload-runtime',
|
|
|
|
|
|
editorDocument: editorDocument,
|
|
|
|
|
|
content: content,
|
|
|
|
|
|
tiptapDocument: tiptapDocument,
|
|
|
|
|
|
blockCount: Array.isArray(editorDocument.blocks) ? editorDocument.blocks.length : 0
|
|
|
|
|
|
};
|
|
|
|
|
|
persistLocalFolderSelfChangeSuppression(documentId, Date.now() + 5000);
|
|
|
|
|
|
var response = await fetch('/api/documents/save', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(savePayload)
|
|
|
|
|
|
});
|
|
|
|
|
|
var result = await response.json().catch(function() { return null; });
|
|
|
|
|
|
if (!response.ok || !result || result.ok !== true) {
|
|
|
|
|
|
var message = result && (result.message || (result.error && result.error.message)) || ('save_failed_' + response.status);
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-save-error', message);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (response.status === 409 && asset && asset.id) {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-orphaned-asset-id', String(asset.id || ''));
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-orphaned-policy', 'asset-kept-reference-unsaved-retry-required');
|
|
|
|
|
|
}
|
2026-05-26 09:44:35 +08:00
|
|
|
|
throw new Error(message);
|
|
|
|
|
|
}
|
2026-05-28 22:01:44 +08:00
|
|
|
|
var saved = result.result && typeof result.result === 'object' ? result.result : {};
|
|
|
|
|
|
var fileVersion = String(saved.fileVersion || saved.conflictDetectionKey || saved.conflict_detection_key || '').trim();
|
|
|
|
|
|
try {
|
|
|
|
|
|
window.dispatchEvent(new CustomEvent('mnote:local-upload-editor-save-completed', {
|
|
|
|
|
|
detail: {
|
|
|
|
|
|
documentId: documentId,
|
|
|
|
|
|
rootUri: rootUri,
|
|
|
|
|
|
fileVersion: fileVersion,
|
|
|
|
|
|
writeIntentId: String(saved.writeIntentId || savePayload.writeIntentId || '').trim(),
|
|
|
|
|
|
saveOperationId: String(saved.saveOperationId || savePayload.saveOperationId || '').trim(),
|
|
|
|
|
|
revision: Number.isInteger(saved.revision) ? saved.revision : null,
|
|
|
|
|
|
serialized: JSON.stringify(tiptapDocument),
|
|
|
|
|
|
source: 'local-upload-runtime'
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
} catch (_) {}
|
2026-05-26 09:44:35 +08:00
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-saved', 'true');
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-last-upload-save-error');
|
2026-05-28 22:01:44 +08:00
|
|
|
|
document.documentElement.removeAttribute('data-mnote-last-upload-orphaned-asset-id');
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-last-upload-orphaned-policy');
|
2026-05-26 09:44:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:42:53 +08:00
|
|
|
|
function currentRootUri() {
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
var fromUrl = (params.get('rootUri') || '').trim();
|
|
|
|
|
|
if (fromUrl) return fromUrl;
|
|
|
|
|
|
return document.body instanceof HTMLElement
|
|
|
|
|
|
? (document.body.getAttribute('data-mnote-root-uri') || '').trim()
|
|
|
|
|
|
: '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 23:43:22 +08:00
|
|
|
|
function editorUploadRootFromElementWithDeps(target, deps) {
|
|
|
|
|
|
if (deps && typeof deps.editorUploadRootFromElement === 'function') {
|
|
|
|
|
|
return deps.editorUploadRootFromElement(target);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!(target instanceof Element)) return null;
|
|
|
|
|
|
var selector = String(deps && deps.rootSelector || '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]');
|
|
|
|
|
|
var root = target.closest(selector);
|
|
|
|
|
|
return root instanceof HTMLElement ? root : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveEditorUploadContext(detail, deps) {
|
|
|
|
|
|
var root = null;
|
|
|
|
|
|
var selector = detail && detail.editorRootSelector ? String(detail.editorRootSelector) : '';
|
|
|
|
|
|
if (selector) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var selected = document.querySelector(selector);
|
|
|
|
|
|
if (selected instanceof HTMLElement) root = selected;
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!root && window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
|
|
|
|
|
|
root = window.__mnoteIntendedSlashRoot;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!root && window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
|
|
|
|
|
|
root = window.__mnoteLastEditorUploadRoot;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!root && document.activeElement instanceof Element) {
|
|
|
|
|
|
root = editorUploadRootFromElementWithDeps(document.activeElement, deps);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!root) {
|
|
|
|
|
|
var rootSelector = String(deps && deps.rootSelector || '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]');
|
|
|
|
|
|
var focused = document.querySelector(rootSelector + ' .ProseMirror:focus-within');
|
|
|
|
|
|
root = editorUploadRootFromElementWithDeps(focused, deps);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!root) {
|
|
|
|
|
|
root = document.querySelector('[data-editor-host-kind="leptos_tiptap_island"][data-pane-role="primary"]');
|
|
|
|
|
|
}
|
|
|
|
|
|
var pane = root instanceof Element ? root.closest('.document-pane[data-pane-role]') : null;
|
|
|
|
|
|
var shell = root instanceof Element ? root.closest('.document-shell[data-document-id]') : null;
|
|
|
|
|
|
var currentDocumentId = deps && typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId : function() { return ''; };
|
|
|
|
|
|
var resolveWorkspaceId = deps && typeof deps.resolveWorkspaceId === 'function' ? deps.resolveWorkspaceId : function() { return ''; };
|
|
|
|
|
|
return {
|
|
|
|
|
|
root: root instanceof HTMLElement ? root : null,
|
|
|
|
|
|
documentId: String(
|
|
|
|
|
|
detail && detail.documentId
|
|
|
|
|
|
|| (root instanceof HTMLElement && root.getAttribute('data-document-id'))
|
|
|
|
|
|
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-document-id'))
|
|
|
|
|
|
|| (shell instanceof HTMLElement && shell.getAttribute('data-document-id'))
|
|
|
|
|
|
|| currentDocumentId()
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
).trim(),
|
|
|
|
|
|
workspaceId: String(
|
|
|
|
|
|
detail && detail.workspaceId
|
|
|
|
|
|
|| (root instanceof HTMLElement && root.getAttribute('data-workspace-id'))
|
|
|
|
|
|
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-workspace-id'))
|
|
|
|
|
|
|| (shell instanceof HTMLElement && shell.getAttribute('data-workspace-id'))
|
|
|
|
|
|
|| resolveWorkspaceId(document.body)
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function editorRootFromUploadOptions(options, deps) {
|
|
|
|
|
|
if (options && options.editorRoot instanceof HTMLElement) return options.editorRoot;
|
|
|
|
|
|
if (window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
|
|
|
|
|
|
return window.__mnoteIntendedSlashRoot;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
|
|
|
|
|
|
return window.__mnoteLastEditorUploadRoot;
|
|
|
|
|
|
}
|
|
|
|
|
|
var rootSelector = String(deps && deps.rootSelector || '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]');
|
|
|
|
|
|
var focused = document.querySelector(rootSelector + ' .ProseMirror:focus-within');
|
|
|
|
|
|
return editorUploadRootFromElementWithDeps(focused, deps);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openEditorUploadFilePicker(detail, deps) {
|
|
|
|
|
|
var uploadContext = resolveEditorUploadContext(detail || {}, deps || {});
|
|
|
|
|
|
var input = document.createElement('input');
|
|
|
|
|
|
input.type = 'file';
|
|
|
|
|
|
input.multiple = detail && detail.multiple !== false;
|
|
|
|
|
|
if (detail && detail.accept) input.accept = String(detail.accept);
|
|
|
|
|
|
input.style.position = 'fixed';
|
|
|
|
|
|
input.style.left = '-9999px';
|
|
|
|
|
|
input.style.top = '-9999px';
|
|
|
|
|
|
document.body.appendChild(input);
|
|
|
|
|
|
input.addEventListener('change', function() {
|
|
|
|
|
|
var files = Array.from(input.files || []);
|
|
|
|
|
|
input.remove();
|
|
|
|
|
|
var resolveWorkspaceId = deps && typeof deps.resolveWorkspaceId === 'function' ? deps.resolveWorkspaceId : function() { return ''; };
|
|
|
|
|
|
var currentDocumentId = deps && typeof deps.currentDocumentId === 'function' ? deps.currentDocumentId : function() { return ''; };
|
|
|
|
|
|
var uploadFilesWithResolvedTarget = deps && typeof deps.uploadFilesWithResolvedTarget === 'function' ? deps.uploadFilesWithResolvedTarget : null;
|
|
|
|
|
|
if (!uploadFilesWithResolvedTarget) return;
|
|
|
|
|
|
void uploadFilesWithResolvedTarget(files, {
|
|
|
|
|
|
workspaceId: uploadContext.workspaceId || resolveWorkspaceId(document.body),
|
|
|
|
|
|
documentId: uploadContext.documentId || currentDocumentId(),
|
|
|
|
|
|
targetRowId: null,
|
|
|
|
|
|
uploadIntent: 'editor.markdown.attach'
|
|
|
|
|
|
}, {
|
|
|
|
|
|
insertIntoEditor: detail && detail.insertIntoEditor !== false,
|
|
|
|
|
|
editorRoot: uploadContext.root
|
|
|
|
|
|
});
|
|
|
|
|
|
}, { once: true });
|
|
|
|
|
|
input.click();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 00:57:52 +08:00
|
|
|
|
async function fetchWithTimeout(input, init, timeoutMs, label) {
|
|
|
|
|
|
var controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
|
|
|
|
var timer = 0;
|
2026-06-13 22:20:01 +08:00
|
|
|
|
var timeout = Math.max(1000, Number(timeoutMs) || 15000);
|
2026-05-25 00:57:52 +08:00
|
|
|
|
try {
|
|
|
|
|
|
if (controller) {
|
|
|
|
|
|
timer = window.setTimeout(function() {
|
|
|
|
|
|
controller.abort();
|
2026-06-13 22:20:01 +08:00
|
|
|
|
}, timeout);
|
2026-05-25 00:57:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
var nextInit = Object.assign({}, init || {});
|
|
|
|
|
|
if (controller) nextInit.signal = controller.signal;
|
|
|
|
|
|
return await fetch(input, nextInit);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error && error.name === 'AbortError') {
|
|
|
|
|
|
throw new Error((label || '请求') + '超时');
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (timer) window.clearTimeout(timer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 22:20:01 +08:00
|
|
|
|
function uploadTimeoutMsForFile(file, requestedMs) {
|
|
|
|
|
|
var requested = Number(requestedMs) || 0;
|
|
|
|
|
|
var size = Number(file && file.size || 0);
|
|
|
|
|
|
if (!Number.isFinite(size) || size <= 0) return Math.max(requested, 15000);
|
|
|
|
|
|
var sizeBased = 15000 + Math.ceil(size / (1024 * 1024)) * 12000;
|
|
|
|
|
|
return Math.min(10 * 60 * 1000, Math.max(requested, 60000, sizeBased));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 01:13:08 +08:00
|
|
|
|
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
|
2026-06-13 22:20:01 +08:00
|
|
|
|
}, uploadTimeoutMsForFile(file, context && context.timeoutMs), '本地上传');
|
2026-05-25 01:13:08 +08:00
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 01:52:11 +08:00
|
|
|
|
async function uploadMediaAsset(file, plan, context) {
|
2026-06-07 10:35:21 +08:00
|
|
|
|
document.documentElement.setAttribute('data-mnote-media-upload-retired', 'true');
|
|
|
|
|
|
throw new Error('旧 Convex Files 上传链已退役;local-first 附件请使用本地文件夹上传目标。');
|
2026-05-25 01:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 01:28:15 +08:00
|
|
|
|
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);
|
2026-05-29 11:13:05 +08:00
|
|
|
|
var markdownHref = typeof deps.uploadedAssetMarkdownHref === 'function' ? deps.uploadedAssetMarkdownHref(asset) : uploadedAssetMarkdownHref(asset);
|
2026-05-25 01:28:15 +08:00
|
|
|
|
var localOpenUrl = typeof deps.localAssetOpenUrl === 'function' ? deps.localAssetOpenUrl(asset, false) : localAssetOpenUrl(asset, false);
|
|
|
|
|
|
var fallbackUrl = typeof deps.uploadedAssetUrl === 'function' ? deps.uploadedAssetUrl(asset) : uploadedAssetUrl(asset);
|
2026-06-02 17:17:49 +08:00
|
|
|
|
var imageUrl = localOpenUrl || fallbackUrl || markdownHref;
|
2026-05-29 11:13:05 +08:00
|
|
|
|
var url = markdownHref || localOpenUrl || fallbackUrl;
|
2026-05-25 01:28:15 +08:00
|
|
|
|
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 {
|
2026-06-02 17:17:49 +08:00
|
|
|
|
if (type === 'image' && imageUrl) {
|
|
|
|
|
|
var imageInserted = editor.chain().focus().setImage({ src: imageUrl, alt: title, title: title }).run() === true;
|
2026-05-29 11:13:05 +08:00
|
|
|
|
if (imageInserted) {
|
|
|
|
|
|
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');
|
|
|
|
|
|
dispatchUploadedEditorChange(editorRoot, editor, deps);
|
|
|
|
|
|
await persistUploadedEditorChange(editor, asset, deps);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'insert_image_failed');
|
|
|
|
|
|
}
|
|
|
|
|
|
return imageInserted;
|
2026-05-25 01:28:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
var isLocalAsset = typeof deps.isLocalUploadedAsset === 'function' ? deps.isLocalUploadedAsset(asset) : isLocalUploadedAsset(asset);
|
2026-05-29 11:13:05 +08:00
|
|
|
|
var href = markdownHref || url;
|
2026-05-25 01:28:15 +08:00
|
|
|
|
if (href) {
|
|
|
|
|
|
var inserted = editor.chain().focus().insertContent([
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'paragraph',
|
|
|
|
|
|
content: [{
|
|
|
|
|
|
type: 'text',
|
|
|
|
|
|
text: title,
|
|
|
|
|
|
marks: [{
|
|
|
|
|
|
type: 'link',
|
|
|
|
|
|
attrs: {
|
2026-05-29 11:13:05 +08:00
|
|
|
|
href: href,
|
2026-05-25 01:28:15 +08:00
|
|
|
|
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');
|
2026-05-26 09:44:35 +08:00
|
|
|
|
dispatchUploadedEditorChange(editorRoot, editor, deps);
|
|
|
|
|
|
await persistUploadedEditorChange(editor, asset, deps);
|
2026-05-25 01:28:15 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'insert_content_failed');
|
|
|
|
|
|
}
|
2026-05-28 22:01:44 +08:00
|
|
|
|
window.setTimeout(function annotateUploadedLink(attempt) {
|
2026-05-25 01:28:15 +08:00
|
|
|
|
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);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (!(link instanceof HTMLElement) && assetId) {
|
|
|
|
|
|
var scope = targetRoot instanceof HTMLElement ? targetRoot : document;
|
|
|
|
|
|
link = Array.from(scope.querySelectorAll('.editor-surface .ProseMirror a')).find(function(candidate) {
|
|
|
|
|
|
var href = String(candidate.getAttribute('href') || '');
|
|
|
|
|
|
try { href = decodeURIComponent(href); } catch (_) {}
|
|
|
|
|
|
return href.indexOf(assetId) >= 0;
|
|
|
|
|
|
}) || null;
|
|
|
|
|
|
}
|
2026-05-25 01:28:15 +08:00
|
|
|
|
if (link instanceof HTMLElement) {
|
|
|
|
|
|
if (assetId) link.setAttribute('data-asset-id', assetId);
|
|
|
|
|
|
if (sizeLabel) link.setAttribute('data-file-size', sizeLabel);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
} else if (Number(attempt) < 20) {
|
|
|
|
|
|
window.setTimeout(function() { annotateUploadedLink(Number(attempt) + 1); }, 50);
|
2026-05-25 01:28:15 +08:00
|
|
|
|
}
|
2026-05-28 22:01:44 +08:00
|
|
|
|
}, 0, 0);
|
2026-05-25 01:28:15 +08:00
|
|
|
|
return inserted;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('[mnote upload] insert uploaded asset failed', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 02:24:48 +08:00
|
|
|
|
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();
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (String(plan && plan.uploadIntent || '').trim() === 'editor.markdown.attach' && documentId) {
|
|
|
|
|
|
var conflict = localUploadSessionConflict(documentId, rootUri);
|
|
|
|
|
|
if (conflict) {
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-blocked-conflict', 'true');
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-last-upload-save-error', conflict.message || '当前文档存在文件冲突,请先处理冲突后再上传附件');
|
|
|
|
|
|
throw new Error(conflict.message || '当前文档存在文件冲突,请先处理冲突后再上传附件');
|
|
|
|
|
|
}
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-last-upload-blocked-conflict');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (String(plan && plan.uploadIntent || '').trim() === 'editor.markdown.attach' && documentId) {
|
|
|
|
|
|
persistLocalFolderSelfChangeSuppression(documentId, Date.now() + 8000);
|
|
|
|
|
|
}
|
2026-05-25 02:24:48 +08:00
|
|
|
|
var localResult = await uploadLocalFolderAsset(file, plan, {
|
|
|
|
|
|
rootUri: rootUri,
|
|
|
|
|
|
documentId: documentId,
|
2026-06-13 22:20:01 +08:00
|
|
|
|
timeoutMs: uploadTimeoutMsForFile(file, 15000)
|
2026-05-25 02:24:48 +08:00
|
|
|
|
});
|
|
|
|
|
|
var localAsset = localResult && localResult.asset ? localResult.asset : null;
|
|
|
|
|
|
if (!localAsset) {
|
|
|
|
|
|
throw new Error('上传失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (options.insertIntoEditor) {
|
|
|
|
|
|
await insertIntoEditor(localAsset, resolveEditorRoot(options));
|
|
|
|
|
|
}
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (!options.insertIntoEditor && typeof deps.refreshLocalFolderSidebarSnapshot === 'function') {
|
2026-05-25 02:24:48 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 02:00:34 +08:00
|
|
|
|
async function uploadFilesWithResolvedTarget(files, detail, options, deps) {
|
|
|
|
|
|
deps = deps || {};
|
|
|
|
|
|
var resolveFileTreeUploadTarget = typeof deps.resolveFileTreeUploadTarget === 'function' ? deps.resolveFileTreeUploadTarget : null;
|
|
|
|
|
|
var uploadFileToMediaAsset = typeof deps.uploadFileToMediaAsset === 'function' ? deps.uploadFileToMediaAsset : null;
|
|
|
|
|
|
if (!resolveFileTreeUploadTarget || !uploadFileToMediaAsset) {
|
|
|
|
|
|
throw new Error('上传运行时缺少目标解析或上传函数');
|
|
|
|
|
|
}
|
|
|
|
|
|
var list = Array.from(files || []).filter(Boolean);
|
|
|
|
|
|
if (!list.length) return [];
|
|
|
|
|
|
var plan = await resolveFileTreeUploadTarget(detail || {});
|
|
|
|
|
|
var uploaded = [];
|
|
|
|
|
|
var errors = [];
|
|
|
|
|
|
for (var i = 0; i < list.length; i += 1) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
uploaded.push(await uploadFileToMediaAsset(list[i], plan, options || {}));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
errors.push(list[i].name + ': ' + (error && error.message ? error.message : '上传失败'));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (errors.length) {
|
|
|
|
|
|
var message = '部分文件上传失败:\n' + errors.slice(0, 6).join('\n') + (errors.length > 6 ? '\n...' : '');
|
|
|
|
|
|
if (typeof deps.alert === 'function') {
|
2026-07-28 17:04:27 +08:00
|
|
|
|
await deps.alert(message);
|
2026-05-25 02:00:34 +08:00
|
|
|
|
} else {
|
2026-07-28 17:04:27 +08:00
|
|
|
|
await window.mnote.alert(message);
|
2026-05-25 02:00:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return uploaded;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:42:53 +08:00
|
|
|
|
function localAssetOpenUrl(asset, download, context) {
|
|
|
|
|
|
if (!isLocalUploadedAsset(asset)) return '';
|
|
|
|
|
|
var rootUri = String(context && context.rootUri || asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri();
|
|
|
|
|
|
var rootRelativePath = String(asset && (asset.rootRelativePath || asset.root_relative_path) || '').trim();
|
2026-07-28 17:04:27 +08:00
|
|
|
|
if (!rootUri || !rootRelativePath || hasPathEscape(rootRelativePath)) return '';
|
2026-05-24 22:42:53 +08:00
|
|
|
|
var url = new URL('/api/local-folder/files/open', window.location.origin);
|
|
|
|
|
|
url.searchParams.set('rootUri', rootUri);
|
|
|
|
|
|
url.searchParams.set('path', rootRelativePath);
|
|
|
|
|
|
if (download) url.searchParams.set('download', 'true');
|
|
|
|
|
|
return url.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function uploadedAssetType(asset) {
|
|
|
|
|
|
return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fileTreeIconKindForFileName(fileName) {
|
|
|
|
|
|
var name = String(fileName || '').trim().toLowerCase();
|
|
|
|
|
|
var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
|
|
|
|
|
|
if (['doc', 'docx', 'odt', 'rtf', 'ppt', 'pptx', 'odp', 'xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'office';
|
|
|
|
|
|
if (ext === 'md' || ext === 'markdown') return 'markdown';
|
|
|
|
|
|
if (ext === 'pdf') return 'pdf';
|
|
|
|
|
|
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'].indexOf(ext) >= 0) return 'image';
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (['html', 'htm', 'css', 'scss', 'less', 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'vue', 'svelte', 'astro'].indexOf(ext) >= 0) return 'web';
|
|
|
|
|
|
if (['json', 'jsonc', 'json5', 'toml', 'yaml', 'yml', 'ini', 'env', 'xml', 'lock', 'hcl', 'tf', 'tfvars', 'nix', 'properties', 'conf', 'cfg', 'config', 'service', 'desktop', 'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'prettierrc', 'eslintrc'].indexOf(ext) >= 0 || ['.dockerignore', '.editorconfig', '.env', '.eslintrc', '.gitattributes', '.gitignore', '.npmrc', '.prettierrc', 'dockerfile', 'containerfile', 'makefile', 'cmakelists.txt', 'gemfile', 'rakefile', 'procfile'].indexOf(name) >= 0) return 'config';
|
|
|
|
|
|
if (['rs', 'py', 'go', 'java', 'c', 'cpp', 'h', 'hpp', 'cs', 'php', 'rb', 'sh', 'bash', 'zsh', 'sql', 'lua', 'dart', 'kt', 'kts', 'swift', 'scala', 'gradle', 'groovy', 'clj', 'ex', 'exs', 'erl', 'hrl', 'fs', 'fsx', 'r', 'jl', 'm', 'mm', 'pl', 'pm', 'ps1', 'bat', 'cmd', 'psm1', 'psd1', 'proto', 'graphql', 'gql', 'prisma', 'cmake', 'bazel', 'bzl'].indexOf(ext) >= 0) return 'code';
|
2026-05-24 22:42:53 +08:00
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isLocalUploadedAsset(asset) {
|
|
|
|
|
|
var id = String(asset && asset.id || '').trim();
|
|
|
|
|
|
return String(asset && asset.sourceKind || '').trim() === 'local_folder' || id.indexOf('local:asset:') === 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function uploadedAssetExtension(asset) {
|
|
|
|
|
|
var match = uploadedAssetTitle(asset).toLowerCase().match(/\.([a-z0-9]+)$/);
|
|
|
|
|
|
return match ? match[1] : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isNonOfficeAttachmentName(name, ext) {
|
|
|
|
|
|
var codeFileNames = [
|
|
|
|
|
|
'.dockerignore', '.editorconfig', '.env', '.eslintrc', '.gitattributes', '.gitignore', '.npmrc', '.prettierrc',
|
|
|
|
|
|
'dockerfile', 'makefile', 'cmakelists.txt', 'gemfile', 'rakefile', 'procfile'
|
|
|
|
|
|
];
|
|
|
|
|
|
return [
|
|
|
|
|
|
'pdf', 'toml', 'json', 'yaml', 'yml', 'md', 'markdown', 'txt', 'ini', 'env', 'xml', 'html', 'htm', 'css', 'scss',
|
|
|
|
|
|
'less', 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'py', 'rs', 'go', 'java', 'c', 'cpp', 'h', 'hpp', 'cs',
|
|
|
|
|
|
'php', 'rb', 'sh', 'bash', 'zsh', 'sql', 'lock', 'log', 'vue', 'svelte', 'astro', 'jsonc', 'json5',
|
|
|
|
|
|
'mts', 'cts', 'lua', 'dart', 'kt', 'kts', 'swift', 'scala', 'gradle', 'groovy', 'clj',
|
|
|
|
|
|
'ex', 'exs', 'erl', 'hrl', 'fs', 'fsx', 'r', 'jl', 'm', 'mm', 'pl', 'pm', 'ps1', 'bat', 'cmd',
|
|
|
|
|
|
'psm1', 'psd1', 'dockerfile', 'containerfile', 'proto', 'graphql', 'gql', 'prisma', 'tf', 'tfvars',
|
|
|
|
|
|
'hcl', 'nix', 'cmake', 'bazel', 'bzl', 'properties', 'conf', 'cfg', 'config', 'service', 'desktop',
|
|
|
|
|
|
'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'prettierrc', 'eslintrc'
|
|
|
|
|
|
].indexOf(ext) >= 0 || codeFileNames.indexOf(name) >= 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function attachmentExtensionFromFileName(fileName) {
|
|
|
|
|
|
var name = String(fileName || '').trim().toLowerCase();
|
|
|
|
|
|
return name.indexOf('.') >= 0 ? name.split('.').pop() : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isPdfAttachmentFileName(fileName) {
|
|
|
|
|
|
return attachmentExtensionFromFileName(fileName) === 'pdf';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isCodeAttachmentFileName(fileName) {
|
|
|
|
|
|
var name = String(fileName || '').trim().toLowerCase();
|
|
|
|
|
|
var ext = attachmentExtensionFromFileName(name);
|
|
|
|
|
|
return ext !== 'pdf' && isNonOfficeAttachmentName(name, ext);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function inferCodeAttachmentLanguage(fileName) {
|
|
|
|
|
|
var name = String(fileName || '').trim().toLowerCase();
|
|
|
|
|
|
var ext = attachmentExtensionFromFileName(name);
|
|
|
|
|
|
var byName = {
|
|
|
|
|
|
'dockerfile': 'dockerfile',
|
|
|
|
|
|
'makefile': 'makefile',
|
|
|
|
|
|
'cmakelists.txt': 'cmake',
|
|
|
|
|
|
'.gitignore': 'gitignore',
|
|
|
|
|
|
'.gitattributes': 'gitattributes',
|
|
|
|
|
|
'.editorconfig': 'ini',
|
|
|
|
|
|
'.env': 'dotenv'
|
|
|
|
|
|
};
|
|
|
|
|
|
if (byName[name]) return byName[name];
|
|
|
|
|
|
var byExt = {
|
|
|
|
|
|
bash: 'bash', bat: 'batch', c: 'c', cjs: 'javascript', cmd: 'batch', conf: 'text', cpp: 'cpp',
|
|
|
|
|
|
cs: 'csharp', css: 'css', cts: 'typescript', dart: 'dart', dockerfile: 'dockerfile', env: 'dotenv',
|
|
|
|
|
|
go: 'go', gql: 'graphql', gradle: 'groovy', graphql: 'graphql', h: 'c', hcl: 'hcl', hpp: 'cpp',
|
|
|
|
|
|
htm: 'html', html: 'html', ini: 'ini', java: 'java', js: 'javascript', json: 'json', json5: 'json',
|
|
|
|
|
|
jsonc: 'jsonc', jsx: 'javascript', kt: 'kotlin', kts: 'kotlin', less: 'less', log: 'text',
|
|
|
|
|
|
lua: 'lua', m: 'objective-c', markdown: 'markdown', md: 'markdown', mjs: 'javascript',
|
|
|
|
|
|
mts: 'typescript', nix: 'nix', php: 'php', pl: 'perl', pm: 'perl', prisma: 'prisma',
|
|
|
|
|
|
proto: 'protobuf', ps1: 'powershell', py: 'python', r: 'r', rb: 'ruby', rs: 'rust',
|
|
|
|
|
|
scss: 'scss', sh: 'bash', sql: 'sql', svelte: 'svelte', swift: 'swift', tf: 'terraform',
|
|
|
|
|
|
tfvars: 'terraform', toml: 'toml', ts: 'typescript', tsx: 'typescript', txt: 'text',
|
|
|
|
|
|
vue: 'vue', xml: 'xml', yaml: 'yaml', yml: 'yaml', zsh: 'bash'
|
|
|
|
|
|
};
|
|
|
|
|
|
return byExt[ext] || 'text';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function attachmentClassForFileName(fileName) {
|
|
|
|
|
|
var name = String(fileName || '').trim().toLowerCase();
|
|
|
|
|
|
var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
|
|
|
|
|
|
if (['doc', 'docx', 'odt', 'rtf'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-word';
|
|
|
|
|
|
if (['ppt', 'pptx', 'odp'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-ppt';
|
|
|
|
|
|
if (['xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-sheet';
|
|
|
|
|
|
if (ext === 'pdf') return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-pdf';
|
|
|
|
|
|
if (isNonOfficeAttachmentName(name, ext)) {
|
|
|
|
|
|
return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-code';
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-file';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function uploadedAttachmentClass(asset) {
|
|
|
|
|
|
return attachmentClassForFileName(uploadedAssetTitle(asset));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function uploadedFileSize(asset) {
|
|
|
|
|
|
var size = Number(asset && (asset.file_size || asset.fileSize) || 0);
|
|
|
|
|
|
if (!Number.isFinite(size) || size <= 0) return '';
|
|
|
|
|
|
if (size >= 1024 * 1024) return (size / 1024 / 1024).toFixed(size >= 10 * 1024 * 1024 ? 1 : 2) + ' MB';
|
|
|
|
|
|
if (size >= 1024) return (size / 1024).toFixed(size >= 100 * 1024 ? 0 : 2) + ' KB';
|
|
|
|
|
|
return String(Math.round(size)) + ' B';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.__mnoteLocalUploadRuntime = {
|
|
|
|
|
|
uploadedAssetTitle: uploadedAssetTitle,
|
|
|
|
|
|
uploadedAssetUrl: uploadedAssetUrl,
|
2026-05-29 11:13:05 +08:00
|
|
|
|
uploadedAssetMarkdownHref: uploadedAssetMarkdownHref,
|
2026-05-24 23:43:22 +08:00
|
|
|
|
resolveEditorUploadContext: resolveEditorUploadContext,
|
|
|
|
|
|
editorRootFromUploadOptions: editorRootFromUploadOptions,
|
|
|
|
|
|
openEditorUploadFilePicker: openEditorUploadFilePicker,
|
2026-05-25 00:57:52 +08:00
|
|
|
|
fetchWithTimeout: fetchWithTimeout,
|
2026-06-13 22:20:01 +08:00
|
|
|
|
uploadTimeoutMsForFile: uploadTimeoutMsForFile,
|
2026-05-25 01:13:08 +08:00
|
|
|
|
uploadLocalFolderAsset: uploadLocalFolderAsset,
|
2026-05-25 01:52:11 +08:00
|
|
|
|
uploadMediaAsset: uploadMediaAsset,
|
2026-05-25 01:28:15 +08:00
|
|
|
|
insertUploadedAssetIntoEditor: insertUploadedAssetIntoEditor,
|
2026-05-25 02:24:48 +08:00
|
|
|
|
uploadFileToMediaAsset: uploadFileToMediaAsset,
|
2026-05-25 02:00:34 +08:00
|
|
|
|
uploadFilesWithResolvedTarget: uploadFilesWithResolvedTarget,
|
2026-05-24 22:42:53 +08:00
|
|
|
|
localAssetOpenUrl: localAssetOpenUrl,
|
|
|
|
|
|
uploadedAssetType: uploadedAssetType,
|
|
|
|
|
|
fileTreeIconKindForFileName: fileTreeIconKindForFileName,
|
|
|
|
|
|
isLocalUploadedAsset: isLocalUploadedAsset,
|
|
|
|
|
|
uploadedAssetExtension: uploadedAssetExtension,
|
|
|
|
|
|
isNonOfficeAttachmentName: isNonOfficeAttachmentName,
|
|
|
|
|
|
attachmentExtensionFromFileName: attachmentExtensionFromFileName,
|
|
|
|
|
|
isPdfAttachmentFileName: isPdfAttachmentFileName,
|
|
|
|
|
|
isCodeAttachmentFileName: isCodeAttachmentFileName,
|
|
|
|
|
|
inferCodeAttachmentLanguage: inferCodeAttachmentLanguage,
|
|
|
|
|
|
attachmentClassForFileName: attachmentClassForFileName,
|
|
|
|
|
|
uploadedAttachmentClass: uploadedAttachmentClass,
|
|
|
|
|
|
uploadedFileSize: uploadedFileSize
|
|
|
|
|
|
};
|