feat(editor): add markdown presets, heading folding and external file refresh

This commit is contained in:
2026-09-06 12:57:09 +08:00
parent 415efc4444
commit 8ad1db33f7
43 changed files with 901 additions and 54 deletions
+1
View File
@@ -116,6 +116,7 @@ class NoteUpdateRequest(Contract):
title: str | None = None
markdown: str | None = None
tags: list[str] | None = None
expected_content_hash: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$")
class NoteMoveRequest(Contract):
+3 -2
View File
@@ -225,7 +225,7 @@ async def open_workspace(request: WorkspaceOpenRequest) -> WorkspaceSnapshot:
@router.get("/workspace/tree", response_model=list[WorkspaceEntry], tags=["Workspace"])
async def get_workspace_tree() -> list[WorkspaceEntry]:
return workspace_service.get_workspace_tree()
return await workspace_service.refresh_workspace_tree()
@router.post("/workspace/folders", response_model=WorkspaceEntry, tags=["Workspace"])
@@ -286,7 +286,8 @@ async def get_note(note_id: str) -> Note:
@router.patch("/notes/{note_id}", response_model=Note, tags=["Notes"])
async def update_note(note_id: str, request: NoteUpdateRequest) -> Note:
return await note_service.update_note(
note_id, title=request.title, markdown=request.markdown, tags=request.tags, defer_vectors=True
note_id, title=request.title, markdown=request.markdown, tags=request.tags,
expected_content_hash=request.expected_content_hash, defer_vectors=True
)
@@ -105,6 +105,14 @@ def get_workspace_tree() -> list[WorkspaceEntry]:
return _tree(get_settings().vault_path.resolve(), locations)
async def refresh_workspace_tree() -> list[WorkspaceEntry]:
"""Observe external creates/deletes without waiting for vector inference."""
if get_workspace_info().requires_refresh:
await _register_workspace_files()
index_service.schedule_workspace_rebuild()
return get_workspace_tree()
async def open_workspace(requested_path: str | None) -> WorkspaceSnapshot:
"""打开只登记文件与全文索引,不让 Embedding 或厂商网络阻塞工作区。"""
+31
View File
@@ -114,3 +114,34 @@ def test_workspace_openapi_paths_are_published() -> None:
"/api/workspace/folders/delete",
"/api/notes/{note_id}/rename",
} <= paths.keys()
def test_external_files_are_registered_and_removed_without_vector_wait(monkeypatch) -> None:
from app.services import index_service
scheduled = []
monkeypatch.setattr(index_service, 'schedule_workspace_rebuild', lambda: scheduled.append(True))
vault = get_settings().vault_path
vault.mkdir(parents=True, exist_ok=True)
external = vault / 'external.md'
external.write_text('# External\n', encoding='utf-8')
tree = asyncio.run(get_workspace_tree())
assert tree[0].note_id is not None
external.rename(vault / 'renamed.md')
tree = asyncio.run(get_workspace_tree())
assert [item.name for item in tree] == ['renamed.md']
(vault / 'renamed.md').unlink()
assert asyncio.run(get_workspace_tree()) == []
assert len(scheduled) == 3
def test_save_rejects_external_content_change() -> None:
import hashlib
from app.contracts import NoteUpdateRequest
from app.routes import update_note
original = '# Original\n'
note = asyncio.run(create_note(NoteCreateRequest(title='Conflict', markdown=original)))
disk = get_settings().vault_path / note.file_path
disk.write_text('# External\n', encoding='utf-8')
with pytest.raises(ApiError) as error:
asyncio.run(update_note(note.note_id, NoteUpdateRequest(markdown='# Editor\n', expected_content_hash=hashlib.sha256(original.encode()).hexdigest())))
assert error.value.code == 'NOTE_CONTENT_CONFLICT'
assert disk.read_text(encoding='utf-8') == '# External\n'