feat: add artifact library and improve public wiki
This commit is contained in:
1 parent
1b43205c1b
commit
dd0ee282df
16 files changed
+4081
-98
No files matched your search
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.gateway import deps
|
||||
from app.gateway.routers import artifacts
|
||||
|
||||
|
||||
class _FakeThreadStore:
|
||||
def __init__(self, rows: list[dict]) -> None:
|
||||
self.rows = rows
|
||||
self.calls: list[dict] = []
|
||||
|
||||
async def search(self, **kwargs) -> list[dict]:
|
||||
self.calls.append(kwargs)
|
||||
offset = int(kwargs.get("offset", 0))
|
||||
limit = int(kwargs.get("limit", 100))
|
||||
return self.rows[offset : offset + limit]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_artifact_library_aggregates_generated_non_media_files(tmp_path: Path, monkeypatch):
|
||||
thread_one = tmp_path / "thread-one" / "outputs"
|
||||
thread_two = tmp_path / "thread-two" / "outputs"
|
||||
thread_one.mkdir(parents=True)
|
||||
thread_two.mkdir(parents=True)
|
||||
|
||||
report = thread_one / "研判报告.docx"
|
||||
table = thread_one / "数据汇总.xlsx"
|
||||
image = thread_one / "配图.png"
|
||||
audio = thread_two / "录音.mp3"
|
||||
notes = thread_two / "结论.md"
|
||||
archive = thread_two / "证据.zip"
|
||||
for path, content in (
|
||||
(report, b"doc"),
|
||||
(table, b"sheet"),
|
||||
(image, b"image"),
|
||||
(audio, b"audio"),
|
||||
(notes, b"# notes"),
|
||||
(archive, b"zip"),
|
||||
):
|
||||
path.write_bytes(content)
|
||||
|
||||
os.utime(report, (100, 100))
|
||||
os.utime(table, (200, 200))
|
||||
os.utime(notes, (300, 300))
|
||||
os.utime(archive, (400, 400))
|
||||
|
||||
rows = [
|
||||
{
|
||||
"thread_id": "thread-one",
|
||||
"display_name": "战场环境综合研判",
|
||||
"metadata": {"agent_id": "analysis-agent"},
|
||||
},
|
||||
{
|
||||
"thread_id": "thread-two",
|
||||
"display_name": "生成发展报告",
|
||||
"metadata": {},
|
||||
},
|
||||
]
|
||||
store = _FakeThreadStore(rows)
|
||||
|
||||
class _Paths:
|
||||
def sandbox_outputs_dir(self, thread_id: str, *, user_id: str) -> Path:
|
||||
assert user_id == "owner"
|
||||
return tmp_path / thread_id / "outputs"
|
||||
|
||||
async def _resolve_owner(_thread_id: str) -> str:
|
||||
return "owner"
|
||||
|
||||
monkeypatch.setattr(artifacts, "get_paths", lambda: _Paths())
|
||||
monkeypatch.setattr(artifacts, "aresolve_path_user_id", _resolve_owner)
|
||||
|
||||
items = await artifacts._scan_artifact_library(store)
|
||||
|
||||
assert [item["name"] for item in items] == ["证据.zip", "结论.md", "数据汇总.xlsx", "研判报告.docx"]
|
||||
assert [item["file_kind"] for item in items] == ["archive", "text_data", "spreadsheet", "document"]
|
||||
assert items[-1]["thread_title"] == "战场环境综合研判"
|
||||
assert items[-1]["agent_id"] == "analysis-agent"
|
||||
assert all(item["name"] not in {"配图.png", "录音.mp3"} for item in items)
|
||||
assert store.calls[0]["exclude_system"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_artifact_library_endpoint_filters_searches_and_pages(tmp_path: Path, monkeypatch):
|
||||
output_dir = tmp_path / "thread-one" / "outputs"
|
||||
output_dir.mkdir(parents=True)
|
||||
(output_dir / "任务报告.md").write_text("report", encoding="utf-8")
|
||||
(output_dir / "任务数据.csv").write_text("a,b", encoding="utf-8")
|
||||
|
||||
store = _FakeThreadStore(
|
||||
[
|
||||
{
|
||||
"thread_id": "thread-one",
|
||||
"display_name": "任务研判对话",
|
||||
"metadata": {},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
class _Paths:
|
||||
def sandbox_outputs_dir(self, thread_id: str, *, user_id: str) -> Path:
|
||||
return tmp_path / thread_id / "outputs"
|
||||
|
||||
async def _resolve_owner(_thread_id: str) -> str:
|
||||
return "owner"
|
||||
|
||||
monkeypatch.setattr(artifacts, "get_paths", lambda: _Paths())
|
||||
monkeypatch.setattr(artifacts, "aresolve_path_user_id", _resolve_owner)
|
||||
monkeypatch.setattr(artifacts, "get_effective_user_id", lambda: "owner")
|
||||
monkeypatch.setattr(deps, "get_thread_store", lambda _request: store)
|
||||
artifacts._artifact_library_cache.clear()
|
||||
|
||||
raw_endpoint = artifacts.list_artifact_library.__wrapped__
|
||||
response = await raw_endpoint(
|
||||
request=SimpleNamespace(),
|
||||
query="研判对话",
|
||||
file_kind="text_data",
|
||||
offset=0,
|
||||
limit=1,
|
||||
refresh=True,
|
||||
)
|
||||
|
||||
assert response.total == 1
|
||||
assert response.offset == 0
|
||||
assert response.limit == 1
|
||||
assert [item.name for item in response.items] == ["任务报告.md"]
|
||||
|
||||
spreadsheet_response = await raw_endpoint(
|
||||
request=SimpleNamespace(),
|
||||
query="任务数据",
|
||||
file_kind="spreadsheet",
|
||||
offset=0,
|
||||
limit=50,
|
||||
refresh=False,
|
||||
)
|
||||
assert spreadsheet_response.total == 1
|
||||
assert spreadsheet_response.items[0].name == "任务数据.csv"
|
||||
@@ -68,7 +68,7 @@ def _published_row() -> dict[str, object]:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_knowledge_base_list_exposes_only_published_wiki_identity() -> None:
|
||||
async def test_public_knowledge_base_list_exposes_live_safe_metadata_and_generated_index(monkeypatch) -> None:
|
||||
store = PublicListStore(
|
||||
[
|
||||
{
|
||||
@@ -83,21 +83,116 @@ async def test_public_knowledge_base_list_exposes_only_published_wiki_identity()
|
||||
"owner_user_id": "system",
|
||||
"name": "对话沉淀",
|
||||
},
|
||||
{
|
||||
**_published_row(),
|
||||
"id": "stale-kb",
|
||||
"weknora_id": "remote-missing",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
class Client:
|
||||
async def list_knowledge_bases(self):
|
||||
return [
|
||||
{
|
||||
"id": "remote-one",
|
||||
"name": "远端公开知识库",
|
||||
"description": "远端公开说明",
|
||||
"type": "wiki",
|
||||
"knowledge_count": 12,
|
||||
"chunk_count": 34,
|
||||
"processing_count": 1,
|
||||
"share_count": 3,
|
||||
"created_at": "2026-07-01T08:00:00+08:00",
|
||||
"updated_at": "2026-08-01T08:00:00+08:00",
|
||||
"tenant_id": 10001,
|
||||
"creator_id": "secret-creator",
|
||||
"storage_config": {"secret_key": "must-not-leak"},
|
||||
}
|
||||
]
|
||||
|
||||
async def get_knowledge_base(self, knowledge_base_id: str):
|
||||
assert knowledge_base_id == "remote-one"
|
||||
return {
|
||||
"id": knowledge_base_id,
|
||||
"vector_store_name": "System default",
|
||||
"vector_store_source": "env",
|
||||
"vector_store_engine_type": "elasticsearch",
|
||||
"vector_store_status": "available",
|
||||
"wiki_config": {"enabled": True},
|
||||
}
|
||||
|
||||
async def list_wiki_pages(self, knowledge_base_id: str, **kwargs):
|
||||
assert knowledge_base_id == "remote-one"
|
||||
assert kwargs == {"page": 1, "page_size": 500, "query": ""}
|
||||
return {
|
||||
"pages": [
|
||||
{
|
||||
"id": "page-one",
|
||||
"slug": "concept/start",
|
||||
"title": "开始使用",
|
||||
"page_type": "concept",
|
||||
"status": "completed",
|
||||
"created_at": "2026-07-02T08:00:00+08:00",
|
||||
"updated_at": "2026-08-02T08:00:00+08:00",
|
||||
},
|
||||
{
|
||||
"id": "page-two",
|
||||
"slug": "summary/overview",
|
||||
"title": "知识概览",
|
||||
"page_type": "summary",
|
||||
"status": "completed",
|
||||
"created_at": "2026-07-03T08:00:00+08:00",
|
||||
"updated_at": "2026-08-03T08:00:00+08:00",
|
||||
},
|
||||
],
|
||||
"total": 2,
|
||||
"page": 1,
|
||||
"page_size": 500,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(llmwiki, "_client_or_503", lambda: Client())
|
||||
|
||||
payload = await llmwiki.list_public_knowledge_bases(_request(store))
|
||||
|
||||
assert store.calls == [("__public_wiki__", "public", False)]
|
||||
assert payload == {
|
||||
"knowledge_bases": [
|
||||
{
|
||||
"id": "1063edee-332a-43b4-bbd9-73bf0a708b94",
|
||||
"name": "公开知识库",
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
assert payload["total"] == 1
|
||||
item = payload["knowledge_bases"][0]
|
||||
assert item["id"] == "1063edee-332a-43b4-bbd9-73bf0a708b94"
|
||||
assert item["name"] == "远端公开知识库"
|
||||
assert item["description"] == "远端公开说明"
|
||||
assert item["type"] == "wiki"
|
||||
assert item["knowledge_count"] == 12
|
||||
assert item["chunk_count"] == 34
|
||||
assert item["processing_count"] == 1
|
||||
assert item["share_count"] == 3
|
||||
assert item["is_processing"] is True
|
||||
assert item["remote_created_at"] == "2026-07-01T08:00:00+08:00"
|
||||
assert item["remote_updated_at"] == "2026-08-01T08:00:00+08:00"
|
||||
assert item["vector_store_engine_type"] == "elasticsearch"
|
||||
assert item["wiki"] == {
|
||||
"status": "ready",
|
||||
"page_count": 2,
|
||||
"loaded_page_count": 2,
|
||||
"stats_complete": True,
|
||||
"page_type_counts": {"concept": 1, "summary": 1},
|
||||
"page_status_counts": {"completed": 2},
|
||||
"index_mode": "generated",
|
||||
"has_explicit_index": False,
|
||||
"index_slug": "__index__",
|
||||
"default_slug": "__index__",
|
||||
"created_at": "2026-07-02T08:00:00+08:00",
|
||||
"updated_at": "2026-08-03T08:00:00+08:00",
|
||||
"embed_route": "/embed/knowledge/1063edee-332a-43b4-bbd9-73bf0a708b94?wikiSlug=__index__",
|
||||
}
|
||||
assert item["local_index"]["state"] == "not_synced"
|
||||
assert item["local_index"]["fully_vectorized"] is False
|
||||
assert item["owner_user_id"] is None
|
||||
assert "weknora_id" not in item
|
||||
assert "tenant_id" not in item
|
||||
assert "creator_id" not in item
|
||||
assert "storage_config" not in item
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -178,6 +273,35 @@ async def test_public_wiki_page_list_is_normalized(monkeypatch) -> None:
|
||||
assert payload["pages"][0]["aliases"] == []
|
||||
|
||||
|
||||
def test_public_wiki_summary_prefers_an_explicit_index_page() -> None:
|
||||
payload = llmwiki._public_wiki_summary(
|
||||
"mapping-one",
|
||||
[
|
||||
{
|
||||
"slug": "guide/start",
|
||||
"title": "开始使用",
|
||||
"page_type": "article",
|
||||
"status": "completed",
|
||||
},
|
||||
{
|
||||
"slug": "home",
|
||||
"title": "知识首页",
|
||||
"page_type": "index",
|
||||
"status": "completed",
|
||||
},
|
||||
],
|
||||
total=2,
|
||||
complete=True,
|
||||
)
|
||||
|
||||
assert payload["status"] == "ready"
|
||||
assert payload["index_mode"] == "explicit"
|
||||
assert payload["has_explicit_index"] is True
|
||||
assert payload["index_slug"] == "home"
|
||||
assert payload["default_slug"] == "home"
|
||||
assert payload["embed_route"] == "/embed/knowledge/mapping-one?wikiSlug=home"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_wiki_file_is_authorized_and_streamed(monkeypatch) -> None:
|
||||
store = PublicStore(_published_row())
|
||||
|
||||
Reference in new issue
Block a user