修复索引首次失败回滚、Vault 扫描边界、Markdown 代码围栏和 UTF-16 Citation 偏移。 收紧 Plugin 权限与 JSON Schema 校验,补齐 Note Move、Task、Attachment 和 Transcript Tool。 接入 OpenAI SSE 与 Ollama JSONL 真流式输出,修正 Provider PATCH 语义并限制运行时内存保留。 新增对应回归测试,后端测试增至 62 项。
47 lines
1.5 KiB
Python
47 lines
1.5 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from app.config import get_settings
|
|
from app.errors import ApiError
|
|
|
|
_ATTACHMENT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$")
|
|
MAX_ATTACHMENT_BYTES = 1024 * 1024
|
|
|
|
|
|
def attachment_path(attachment_id: str) -> Path:
|
|
if not _ATTACHMENT_ID.fullmatch(attachment_id):
|
|
raise ApiError(
|
|
400, "INVALID_ATTACHMENT_ID", "attachment_id is invalid",
|
|
{"attachment_id": attachment_id},
|
|
)
|
|
root = get_settings().attachments_path.resolve()
|
|
candidate = (root / attachment_id).resolve()
|
|
if not candidate.is_relative_to(root):
|
|
raise ApiError(400, "INVALID_ATTACHMENT_ID", "attachment path escapes storage")
|
|
return candidate
|
|
|
|
|
|
def read_attachment(attachment_id: str, *, max_chars: int = 100_000) -> dict[str, object]:
|
|
path = attachment_path(attachment_id)
|
|
if not path.is_file():
|
|
raise ApiError(
|
|
404, "ATTACHMENT_NOT_FOUND", "attachment not found",
|
|
{"attachment_id": attachment_id},
|
|
)
|
|
size = path.stat().st_size
|
|
if size > MAX_ATTACHMENT_BYTES:
|
|
raise ApiError(
|
|
413, "ATTACHMENT_TOO_LARGE", "attachment exceeds the Tool read limit",
|
|
{"attachment_id": attachment_id, "size": size},
|
|
)
|
|
text = path.read_text(encoding="utf-8")
|
|
truncated = len(text) > max_chars
|
|
return {
|
|
"attachment_id": attachment_id,
|
|
"content": text[:max_chars],
|
|
"size": size,
|
|
"truncated": truncated,
|
|
}
|