Files
diary-news/README.md

626 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Diary News · 私人多源新闻翻译系统
> 抓境外权威源 → 自动中英对照 → 智能排版/分类/插图/点评。
> 跑在一台 2C/2G/30G 的香港 VPS 上,自用 + 家人/小圈子。
---
## 目录
1. [项目目标](#项目目标)
2. [关键特性](#关键特性)
3. [架构概览](#架构概览)
4. [技术栈](#技术栈)
5. [仓库结构](#仓库结构)
6. [数据模型](#数据模型)
7. [快速开始](#快速开始)
8. [功能详解](#功能详解)
9. [LLM 智能增强](#llm-智能增强)
10. [API 概览](#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)
```bash
# 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:
```powershell
# 后端
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`](./DEPLOY.md)。**已部署过一次的机器**只需 2 步:
```bash
# 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.py``DEFAULT_PROMPTS`,支持占位符:
- `{body}` — 译文正文
- `{title}` — 译后标题
- `{summary}` — 摘要
### 失败隔离
每个任务独立 try/except,失败标 `*_status='failed'`,**不影响**其他任务。
`enrichment_loop``*_status``pending/failed/n/a` 的文章,自动重试 failed。
### 历史文章批量 enrich
新功能**只对**翻译完成后入库的文章生效。历史已翻译文章,手动 reset:
```sql
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 │
└─────────────────────────────────────┘
```
**完整循环命令**(本机跑):
```bash
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 完整参数
```bash
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.`.env``TENCENT_TMT_QUOTA_BUFFER=0.1` 给更多缓冲
### Q: 某个 RSS 源一直 fail?
1. `/admin/health``consecutive_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: 服务器磁盘快满?
```sql
DELETE FROM articles WHERE published_at < now() - interval '90 day' AND duplicate_of IS NULL;
```
### Q: deploy 失败,想回退?
```bash
python scripts\deploy_pull.py --rollback <之前的好 sha>
# 或者手动
ssh hknews "cd /root/diary-news && git reset --hard <sha>"
```
### Q: 中文用户名乱码?
PowerShell 默认 GBK,运行前先 `chcp 65001` 切 UTF-8。
---
## 路线图
- [x] **Phase 1 (MVP)**:5 RSS 源 + 翻译 + 网页 + admin CRUD
- [x] **Phase 1.5**:LLM 智能增强(排版/分类/插图/点评) ✅ 2026-06-08
- [ ] **Phase 2**:PWA 离线缓存 / 关键词订阅推送(Telegram)
- [ ] **Phase 3**:Android 客户端(API Token 已预留)
- [ ] **Phase 4**:自动分类/点评/实体识别(目前是 LLM 一次性,无 ML pipeline)
- [ ] **Phase 5**:跨源立场对照 / 主题聚类
---
## 文档导航
- 📄 [DEPLOY.md](./DEPLOY.md) — 完整部署手册(新机器从 0 到能访问)
- 📐 [docs/architecture.md](./docs/architecture.md) — 实现版架构(77 行)
- ✅ [docs/acceptance.md](./docs/acceptance.md) — MVP 验收清单
- 📜 [news-aggregator-plan.md](./news-aggregator-plan.md) — 613 行方案设计 v0.1(选型决策背景)
- 🛠 [scripts/deploy_pull.py](./scripts/deploy_pull.py) — 部署工具源码
---
## 设计原则
- **轻量**:单机 30G 能跑,不堆重型服务
- **可控**:源管理 / 翻译配额 / 抓取调度 / LLM 提示词 全部可视化
- **可扩展**:ML 字段(category/commentary/entities/sentiment/bias)已建好,后续直接写值不动表
- **不反爬对抗**:愿意被 ban IP 就 ban,优先合规
- **透明失败**:不静默吞错,每个 stage 都有 `*_status` 字段记录
- **幂等可重跑**:所有运维脚本(server_init / deploy_pull)都幂等,跑多遍无副作用
---
**License**: Private use only.