Mavis 921e674a30 feat(translate): 加 Agnes 翻译 fallback,buffer 改 0.5
腾讯 TMT 月度配额快满时(腾讯后台口径已用 2M/5M),翻译降级链:

1. tencent TMT(主,按月配额)
2. tencent MaaS u2(第二级,翻译专用,无配额)
3. agnes 通用 LLM(第三级,质量次之但够用)
4. local NLLB(最后兜底)

新增 backend/app/services/translation/agnes.py: AgnesTranslator
复用 LlmClient 做限速 + 重试,系统 prompt 强约束只输出译文,
去除 "以下是翻译" 等常见 LLM 翻译前缀。

service.py 改动:
- fallback 链 maas -> agnes -> local
- cache 接受 agnes 结果(30天)
- add_usage 只算 tencent TMT

buffer 调整: TENCENT_TMT_QUOTA_BUFFER 0.05 -> 0.5
腾讯云后台按请求字节计费,与我们 redis 字符累加口径差约 2.5x;
按腾讯后台口径 redis 累加到 1M 字符即触发降级(对应腾讯约 2.5M 字节 =50% 用量),
留足 buffer,避免月底真爆。
2026-06-10 17:44:47 +08:00

Diary News · 私人多源新闻翻译系统

抓境外权威源 → 自动中英对照 → 智能排版/分类/插图/点评。 跑在一台 2C/2G/30G 的香港 VPS 上,自用 + 家人/小圈子。


目录

  1. 项目目标
  2. 关键特性
  3. 架构概览
  4. 技术栈
  5. 仓库结构
  6. 数据模型
  7. 快速开始
  8. 功能详解
  9. LLM 智能增强
  10. API 概览
  11. 开发-部署工作流
  12. 运维工具
  13. 故障排查
  14. 路线图

项目目标

Why 存在:

痛点 解决
信息茧房(算法推荐让你只看一类) 多源并列,无个性化排序,纯时间序
翻译门槛(英文看不动) 自动翻译(腾讯云 TMT + 本地 NLLB 降级)
内容保存难(网页 404、推文删) 抓取入库 + 全文 + 译文,永久可查
单台服务器成本敏感 30G 跑全栈,月费 ≤ 50 HKD

Who 给谁用:

  • owner: 唯一管理员,管理源 / 提示词 / 看健康看板
  • member: 家庭成员 / 朋友,登录看文章 / 收藏 / 订阅关键词

关键特性

  • 🌍 多源 RSS 抓取:Reuters / BBC / Al Jazeera / NHK / DW,带失败退避(连续 3 次失败把间隔 × 2,封顶 12 小时)
  • 🌐 智能翻译:腾讯云 TMT(月 500 万字符配额)→ 本地 NLLB-200 降级,30 天 Redis 缓存避免重复
  • 🤖 LLM 智能增强 (新):翻译完成后自动跑 4 项 LLM 任务 — 排版 / 分类 / 插图 / 点评
  • 🎨 AI 配图:文生图模型自动为每篇文章生成插图(走 Agnes 平台,带限速)
  • 👤 双角色鉴权:JWT(access 60min + refresh 14d) + API Token(sha256,可撤销,给 Android 预留)
  • 📌 收藏 + 关键词订阅:用户级书签,服务端定时按关键词命中推送(预留 Telegram 通道)
  • 📊 管理看板:源健康度 / 翻译配额 / LLM 状态,全部可视化
  • 🔄 热加载:源/提示词改了不用重启,worker 每天 00:30 重建 job
  • 🚀 一键部署:SSH 推公钥 + 一键 git pull 流程
  • 🔒 安全默认:bcrypt 密码、API Token 加密、SQL 注入免疫(SQLAlchemy 2.0 参数化)

架构概览

