Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
641bc7b
feat: Agent Harness 架构重构 Phase1-3 完成
Color2333 Mar 20, 2026
254ccd5
feat(agent): 重构 agent_service 使用 StreamingAgentLoop + 新架构
Color2333 Mar 20, 2026
bde83be
feat(agent): 实现 Context Compact + TodoWrite + Subagents 三大模块
Color2333 Mar 20, 2026
226ce21
fix(agent): 修复消息持久化丢失 + SSE解析bug
Color2333 Mar 20, 2026
c83d00a
fix(agent_tools): 修复 get_system_status SQLAlchemy session detached bug
Color2333 Mar 20, 2026
4a21cc0
fix(agent_core): 修复 StreamingAgentLoop 忽略 tool result 的严重 bug
Color2333 Mar 20, 2026
907e272
Merge branch 'main' of https://github.com/Color2333/PaperMind into dev
Color2333 Mar 23, 2026
d87be5d
chore: add worktrees to gitignore
Color2333 Mar 23, 2026
32f3345
feat: 集成 PaperSenseMaking 认知重构系统
Color2333 Mar 23, 2026
cbad405
feat: 注册 PaperSenseMaking 路由和数据库模型
Color2333 Mar 23, 2026
3795110
docs: 更新 README 添加 PaperSenseMaking 认知重构工作流
Color2333 Mar 23, 2026
6b607c7
fix: 修正 PaperSenseMaking GitHub 链接
Color2333 Mar 23, 2026
61925c1
feat: PdfReader 改为左右分屏布局
Color2333 Mar 23, 2026
b6232c0
fix: TranslationPanel 添加 PDF 下载功能
Color2333 Mar 23, 2026
5ff910c
fix: PdfReader 优先加载本地 PDF
Color2333 Mar 23, 2026
cddc2ea
fix: PdfReader 使用本地 pdfjs worker
Color2333 Mar 23, 2026
23c6ab8
fix: PdfReader 移除手动 worker 配置,使用 react-pdf v10 默认配置
Color2333 Mar 23, 2026
f88eccc
fix: PdfReader worker 配置使用 import.meta.url
Color2333 Mar 23, 2026
cb4607a
fix: 移除 pdfjs.version 赋值(只读属性)
Color2333 Mar 23, 2026
96590b4
fix: PdfReader 使用绝对 URL 访问后端 API
Color2333 Mar 23, 2026
e8efbeb
fix: ToolPanel 组件使用绝对 URL 访问后端 API
Color2333 Mar 23, 2026
34a2bec
fix: TranslationPanel 双语对照模式 + PdfReader props 修复
Color2333 Mar 23, 2026
57e92f2
feat: get_paper_segments 返回 pageNumber 字段
Color2333 Mar 23, 2026
7df57cb
feat: TranslationPanel 基于页码的滚动同步
Color2333 Mar 23, 2026
a71951b
feat: 混合翻译方案 - 快速翻译 + 布局保留双模式
Color2333 Mar 23, 2026
e2b4005
feat(frontend): 翻译模式选择 UI
Color2333 Mar 23, 2026
b301dfd
docs: 更新 README 翻译功能说明
Color2333 Mar 23, 2026
82d8de5
feat(frontend): LLM 模型配置管理界面
Color2333 Mar 23, 2026
c44e00d
docs: 更新 README 添加 LLM 模型管理说明
Color2333 Mar 23, 2026
8710681
chore: 统一所有模型配置为 GLM-4.7 和 GLM-4.6V
Color2333 Mar 23, 2026
be38276
docs: 更新 README 说明统一模型配置
Color2333 Mar 23, 2026
867595b
ui: 优化 LLM 配置页面显示
Color2333 Mar 24, 2026
b40910f
feat(settings): 迁移到Claude风格设置页面
Color2333 Mar 24, 2026
eb41f04
fix(settings): 侧边栏设置改为路由导航
Color2333 Mar 24, 2026
1bee467
feat(settings): 完善所有设置功能与SettingsDialog对齐
Color2333 Mar 24, 2026
7dae78a
fix(settings): 补充缺失功能和修复状态管理bug
Color2333 Mar 24, 2026
c700aba
feat(collect): 添加多源搜索 tab 入口
Color2333 Mar 24, 2026
dadcf99
fix(multi-source): 修复多源搜索严重问题
Color2333 Mar 26, 2026
04d6eed
fix(agent/arxiv): 修复 arXiv 检索/状态机/错误处理等核心体验问题
Color2333 Apr 24, 2026
fcc241a
fix(arxiv): 删除 FALLBACK_CS_CATEGORIES 中重复的 cs.CL 分类
Color2333 Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ htmlcov/
# Environment
.env

