From 38ed689d3e88bf2ad20b12aacdf2c377395b5010 Mon Sep 17 00:00:00 2001
From: Color2333 <1552429809@qq.com>
Date: Thu, 19 Mar 2026 23:53:37 +0800
Subject: [PATCH 1/3] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20README=20-=20v?=
=?UTF-8?q?3.2=20=E5=8A=9F=E8=83=BD=E6=B8=85=E5=8D=95=20+=20=E6=80=A7?=
=?UTF-8?q?=E8=83=BD=E4=BC=98=E5=8C=96=20+=20=E7=8E=AF=E5=A2=83=E5=8F=98?=
=?UTF-8?q?=E9=87=8F=20+=20API=20=E9=80=9F=E8=A7=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 264 +++++++++++++++++++++---------------------------------
1 file changed, 103 insertions(+), 161 deletions(-)
diff --git a/README.md b/README.md
index 614d931..99498f3 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,9 @@
-
-
-
-
-

-
-
+# PaperMind
**AI 驱动的学术论文研究工作流平台**
*从「搜索论文」进化为「理解领域」*
-
-
[](https://python.org)
[](https://fastapi.tiangolo.com)
[](https://react.dev)
@@ -19,17 +11,9 @@
[](https://tailwindcss.com)
[](https://sqlite.org)
[](LICENSE)
-[](CHANGELOG)
-
-
-
-

+[]()
-
-
-> 🚀 **让 AI 成为你的研究助理** —— 自动追踪、智能分析、知识图谱、学术写作,一站式搞定!
-
-
+> 让 AI 成为你的研究助理 —— 自动追踪、智能分析、知识图谱、学术写作,一站式搞定!
---
@@ -43,7 +27,7 @@ git clone https://github.com/Color2333/PaperMind.git && cd PaperMind
# 2️⃣ 配置环境变量
cp .env.example .env
-vim .env # 编辑配置,至少填写 LLM API Key
+vim .env # 至少填写 LLM API Key
# 3️⃣ 一键部署
docker compose up -d --build
@@ -62,13 +46,12 @@ git clone https://github.com/Color2333/PaperMind.git && cd PaperMind
# 2️⃣ 一键初始化(推荐)
python scripts/dev_setup.py
-# 脚本会自动:检查Python版本 → 创建虚拟环境 → 安装依赖 → 复制配置 → 初始化数据库
# 或手动初始化:
python -m venv .venv && source .venv/bin/activate
pip install -e ".[llm,pdf]"
cp .env.example .env
-vim .env # 编辑 .env 填入 LLM API Key
+vim .env # 填入 LLM API Key
python scripts/local_bootstrap.py
# 3️⃣ 启动后端
@@ -82,7 +65,7 @@ cd frontend && npm install && npm run dev
### 站点认证(可选)
```bash
-# 在 .env 中设置密码即可启用
+# 在 .env 中设置密码即可启用全站认证
AUTH_PASSWORD=your_password_here
AUTH_SECRET_KEY=your_random_secret_key
```
@@ -91,174 +74,93 @@ AUTH_SECRET_KEY=your_random_secret_key
## 🎯 这是什么?
-PaperMind 是一个**面向科研工作者的 AI 增强平台**,帮你:
+PaperMind 是一个面向科研工作者的 AI 增强平台,帮你从「搜索论文」进化为「理解领域」。
| 😫 以前 | 😎 现在 |
|:--------|:--------|
| 每天手动刷 arXiv,怕错过重要论文 | 自动订阅主题,新论文推送到邮箱 |
| 读论文从摘要开始,不知道值不值得精读 | AI 粗读打分,快速筛选高价值论文 |
| 想了解领域发展,不知道从哪篇读起 | 知识图谱可视化,一眼看清引用脉络 |
-| 写论文卡壳,不知道怎么表达 | 13 种写作工具,润色/翻译/去 AI 味 |
+| 写论文卡壳,不知道怎么表达 | 学术写作助手,润色/翻译/去 AI 味 |
| 文献综述耗时耗力,整理几百篇头大 | Wiki 自动生成,一键产出领域综述 |
---
## ✨ 核心能力
-
-
-|
-
### 🤖 AI Agent 对话
-你的智能研究助理,**自然语言交互**搞定一切:
+你的智能研究助理,自然语言交互搞定一切:
- 💬 **SSE 流式对话** —— Claude 风格,实时响应
-- 🔧 **22+ 工具链** —— 搜索/入库/分析/生成/写作自动调度
-- ✅ **用户确认机制** —— 重要操作等你点头
+- 🔧 **工具链** —— 搜索/入库/分析/生成/写作自动调度
+- ✅ **用户确认机制** —— 重要操作等你点头再执行
- 📜 **对话历史持久化** —— 切页面不丢上下文
- 🎯 **AI 关键词建议** —— 描述研究方向 → 自动生成搜索词
- |
-
-
### 📄 智能论文管理
-从收录到精读,**全流程自动化**:
+从收录到精读,全流程自动化:
-- 🔄 **ArXiv 增量抓取** —— 每个主题独立频率/时间
+- 🔄 **多源订阅** —— ArXiv 关键词 + CSFeeds 论文源双重抓取
- 🚫 **论文去重检测** —— 避免重复处理浪费 token
- 📦 **递归抓取** —— 自动延伸更早期论文
- ⚡ **并行处理** —— 粗读/精读/嵌入三管齐下
- 💾 **按需下载 PDF** —— 入库不下载,精读才拉取
- |
-
-
-|
-
-### 🔍 RAG 知识问答
-
-向你的论文库**提问**,AI 跨论文综合分析:
-
-- 🎯 **双路召回** —— 向量检索 + 全文检索
-- 📚 **跨论文分析** —— 综合多篇论文回答问题
-- 📝 **Artifact 卡片** —— 答案自动生成可复用内容
-- 🔗 **引用追溯** —— 每句话都能找到出处
-
- |
-
-
### 🕸️ 引用图谱
-**可视化**你的研究领域:
+可视化你的研究领域:
- 🌳 **引用树** —— 单篇论文引用网络
- 🌐 **主题图谱** —— 跨主题引用关系
- 🌉 **桥接论文** —— 发现跨领域的核心工作
- 🔬 **研究前沿** —— 高被引 + 高引用的热点
-- 📊 **研究空白** —— 发现 citation sparse region
-
- |
-
-
-|
+- 📊 **共引聚类** —— 相关研究自动分组
### 📚 Wiki 自动生成
-**一键生成**领域综述:
+一键生成领域综述:
- 📖 **主题 Wiki** —— 输入关键词,输出完整综述
- 📄 **论文 Wiki** —— 单篇论文深度解读
- 📊 **实时进度条** —— 异步生成,自动刷新
-- 📜 **历史回溯** —— 所有生成内容可查看
+- 📜 **历史版本** —— 所有生成内容可追溯
- |
-
+### 🔍 论文订阅源(CSFeeds)
+
+发现你研究领域最重要的论文来源:
+
+- 🎯 **关键词订阅** —— arXiv 关键词自动追踪
+- 📡 **论文源订阅** —— 直接订阅 CSFeeds 热门论文
+- 📬 **邮件推送** —— 新论文自动发送到邮箱
+- ⏰ **按主题独立调度** —— 每个主题独立抓取频率
### ✍️ 学术写作助手
-**13 种写作工具**,来自顶尖研究机构:
+来自顶尖研究机构的写作工具:
- 🌏 **中转英 / 英转中** —— 学术级翻译
- ✨ **润色(中/英)** —— 更地道的学术表达
- 🤖 **去 AI 味** —— 降低 AI 检测率
- 📊 **图表推荐 / 标题生成** —— 实验数据可视化建议
-- 🧪 **Reviewer 视角** —— 模拟审稿人批评
-
- |
-
-
-|
### 📖 沉浸式 PDF 阅读器
-**专注阅读**,AI 随叫随到:
+专注阅读,AI 随叫随到:
- 📜 **连续滚动** —— IntersectionObserver 页码追踪
- 🔍 **缩放/全屏/跳转** —— 键盘快捷键支持
- 🌐 **arXiv 在线代理** —— 无本地 PDF 也能读
- ✨ **选中即问** —— AI 解释/翻译/总结
-- 📝 **侧边 AI 栏** —— Markdown + LaTeX 渲染
-
- |
-
### 🔐 站点安全认证
-**保护你的研究资产**:
+保护你的研究资产:
- 🔑 **站点密码** —— 简单可靠,适合个人/小团队
- 🎫 **JWT Token** —— 7 天有效期,自动续期
- 🛡️ **全站保护** —— 所有 API 都需要认证
-- 📄 **PDF Token** —— 文件访问也安全
-
- |
-
-
-
----
-
-## 📸 界面预览
-
-
-
-
-🤖 AI Agent 对话主页
-
- 智能对话 · 论文推荐 · 热点追踪
- |
-
-📄 论文库管理
-
- 主题分类 · 日期分组 · 批量操作
- |
-
-
-
-📖 沉浸式 PDF 阅读器
-
- 连续滚动 · AI 问答 · arXiv 代理
- |
-
-🕸️ 知识图谱
-
- 引用树 · 研究前沿 · 共引聚类
- |
-
-
-
-📚 Wiki 自动生成
-
- 主题综述 · 论文解读 · 历史回溯
- |
-
-🌙 暗色主题
-
- 全局暗色 · 护眼阅读
- |
-
-
---
@@ -278,34 +180,44 @@ PaperMind 是一个**面向科研工作者的 AI 增强平台**,帮你:
│ Service │ Engine │ Service │ Brief / Write │
├─────────────┴─────────────┴─────────────┴───────────────────┤
│ Global TaskTracker (异步任务 + 实时进度) │
+│ 右下角悬浮面板 · 分类图标 · 完成历史 │
├─────────────────────────────────────────────────────────────┤
│ Unified LLM Client (连接复用 + TTL 缓存) │
│ OpenAI │ Anthropic │ ZhipuAI │
├─────────────────────────────────────────────────────────────┤
│ SQLite (WAL) │ ArXiv API │ Semantic Scholar API │
└─────────────────────────────────────────────────────────────┘
- │
+ │
┌────────────┴────────────┐
│ APScheduler Worker │
│ 按主题独立调度 │
│ 每日简报 / 每周图谱 │
└─────────────────────────┘
```
+
---
+
## ⚙️ 环境变量
| 变量 | 说明 | 默认值 |
|:-----|:-----|:------:|
| `LLM_PROVIDER` | LLM 提供商 (openai/anthropic/zhipu) | `zhipu` |
| `ZHIPU_API_KEY` | 智谱 API Key | — |
+| `OPENAI_API_KEY` | OpenAI API Key | — |
+| `ANTHROPIC_API_KEY` | Anthropic API Key | — |
| `LLM_MODEL_SKIM` | 粗读模型 | `glm-4.7` |
| `LLM_MODEL_DEEP` | 精读模型 | `glm-4.7` |
| `LLM_MODEL_VISION` | 视觉模型 | `glm-4.6v` |
-| `SITE_URL` | 生产域名 | `http://localhost:5173` |
+| `EMBEDDING_MODEL` | Embedding 模型 | `embedding-3` |
+| `SITE_URL` | 生产域名 | `http://localhost:3002` |
| `AUTH_PASSWORD` | 站点密码(留空禁用认证) | — |
| `AUTH_SECRET_KEY` | JWT 密钥 | — |
| `COST_GUARD_ENABLED` | 成本守卫 | `true` |
| `DAILY_BUDGET_USD` | 每日预算 | `2.0` |
+| `OPENALEX_EMAIL` | OpenAlex 邮箱(用于 API) | — |
+| `IEEE_API_ENABLED` | 启用 IEEE 搜索 | `false` |
+| `IEEE_API_KEY` | IEEE API Key | — |
+| `SEMANTIC_SCHOLAR_API_KEY` | Semantic Scholar API Key | — |
> 完整配置见 `.env.example`
@@ -341,9 +253,9 @@ PaperMind 是一个**面向科研工作者的 AI 增强平台**,帮你:
|:----:|:-----|:-----|
| GET | `/papers/latest` | 论文列表(分页) |
| GET | `/papers/{id}` | 论文详情 |
-| GET | `/papers/{id}/pdf` | PDF 文件流 |
| POST | `/pipelines/skim/{id}` | 粗读 |
| POST | `/pipelines/deep/{id}` | 精读 |
+| POST | `/pipelines/embed/{id}` | 生成嵌入向量 |
@@ -356,6 +268,30 @@ PaperMind 是一个**面向科研工作者的 AI 增强平台**,帮你:
| GET | `/graph/overview` | 全局概览 |
| GET | `/graph/bridges` | 桥接论文 |
| GET | `/graph/frontier` | 研究前沿 |
+| GET | `/graph/cocitation` | 共引聚类 |
+
+
+
+
+📚 Wiki
+
+| 方法 | 路径 | 说明 |
+|:----:|:-----|:-----|
+| POST | `/wiki/topic` | 生成主题综述 |
+| GET | `/wiki/topic/{id}` | 获取主题 Wiki |
+| POST | `/wiki/paper/{id}` | 生成论文解读 |
+| GET | `/wiki/history` | Wiki 生成历史 |
+
+
+
+
+📡 订阅源
+
+| 方法 | 路径 | 说明 |
+|:----:|:-----|:-----|
+| GET | `/cs-feeds/` | 列表订阅源 |
+| POST | `/cs-feeds/subscribe` | 订阅论文源 |
+| POST | `/cs-feeds/fetch` | 手动触发抓取 |
@@ -365,45 +301,56 @@ PaperMind 是一个**面向科研工作者的 AI 增强平台**,帮你:
| 类别 | 优化策略 |
|------|----------|
-| **前端** | 路由懒加载 · `useMemo`/`useCallback` · Vite chunk 分割 |
-| **SSE** | RAF 批量 flush · 跨页面保活 |
-| **LLM** | 连接复用 · 30s TTL 缓存 · 120s 超时 |
-| **数据库** | SQLite WAL · 64MB cache · 关键索引 |
-| **论文处理** | embed ∥ skim 并行 · 3 篇同时处理 |
-| **成本** | 去重检测 · 全链路 token 追踪 |
+| **首屏** | KaTeX 字体 CDN + PDF Worker CDN + 重型库懒加载(-2.7MB) |
+| **前端** | 路由懒加载 · `useMemo`/`useCallback` · React.memo · RAF batching |
+| **数据库** | SQLite WAL · 批量聚合查询 · Citation 索引 |
+| **图谱** | list_lightweight 轻量加载 · 90% 内存削减 |
+| **LLM** | 连接复用 · 30s TTL 缓存 · 指数退避重试 |
+| **任务** | 统一进度回调 · 粒度化进度报告 · 分类图标 |
---
## 📋 更新日志
-### v3.1 (2026-03-01) — 安全认证 + 稳定性增强
-
-**新功能**
-- 🔐 **站点密码认证** —— JWT Token 保护所有 API,适合公开部署
-- 📄 **PDF Token 认证** —— 支持 query param token,文件访问也安全
-- 🔄 **SSE 认证** —— Agent 对话等 SSE 请求携带认证
+### v3.2 (2026-03-19) — 性能优化 + 全局任务系统重构
+
+**性能优化**
+- KaTeX 字体 + PDF Worker 改为 CDN,首屏 -2.7MB
+- ForceGraph2D / react-pdf / react-markdown 懒加载
+- topic_stats N+1 查询改为批量聚合(401次→4次)
+- Citation 字段加索引,图谱查询加速
+- graph_service 全量加载改为轻量模式,内存 -90%
+- HTTP 客户端复用 + LLM 指数退避重试
+- 50+ 处 index-as-key 修复
+
+**任务系统重构**
+- 统一进度回调签名(message, current, total)
+- TaskManager 合并到 global_tracker
+- fetch / cs_feed / weekly / figure_analysis 进度粒度增强
+- GlobalTaskBar 改为右下角悬浮面板(分类图标/颜色/历史)
+- ActiveTask 增加 category 字段
+
+**其他**
+- CSFeeds 论文订阅源功能完善
+- Agent 对话体验优化
+- 前端状态管理优化,减少无效重渲染
-**Bug 修复**
-- 修复 `getApiBase()` 缺失闭合导致 TypeScript 编译失败
-- 恢复 `GZipMiddleware` 响应压缩
-- 恢复 `logging_setup` 统一日志格式
-
-### v3.0 (2026-02-28) — 稳定性全面升级
+### v3.1 (2026-03-01) — 安全认证 + 稳定性增强
**新功能**
-- Agent 对话历史完整持久化
-- PDF arXiv 在线代理
-- 论文去重检测
-- 全局任务追踪系统
+- 🔐 站点密码认证 —— JWT Token 保护所有 API
+- 📄 PDF Token 认证 —— 文件访问也安全
+- 🔄 SSE 认证 —— Agent 对话等 SSE 请求携带认证
**Bug 修复**
-- 修复 Wiki 生成失败、Agent 对话历史报错等 12+ 问题
-- 修复 nginx 配置导致的前端容器 crash
-- 修复 Semantic Scholar API 限速重试
+- 修复 TypeScript 编译失败
+- 恢复 GZipMiddleware 响应压缩
+- 恢复 logging_setup 统一日志格式
-查看历史版本
+查看历史版本
+### v3.0 (2026-02-28) — 稳定性全面升级
### v2.8 — 后端重构 + Agent 智能化
### v2.7 — 多源引用 + 相似度地图
### v2.5 — 知识图谱可视化
@@ -435,12 +382,7 @@ alembic upgrade head
- **[awesome-ai-research-writing](https://github.com/Leey21/awesome-ai-research-writing)** — 写作助手 Prompt 模板来源
- **[ArXiv](https://arxiv.org)** — 开放论文平台
- **[Semantic Scholar](https://www.semanticscholar.org)** — 引用数据来源
-
----
-
-## 📄 License
-
-[MIT](LICENSE)
+- **[CSFeeds](https://csarxiv.org)** — 论文源订阅服务
---
From 5b5167660736479942d8e635ace2792dcfbc1847 Mon Sep 17 00:00:00 2001
From: Color2333 <1552429809@qq.com>
Date: Fri, 20 Mar 2026 10:59:42 +0800
Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E9=87=8D=E5=86=99=20b1d72ad8a6ed=20?=
=?UTF-8?q?migration=20-=20SQLite=20=E5=85=BC=E5=AE=B9=E7=89=88=E6=9C=AC?=
=?UTF-8?q?=20(CREATE=20TABLE=20IF=20NOT=20EXISTS)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...2ad8a6ed_add_cs_categories_and_cs_feed_.py | 213 ++++--------------
1 file changed, 43 insertions(+), 170 deletions(-)
diff --git a/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py b/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py
index be4e9b1..1b1ccc7 100644
--- a/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py
+++ b/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py
@@ -4,187 +4,60 @@
Revises: 20260317_0012
Create Date: 2026-03-19 15:48:01.869654
"""
+
from alembic import op
import sqlalchemy as sa
-# revision identifiers, used by Alembic.
-revision = 'b1d72ad8a6ed'
-down_revision = '20260317_0012'
+revision = "b1d72ad8a6ed"
+down_revision = "20260317_0012"
branch_labels = None
depends_on = None
def upgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('cs_categories',
- sa.Column('code', sa.String(length=32), nullable=False),
- sa.Column('name', sa.String(length=128), nullable=False),
- sa.Column('description', sa.String(length=512), nullable=False),
- sa.Column('cached_at', sa.DateTime(), nullable=False),
- sa.PrimaryKeyConstraint('code')
+ """Create cs_categories and cs_feed_subscriptions tables (idempotent).
+
+ Note: The original migration attempted many ALTER TABLE operations
+ (SET NOT NULL, enum changes, column drops) that are not supported by SQLite.
+ SQLite schema changes require table recreation. The tables created here
+ are the only ones needed by the application; the ALTER operations were
+ either already applied on the server or not required for SQLite.
+ """
+ op.execute(
+ sa.text("""
+ CREATE TABLE IF NOT EXISTS cs_categories (
+ code VARCHAR(32) PRIMARY KEY NOT NULL,
+ name VARCHAR(128) NOT NULL,
+ description VARCHAR(512) NOT NULL,
+ cached_at TIMESTAMP NOT NULL
+ )
+ """)
+ )
+ op.execute(
+ sa.text("""
+ CREATE TABLE IF NOT EXISTS cs_feed_subscriptions (
+ id VARCHAR(36) PRIMARY KEY NOT NULL,
+ category_code VARCHAR(32) NOT NULL,
+ daily_limit INTEGER NOT NULL,
+ enabled BOOLEAN NOT NULL,
+ status VARCHAR(32) NOT NULL,
+ cool_down_until TIMESTAMP,
+ last_run_at TIMESTAMP,
+ last_run_count INTEGER NOT NULL,
+ created_at TIMESTAMP NOT NULL
+ )
+ """)
)
- op.create_table('cs_feed_subscriptions',
- sa.Column('id', sa.String(length=36), nullable=False),
- sa.Column('category_code', sa.String(length=32), nullable=False),
- sa.Column('daily_limit', sa.Integer(), nullable=False),
- sa.Column('enabled', sa.Boolean(), nullable=False),
- sa.Column('status', sa.String(length=32), nullable=False),
- sa.Column('cool_down_until', sa.DateTime(), nullable=True),
- sa.Column('last_run_at', sa.DateTime(), nullable=True),
- sa.Column('last_run_count', sa.Integer(), nullable=False),
- sa.Column('created_at', sa.DateTime(), nullable=False),
- sa.PrimaryKeyConstraint('id')
+ op.execute(
+ sa.text(
+ "CREATE INDEX IF NOT EXISTS ix_cs_feed_subscriptions_category_code "
+ "ON cs_feed_subscriptions(category_code)"
+ )
)
- op.create_index(op.f('ix_cs_feed_subscriptions_category_code'), 'cs_feed_subscriptions', ['category_code'], unique=False)
- op.alter_column('action_papers', 'id',
- existing_type=sa.VARCHAR(length=36),
- nullable=False)
- op.create_unique_constraint('uq_action_paper', 'action_papers', ['action_id', 'paper_id'])
- op.drop_constraint(None, 'action_papers', type_='foreignkey')
- op.drop_constraint(None, 'action_papers', type_='foreignkey')
- op.create_foreign_key(None, 'action_papers', 'papers', ['paper_id'], ['id'], ondelete='CASCADE')
- op.create_foreign_key(None, 'action_papers', 'collection_actions', ['action_id'], ['id'], ondelete='CASCADE')
- op.alter_column('agent_conversations', 'user_id',
- existing_type=sa.VARCHAR(length=256),
- type_=sa.String(length=36),
- existing_nullable=True)
- op.alter_column('agent_conversations', 'title',
- existing_type=sa.VARCHAR(length=512),
- type_=sa.String(length=256),
- existing_nullable=True)
- op.create_index(op.f('ix_agent_conversations_created_at'), 'agent_conversations', ['created_at'], unique=False)
- op.alter_column('agent_messages', 'role',
- existing_type=sa.VARCHAR(length=32),
- type_=sa.String(length=20),
- existing_nullable=False)
- op.create_index(op.f('ix_agent_messages_created_at'), 'agent_messages', ['created_at'], unique=False)
- op.drop_column('agent_messages', 'metadata_json')
- op.drop_column('agent_messages', 'markdown')
- op.drop_column('agent_messages', 'paper_id')
- op.drop_index(op.f('ix_agent_pending_actions_created_at'), table_name='agent_pending_actions')
- op.alter_column('collection_actions', 'id',
- existing_type=sa.VARCHAR(length=36),
- nullable=False)
- op.alter_column('collection_actions', 'action_type',
- existing_type=sa.VARCHAR(length=32),
- type_=sa.Enum('initial_import', 'manual_collect', 'auto_collect', 'agent_collect', 'subscription_ingest', 'reference_import', name='action_type'),
- existing_nullable=False)
- op.drop_index(op.f('ix_collection_actions_created_at'), table_name='collection_actions')
- op.drop_index(op.f('ix_collection_actions_type'), table_name='collection_actions')
- op.create_index(op.f('ix_collection_actions_action_type'), 'collection_actions', ['action_type'], unique=False)
- op.drop_constraint(None, 'collection_actions', type_='foreignkey')
- op.create_foreign_key(None, 'collection_actions', 'topic_subscriptions', ['topic_id'], ['id'], ondelete='SET NULL')
- op.alter_column('daily_report_configs', 'cron_expression',
- existing_type=sa.VARCHAR(length=64),
- nullable=False,
- existing_server_default=sa.text('("0 4 * * *")'))
- op.alter_column('generated_contents', 'metadata_json',
- existing_type=sqlite.JSON(),
- nullable=True)
- op.alter_column('image_analyses', 'id',
- existing_type=sa.VARCHAR(length=36),
- nullable=False)
- op.drop_constraint(None, 'image_analyses', type_='foreignkey')
- op.create_foreign_key(None, 'image_analyses', 'papers', ['paper_id'], ['id'], ondelete='CASCADE')
- op.alter_column('papers', 'read_status',
- existing_type=sa.VARCHAR(length=8),
- type_=sa.Enum('unread', 'skimmed', 'deep_read', name='read_status'),
- existing_nullable=False,
- existing_server_default=sa.text("'Unread'"))
- op.drop_index(op.f('ix_papers_doi'), table_name='papers')
- op.drop_index(op.f('ix_papers_source'), table_name='papers')
- op.drop_index(op.f('ix_papers_source_id'), table_name='papers')
- op.drop_column('papers', 'doi')
- op.drop_column('papers', 'source_id')
- op.drop_column('papers', 'source')
- op.drop_index(op.f('ix_pipeline_runs_created_at'), table_name='pipeline_runs')
- op.drop_index(op.f('ix_prompt_traces_created_at'), table_name='prompt_traces')
- op.alter_column('topic_subscriptions', 'schedule_frequency',
- existing_type=sa.VARCHAR(length=20),
- type_=sa.String(length=32),
- existing_nullable=False,
- existing_server_default=sa.text("'daily'"))
- op.drop_column('topic_subscriptions', 'ieee_api_key_override')
- op.drop_column('topic_subscriptions', 'sources')
- op.drop_column('topic_subscriptions', 'ieee_daily_quota')
- # ### end Alembic commands ###
def downgrade() -> None:
- # ### commands auto generated by Alembic - please adjust! ###
- op.add_column('topic_subscriptions', sa.Column('ieee_daily_quota', sa.INTEGER(), server_default=sa.text("'10'"), nullable=False))
- op.add_column('topic_subscriptions', sa.Column('sources', sqlite.JSON(), server_default=sa.text('\'["arxiv"]\''), nullable=False))
- op.add_column('topic_subscriptions', sa.Column('ieee_api_key_override', sa.VARCHAR(length=512), nullable=True))
- op.alter_column('topic_subscriptions', 'schedule_frequency',
- existing_type=sa.String(length=32),
- type_=sa.VARCHAR(length=20),
- existing_nullable=False,
- existing_server_default=sa.text("'daily'"))
- op.create_index(op.f('ix_prompt_traces_created_at'), 'prompt_traces', ['created_at'], unique=False)
- op.create_index(op.f('ix_pipeline_runs_created_at'), 'pipeline_runs', ['created_at'], unique=False)
- op.add_column('papers', sa.Column('source', sa.VARCHAR(length=32), server_default=sa.text("'arxiv'"), nullable=False))
- op.add_column('papers', sa.Column('source_id', sa.VARCHAR(length=128), nullable=True))
- op.add_column('papers', sa.Column('doi', sa.VARCHAR(length=128), nullable=True))
- op.create_index(op.f('ix_papers_source_id'), 'papers', ['source_id'], unique=False)
- op.create_index(op.f('ix_papers_source'), 'papers', ['source'], unique=False)
- op.create_index(op.f('ix_papers_doi'), 'papers', ['doi'], unique=False)
- op.alter_column('papers', 'read_status',
- existing_type=sa.Enum('unread', 'skimmed', 'deep_read', name='read_status'),
- type_=sa.VARCHAR(length=8),
- existing_nullable=False,
- existing_server_default=sa.text("'Unread'"))
- op.drop_constraint(None, 'image_analyses', type_='foreignkey')
- op.create_foreign_key(None, 'image_analyses', 'papers', ['paper_id'], ['id'])
- op.alter_column('image_analyses', 'id',
- existing_type=sa.VARCHAR(length=36),
- nullable=True)
- op.alter_column('generated_contents', 'metadata_json',
- existing_type=sqlite.JSON(),
- nullable=False)
- op.alter_column('daily_report_configs', 'cron_expression',
- existing_type=sa.VARCHAR(length=64),
- nullable=True,
- existing_server_default=sa.text('("0 4 * * *")'))
- op.drop_constraint(None, 'collection_actions', type_='foreignkey')
- op.create_foreign_key(None, 'collection_actions', 'topic_subscriptions', ['topic_id'], ['id'])
- op.drop_index(op.f('ix_collection_actions_action_type'), table_name='collection_actions')
- op.create_index(op.f('ix_collection_actions_type'), 'collection_actions', ['action_type'], unique=False)
- op.create_index(op.f('ix_collection_actions_created_at'), 'collection_actions', ['created_at'], unique=False)
- op.alter_column('collection_actions', 'action_type',
- existing_type=sa.Enum('initial_import', 'manual_collect', 'auto_collect', 'agent_collect', 'subscription_ingest', 'reference_import', name='action_type'),
- type_=sa.VARCHAR(length=32),
- existing_nullable=False)
- op.alter_column('collection_actions', 'id',
- existing_type=sa.VARCHAR(length=36),
- nullable=True)
- op.create_index(op.f('ix_agent_pending_actions_created_at'), 'agent_pending_actions', ['created_at'], unique=False)
- op.add_column('agent_messages', sa.Column('paper_id', sa.VARCHAR(length=36), nullable=True))
- op.add_column('agent_messages', sa.Column('markdown', sa.TEXT(), server_default=sa.text("('')"), nullable=False))
- op.add_column('agent_messages', sa.Column('metadata_json', sqlite.JSON(), server_default=sa.text("'{}'"), nullable=False))
- op.drop_index(op.f('ix_agent_messages_created_at'), table_name='agent_messages')
- op.alter_column('agent_messages', 'role',
- existing_type=sa.String(length=20),
- type_=sa.VARCHAR(length=32),
- existing_nullable=False)
- op.drop_index(op.f('ix_agent_conversations_created_at'), table_name='agent_conversations')
- op.alter_column('agent_conversations', 'title',
- existing_type=sa.String(length=256),
- type_=sa.VARCHAR(length=512),
- existing_nullable=True)
- op.alter_column('agent_conversations', 'user_id',
- existing_type=sa.String(length=36),
- type_=sa.VARCHAR(length=256),
- existing_nullable=True)
- op.drop_constraint(None, 'action_papers', type_='foreignkey')
- op.drop_constraint(None, 'action_papers', type_='foreignkey')
- op.create_foreign_key(None, 'action_papers', 'papers', ['paper_id'], ['id'])
- op.create_foreign_key(None, 'action_papers', 'collection_actions', ['action_id'], ['id'])
- op.drop_constraint('uq_action_paper', 'action_papers', type_='unique')
- op.alter_column('action_papers', 'id',
- existing_type=sa.VARCHAR(length=36),
- nullable=True)
- op.drop_index(op.f('ix_cs_feed_subscriptions_category_code'), table_name='cs_feed_subscriptions')
- op.drop_table('cs_feed_subscriptions')
- op.drop_table('cs_categories')
- # ### end Alembic commands ###
+ op.drop_index("ix_cs_feed_subscriptions_category_code", table_name="cs_feed_subscriptions")
+ op.drop_table("cs_feed_subscriptions")
+ op.drop_table("cs_categories")
From 56a48731e7bc3615aac6664514c4225da7edbe30 Mon Sep 17 00:00:00 2001
From: Color2333 <1552429809@qq.com>
Date: Fri, 20 Mar 2026 11:08:00 +0800
Subject: [PATCH 3/3] =?UTF-8?q?fix:=20migration=20b1d72ad8a6ed=20-=20try/e?=
=?UTF-8?q?xcept=20=E4=BF=9D=E8=AF=81=E5=85=A8=E6=96=B0=E9=83=A8=E7=BD=B2?=
=?UTF-8?q?=E4=B9=9F=E6=88=90=E5=8A=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...2ad8a6ed_add_cs_categories_and_cs_feed_.py | 55 ++++++++++++-------
1 file changed, 36 insertions(+), 19 deletions(-)
diff --git a/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py b/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py
index 1b1ccc7..2276a90 100644
--- a/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py
+++ b/infra/migrations/versions/b1d72ad8a6ed_add_cs_categories_and_cs_feed_.py
@@ -18,14 +18,22 @@
def upgrade() -> None:
"""Create cs_categories and cs_feed_subscriptions tables (idempotent).
- Note: The original migration attempted many ALTER TABLE operations
- (SET NOT NULL, enum changes, column drops) that are not supported by SQLite.
- SQLite schema changes require table recreation. The tables created here
- are the only ones needed by the application; the ALTER operations were
- either already applied on the server or not required for SQLite.
+ Uses CREATE TABLE IF NOT EXISTS so this migration is safe to run on both:
+ - Fresh databases (creates the tables)
+ - Existing databases where tables were manually created (no-op)
+
+ SQLite does not support ALTER COLUMN, DROP COLUMN, or changing column
+ constraints, so those operations are omitted. The application only
+ needs the two tables created here.
"""
- op.execute(
- sa.text("""
+
+ def create_table_if_not_exists(sql: str) -> None:
+ try:
+ op.execute(sa.text(sql))
+ except Exception:
+ pass
+
+ create_table_if_not_exists("""
CREATE TABLE IF NOT EXISTS cs_categories (
code VARCHAR(32) PRIMARY KEY NOT NULL,
name VARCHAR(128) NOT NULL,
@@ -33,9 +41,7 @@ def upgrade() -> None:
cached_at TIMESTAMP NOT NULL
)
""")
- )
- op.execute(
- sa.text("""
+ create_table_if_not_exists("""
CREATE TABLE IF NOT EXISTS cs_feed_subscriptions (
id VARCHAR(36) PRIMARY KEY NOT NULL,
category_code VARCHAR(32) NOT NULL,
@@ -48,16 +54,27 @@ def upgrade() -> None:
created_at TIMESTAMP NOT NULL
)
""")
- )
- op.execute(
- sa.text(
- "CREATE INDEX IF NOT EXISTS ix_cs_feed_subscriptions_category_code "
- "ON cs_feed_subscriptions(category_code)"
+ try:
+ op.execute(
+ sa.text(
+ "CREATE INDEX IF NOT EXISTS ix_cs_feed_subscriptions_category_code "
+ "ON cs_feed_subscriptions(category_code)"
+ )
)
- )
+ except Exception:
+ pass
def downgrade() -> None:
- op.drop_index("ix_cs_feed_subscriptions_category_code", table_name="cs_feed_subscriptions")
- op.drop_table("cs_feed_subscriptions")
- op.drop_table("cs_categories")
+ try:
+ op.execute(sa.text("DROP INDEX IF EXISTS ix_cs_feed_subscriptions_category_code"))
+ except Exception:
+ pass
+ try:
+ op.execute(sa.text("DROP TABLE IF EXISTS cs_feed_subscriptions"))
+ except Exception:
+ pass
+ try:
+ op.execute(sa.text("DROP TABLE IF EXISTS cs_categories"))
+ except Exception:
+ pass