┌─────────────────────────────────────────────────────────┐
│  VPS (HK-News · 2C/2G/30G · Ubuntu 24)                │
│                                                          │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐         │
│  │ postgres │    │  redis   │    │  caddy   │ ← 唯一  │
│  └──────────┘    └──────────┘    │  :80/443 │   对外   │
│       ▲              ▲           └────┬─────┘          │
│       │              │                │                 │
│  ┌────┴──────┐  ┌────┴────┐  ┌────────┴───────┐        │
│  │   api     │  │ worker  │  │   frontend     │        │
│  │  FastAPI  │  │APSch+任务│  │   (nginx + SPA)│        │
│  └────┬──────┘  └────┬────┘  └────────────────┘        │
│       │              │                                  │
│       └──────┬───────┘                                  │
│              │                                          │
│  ┌───────────▼──────────┐                               │
│  │   RSS 抓取(feedparser)│                              │
│  │  翻译(Tencent+NLLB)  │                              │
│  │  LLM 增强(Agnes)      │ ← 排版/分类/插图/点评        │
│  │  url_hash 去重        │                              │
│  └──────────────────────┘                               │
└──────────────────────────────────────────────────────────┘
              │
              │ HTTPS / Push / Pull
              ▼
   ┌──────────────────────┐
   │  Gitea(代码托管)     │
   │  http://...:3000      │
   └──────────────────────┘

数据流(单篇文章的一生):

RSS Feed → feedparser → FetchedItem
   ↓
url_hash = SHA1(url) + ON CONFLICT DO NOTHING  ← 去重
   ↓ (新文章入库,translation_status=pending)
   ↓
[translation_loop]  1篇/秒  Semaphore(1)
   ↓ 调 腾讯 TMT → body_zh_text/html
   ↓ status: pending → ok
   ↓
[enrichment_loop]  扫描 *_status=pending 的已译文章
   ↓ 调 Agnes LLM: 排版 → 分类 → 插图 → 点评
   ↓ 4 任务独立 try/except,共享 chat_sem + image_sem 限速
   ↓ status: ok / failed
   ↓
[文章详情接口]  原文 + 译文 + AI 排版版 + 分类 + 插图 + 点评  全部展示

技术栈

后端

选型
语言 Python 3.12
Web 框架 FastAPI 0.115 + Uvicorn
ORM SQLAlchemy 2.0(asyncio)+ Alembic
数据库 PostgreSQL 16(asyncpg + psycopg2)
缓存/限速 Redis 7(256MB LRU)
HTTP 客户端 httpx 0.28(异步)
RSS 解析 feedparser 6
HTML 抽取 trafilatura 2 + BeautifulSoup4 + lxml
翻译主 腾讯云 TMT(SDK: tencentcloud-sdk-python)
翻译降级 transformers + NLLB-200-distilled-600M
LLM Agnes(OpenAI 兼容): agnes-2.0-flash / agnes-image-2.1-flash
调度 APScheduler 3.10(AsyncIO + Cron + Interval)
鉴权 passlib[bcrypt] 1.7 + bcrypt 4.0.1 + PyJWT 2.10

前端

选型
框架 Vue 3.5 + Vite 5
UI 库 Naive UI 2.40
状态 Pinia 2.2
路由 vue-router 4.4
HTTP axios 1.7(自动 401 refresh)
时间 dayjs 1.11

部署

选型
容器化 Docker Compose(7 服务)
反代 Caddy 2(alpine)
静态 nginx 1.27-alpine(SPA fallback)
构建 pyproject.toml(setuptools) + npm

仓库结构