# Worktrees
.worktrees/
worktrees/

# AI agent config (local only)
.claude/
CLAUDE.md
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ PaperMind 是一个面向科研工作者的 AI 增强平台,帮你从「搜索

## ✨ 核心能力

### 🧠 认知重构工作流 (PaperSenseMaking)

「阅读→理解→重构」的完整论文工作流:

- 📝 **Act 1 理解** —— 摘要 + 关键发现,厘清论文核心
- ⚡ **Act 2 碰撞** —— 冲突 + 疑问,与已有知识对话
- 🔄 **Act 3 重构** —— 前后对比 + 认知变化,形成新认知
- 📖 **全文对照翻译** —— 段落级中英对照,支持两种模式
- ⚡ **快速翻译**:1-2 分钟,PyMuPDF 分段 + 并发翻译
- 📐 **布局保留**:3-5 分钟,PDFMathTranslate 完整排版(公式/图表保留)

### 🤖 AI Agent 对话

你的智能研究助理,自然语言交互搞定一切:
Expand Down Expand Up @@ -162,6 +173,21 @@ PaperMind 是一个面向科研工作者的 AI 增强平台,帮你从「搜索
- 🎫 **JWT Token** —— 7 天有效期,自动续期
- 🛡️ **全站保护** —— 所有 API 都需要认证

### ⚙️ LLM 模型管理

灵活控制成本,按场景分配模型:

- 📊 **统一配置** —— 默认使用 GLM-4.7(文本)+ GLM-4.6V(视觉)
- 🔄 **一键切换** —— 在设置页面随时切换配置
- 🎯 **场景映射** —— 所有文本任务自动使用 GLM-4.7
- 💰 **成本优化** —— 单一模型配置,避免管理复杂度
- 📈 **Token 追踪** —— 所有 API 调用自动记录成本和用量

**默认模型配置**:
- 文本任务(粗读/精读/翻译/写作):GLM-4.7
- 视觉任务(图表分析/OCR):GLM-4.6V
- 降级备用:GLM-4.7

---

