feat: add artifact library and improve public wiki

This commit is contained in:
吉鑫升 committed 2026-09-01 18:18:28 +08:00
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())