fix: stabilize local folder AI document workflow

- scope local-folder PageTree revision and document sidebar rendering to fileTreeScope

- preserve projected table/image attrs for local Markdown aggregate fallback

- avoid FileTree restore forced layouts on cold design open

- add API ChatOnly provider runtime and local OCR task handling regressions
This commit is contained in:
lix-2026
2026-06-02 17:17:49 +08:00
parent 610b23d3a5
commit 9f4b5c4d48
27 changed files with 3825 additions and 229 deletions
@@ -51,11 +51,12 @@ export const legacyStylesToTiptapMarks = (styles) => {
export const legacyMarkArrayToTiptapMarks = (inlineMarks) => {
if (!Array.isArray(inlineMarks)) return [];
return inlineMarks.flatMap((mark) => {
if (!mark || typeof mark !== 'object') return [];
if (mark.type === 'bold' || mark.type === 'italic' || mark.type === 'underline' || mark.type === 'strike' || mark.type === 'code') {
return [{ type: mark.type }];
const markType = typeof mark === 'string' ? mark : typeof mark?.type === 'string' ? mark.type : '';
if (!markType) return [];
if (markType === 'bold' || markType === 'italic' || markType === 'underline' || markType === 'strike' || markType === 'code') {
return [{ type: markType }];
}
if (mark.type === 'link') {
if (markType === 'link') {
const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : '';
return href ? [{ type: 'link', attrs: { href } }] : [];
}
@@ -77,10 +78,19 @@ export const legacyInlineContentToTiptap = (value) => {
if (typeof value === 'string') return value ? [{ type: 'text', text: value }] : [];
if (Array.isArray(value)) return value.flatMap(legacyInlineContentToTiptap);
if (value && typeof value === 'object') {
const text = typeof value.text === 'string' ? value.text : '';
const payload = value.payload && typeof value.payload === 'object' ? value.payload : null;
if (payload?.type === 'hard_break') return [{ type: 'hardBreak' }];
const text = typeof value.text === 'string'
? value.text
: typeof payload?.text === 'string'
? payload.text
: '';
if (text) {
const attrs = value.attrs && typeof value.attrs === 'object' ? value.attrs : {};
const marks = mergeTiptapMarks(
legacyStylesToTiptapMarks(attrs.styles),
legacyStylesToTiptapMarks(value.styles),
legacyMarkArrayToTiptapMarks(payload?.marks),
legacyMarkArrayToTiptapMarks(value.marks)
);
return [{ type: 'text', text, ...(marks.length ? { marks } : {}) }];
@@ -182,7 +192,7 @@ export const legacyBlockToTiptap = (block, index = 0) => {
return { type: 'paragraph', attrs: withTextAlign({ blockId }), content: mediaContent };
}
if (type === 'table') {
const tableSnapshot = block?.props?.tiptapTable;
const tableSnapshot = block?.props?.tiptapTable || block?.attrs?.tiptapTable;
if (tableSnapshot && typeof tableSnapshot === 'object' && tableSnapshot.type === 'table') return tableSnapshot;
return {
type: 'table',
@@ -209,12 +219,13 @@ export const legacyBlockToTiptap = (block, index = 0) => {
};
}
if (type === 'image') {
const imageSnapshot = block?.props?.tiptapImage;
const imageSnapshot = block?.props?.tiptapImage || block?.attrs?.tiptapImage;
if (imageSnapshot && typeof imageSnapshot === 'object' && imageSnapshot.type === 'image') return imageSnapshot;
const attrsSource = block?.attrs && typeof block.attrs === 'object' ? block.attrs : {};
const attrs = {
src: String(block?.props?.src || block?.src || ''),
alt: block?.props?.alt || block?.alt || null,
title: block?.props?.title || block?.title || null,
src: String(block?.props?.src || attrsSource.src || block?.src || ''),
alt: block?.props?.alt || attrsSource.alt || block?.alt || null,
title: block?.props?.title || attrsSource.title || block?.title || null,
};
return attrs.src ? { type: 'image', attrs: { blockId, ...attrs } } : null;
}
@@ -318,7 +329,7 @@ export const localMarkdownDirectoryFromDocumentId = (documentId) => {
};
export const isExternalOrSpecialUrl = (value) => {
const text = String(value || '').trim();
const text = unwrapMarkdownLinkTarget(value);
return !text
|| text.startsWith('#')
|| text.startsWith('data:')
@@ -329,8 +340,16 @@ export const isExternalOrSpecialUrl = (value) => {
|| text.startsWith('/api/');
};
export const normalizeLocalAssetRelativePath = (value, context) => {
export const unwrapMarkdownLinkTarget = (value) => {
const text = String(value || '').trim();
if (text.length >= 2 && text.startsWith('<') && text.endsWith('>')) {
return text.slice(1, -1).trim();
}
return text;
};
export const normalizeLocalAssetRelativePath = (value, context) => {
const text = unwrapMarkdownLinkTarget(value);
if (!text || isExternalOrSpecialUrl(text)) return text;
if (text.startsWith('/')) return text.replace(/^\/+/, '');
const baseDir = localMarkdownDirectoryFromDocumentId(context?.documentId);