diary-news/
├── backend/                          # FastAPI 后端
│   ├── app/
│   │   ├── api/                      # HTTP 路由
│   │   │   ├── auth.py               #   /auth/login, /auth/refresh
│   │   │   ├── me.py                 #   /me, /me/usage(翻译配额)
│   │   │   ├── articles.py           #   /articles 列表 + 详情 + 游标分页
│   │   │   ├── sources.py            #   /sources 只读列表
│   │   │   ├── bookmarks.py          #   /bookmarks 收藏
│   │   │   ├── subscriptions.py      #   /subscriptions 关键词订阅
│   │   │   ├── admin.py              #   /admin/sources, /admin/health, /admin/refresh
│   │   │   └── admin_llm.py          #   /admin/llm/settings, /admin/llm/enrich/{id}
│   │   ├── core/
│   │   │   ├── security.py           #   bcrypt + JWT + API Token
│   │   │   └── deps.py               #   get_current_user, require_owner
│   │   ├── models/                   # SQLAlchemy 2.0 ORM
│   │   │   ├── user.py
│   │   │   ├── source.py
│   │   │   ├── article.py
│   │   │   ├── bookmark.py
│   │   │   ├── subscription.py
│   │   │   ├── api_token.py
│   │   │   └── llm_setting.py        # LLM 设置(单行表)
│   │   ├── schemas/                  # Pydantic v2 I/O 模型
│   │   │   ├── auth.py
│   │   │   ├── article.py
│   │   │   ├── source.py
│   │   │   ├── llm.py                # LLM 设置 schemas + 默认提示词
│   │   │   └── misc.py
│   │   ├── services/
│   │   │   ├── fetchers/             #   RSS / HTML / Telegram 采集器
│   │   │   ├── translation/          #   腾讯 TMT + 本地 NLLB + 配额门面
│   │   │   └── llm/                  #   Agnes client + 智能增强
│   │   │       ├── client.py
│   │   │       └── enrichment.py
│   │   ├── workers/
│   │   │   ├── __main__.py           #   APScheduler + translation_loop + enrichment_loop
│   │   │   └── pipeline.py           #   fetch_one_source / translate_article
│   │   ├── scripts/                  # 初始化(create_user / seed_sources)
│   │   ├── config.py                 # Pydantic Settings(从 .env 读)
│   │   ├── database.py               # 异步 SQLAlchemy 引擎
│   │   ├── redis_client.py           # Redis 单例
│   │   └── main.py                   # FastAPI 入口
│   ├── alembic/
│   │   ├── env.py
│   │   ├── versions/
│   │   │   ├── 0001_initial.py       #   6 张表 + 枚举
│   │   │   └── 0002_llm_settings_and_articles_ai.py
│   │   └── alembic.ini
│   ├── Dockerfile                    # python:3.12-slim
│   ├── pyproject.toml
│   └── .env.example
│
├── frontend/                         # Vue 3 + Vite + Naive UI
│   ├── src/
│   │   ├── main.ts                   # createApp + Pinia + Router
│   │   ├── App.vue
│   │   ├── router.ts                 # 路由守卫(requiresAuth / ownerOnly)
│   │   ├── api/
│   │   │   ├── client.ts             # axios + 401 refresh 拦截器
│   │   │   └── articles.ts           # articles / sources / me / bookmarks / admin
│   │   ├── stores/auth.ts            # Pinia 鉴权状态(localStorage 持久化)
│   │   ├── components/
│   │   │   └── AppLayout.vue         # 顶栏 + 侧栏 + 配额条
│   │   └── views/
│   │       ├── Login.vue
│   │       ├── Feed.vue              # 24h 列表 + 游标分页
│   │       ├── ArticleDetail.vue     # 原文/译文/AI 排版/分类/插图/点评
│   │       ├── Bookmarks.vue
│   │       ├── Sources.vue
│   │       ├── AdminSources.vue      # owner: 源管理 CRUD
│   │       └── AdminLlmSettings.vue  # owner: LLM 提示词 + 测连接 + 触发
│   ├── Dockerfile
│   ├── nginx.conf
│   ├── vite.config.ts
│   ├── tsconfig.json
│   └── package.json
│
├── docs/
│   ├── architecture.md               # 77 行实现版架构
│   └── acceptance.md                 # MVP 验收清单
│
├── scripts/                          # 工具脚本
│   ├── deploy_pull.py                # 免密部署:clone/pull + 失败回滚
│   ├── server_init.py                # 远程服务器初始化(推公钥 + 7 项系统级运维)
│   ├── push_ssh_key.py               # 单推公钥(已用 fingerprint 去重)
│   ├── _*.py                         # 临时调试脚本(gitignored)
│   └── deploy_remote.sh              # 远程一键部署(老脚本)
│
├── .env.example                      # 配置示例(可 commit)
├── .gitignore                        # 忽略 .env / scripts/_*.py / node_modules 等
├── Caddyfile                         # 反代规则
├── docker-compose.yml                # 7 服务
├── DEPLOY.md                         # 完整部署手册(165 行)
├── README.md                         # 本文件
└── news-aggregator-plan.md           # 613 行方案设计 v0.1(决策背景)

数据模型

6 张表 + 1 张配置表(全部 PostgreSQL):

关键字段 说明
users role(enum: owner/member), password_hash 用户 + 角色
sources slug(uniq), kind(rss/html_list/tg_channel), priority, fetch_interval_min, consecutive_failures 采集源
articles url_hash(uniq), translation_status(pending/ok/partial/failed), category, commentary, body_zh_formatted, image_ai_url, *_status, category, entities(JSONB), sentiment, topic_id, bias 文章 + 译文 + LLM 增强
bookmarks (user_id, article_id) UNIQUE 收藏
subscriptions keyword, match_in(any/title/body), channel 关键词订阅
api_tokens token_hash(sha256), expires_at, revoked_at Android 预留
llm_settings format_prompt, classify_prompt, commentary_prompt, image_prompt_template, image_size, chat_model, image_model, interval_sec, enabled LLM 提示词(单行)

ER 关系:

  • users 1:N → bookmarks / subscriptions / api_tokens
  • sources 1:N → articles(cascade delete)
  • articles 1:N → bookmarks, self-ref duplicate_of(去重链)

快速开始

本地开发(Linux/Mac/WSL2)

# 1. 克隆
git clone http://<gitea>/xiaji/diary-news.git
cd diary-news

# 2. 配置
cp .env.example .env
# 编辑 .env 填入:
#   POSTGRES_PASSWORD / REDIS_PASSWORD / JWT_SECRET(用 openssl rand -hex)
#   TENCENTCLOUD_SECRET_ID / TENCENTCLOUD_SECRET_KEY
#   AGNES_API_KEY

# 3. 启动
docker compose up -d --build

# 4. 初始化
docker compose exec api alembic upgrade head
docker compose exec api python -m app.scripts.create_user --username owner --password YOUR_PASS
docker compose exec api python -m app.scripts.seed_sources

# 5. 触发一次抓取
docker compose exec api python -c "import asyncio; from app.workers.pipeline import run_once; asyncio.run(run_once())"

# 6. 打开
open http://localhost/   # macOS
xdg-open http://localhost/  # Linux

Windows 本机(无 WSL)

直接装 Python 3.12 + Node 20:

# 后端
cd backend
py -3.12 -m venv .venv
.venv\Scripts\activate
pip install -e .
# 起 postgres / redis(用 docker run -d 或者本机服务)
$env:DATABASE_URL = 'postgresql+asyncpg://...'
alembic upgrade head
uvicorn app.main:app --reload

# 前端
cd ../frontend
npm install
npm run dev

远程部署

完整步骤见 DEPLOY.md已部署过一次的机器只需 2 步:

# 1. 在本机 push 代码 + 跑 deploy_pull.py
git push origin main
python scripts\deploy_pull.py        # 自动 clone/pull + 失败回滚

# 2. 在服务器上重启应用(代码变了)
ssh hknews
cd /root/diary-news
docker compose restart api worker
docker compose exec api alembic upgrade head

功能详解

RSS 抓取

  • 5 个种子源:Reuters / BBC / Al Jazeera / NHK / DW
  • 抓取频率:每源 60 分钟(可调),失败连续 3 次后间隔 × 2(封顶 12 小时)
  • 去重:url_hash = SHA1(url),PG ON CONFLICT DO NOTHING 幂等
  • HTML 抽取:trafilatura 抓全文,RSS 摘要短时自动补抓
  • 可见性:/admin/sources + /admin/health 看板

翻译(配额 + 缓存 + 降级)

[译文不存在]
   ↓ Semaphore(1) — 1 篇/秒
   ↓
[缓存命中?]
   ├─ 是 → 直接返回(30 天有效)
   └─ 否 ↓
[主引擎(腾讯 TMT)可配额?]
   ├─ 是 → 调 TMT → 写缓存 + 加计数
   └─ 否 ↓
[本地 NLLB 启用?]
   ├─ 是 → 调 NLLB → 写缓存
   └─ 否 → 原文 + [本条未翻译:配额耗尽] 标记

配额在 Redis translation:month:YYYYMM 计数器(月度自动滚动)。TENCENT_TMT_QUOTA_BUFFER=0.05 表示 95% 触发后切本地(避免爆配额)。

用户 / 鉴权

  • JWT:access 60min + refresh 14d,HS256 签名
  • API Token:Android 客户端用,sha256 存储(可撤销 + 过期)
  • 角色:owner 全部权限,member 看文章/收藏/订阅
  • 密码:bcrypt 4.0.1(锁版兼容 passlib)

收藏 / 关键词订阅

  • 收藏:点星星,加到 bookmarks,有 note 字段
  • 关键词订阅:扫 articles.body_text/title,命中后写入 subscriptions.last_hit_at,预留 Telegram 通道(MVP 不发)

LLM 智能增强

新功能(2026-06-08 加入)。翻译完成后,自动调 Agnes LLM 跑 4 项独立任务。

4 项任务

任务 输出字段 LLM 类型 用途
排版 articles.body_zh_formatted chat 重写译文为网页排版(分段/加粗/列表)
分类 articles.category chat(返 JSON) 给文章打 1-3 个分类标签
插图 articles.image_ai_url image 文生图,英文 prompt 拼自 title
点评 articles.commentary chat 100-200 字评论,客观有深度

限速

  • 单一 LlmClient,内部 chat_sem + image_sem 各 1 个并发
  • 每次调用后 await asyncio.sleep(LLM_INTERVAL_SEC)(默认 2.0s)
  • chat 和 image 互不阻塞
  • 4 个任务在 enrich_article串行(已过 client 限速)

设置页(Owner only)

/admin/llm → 4 个 textarea + 几个 input:

  • 总开关 enabled
  • 4 个提示词(可重置默认):format_prompt / classify_prompt / commentary_prompt / image_prompt_template
  • 模型:chat_model / image_model(默认 agnes-2.0-flash / agnes-image-2.1-flash)
  • 插图尺寸:image_size(默认 1024x768)
  • 限速:interval_sec(默认 2.0)
  • 测连接:发个 ping chat 请求,1-2 秒内返 OK
  • 手动触发:POST /admin/llm/enrich/{article_id} 跑一篇 4 任务

默认提示词

backend/app/schemas/llm.pyDEFAULT_PROMPTS,支持占位符:

  • {body} — 译文正文
  • {title} — 译后标题
  • {summary} — 摘要

失败隔离

每个任务独立 try/except,失败标 *_status='failed',不影响其他任务。 enrichment_loop*_statuspending/failed/n/a 的文章,自动重试 failed。

历史文章批量 enrich

新功能只对翻译完成后入库的文章生效。历史已翻译文章,手动 reset:

UPDATE articles
SET format_status='pending', classify_status='pending',
    image_ai_status='pending', commentary_status='pending'
WHERE translation_status='ok';

API 概览

所有 API 在 /api/v1 前缀下,完整定义见 http://<host>/api/docs(DEBUG 模式)。

公开

  • POST /auth/login — 用户名 + 密码 → access + refresh token
  • POST /auth/refresh — refresh token → 新 access

需要登录(任意角色)

  • GET /me — 当前用户信息
  • GET /me/usage — 翻译配额(已用 / 总额 / 百分比)
  • GET /articles?since=...&source=...&q=...&cursor=... — 列表(游标分页)
  • GET /articles/{id} — 详情
  • GET /sources — 源列表(只读)
  • GET /bookmarks / POST /bookmarks / DELETE /bookmarks/{id}
  • GET /subscriptions / POST /subscriptions / DELETE /subscriptions/{id}

