diff --git a/backend/app/api/v1/audio_overviews.py b/backend/app/api/v1/audio_overviews.py index a1195bf3..ad807bda 100644 --- a/backend/app/api/v1/audio_overviews.py +++ b/backend/app/api/v1/audio_overviews.py @@ -199,7 +199,17 @@ async def delete_audio_overview( def _parse_authors(authors_field: str | list | None) -> list[str]: """Parse authors field into a list of strings.""" if isinstance(authors_field, list): - return authors_field + authors: list[str] = [] + for author in authors_field: + if isinstance(author, str): + name = author + elif isinstance(author, dict): + name = str(author.get("name") or author.get("full_name") or "") + else: + name = str(author) + if name.strip(): + authors.append(name.strip()) + return authors if isinstance(authors_field, str): return [a.strip() for a in authors_field.split(";") if a.strip()] return [] diff --git a/backend/app/api/v1/rag.py b/backend/app/api/v1/rag.py index 62bf2b2f..939d0cca 100644 --- a/backend/app/api/v1/rag.py +++ b/backend/app/api/v1/rag.py @@ -4,7 +4,7 @@ import json import logging -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field from sqlalchemy import select @@ -78,15 +78,21 @@ def _reset_chroma_client_if_closed(rag: RAGService, exc: Exception) -> None: rag._count_cache.clear() -async def _preserve_existing_index(rag: RAGService, project_id: int) -> dict: +async def _preserve_existing_index(rag: RAGService, project_id: int, source_error: Exception) -> dict: try: existing_count = await rag._get_count(project_id) except Exception: logger.warning("Failed to count existing index for project %d; returning zero", project_id, exc_info=True) existing_count = 0 + if existing_count <= 0: + raise HTTPException( + status_code=503, + detail=f"RAG indexing failed before any chunks were available in the index: {source_error}", + ) from source_error return { "indexed": existing_count, "collection": f"project_{project_id}", + "reused_existing_index": True, } @@ -108,7 +114,7 @@ async def _index_chunks_with_recovery(rag: RAGService, project_id: int, chunks: raise logger.exception("Index retry failed with recoverable error; preserving existing index") _reset_chroma_client_if_closed(rag, retry_exc) - return await _preserve_existing_index(rag, project_id) + return await _preserve_existing_index(rag, project_id, retry_exc) @router.post("/query", response_model=ApiResponse[RAGQueryResponse], summary="RAG query over literature") diff --git a/backend/app/services/audio_overview_service.py b/backend/app/services/audio_overview_service.py index 02d7434f..56e8e32f 100644 --- a/backend/app/services/audio_overview_service.py +++ b/backend/app/services/audio_overview_service.py @@ -2,9 +2,12 @@ from __future__ import annotations +import asyncio import logging from typing import TYPE_CHECKING +from app.config import settings + if TYPE_CHECKING: from app.services.llm.client import LLMClient @@ -88,10 +91,13 @@ async def generate_dialogue( ] try: - result = await self.llm.chat_json( - messages, - temperature=0.7, - task_type="audio_overview_dialogue", + result = await asyncio.wait_for( + self.llm.chat_json( + messages, + temperature=0.7, + task_type="audio_overview_dialogue", + ), + timeout=min(settings.rewrite_timeout, 15.0), ) if not result or "script" not in result: logger.warning("LLM returned invalid dialogue format, using fallback") diff --git a/backend/app/services/embedding_service.py b/backend/app/services/embedding_service.py index fcae7cd4..3a0fb9b6 100644 --- a/backend/app/services/embedding_service.py +++ b/backend/app/services/embedding_service.py @@ -4,6 +4,7 @@ import logging import os +from pathlib import Path from typing import TYPE_CHECKING from app.config import settings @@ -167,17 +168,67 @@ def _build_local_embedding(model_name: str) -> BaseEmbedding: _inject_hf_env() _cleanup_gpu_memory() - has_gpu, _count, device = detect_gpu(pinned_gpu_id=settings.embed_gpu_id) + _has_gpu, _count, device = detect_gpu(pinned_gpu_id=settings.embed_gpu_id) batch_size = settings.embed_batch_size - logger.info("Loading local embedding model=%s device=%s batch_size=%d", model_name, device, batch_size) + resolved_model_name = _resolve_cached_hf_snapshot(model_name) or model_name + logger.info( + "Loading local embedding model=%s resolved=%s device=%s batch_size=%d", + model_name, + resolved_model_name, + device, + batch_size, + ) return HuggingFaceEmbedding( - model_name=model_name, + model_name=resolved_model_name, device=device, embed_batch_size=batch_size, + local_files_only=True, ) +def _resolve_cached_hf_snapshot(model_name: str) -> str | None: + """Return a complete local HuggingFace snapshot path when one is cached.""" + if Path(model_name).exists(): + return model_name + + if "/" not in model_name: + return None + + try: + from huggingface_hub.constants import HF_HUB_CACHE + except Exception: + return None + + model_dir = Path(HF_HUB_CACHE) / f"models--{model_name.replace('/', '--')}" + snapshots_dir = model_dir / "snapshots" + if not snapshots_dir.exists(): + return None + + ref_file = model_dir / "refs" / "main" + snapshot: Path | None = None + if ref_file.exists(): + ref = ref_file.read_text(encoding="utf-8").strip() + candidate = snapshots_dir / ref + if candidate.exists(): + snapshot = candidate + + if snapshot is None: + candidates = sorted((p for p in snapshots_dir.iterdir() if p.is_dir()), key=lambda p: p.stat().st_mtime) + if candidates: + snapshot = candidates[-1] + + if snapshot is None: + return None + + required_files = ("config.json", "modules.json") + if not all((snapshot / file_name).exists() for file_name in required_files): + logger.warning("Ignoring incomplete HuggingFace cache snapshot for %s: %s", model_name, snapshot) + return None + + return str(snapshot) + + def _build_api_embedding(model_name: str) -> BaseEmbedding: from llama_index.embeddings.openai import OpenAIEmbedding diff --git a/backend/app/services/rag_service.py b/backend/app/services/rag_service.py index be6b43d2..1a24d3b1 100644 --- a/backend/app/services/rag_service.py +++ b/backend/app/services/rag_service.py @@ -356,9 +356,13 @@ async def query( context_text = "\n\n---\n\n".join(contexts) if self.llm: - answer = await self._generate_answer(question, context_text) + try: + answer = await self._generate_answer(question, context_text) + except Exception as exc: + logger.warning("RAG answer generation failed; returning retrieval-only answer: %s", exc) + answer = self._build_retrieval_only_answer(question, sources, error=str(exc)) else: - answer = f"Retrieved {len(sources)} relevant passages. LLM not available for answer generation." + answer = self._build_retrieval_only_answer(question, sources) avg_score = sum(s["relevance_score"] for s in sources) / len(sources) if sources else 0 @@ -368,6 +372,27 @@ async def query( "confidence": round(avg_score, 3), } + def _build_retrieval_only_answer(self, question: str, sources: list[dict], *, error: str | None = None) -> str: + if not sources: + return "No relevant documents found." + + lines = [ + f"Retrieved {len(sources)} relevant passages for: {question}", + "LLM answer generation is unavailable, so this response is based on retrieved source excerpts only.", + ] + if error: + lines.append(f"Generation error: {error}") + + lines.append("Top evidence:") + for source in sources[:3]: + title = source.get("paper_title") or "Unknown paper" + page = source.get("page_number") or "?" + score = source.get("relevance_score", 0) + excerpt = (source.get("excerpt") or "").strip() + lines.append(f"- {title} (p.{page}, score {score}): {excerpt}") + + return "\n".join(lines) + async def retrieve_only( self, project_id: int, diff --git a/docs/guide/testing.md b/docs/guide/testing.md index a4e4a47d..e2705102 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -1,159 +1,58 @@ -# Testing Guide / 测试指南 +# Testing Guide -## 后端测试 - -### 运行测试 +## Backend Tests ```bash cd backend pytest tests/ -v --tb=short +# 857 tests · pytest-asyncio · 42 skipped (GPU/LLM-dependent) ``` -### 测试覆盖(229 个测试,最近验证:2026-03-15) - -| 测试文件 | 覆盖范围 | 测试数 | -|----------|----------|--------| -| `test_projects.py` | Projects CRUD API | 5 | -| `test_chat.py` | Conversations CRUD API | 8 | -| `test_chat_pipeline.py` | Chat Stream SSE (LangGraph) | 14 | -| `test_completion.py` | CompletionService + `/chat/complete` API | 7 | -| `test_citation_graph.py` | CitationGraphService + `/citation-graph` API | 4 | -| `test_keywords.py` | Keywords CRUD + Expand + Search Formula | 12 | -| `test_search.py` | SearchService + `/search/execute` | 9 | -| `test_dedup.py` | DedupService + Dedup APIs | 12 | -| `test_subscription.py` | SubscriptionService + Subscription APIs | 8 | -| `test_crawler.py` | CrawlerService + `/crawl/*` | 8 | -| `test_ocr.py` | OCRService + `/ocr/*` | 10 | -| `test_rag.py` | RAGService + `/rag/*` | 10 | -| `test_writing.py` | WritingService + Writing APIs + Stream | 18 | -| `test_llm_settings.py` | LLM Factory + Settings APIs | 15 | -| `test_pipelines.py` | LangGraph Pipeline + Pipeline APIs | 10 | -| `test_knowledge_base.py` | PDF Upload + Dedup Resolve | 4 | -| `test_embedding.py` | EmbeddingService (mock/local/api) | 5 | -| `test_paper_processor.py` | PDF Metadata Extraction | 5 | -| `test_pipeline_e2e.py` | End-to-end Pipeline Flows | 4 | -| `test_integration.py` | Cross-module Integration | 12 | -| `test_mcp.py` | MCP Tools | 8 | - -### API 端点测试覆盖 - -| 端点分组 | 端点数 | 已测试 | 覆盖率 | -|----------|--------|--------|--------| -| Projects | 7 | 5 | 71% | -| Papers | 8 | 6 | 75% | -| Keywords | 7 | 7 | 100% | -| Search | 2 | 2 | 100% | -| Dedup | 5 | 5 | 100% | -| Crawler | 2 | 2 | 100% | -| OCR | 2 | 2 | 100% | -| Subscription | 10 | 6 | 60% | -| RAG | 5 | 4 | 80% | -| Writing | 7 | 7 | 100% | -| Chat | 3 | 3 | 100% | -| Conversations | 5 | 4 | 80% | -| Settings | 5 | 5 | 100% | -| Tasks | 3 | 1 | 33% | -| Pipelines | 5 | 4 | 80% | +### Lint & Type Check -**总覆盖率**: 76/76 核心端点已测试,部分辅助端点(如 tasks cancel)尚未覆盖。 - -## 前端测试 +```bash +ruff check app/ # Python lint +ruff format --check app/ # Format check +mypy app/ # Type check +``` -### 运行测试 +## Frontend Tests ```bash cd frontend -npm test # Vitest 单元测试 -npx tsc --noEmit # TypeScript 类型检查 -npm run build # 构建验证 +npm test # 264 tests · Vitest + Testing Library + MSW +npx tsc --noEmit # TypeScript strict mode +npx eslint src/ # Lint ``` -### 类型检查 - -前端使用 TypeScript strict mode,`npx tsc --noEmit` 确保无类型错误。 - -## E2E 测试(Playwright) +## E2E Tests ```bash -# 需要前后端服务运行中(CI=1 时自动启动 frontend dev server) -CI= npx playwright test - -# 运行指定测试文件 -CI= npx playwright test e2e/integration.spec.ts +npx playwright test # 39 tests +npx playwright test --ui # Interactive mode ``` -配置文件:`playwright.config.ts` - -### E2E 测试覆盖(19 个测试,最近验证:2026-03-15) - -| 测试文件 | 覆盖范围 | 测试数 | -|----------|----------|--------| -| `smoke.spec.ts` | 首页 Playground 加载 | 1 | -| `chat-flow.spec.ts` | 聊天流程、KB 选择器、新建对话 | 3 | -| `chat-restore.spec.ts` | 对话历史、无效 ID 处理 | 2 | -| `kb-paper-flow.spec.ts` | 知识库列表、项目导航、路由重定向、任务页 | 4 | -| `integration.spec.ts` | 创建项目、项目详情、写作页、发现页、设置页、导航 | 9 | - -## 后端 API 联调(curl) - -### 已验证端点(31 个端点,最近验证:2026-03-15) +## Current Test Counts -| # | 端点 | 方法 | 状态 | -|---|------|------|------| -| 1 | `/api/v1/settings/health` | GET | 通过 | -| 2 | `/api/v1/projects` | POST | 通过 | -| 3 | `/api/v1/projects` | GET | 通过 | -| 4 | `/api/v1/projects/{id}` | GET | 通过 | -| 5 | `/api/v1/projects/{id}/papers` | POST | 通过 | -| 6 | `/api/v1/projects/{id}/papers` | GET | 通过 | -| 7 | `/api/v1/projects/{id}/papers/{pid}` | GET | 通过 | -| 8 | `/api/v1/projects/{id}/keywords` | POST | 通过 | -| 9 | `/api/v1/projects/{id}/keywords` | GET | 通过 | -| 10 | `/api/v1/projects/{id}/keywords/expand` | POST | 通过 | -| 11 | `/api/v1/projects/{id}/keywords/search-formula` | GET | 通过 | -| 12 | `/api/v1/projects/{id}/search/sources` | GET | 通过 | -| 13 | `/api/v1/projects/{id}/search/execute` | POST | 通过 | -| 14 | `/api/v1/projects/{id}/dedup/run` | POST | 通过 | -| 15 | `/api/v1/conversations` | POST | 通过 | -| 16 | `/api/v1/conversations` | GET | 通过 | -| 17 | `/api/v1/conversations/{id}` | GET | 通过 | -| 18 | `/api/v1/settings` | GET | 通过 | -| 19 | `/api/v1/settings/models` | GET | 通过 | -| 20 | `/api/v1/settings/test-connection` | POST | 通过 | -| 21 | `/api/v1/projects/{id}/writing/summarize` | POST | 通过 | -| 22 | `/api/v1/projects/{id}/writing/citations` | POST | 通过 | -| 23 | `/api/v1/projects/{id}/subscriptions/feeds` | GET | 通过 | -| 24 | `/api/v1/projects/{id}/subscriptions` | POST | 通过 | -| 25 | `/api/v1/projects/{id}/subscriptions` | GET | 通过 | -| 26 | `/api/v1/projects/{id}/ocr/stats` | GET | 通过 | -| 27 | `/api/v1/projects/{id}/crawl/stats` | GET | 通过 | -| 28 | `/api/v1/tasks` | GET | 通过 | -| 29 | `/api/v1/chat/complete` | POST | 通过 | +| Suite | Framework | Tests | +|-------|-----------|-------| +| Backend | pytest-asyncio | 857 | +| Frontend | Vitest + MSW | 264 | +| E2E | Playwright | 39 | +| **Total** | | **1,160** | -## 联调测试清单 +## CI Pipeline -### 已验证流程 +GitHub Actions runs on every push and PR: +1. Backend: ruff → ruff-format → mypy → pytest +2. Frontend: ESLint → tsc → vitest +3. Docs: VitePress build +4. E2E: Playwright (on PR) -- [x] 后端 229 个 pytest 测试全部通过 -- [x] 后端 lint 零错误(ruff check + ruff format) -- [x] 后端 API 联调 29 个端点全部通过(LLM_PROVIDER=mock) -- [x] 前端 19 个 Playwright E2E 测试全部通过 -- [x] .env.example 与 config.py 配置项对齐 -- [x] pyproject.toml / package.json 依赖与实际安装一致 -- [x] VitePress 侧边栏与文档文件一一对应(16 个 API + Deployment guide) -- [x] README API 列表包含 Phase 4 新端点 -- [x] 所有页面可正常加载(Playground、知识库、设置、历史、任务) -- [x] 跨页面导航无错误 +All checks must pass before merge. -### 已修复的问题 +## Test Fixtures -| 问题 | 修复方式 | -|------|----------| -| VitePress sidebar 缺少 9 个 API 入口 | 补全 EN/ZH 各 9 个条目 | -| .env.example 缺少 14 个配置项 | 添加 LLM providers, embedding, OCR 等配置 | -| deployment.md 引用错误路径 | `docs/guides/mineru-setup.md` → `/deployment/mineru-setup` | -| Guide sidebar 缺少 Deployment 入口 | EN/ZH 均添加 Deployment 链接 | -| README 缺少 Phase 4 API 端点 | 添加 complete, citation-graph, review-draft/stream | -| KB alias 测试访问不存在路由 | 移除 3 个无效测试 | -| LangGraph checkpointer 返回 context manager | 使用 MemorySaver 替代 AsyncSqliteSaver | -| ALIYUN_BASE_URL 默认值不一致 | 统一为 `dashscope.aliyuncs.com/compatible-mode/v1` | +- Backend: `conftest.py` with async test client, test database, mock LLM +- Frontend: MSW handlers in `src/test/mocks/handlers.ts` mock all API endpoints +- E2E: Real browser against dev server with test data diff --git a/docs/zh/guide/testing.md b/docs/zh/guide/testing.md index 0c67057f..a264eb27 100644 --- a/docs/zh/guide/testing.md +++ b/docs/zh/guide/testing.md @@ -1,159 +1,52 @@ -# Testing Guide / 测试指南 +# 测试指南 ## 后端测试 -### 运行测试 - ```bash cd backend pytest tests/ -v --tb=short +# 857 测试 · pytest-asyncio · 42 跳过(需要 GPU/LLM) ``` -### 测试覆盖(229 个测试,最近验证:2026-03-15) - -| 测试文件 | 覆盖范围 | 测试数 | -|----------|----------|--------| -| `test_projects.py` | Projects CRUD API | 5 | -| `test_chat.py` | Conversations CRUD API | 8 | -| `test_chat_pipeline.py` | Chat Stream SSE (LangGraph) | 14 | -| `test_completion.py` | CompletionService + `/chat/complete` API | 7 | -| `test_citation_graph.py` | CitationGraphService + `/citation-graph` API | 4 | -| `test_keywords.py` | Keywords CRUD + Expand + Search Formula | 12 | -| `test_search.py` | SearchService + `/search/execute` | 9 | -| `test_dedup.py` | DedupService + Dedup APIs | 12 | -| `test_subscription.py` | SubscriptionService + Subscription APIs | 8 | -| `test_crawler.py` | CrawlerService + `/crawl/*` | 8 | -| `test_ocr.py` | OCRService + `/ocr/*` | 10 | -| `test_rag.py` | RAGService + `/rag/*` | 10 | -| `test_writing.py` | WritingService + Writing APIs + Stream | 18 | -| `test_llm_settings.py` | LLM Factory + Settings APIs | 15 | -| `test_pipelines.py` | LangGraph Pipeline + Pipeline APIs | 10 | -| `test_knowledge_base.py` | PDF Upload + Dedup Resolve | 4 | -| `test_embedding.py` | EmbeddingService (mock/local/api) | 5 | -| `test_paper_processor.py` | PDF Metadata Extraction | 5 | -| `test_pipeline_e2e.py` | End-to-end Pipeline Flows | 4 | -| `test_integration.py` | Cross-module Integration | 12 | -| `test_mcp.py` | MCP Tools | 8 | - -### API 端点测试覆盖 - -| 端点分组 | 端点数 | 已测试 | 覆盖率 | -|----------|--------|--------|--------| -| Projects | 7 | 5 | 71% | -| Papers | 8 | 6 | 75% | -| Keywords | 7 | 7 | 100% | -| Search | 2 | 2 | 100% | -| Dedup | 5 | 5 | 100% | -| Crawler | 2 | 2 | 100% | -| OCR | 2 | 2 | 100% | -| Subscription | 10 | 6 | 60% | -| RAG | 5 | 4 | 80% | -| Writing | 7 | 7 | 100% | -| Chat | 3 | 3 | 100% | -| Conversations | 5 | 4 | 80% | -| Settings | 5 | 5 | 100% | -| Tasks | 3 | 1 | 33% | -| Pipelines | 5 | 4 | 80% | +### 代码检查 -**总覆盖率**: 76/76 核心端点已测试,部分辅助端点(如 tasks cancel)尚未覆盖。 +```bash +ruff check app/ # Python 代码检查 +ruff format --check app/ # 格式检查 +mypy app/ # 类型检查 +``` ## 前端测试 -### 运行测试 - ```bash cd frontend -npm test # Vitest 单元测试 -npx tsc --noEmit # TypeScript 类型检查 -npm run build # 构建验证 +npm test # 264 测试 · Vitest + Testing Library + MSW +npx tsc --noEmit # TypeScript 严格模式 +npx eslint src/ # 代码检查 ``` -### 类型检查 - -前端使用 TypeScript strict mode,`npx tsc --noEmit` 确保无类型错误。 - -## E2E 测试(Playwright) +## E2E 测试 ```bash -# 需要前后端服务运行中(CI=1 时自动启动 frontend dev server) -CI= npx playwright test - -# 运行指定测试文件 -CI= npx playwright test e2e/integration.spec.ts +npx playwright test # 39 测试 +npx playwright test --ui # 交互模式 ``` -配置文件:`playwright.config.ts` - -### E2E 测试覆盖(19 个测试,最近验证:2026-03-15) - -| 测试文件 | 覆盖范围 | 测试数 | -|----------|----------|--------| -| `smoke.spec.ts` | 首页 Playground 加载 | 1 | -| `chat-flow.spec.ts` | 聊天流程、KB 选择器、新建对话 | 3 | -| `chat-restore.spec.ts` | 对话历史、无效 ID 处理 | 2 | -| `kb-paper-flow.spec.ts` | 知识库列表、项目导航、路由重定向、任务页 | 4 | -| `integration.spec.ts` | 创建项目、项目详情、写作页、发现页、设置页、导航 | 9 | - -## 后端 API 联调(curl) - -### 已验证端点(29 个端点,最近验证:2026-03-15) - -| # | 端点 | 方法 | 状态 | -|---|------|------|------| -| 1 | `/api/v1/settings/health` | GET | 通过 | -| 2 | `/api/v1/projects` | POST | 通过 | -| 3 | `/api/v1/projects` | GET | 通过 | -| 4 | `/api/v1/projects/{id}` | GET | 通过 | -| 5 | `/api/v1/projects/{id}/papers` | POST | 通过 | -| 6 | `/api/v1/projects/{id}/papers` | GET | 通过 | -| 7 | `/api/v1/projects/{id}/papers/{pid}` | GET | 通过 | -| 8 | `/api/v1/projects/{id}/keywords` | POST | 通过 | -| 9 | `/api/v1/projects/{id}/keywords` | GET | 通过 | -| 10 | `/api/v1/projects/{id}/keywords/expand` | POST | 通过 | -| 11 | `/api/v1/projects/{id}/keywords/search-formula` | GET | 通过 | -| 12 | `/api/v1/projects/{id}/search/sources` | GET | 通过 | -| 13 | `/api/v1/projects/{id}/search/execute` | POST | 通过 | -| 14 | `/api/v1/projects/{id}/dedup/run` | POST | 通过 | -| 15 | `/api/v1/conversations` | POST | 通过 | -| 16 | `/api/v1/conversations` | GET | 通过 | -| 17 | `/api/v1/conversations/{id}` | GET | 通过 | -| 18 | `/api/v1/settings` | GET | 通过 | -| 19 | `/api/v1/settings/models` | GET | 通过 | -| 20 | `/api/v1/settings/test-connection` | POST | 通过 | -| 21 | `/api/v1/projects/{id}/writing/summarize` | POST | 通过 | -| 22 | `/api/v1/projects/{id}/writing/citations` | POST | 通过 | -| 23 | `/api/v1/projects/{id}/subscriptions/feeds` | GET | 通过 | -| 24 | `/api/v1/projects/{id}/subscriptions` | POST | 通过 | -| 25 | `/api/v1/projects/{id}/subscriptions` | GET | 通过 | -| 26 | `/api/v1/projects/{id}/ocr/stats` | GET | 通过 | -| 27 | `/api/v1/projects/{id}/crawl/stats` | GET | 通过 | -| 28 | `/api/v1/tasks` | GET | 通过 | -| 29 | `/api/v1/chat/complete` | POST | 通过 | - -## 联调测试清单 +## 测试统计 -### 已验证流程 +| 套件 | 框架 | 测试数 | +|------|------|--------| +| 后端 | pytest-asyncio | 857 | +| 前端 | Vitest + MSW | 264 | +| E2E | Playwright | 39 | +| **合计** | | **1,160** | -- [x] 后端 229 个 pytest 测试全部通过 -- [x] 后端 lint 零错误(ruff check + ruff format) -- [x] 后端 API 联调 29 个端点全部通过(LLM_PROVIDER=mock) -- [x] 前端 19 个 Playwright E2E 测试全部通过 -- [x] .env.example 与 config.py 配置项对齐 -- [x] pyproject.toml / package.json 依赖与实际安装一致 -- [x] VitePress 侧边栏与文档文件一一对应(16 个 API + Deployment guide) -- [x] README API 列表包含 Phase 4 新端点 -- [x] 所有页面可正常加载(Playground、知识库、设置、历史、任务) -- [x] 跨页面导航无错误 +## CI 流程 -### 已修复的问题 +GitHub Actions 在每次推送和 PR 时运行: +1. 后端:ruff → ruff-format → mypy → pytest +2. 前端:ESLint → tsc → vitest +3. 文档:VitePress 构建 +4. E2E:Playwright(PR 时) -| 问题 | 修复方式 | -|------|----------| -| VitePress sidebar 缺少 9 个 API 入口 | 补全 EN/ZH 各 9 个条目 | -| .env.example 缺少 14 个配置项 | 添加 LLM providers, embedding, OCR 等配置 | -| deployment.md 引用错误路径 | `docs/guides/mineru-setup.md` → `/deployment/mineru-setup` | -| Guide sidebar 缺少 Deployment 入口 | EN/ZH 均添加 Deployment 链接 | -| README 缺少 Phase 4 API 端点 | 添加 complete, citation-graph, review-draft/stream | -| KB alias 测试访问不存在路由 | 移除 3 个无效测试 | -| LangGraph checkpointer 返回 context manager | 使用 MemorySaver 替代 AsyncSqliteSaver | -| ALIYUN_BASE_URL 默认值不一致 | 统一为 `dashscope.aliyuncs.com/compatible-mode/v1` | +所有检查必须在合并前通过。 diff --git a/frontend/src/components/citation/BibliographyBuilderDialog.tsx b/frontend/src/components/citation/BibliographyBuilderDialog.tsx index f0caa20d..cc1c43ea 100644 --- a/frontend/src/components/citation/BibliographyBuilderDialog.tsx +++ b/frontend/src/components/citation/BibliographyBuilderDialog.tsx @@ -30,8 +30,8 @@ export function BibliographyBuilderDialog({ projectId, onClose }: BibliographyBu const [copied, setCopied] = useState(false); const { data: papersData, isLoading } = useQuery({ - queryKey: queryKeys.papers.list(projectId, { page: 1, page_size: 200 }), - queryFn: () => paperApi.list(projectId, { page: 1, page_size: 200 }), + queryKey: queryKeys.papers.list(projectId, { page: 1, page_size: 100 }), + queryFn: () => paperApi.list(projectId, { page: 1, page_size: 100 }), }); const papers = useMemo(() => papersData?.items ?? [], [papersData]); diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index e99205d2..58c98515 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -128,6 +128,8 @@ export default function SettingsPage() { }; const currentProvider = form.llm_provider ?? 'mock'; + const healthStatus = (healthData as { status?: string } | undefined)?.status; + const isHealthy = healthStatus === 'ok' || healthStatus === 'healthy'; const saveButton = (