## 🏗️ 架构总览
Expand Down Expand Up @@ -384,6 +410,7 @@ alembic upgrade head
- **[Semantic Scholar](https://www.semanticscholar.org)** — 引用数据来源
- **[CSFeeds](https://csarxiv.org)** — 论文源订阅服务
- **[learn-claude-code](https://github.com/shareAI-lab/learn-claude-code)** — Agent Harness 工程体系启发,s01-s12 渐进式解构:Loop → Tools → Planning → Subagents → Skills → Context → Tasks → Background → Teams → Protocols → Autonomous
- **[PaperSenseMaking](https://github.com/edu-ai-builders/paper-sense-making)** — 论文阅读「阅读→理解→重构」工作流设计灵感

---

Expand Down
6 changes: 6 additions & 0 deletions apps/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,13 @@ async def app_error_handler(_request: Request, exc: AppError):
cs_feeds,
graph,
jobs,
llm_configs,
papers,
pipelines,
sensemaking,
system,
topics,
translate,
writing,
)
from apps.api.routers import (
Expand All @@ -177,3 +180,6 @@ async def app_error_handler(_request: Request, exc: AppError):
app.include_router(writing.router)
app.include_router(jobs.router)
app.include_router(auth.router)
app.include_router(sensemaking.router)
app.include_router(translate.router)
app.include_router(llm_configs.router)
222 changes: 222 additions & 0 deletions apps/api/routers/llm_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""
LLM 模型管理路由 - 配置管理 + 场景化切换
"""

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field

from packages.storage.db import session_scope
from packages.storage.repositories import LLMConfigRepository

router = APIRouter(prefix="/llm-configs", tags=["llm-configs"])


class LLMConfigItem(BaseModel):
id: str
name: str
provider: str
api_base_url: str | None
model_skim: str
model_deep: str
model_vision: str | None
model_embedding: str
model_fallback: str
is_active: bool


class LLMConfigCreate(BaseModel):
name: str = Field(..., description="配置名称")
provider: str = Field(..., description="提供商:zhipu/openai/anthropic/siliconflow")
api_key: str = Field(..., description="API Key")
api_base_url: str | None = Field(None, description="自定义 API Base URL")
model_skim: str = Field(..., description="粗读/简单任务模型")
model_deep: str = Field(..., description="精读/复杂任务模型")
model_vision: str | None = Field(None, description="视觉模型")
model_embedding: str = Field(..., description="嵌入模型")
model_fallback: str = Field(..., description="降级备用模型")


class LLMConfigUpdate(BaseModel):
name: str | None = None
provider: str | None = None
api_key: str | None = None
api_base_url: str | None = None
model_skim: str | None = None
model_deep: str | None = None
model_vision: str | None = None
model_embedding: str | None = None
model_fallback: str | None = None


class LLMConfigActivate(BaseModel):
config_id: str


class LLMConfigList(BaseModel):
configs: list[LLMConfigItem]
active_id: str | None


class LLMConfigDetail(BaseModel):
config: LLMConfigItem


@router.get("", response_model=LLMConfigList)
def list_configs():
"""获取所有 LLM 配置列表"""
with session_scope() as session:
repo = LLMConfigRepository(session)
configs = repo.list_all()
active_cfg = repo.get_active()
return LLMConfigList(
configs=[
LLMConfigItem(
id=c.id,
name=c.name,
provider=c.provider,
api_base_url=c.api_base_url,
model_skim=c.model_skim,
model_deep=c.model_deep,
model_vision=c.model_vision,
model_embedding=c.model_embedding,
model_fallback=c.model_fallback,
is_active=c.is_active,
)
for c in configs
],
active_id=active_cfg.id if active_cfg else None,
)


@router.get("/{config_id}", response_model=LLMConfigDetail)
def get_config(config_id: str):
"""获取单个配置详情"""
with session_scope() as session:
repo = LLMConfigRepository(session)
try:
cfg = repo.get_by_id(config_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
return LLMConfigDetail(
config=LLMConfigItem(
id=cfg.id,
name=cfg.name,
provider=cfg.provider,
api_base_url=cfg.api_base_url,
model_skim=cfg.model_skim,
model_deep=cfg.model_deep,
model_vision=cfg.model_vision,
model_embedding=cfg.model_embedding,
model_fallback=cfg.model_fallback,
is_active=cfg.is_active,
)
)


@router.post("", response_model=LLMConfigDetail)
def create_config(req: LLMConfigCreate):
"""创建新的 LLM 配置"""
with session_scope() as session:
repo = LLMConfigRepository(session)
cfg = repo.create(
name=req.name,
provider=req.provider,
api_key=req.api_key,
api_base_url=req.api_base_url,
model_skim=req.model_skim,
model_deep=req.model_deep,
model_vision=req.model_vision,
model_embedding=req.model_embedding,
model_fallback=req.model_fallback,
)
session.commit()
return LLMConfigDetail(
config=LLMConfigItem(
id=cfg.id,
name=cfg.name,
provider=cfg.provider,
api_base_url=cfg.api_base_url,
model_skim=cfg.model_skim,
model_deep=cfg.model_deep,
model_vision=cfg.model_vision,
model_embedding=cfg.model_embedding,
model_fallback=cfg.model_fallback,
is_active=cfg.is_active,
)
)


@router.patch("/{config_id}", response_model=LLMConfigDetail)
def update_config(config_id: str, req: LLMConfigUpdate):
"""更新配置"""
with session_scope() as session:
repo = LLMConfigRepository(session)
try:
cfg = repo.update(
config_id,
name=req.name,
provider=req.provider,
api_key=req.api_key,
api_base_url=req.api_base_url,
model_skim=req.model_skim,
model_deep=req.model_deep,
model_vision=req.model_vision,
model_embedding=req.model_embedding,
model_fallback=req.model_fallback,
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
session.commit()
return LLMConfigDetail(
config=LLMConfigItem(
id=cfg.id,
name=cfg.name,
provider=cfg.provider,
api_base_url=cfg.api_base_url,
model_skim=cfg.model_skim,
model_deep=cfg.model_deep,
model_vision=cfg.model_vision,
model_embedding=cfg.model_embedding,
model_fallback=cfg.model_fallback,
is_active=cfg.is_active,
)
)


@router.delete("/{config_id}")
def delete_config(config_id: str):
"""删除配置(不能删除当前激活的配置)"""
with session_scope() as session:
repo = LLMConfigRepository(session)
cfg = repo.get_by_id(config_id)
if cfg.is_active:
raise HTTPException(status_code=400, detail="不能删除当前激活的配置")
repo.delete(config_id)
session.commit()
return {"message": "删除成功"}


@router.post("/activate", response_model=LLMConfigDetail)
def activate_config(req: LLMConfigActivate):
"""激活指定配置"""
with session_scope() as session:
repo = LLMConfigRepository(session)
try:
cfg = repo.activate(req.config_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
session.commit()
return LLMConfigDetail(
config=LLMConfigItem(
id=cfg.id,
name=cfg.name,
provider=cfg.provider,
api_base_url=cfg.api_base_url,
model_skim=cfg.model_skim,
model_deep=cfg.model_deep,
model_vision=cfg.model_vision,
model_embedding=cfg.model_embedding,
model_fallback=cfg.model_fallback,
is_active=cfg.is_active,
)
)
53 changes: 53 additions & 0 deletions apps/api/routers/papers.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,59 @@ def serve_paper_pdf(paper_id: UUID) -> FileResponse:
)


@router.get("/papers/{paper_id}/segments")
def get_paper_segments(paper_id: UUID) -> dict:
"""获取论文分段(用于全文对照翻译)- 包含页码信息"""
with session_scope() as session:
repo = PaperRepository(session)
try:
paper = repo.get_by_id(paper_id)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
pdf_path = paper.pdf_path

if not pdf_path:
raise HTTPException(status_code=404, detail="论文没有 PDF 文件")

full_path = Path(pdf_path)
if not full_path.exists():
raise HTTPException(status_code=404, detail="PDF 文件不存在")

try:
import fitz

doc = fitz.open(str(full_path))
paragraphs: list[dict] = []
para_idx = 0
max_pages = 30

for page_num in range(min(max_pages, len(doc))):
page = doc.load_page(page_num)
page_text = page.get_text("text").strip()
if not page_text:
continue

blocks = page_text.split("\n\n")
for block in blocks:
block = block.strip()
if len(block) < 20:
continue
para_idx += 1
paragraphs.append(
{
"id": f"p-{para_idx}",
"type": "paragraph",
"content": block[:2000],
"pageNumber": page_num + 1,
}
)

doc.close()
return {"segments": paragraphs}
except Exception as e:
return {"segments": [], "error": str(e)}


@router.post("/papers/{paper_id}/ai/explain")
def ai_explain_text(paper_id: UUID, body: AIExplainReq) -> dict:
"""AI 解释/翻译选中文本"""
Expand Down
Loading