Owner only(/admin/*)

  • GET /admin/sources / POST / PATCH /{id} / DELETE /{id} — 源 CRUD
  • POST /admin/refresh/{source_id} — 立即触发抓取
  • POST /admin/translation/rerun/{article_id} — 重译
  • GET /admin/health — 源健康看板
  • POST /admin/translation/quota/reset — 重置本月配额
  • GET /admin/llm/settings / PUT / POST /reset / POST /test — LLM 设置
  • POST /admin/llm/enrich/{article_id} — 手动触发某篇 enrich

开发-部署工作流

┌─────────────────────────────────────┐
│  本地 Windows (你)                  │
│  ─ 编辑代码                          │
│  ─ git add / commit                  │
│  ─ git push origin main             │
└─────────────────┬───────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│  Gitea(代码托管)                    │
└─────────────────┬───────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│  跑 python scripts\deploy_pull.py  │
│  ─ 免密登录(SSH key)               │
│  ─ git clone/pull                   │
│  ─ 成功:保持 + 报告                 │
│  ─ 失败:git reset --hard <前 sha>   │
└─────────────────┬───────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│  远程 HK-News                       │
│  ─ 代码最新                         │
│  ─ (如果需要)docker compose restart│
│  ─ (如果需要)alembic upgrade head   │
└─────────────────────────────────────┘

完整循环命令(本机跑):

git add -A
git commit -m "feat: ..."
git push origin main
python scripts\deploy_pull.py          # 我帮你跑,你说"拉一下"即可

运维工具

脚本 用途 调用
scripts/deploy_pull.py 免密拉取 + 失败回滚 python scripts\deploy_pull.py
scripts/server_init.py 服务器系统级初始化(推公钥 + 7 项运维) REMOTE_PASS=xxx python scripts/server_init.py
scripts/push_ssh_key.py 单推 SSH 公钥(SSH key fingerprint 去重) REMOTE_PASS=xxx python scripts/push_ssh_key.py
docker compose logs -f worker 看 worker 日志 服务器上
docker compose exec api alembic upgrade head 跑 migration 服务器上

deploy_pull.py 完整参数

python scripts\deploy_pull.py \
  --host 207.57.129.228 \
  --port 19717 \
  --user root \
  --repo-dir /root/diary-news \
  --repo-url http://124.223.26.33:3000/xiaji/diary-news.git \
  --branch main

# 干跑
python scripts\deploy_pull.py --dry-run

# 手动回退
python scripts\deploy_pull.py --rollback <sha>

也支持 env var:DEPLOY_HOST / DEPLOY_PORT / DEPLOY_USER / DEPLOY_REPO_DIR / DEPLOY_REPO_URL / DEPLOY_SSH_KEY


故障排查

Q: 翻译一直失败?

  1. docker compose logs worker | grep -E "translate|tencent"
  2. translation:month:YYYYMM Redis key 是不是满了
  3. .envTENCENT_TMT_QUOTA_BUFFER=0.1 给更多缓冲

Q: 某个 RSS 源一直 fail?

  1. /admin/healthconsecutive_failures 字段
  2. docker compose logs worker | grep <source_slug>
  3. 大概率是 RSS URL 失效或被反爬,先 enabled=false 暂停,在 /admin/sources 编辑后重启 worker

Q: LLM 增强不工作?

  1. /admin/llm → 点 "测连接"
  2. 看后端日志 docker compose logs worker | grep -E "enrich|chat"
  3. 确认 .env 里有 AGNES_API_KEY

Q: 服务器磁盘快满?

DELETE FROM articles WHERE published_at < now() - interval '90 day' AND duplicate_of IS NULL;

Q: deploy 失败,想回退?

python scripts\deploy_pull.py --rollback <之前的好 sha>
# 或者手动
ssh hknews "cd /root/diary-news && git reset --hard <sha>"

Q: 中文用户名乱码?

PowerShell 默认 GBK,运行前先 chcp 65001 切 UTF-8。


路线图

  • Phase 1 (MVP):5 RSS 源 + 翻译 + 网页 + admin CRUD
  • Phase 1.5:LLM 智能增强(排版/分类/插图/点评) 2026-06-08
  • Phase 2:PWA 离线缓存 / 关键词订阅推送(Telegram)
  • Phase 3:Android 客户端(API Token 已预留)
  • Phase 4:自动分类/点评/实体识别(目前是 LLM 一次性,无 ML pipeline)
  • Phase 5:跨源立场对照 / 主题聚类

文档导航


设计原则

  • 轻量:单机 30G 能跑,不堆重型服务
  • 可控:源管理 / 翻译配额 / 抓取调度 / LLM 提示词 全部可视化
  • 可扩展:ML 字段(category/commentary/entities/sentiment/bias)已建好,后续直接写值不动表
  • 不反爬对抗:愿意被 ban IP 就 ban,优先合规
  • 透明失败:不静默吞错,每个 stage 都有 *_status 字段记录
  • 幂等可重跑:所有运维脚本(server_init / deploy_pull)都幂等,跑多遍无副作用

License: Private use only.

Description
每日简报系统
Readme 697 KiB
Languages
Python 75.7%
Vue 16.5%
TypeScript 3.7%
CSS 1.7%
Shell 1.6%
Other 0.8%