fix(backend): 修复全面审阅发现的核心问题

修复索引首次失败回滚、Vault 扫描边界、Markdown 代码围栏和 UTF-16 Citation 偏移。

收紧 Plugin 权限与 JSON Schema 校验,补齐 Note Move、Task、Attachment 和 Transcript Tool。

接入 OpenAI SSE 与 Ollama JSONL 真流式输出,修正 Provider PATCH 语义并限制运行时内存保留。

新增对应回归测试,后端测试增至 62 项。
This commit is contained in:
2026-08-28 09:56:07 +08:00
parent 167ae24796
commit 36bc1022f1
26 changed files with 1531 additions and 99 deletions
+55
View File
@@ -19,6 +19,7 @@ from app.errors import ApiError
from app.knowledge.parser import ParsedNote, parse_note
from app.retrieval.embedding import HashEmbeddingProvider
from app.retrieval.vectorstore import SqliteVecStore, VectorRecord
from app.services.coordination import serialized_vault_mutation
# 轻量实现实例(无状态,可直接复用);接入真实模型后替换为对应 Provider
embedding = HashEmbeddingProvider()
@@ -148,6 +149,7 @@ async def index_note(parsed: ParsedNote) -> None:
conn.close()
@serialized_vault_mutation
async def create_note(*, title: str, markdown: str, folder: str | None, tags: list[str]) -> Note:
rel_path, clean_folder = _rel_path(folder, title)
now = datetime.now(timezone.utc)
@@ -183,6 +185,7 @@ async def get_note(note_id: str) -> Note | None:
record.created_at, record.updated_at, record.blocks, markdown)
@serialized_vault_mutation
async def update_note(
note_id: str, *, title: str | None = None, markdown: str | None = None, tags: list[str] | None = None
) -> Note:
@@ -213,6 +216,58 @@ async def update_note(
parsed.created_at, parsed.updated_at, parsed.blocks, new_md)
@serialized_vault_mutation
async def move_note(note_id: str, *, folder: str) -> Note:
record = repository.get_note_record(note_id)
if record is None:
raise ApiError(404, "RESOURCE_NOT_FOUND", "note not found", {"note_id": note_id})
clean_folder = _normalize_folder(folder)
filename = Path(record.file_path).name
new_rel_path = f"{clean_folder}/{filename}" if clean_folder else filename
if new_rel_path == record.file_path:
note = await get_note(note_id)
assert note is not None
return note
source = _abs_path(record.file_path)
target = _abs_path(new_rel_path)
if not source.is_file():
raise ApiError(
409, "NOTE_FILE_MISSING", "note file is missing from the Vault",
{"note_id": note_id, "file_path": record.file_path},
)
if target.exists():
raise ApiError(
409, "RESOURCE_CONFLICT", "a note already exists at the target path",
{"note_id": note_id, "file_path": new_rel_path},
)
markdown = source.read_text(encoding="utf-8")
target.parent.mkdir(parents=True, exist_ok=True)
source.replace(target)
try:
parsed = parse_note(
markdown=markdown,
file_path=new_rel_path,
folder=clean_folder,
tags=record.tags,
created_at=record.created_at,
updated_at=datetime.now(timezone.utc),
note_id=record.note_id,
)
parsed.title = record.title
await index_note(parsed)
except BaseException:
target.replace(source)
raise
return _build_note(
parsed.note_id, parsed.title, parsed.file_path, parsed.tags,
parsed.created_at, parsed.updated_at, parsed.blocks, markdown,
)
@serialized_vault_mutation
async def delete_note(note_id: str) -> bool:
record = repository.get_note_record(note_id)
if record is None: