xiaji
|
6a45f84857
|
perf(llm): 短新闻 enrich 跳过时不再标 'n/a',避免 enrichment_loop 反复扫
问题:
enrichment_loop 的 in-loop 过滤 (line 616) 把 'n/a' 当成
"未 enrich" 信号:
if any(s in ("pending", "failed", "n/a") for s in statuses):
todo_ids.append(a.id)
短新闻入表时 format_status='n/a'、image_ai_status='n/a'
(commit 1 决定),而 enrich_article 在 is_short 跳过这两任务时
保留 'n/a'。后果:enrichment_loop 永远认为"这两任务未完成",
每 ~3.5 分钟反复把同一篇短新闻捞进队列 enrich 一次。
实测(commit 10 修复前 1 小时数据):
- 9 篇短新闻 × 18 次 enrich = 131 次完全 ok
- 131 - 9 = 122 次纯浪费(占 95%)
- 每篇 enrich 内部 Angel + 美团两个 LLM 调用是并行的
- 浪费配额:Agnes 免费 plan 风险、Angel 月配额、美团限速窗口
修法:
enrich 跳过某任务时,把对应 status 标 'ok' (语义 = 该任务
"已完成" = 不需要做),而不是 'n/a' (语义 = 任务存在但跳过)。
改动 2 处:format 跳过 + image 跳过。
短新闻 enrich 后,所有 *_status 都是 'ok',enrichment_loop
看到后不再扫,只跑一次。
注释同步更新:解释 'n/a' vs 'ok' 的语义区别,提醒后续修改者
不要把 'n/a' 写回 status 字段。
存量修复(不写入 commit,运维动作):
修复前入库存量的短新闻 format/image_ai_status 仍是 'n/a',
deploy 后第一次循环会把它们 enrich 一遍改成 'ok'。如果你想
立刻修,跑:
UPDATE articles
SET format_status='ok', image_ai_status='ok'
WHERE is_short_news=true
AND (format_status='n/a' OR image_ai_status='n/a');
不改:
- enrichment_loop 的 SQL/loop 过滤条件(不动 'n/a' 状态值,
仍有合法场景使用:meituan_client 未配置时 commentary_meituan
会标 'n/a'。但生产环境 meituan key 已配置,该 case 不存在)
- ingest 路由的 *_status='n/a' 初始化(那是在入库阶段,
enrichment_loop 不会立即扫到,有 2 秒起步延迟 +
is_short_news/body_zh_text 过滤)
- comment_status 等其他 status 字段(短新闻也会跑,设 'ok')
性能影响(预估):
- 修复前:每篇短新闻每小时 18 次 enrich × 1 路串行 ~18 秒/次
= 占 enrichment_loop 单 worker 18 * 18s = 5.4 分钟/小时
- 修复后:每篇短新闻只 enrich 1 次,约 5 秒
- 节省:短新闻 LLM 调用约减少 95%
- 副作用:长新闻 enrich 不再被短新闻"挤占"semaphore 资源,
长新闻 enrich 速度会加快(从当前 ~18s 串行,可能回到 ~5s)
|
2026-06-14 22:03:06 +08:00 |
|
xiaji
|
a81b373b8b
|
chore(tz): 时区统一为 Asia/Shanghai
仓库里有两处时区声明写成 Asia/Hong_Kong,虽然跟 Shanghai
都是 UTC+8 在 C 库时区行为上一致(无夏令时),但:
- VPS 物理位置虽然在香港,本机 /etc/localtime 已指向 Shanghai
- 配置文件/默认值/示例 env 写成 Shanghai 更准确反映项目
实际部署位置
- /etc/timezone 文件本机之前是 Etc/UTC 跟 timedatectl 不一致
(本次单独在服务器上同步,不入 commit — 是运维状态)
改动:
- .env.example: TZ Asia/Hong_Kong -> Asia/Shanghai
- backend/app/config.py: settings.tz 默认值 Asia/Hong_Kong
-> Asia/Shanghai(只有日志打了一下,不影响实际行为)
不影响:
- 生产 .env 文件没动(里面有 JWT_SECRET 等真实值,不该入库;
新机器按 .env.example 复制出来就是 Shanghai)
- docker-compose.yml 不动 — api/worker service 用 env_file: .env
模式,TZ 已经自动注入容器(验证过 TZ=Asia/Hong_Kong 在容器内可见)
- 应用代码所有 datetime.now() 都是 naive datetime,实际行为
不变(数据库存的是 UTC 绝对时刻,PG TIMESTAMPTZ 转换正确)
- 文档/README 不动 — line 4 写"香港 VPS"是物理位置描述,
时区是上海,两者并存(都是 UTC+8)不矛盾
|
2026-06-14 21:12:36 +08:00 |
|
xiaji
|
7e50e88ef9
|
docs(readme): 同步 commit 1-7 的功能和数据模型
README 在 commit 3 时只加过 API Push 的"关键特性"和"API 概览"两段,
但漏了以下 5 处,本次统一补上:
1. 数据模型表:
- sources.kind 加 api_push(commit 1 alembic 0008 + commit 3 worker 跳过)
- articles 加 is_short_news / external_id / source_ref / content_hash(commit 1)
- articles.translation_status 加 'n/a' 状态值
- api_tokens 加 purpose + source_id(commit 1 + commit 2)
- ER 关系: sources 1:N api_tokens(ingest token 绑定 source)
- 新增"短新闻(API Push)特性"子节,描述 content_hash 算法
和 url 合成策略
2. API 概览:
- POST /admin/refresh/{source_id} 注明对 api_push 源无效
- POST /admin/translation/rerun 注明对短新闻/中文源返 400(commit 4)
- 新增"端到端测试"小节,引用 scripts/smoke_ingest.py(commit 5)
3. 故障排查:
- 新增 3 个 Q:API Push 相关 / 中文 RSS 详情页 / alembic 部署顺序
- 最后一个 Q 引用 agent memory "FastAPI + alembic 部署 SOP"(commit 部署)
4. 路线图:
- 新增 Phase 1.6 (API Push 短新闻) ✅ 2026-06-14
- 新增 Phase 1.7 (中文源头 RSS 优化) ✅ 2026-06-14
无代码改动,纯文档同步。
|
2026-06-14 21:05:32 +08:00 |
|
xiaji
|
55e20e923a
|
perf(translate): translation_loop 跳过中文源头,省 TMT 配额
中文 RSS 长新闻(原文就是中文)走 TMT 中翻中,纯粹浪费
月配额(500 万字符)且产生无意义译文。前端 commit 6
已经隐藏"译文"板块;本 commit 在后端拦截,从源头不跑翻译。
改动:
- translation_loop SQL 加 WHERE lang_src IS NULL OR NOT LIKE 'zh%'
- lang_src 为 NULL 时仍走翻译(英文 RSS 没设 language_src 的合法场景)
- LIKE 'zh%' 覆盖 zh / zh-CN / zh-Hans / zh-TW 等区域码
- translate_article() 函数内加防御性 guard:中文源直接返
并把 translation_status 改 'n/a',避免反复入队
(主路径 SQL 过滤已足够,这里是兜底,应对手动 reset status 的情况)
不影响:
- 短新闻(commit 1 已是 translation_status='n/a',根本不进队列)
- 外文 RSS(走翻译)
- 历史已被错误翻译的中文长新闻:保留 translation_status='ok'
+ body_zh_text 中文(空跑产生的) — commit 6 前端已隐藏,
不影响用户感知;回滚存量不在本 commit 范围(独立 SQL 即可,
风险与收益需要单独评估)
- enrichment_loop(commit 1 已经能扫到中文源头的 is_short_news,
长新闻 lang_src=zh 仍能被 enrichment 处理,排版+插图+评论都跑)
范围:仅 backend/app/workers/pipeline.py,+20/-2 行。
|
2026-06-14 20:57:11 +08:00 |
|
xiaji
|
57784588c8
|
fix(detail): 中文源头 RSS 文章不显示译文板块
之前 commit 3/4 只针对 is_short_news 隐藏译文,
中文 RSS 长新闻(原文就是中文)走的是 TMT 中翻中,
详情页仍显示"文章译文"和"文章原文"两个冗余板块。
新规则:isChineseSource = isShort || lang_src.startsWith('zh')
- 短新闻 -> 显示"短讯正文"卡(原有)
- 中文 RSS 长新闻 -> 显示"文章正文"卡(新增,只一张卡,无译文)
- 其他长新闻 -> 显示"文章译文" + 可切换"文章原文"(原有)
隐藏规则(对所有中文源头):
- 翻译状态/引擎 tag
- 重译按钮(commit 4 的 guard 已拒绝;前端再次隐藏)
- 原文链接按钮
- AI 插图卡片(中文源头不需配图)
- "文章原文"卡片(在"文章正文"已经显示了)
后端不拦截(中翻中不出错,只是浪费一点配额) --
影响:腾讯 TMT 月配额可能略快用完,可下次单独优化
(在 translation_loop 加 lang_src=zh 跳过)。
范围:仅 ArticleDetail.vue 一个文件,+25/-6 行。
|
2026-06-14 20:50:34 +08:00 |
|
xiaji
|
f5fcde1153
|
test(ingest): 端到端 smoke 脚本
scripts/smoke_ingest.py — 验证 /api/v1/ingest 完整链路,6 步:
1) 新建 → 201 created
2) external_id 重复 → 200 duplicate reason=external_id_match
3) 不带 external_id 新建 → 201 created
4) 内容指纹重复 → 200 duplicate reason=content_hash_match
5) 错误 token → 401
6) body 超 5000 字 → 400 / 422
用法:python scripts/smoke_ingest.py --token <RAW_TOKEN>
默认 host=207.57.129.228 port=19717(直连服务器),
生产走 caddy 时:--scheme https --host your-domain --port 443
需要 owner 在 web 端生成 ingest token(API Push 源的 🔑 Token 按钮)。
smoke 数据会留在 Feed 里(source_ref='smoke', tags 含 'smoke'),
如果想清理:DELETE FROM articles WHERE source_ref='smoke';
或用 source_ref 过滤后手动删。
|
2026-06-14 20:30:35 +08:00 |
|
xiaji
|
6ae5dfae60
|
fix(ingest): rerun_translation 拒绝短新闻
短新闻(API Push)是中文原生,translation_status 固定为 n/a,
不应当走翻译链路。
前端 ArticleDetail.vue 已经在按钮上加了 v-if='isOwner && !isShort'
(commit 3),web UI 上 owner 看不到重译按钮;但 curl 直接调
POST /admin/translation/rerun/{id} 仍能绕过前端触发翻译,
把 status 改 pending 并入队 translate_article。
本 commit 加后端 guard:is_short_news=True 直接返 400,
跟前端形成双保险。
防呆:
- 修法:在 art 找到后、状态字段重置前先 raise
- 影响范围:仅 rerun_translation 路由,其他 rerun 路径不动
- 不需要 schema/迁移
|
2026-06-14 20:27:53 +08:00 |
|
xiaji
|
e274246056
|
feat(ingest): API Push 前端层 + 文档 + 端到端联通
后端(支持 api_push source 创建/调度):
- schemas/source.py:SourceIn.url 改成 str(允许 api_push 的 api-push:// 占位)
- admin.py create_source 简化 url 传递
- workers/__main__.py:_rebuild_jobs 跳过 api_push 源(它是被动接收,不抓取)
- workers/pipeline.py:run_once 也加同条件,api_push 不进抓取循环
前端:
- api/articles.ts:ArticleListItem 加 is_short_news(required)/source_ref;
ArticleDetail 加 external_id;导出 IngestTokenOut;adminApi 加
list/create/revoke ingest token 三个方法
- views/Feed.vue:卡片根 class 短新闻加 short-card(淡蓝底 #f6f9fc +
左侧 3px 蓝色色条 #4f9eff);元信息栏加 📰 短讯 角标;长新闻摘要
body_zh_text 截前 200 字,短新闻不截取保留换行(white-space: pre-wrap);
短新闻不显示 AI 插图
- views/ArticleDetail.vue:tag 行加 📰 短讯 + source_ref 角标;短新闻
路径下隐藏翻译状态/重译/原文链接按钮;正文区短新闻直接渲染
body_zh_text,跳过译文/原文/AI 配图卡片;Angel + 美团双评论卡片
都保留
- views/AdminSources.vue:kind 加 api_push 选项;api_push 源 URL 字段
变只读占位、隐藏抓取间隔;列表操作列加 🔑 Token 按钮;
弹窗支持生成(raw_token 一次性显示 + 复制)/列表/撤销
文档:
- docs/api-push.md:调用方契约 + 三层去重 + 限速 + lifecycle +
owner 操作手册 + curl/Python 示例 + 重试策略 + 故障排查
- README.md:关键特性加 API Push;API 概览加 /api/v1/ingest 和
3 个 /admin/.../ingest-tokens 端点
|
2026-06-14 16:15:21 +08:00 |
|
xiaji
|
07534eb144
|
feat(ingest): API Push 短新闻接口层
- POST /api/v1/ingest:鉴权(X-Ingest-Token) + 限速(每 token 2 篇/秒,
Redis 滑动桶,INGEST_RATE_PER_SEC 可调) + 三层去重(L1 external_id /
L2 content_hash / L3 DB UNIQUE 兜底,均带 reason)
- 写入字段:is_short_news=True、translation/format/image_ai_status='n/a'、
classify_status=(有 tags?'ok':'pending')、commentary_{angel,meituan}_status='pending'、
body_zh_text=body_text(走统一路径,前端/prompt 不用改)
- services/fetchers/api_push.py:compute_content_hash + synthesize_url +
normalize_published_at + build_initial_status 纯函数
- schemas/ingest.py:IngestPayload(title 1-200/body 1-5000/tags 去重去空) +
IngestResponse(article_id/content_hash/status/reason/matched_external_id)
- admin.py:POST/GET/DELETE /admin/sources/{id}/ingest-tokens — owner 生成
(raw_token 仅一次性返回)、列出、撤销
- schemas/article.py:ArticleListItem 加 is_short_news/source_ref;
ArticleDetail 加 is_short_news/source_ref/external_id
- main.py:挂 ingest router;config.py + .env.example:ingest_rate_per_sec 默认 2
短新闻由 commit 1 enrichment_loop 自动接管 classify + 双 provider commentary,
跳过 format/image。
|
2026-06-14 16:04:45 +08:00 |
|
xiaji
|
3091f291b2
|
feat(ingest): API Push 短新闻数据层
- alembic 0008:articles 加 is_short_news/external_id/source_ref/content_hash
(UNIQUE);sources.kind 加 'api_push';api_tokens 加 purpose + source_id
- SourceKind.API_PUSH enum;Article/ApiToken model 加新字段
- enrichment_article 短新闻跳过 format/image;
enrichment_loop SQL 加 is_short_news 路径(并入'可 enrich' 条件)
- 入库侧由 commit 2(ingest 接口)负责:写 body_zh_text=body_text,
format/image/commentary_meituan_status='n/a',
classify/commentary_status='pending'(带 tags 时 classify='ok')
无迁移爆炸半径:articles.url 保持 NOT NULL,短新闻合成 api-push:// 占位
|
2026-06-14 15:51:22 +08:00 |
|
xiaji
|
f690f1f108
|
fix(fe): mark-read btn 到 Feed 卡片右下角新样式 + hide_read 模式卡片滑出动画
|
2026-06-14 09:35:57 +08:00 |
|
xiaji
|
7de018676a
|
fix(fe): 3 个 TS 错
1. articles.ts: ArticleDetail extends ArticleListItem 重复定义 is_read
删了(继承父类的 boolean)
2. Feed.vue: import readsApi + useMessage
3. Feed.vue: 加 const message = useMessage()
|
2026-06-13 21:18:06 +08:00 |
|
xiaji
|
7057992136
|
fix(alembic 0007): 改用 sa.Column 内嵌 ForeignKey 形式,避免列数不匹配
之前用独立 sa.ForeignKeyConstraint 报 ArgumentError,
改用 sa.Column(..., sa.ForeignKey(...), ...) 形式(
跟 model 里的 mapped_column 形式对齐)。
|
2026-06-13 21:15:22 +08:00 |
|
xiaji
|
4ca05b8b7d
|
fix: 修两个 bug
1. ArticleRead.user_id 改 Integer(users.id 是 Integer,不是 BigInteger)
alembic 0007 同样改 Integer
2. ArticleDetail.vue toggleRead 重复 catch 块导致 build 失败
(edit 时新加的 catch 跟 toggleStar 残留的 catch 撞了)
|
2026-06-13 21:10:22 +08:00 |
|
xiaji
|
6c71ab2e79
|
feat(read): 已读功能 — 每账号标已读,列表默认隐藏
需求: 每个账号可标已读,已读过的文章刷新/重载后不在 24h feed 中显示。
设计:
- 新表 article_reads (user_id, article_id, read_at) 复合主键,on-delete CASCADE
- 迁移 0007_article_reads
- /me/reads/{id} POST/DELETE 标记 / 取消(幂等,PG upsert on_conflict_do_nothing)
- /me/reads GET 列出已读 IDs(默认 7 天,limit 500)
- articles.py 列表查询加 hide_read=true 参数(默认 true),用 NOT EXISTS 排除已读
- ArticleListItem / ArticleDetail schema 加 is_read 字段
前端:
- types 加 is_read + readsApi(mark/unmark/list)
- Feed 列表:
顶部加 '隐藏已读' 开关,默认 ON
每张卡片加 '标为已读 / 标为未读' 按钮(乐观更新,失败回滚)
已读卡片 opacity 0.7 + 灰背景,标识弱化
- ArticleDetail 详情页操作栏加 '标为已读' 按钮(同样乐观)
|
2026-06-13 21:04:47 +08:00 |
|
xiaji
|
8b3c7caf87
|
chore: 去掉 active_ip 的临时 debug log(已通过限流验证)
smoke_limit2 验证成功(临时 kick 真实 IP + 30 假 IP → 真实 IP 登录返 429,
限流逻辑按设计工作)。
清理掉 [CHECK] [TOUCH_DEP] stderr 调试 log,恢复代码干净。
|
2026-06-13 20:29:08 +08:00 |
|
xiaji
|
d1bf5ea2df
|
fix(auth): Caddy 显式写入 X-Forwarded-For + uvicorn 信任 docker 网络
根因(在 smoke test 中定位):
- Caddy reverse_proxy 默认会覆盖客户端的 X-Forwarded-For 头,
写入它自己识别的 client IP(防伪造)
- uvicorn --forwarded-allow-ips 默认只信任 127.0.0.1,
docker 网络 172.18.0.x 不在白名单
- 结果: api 端读 X-Forwarded-For 时,看到的是 Caddy 替换后的值
(Caddy 识别的真实 client IP),不是客户端伪造的值 — 这其实是正确的!
但 uvicorn 不会用这个值更新 client scope
修法:
- Caddyfile: header_up X-Forwarded-For {remote_host}
显式让 Caddy 把自己识别的 client IP 写入 X-Forwarded-For
- docker-compose api command: 加 --forwarded-allow-ips 172.18.0.0/16
信任 docker 网络(让 uvicorn 采用 X-Forwarded-For 的值)
- api 端 get_client_ip 不变,读 X-Forwarded-For 拿真实 client IP
效果: X-Forwarded-For 在代理链中始终代表真实 client IP,
不再被任何中间件覆盖或丢弃
|
2026-06-13 19:47:11 +08:00 |
|
xiaji
|
0d5f29fd37
|
fix(active_ip): 修 debug log 引用未定义变量导致 500
bug: 之前 smoke 加 print 时把 print 放到了 'limit = ...' 之前,
Python 看到函数内有 'limit =' 就把整个 limit 视为 local,
print 在赋值前引用 → UnboundLocalError → 整个 login 返 500。
修: 把 print 挪到 limit 赋值之后。
|
2026-06-13 19:16:58 +08:00 |
|
xiaji
|
7c5aff6345
|
debug: 临时加 stderr log 看 IP 提取 + touch_ip 调用
|
2026-06-13 19:12:23 +08:00 |
|
xiaji
|
b500613d22
|
feat(auth): 限制同时在线 IP 数 (默认 30, 第 31 拒绝)
背景: 防 token 泄漏被滥用 + 限共享账号人数。
- 新增 app/services/active_ip.py:
Redis ZSET 'active_ips' 存 IP,score=last_seen_unix
登录/refresh 时 check_or_register_login_ip():
IP 已在 set → 刷新 score,放行(老用户重连)
IP 不在 set + ZCARD < 30 → 加入,放行
IP 不在 set + ZCARD >= 30 → raise 429
每个已认证请求 _resolve_user() 调 touch_ip_dependency()
滑动 TTL,30 天没活动自动从 set 剔除
- get_client_ip() 取真实 IP,优先级 X-Forwarded-For > X-Real-IP > client.host
trust_x_forwarded_for 默认 True(生产 Caddy/Nginx 后面)
- config 加 3 个开关:
site_max_active_ips: int = 30
site_active_ip_idle_days: int = 30
trust_x_forwarded_for: bool = True
- admin.py 加 3 个端点:
GET /admin/active-ips — 看当前活跃 IP 列表 + last_seen
POST /admin/active-ips/kick — 强制踢出指定 IP(body={ip})
DELETE /admin/active-ips/{ip}— 简写踢出
- 注: refresh 也算 IP 占用(拿到 access token 就能用)
但已存在的 IP 直接放行,不会踢自己
|
2026-06-13 18:22:40 +08:00 |
|
xiaji
|
0df6e79e56
|
style(ui): 移除 ok 状态的'已生成'绿色徽章
- 已生成的评论不再显示徽章,回归简洁(只剩标题和内容)
- 失败/生成中徽章保留(红/灰,辨识度需要)
- .commentary-badge-ok 样式移除
|
2026-06-12 23:48:52 +08:00 |
|
xiaji
|
ddf2bc98e0
|
style(ui): 评论三态视觉化 — 等待/失败卡片增强辨识度
旧版问题:等待中卡片视觉太低调(灰小斜体一行),失败也不显眼,
用户截图反馈:看不出状态,无法判断是 bug 还是没跑。
新版:
- 等待中: 浅蓝底 + 1.5px 虚线边框 + 旋转 spinner + '生成中' 徽章
- 失败: 浅红底 + 1.5px 实线红边框 + '失败' 徽章(详情页显示原因)
- 成功: 保留原木色左边框样式
- 加 commentary-badge(带 spinner / 状态文字) 替代原 NTag,辨识度更高
- 列表/详情 min-height 60px,避免占位卡片太矮导致视觉跳
- 失败时详情页显示 commentary_meituan_error(给 owner 排查用)
|
2026-06-12 23:37:48 +08:00 |
|
xiaji
|
66e57c6e07
|
feat(ui): 评论三态显式 — 有内容 / 等待中(灰斜体) / 失败(红)
- 之前: 只有 commentary 内容才显示 v-if 块,否则空白
- 现在: 评论卡永远显示,三态语义明确:
ok + 有内容 → 评论文本(正常)
pending / n/a / null → 等待中(灰斜体 + 提示 enrichment_loop 会跑)
failed → 评论生成失败(红斜体 + 失败原因)
- 列表 Feed + 详情 ArticleDetail 同步改造
- 加 commentaryState() 辅助函数:status+content → 'ok'|'waiting'|'failed'
|
2026-06-12 23:24:30 +08:00 |
|
xiaji
|
16536fe3a0
|
feat(meituan): 政治类文章拦截 + 写'无可奉告' + Angel 并发 3→1
- llm_settings 加 meituan_blocked_topics / blocked_keywords / no_comment_text
- alembic 0006 迁移,默认 topics=[时政/国际/军事/政治/战争/冲突/制裁/选举], 默认文案='无可奉告'
- enrichment._is_meituan_blocked 预检:category 命中 topic 或 关键词 → 直接写'无可奉告',不调美团 API
- 命中后 commentary_meituan_model='policy-block' 标识非真实生成
- enrichment_loop Semaphore(3)→(1),Agnes 免费 plan 不再 429
- 前端 AdminLlmSettings 美团卡片加 3 字段 UI(主题/关键词/固定文案)
|
2026-06-12 22:44:00 +08:00 |
|
xiaji
|
aaf728f3f4
|
feat(admin): Angel(Agnes) provider 凭据 DB 化 + 安全 key_set 字段
- llm_settings.agnes_api_key TEXT (DB key 优先,.env 兜底)
- llm_settings.agnes_base_url_override VARCHAR (留空 = 用 .env)
- alembic 0005_agnes_key 迁移
- LlmSettingOut.agnes_api_key_set (bool) 替代直接回传 key
- LlmSettingUpdate 加 agnes_api_key / agnes_base_url_override(可空可清空)
- providers.get_angel_client 改用 DB key 优先
- enrichment.py 改为 get_angel_client() 工厂调用(热改 key 不需重启)
- /admin/llm/settings/test 走 get_angel_client(测的是 DB 里的 key)
- 前端 AdminLlmSettings 在'总开关 + 模型'卡里加 Angel api_key 输入框 +
base_url 覆盖 + 已配置/未配置指示灯 + 清空按钮
- 顶部'测连接'按钮复用(测的就是 Angel)
|
2026-06-12 20:43:54 +08:00 |
|
xiaji
|
785b63cfed
|
fix(ci): docker-compose 加 alembic/alembic.ini 挂载,让容器看到新迁移
|
2026-06-12 19:08:40 +08:00 |
|
xiaji
|
bc36a1fc38
|
feat(commentary): 双 provider 评论 — Angel(Agnes) + 美团大模型(LongCat)
- 新增 articles.commentary_meituan{_status,_model,_error} 4 列 + commentary_engine
- LlmSetting 加 meituan_api_key/base_url/chat_model/interval_sec/enabled/commentary_prompt
- 新 app/services/llm/providers.py 工厂,支持多 provider 客户端
- enrichment 流程改为 commentary_angel + commentary_meituan 并行(asyncio.gather),
任一 provider 失败不影响另一个
- enrichment_loop 状态判定:任一 provider 状态不是 ok 都视为待 enrich
- alembic 0004_dual_commentary 迁移
- 前端 Feed 卡片 + ArticleDetail 详情页各加一条'美团评论'卡
- AdminLlmSettings 加美团 provider 配置卡(独立 api_key 编辑器,不回显明文)
- LlmSettingOut.meituan_api_key_set (bool) 替代直接回传 key
- 默认 URL https://api.longcat.chat/openai/v1 / 默认模型 LongCat-2.0-Preview
|
2026-06-12 19:00:00 +08:00 |
|
Mavis
|
3ab6e4c7d0
|
fix(detail): 原始译文/原文标签字号 16→19px,加段落切分+行高 1.95
问题:'文章译文(原始)' 和 '文章原文' 标签下,内容是 body_zh_text / body_text 一坨没分段,
走 .article-body-fallback 16px,排版难看。
修复:
1) .article-body-fallback 字号 16→19,行高 1.85→1.95,加 max-width 720px 居中 + 0.02em letter-spacing
2) 加 splitIntoParagraphs: 已经是 HTML 标签的不动;否则按 \n 切;一坨纯文字按 [。!?!?] 切
3) 段首 text-indent 2em(中文书报排版)
4) translationBody / originalBody computed 替代内联三元
LLM 排版过的 (body_zh_formatted) 已经 <p> 段标签,不受影响。
|
2026-06-11 23:54:32 +08:00 |
|
Mavis
|
e4733ab495
|
style(ai): 插图默认尺寸 768x512 → 512x384(更省 CDN 流量)
512x384 是 4:3 经典比例,后端 DEFAULT_IMAGE_SIZE 常量值。
老文章(已 enrich)的 image_ai_url 仍是 768x512 不会自动变;
新 enrich 走 512x384。Setting.image_size 字段保留(用户在 UI 改生效),
但默认行为不依赖它,避免被改大。
|
2026-06-11 23:25:05 +08:00 |
|
Mavis
|
e96e1cf1ff
|
style(article): 正文 p 字号 17→19px,行高 1.7→1.75(阅读更舒服)
- 后端 enrichment.ARTICLE_BODY_FONT_SIZE: 17 → 19(新写库的 LLM 排版版)
- 前端 style.css .article-body / .diary-para: 17 → 19,line-height 1.7 → 1.75
- 768px 媒体查询: 16 → 18,line-height 1.8 → 1.85
- 480px 媒体查询: 15.5 → 17
老文章也立即变大(前端 .article-body 兜底);新排版文章用新 inline style。
后端要等下一次 enrich 才落新行(原 inline style 还是 17px),
如果想老排版版立即也变,在 /admin/llm 手动 trigger 几篇 enrich 即可。
|
2026-06-11 22:53:38 +08:00 |
|
Mavis
|
847af6c104
|
docs(readme): 重写'单篇文章的一生'数据流章节
旧版只画了一个简单流程图,信息过时(只提腾讯 TMT + Agnes,没提 spark/zhipu/normalize)。
新章节:
- ASCII 流程图:APScheduler 启动 3 个常驻 loop(translation_loop / enrichment_loop),
全部经 articles 表 → /api/v1/articles/{id} → 前端
- T+0s~T+任意 时间线:每个阶段约几秒,清晰标出 spark/zhipu/tencent 引擎选择点
- 字段生命周期表:7 阶段 × 7 字段(翻译/4 LLM 状态),列 NULL/n/a/ok 真实状态
- 翻译引擎优先级表:spark → zhipu → tencent TMT → tencent_maas → agnes → local,
6 个引擎 + 配置/默认 model/配额/失败处理
- LLM 智能增强流程图:4 任务(classify/format/image/commentary)独立 try/except
+ 限速(LlmClient 内部 sem + enrichment_loop 外层 Semaphore(3))
- 故障模式速查表:6 个常见 case(pending 停滞 / 单项 failed / 401 / worker loop 没起
/ RSS 抓不到),给排查命令和修复方向
目的:让看 README 的人能一眼看出当前 6 翻译引擎链路 + 4 LLM 任务是不是真在工作。
便于在交接 / 出问题时,新人按文档直接对线。
|
2026-06-11 22:09:01 +08:00 |
|
Mavis
|
24478604b1
|
bfddfdfdfewe
|
2026-06-11 17:24:46 +08:00 |
|
Mavis
|
fd7817b881
|
fix(translate): 拦截引擎错误 marker + pipeline 严格 status 判定,避免 TMT AuthFailure 伪装 ok
|
2026-06-11 10:01:19 +08:00 |
|
Mavis
|
6293f82a3a
|
chore(translate): 降频 2秒/次 + 改 spark 为 wss WebSocket 鉴权(智谱/zhipu=第一)
|
2026-06-11 09:34:01 +08:00 |
|
Mavis
|
4b8d776aac
|
feat(scripts): 添加 retranslate_history 脚本,支持软/硬重译 + dry-run + 按源/数量过滤
|
2026-06-11 09:28:26 +08:00 |
|
Mavis
|
d90c5955f5
|
feat(web): 手机端排版适配 — 媒体查询 + 抽屉式侧栏 + 过滤区 wrap
|
2026-06-11 09:28:14 +08:00 |
|
Mavis
|
2e0e5ea80c
|
fix(translate): 调整优先级为 zhipu→spark(智谱第一,星火第二)
|
2026-06-11 09:28:03 +08:00 |
|
Mavis
|
3f183d14db
|
feat(translate): service.py 接 zhipu / config 加 zhipu_* / .env.example 加 ZHIPU 配置
配 zhipu.py(zhipu 模块)一起使用:
- service.py: 新增 _zhipu_translator(),引擎选择链路 spark → zhipu → tencent → maas → agnes → local
- config.py: 新增 zhipu_api_key / zhipu_base_url / zhipu_model / zhipu_interval_sec
- .env.example: 补 ZHIPU_* 字段说明
留空 zhipu_api_key = spark 不可用时直接降级 tencent(向后兼容)
|
2026-06-10 23:48:14 +08:00 |
|
Mavis
|
b6fc1b322f
|
feat(translate): 加智谱 GLM 作为第二序位翻译引擎(spark → zhipu → tencent)
- 新增 app/services/translation/zhipu.py: OpenAI 兼容协议客户端,
URL = https://open.bigmodel.cn/api/paas/v4/chat/completions,
鉴权 = Bearer <api_key>, model = glm-4-flash(默认,免费)/glm-4-air/glm-4.5 等
- service.py 引擎链路调整为: spark → zhipu → tencent(配额)→ maas → agnes → local
- 配置: 新增 ZHIPU_API_KEY / ZHIPU_BASE_URL / ZHIPU_MODEL / ZHIPU_INTERVAL_SEC
(留空 ZHIPU_API_KEY = spark 不可用时直接降级 tencent,向后兼容)
- 实测 GLM-4-Flash 2.3s 返回 OK;GLM-4.7-Flash 当前限流 1305
|
2026-06-10 23:47:55 +08:00 |
|
Mavis
|
a8e93cf7c7
|
fix(translate): pipeline 写库用 service 返回的 engine(之前写死 tencent)
pipeline.translate_article 调 translation_service.translate() 后,
自己写 engine_label='tencent',忽略 service 实际返回的 engine。
效果:即使 service 内部按 spark 优先链路跑成功了,
DB 里 translation_engine 也只显示 tencent。
修法: 改用 tr_title.engine(or 默认 tencent 兜底)。
附带: deploy_pull.py 加 sys.stdout.reconfigure(encoding='utf-8'),
Windows GBK 终端下 Unicode 字符不会再 UnicodeEncodeError。
|
2026-06-10 23:31:03 +08:00 |
|
Mavis
|
b27643123e
|
feat(translate): 加星火 Spark(Lite)作为优先翻译引擎
- 新增 app/services/translation/spark.py: OpenAI 兼容协议客户端,
URL = https://spark-api-open.xf-yun.com/v1/chat/completions,
鉴权 = Bearer <APIPassword>, model = lite(默认)/generalv3.5/4.0Ultra 可切换
- service.py 引擎链路调整为: spark → tencent(配额)→ maas → agnes → local。
优先级降序: spark 配了 key 就用它,失败再走 tencent(继续吃配额,不绕过)。
要完全绕开 tencent,把 TENCENTCLOUD_SECRET_ID 留空即可。
- 配置: 新增 SPARK_API_PASSWORD / SPARK_BASE_URL / SPARK_MODEL / SPARK_INTERVAL_SEC
(留空 SPARK_API_PASSWORD = 走原 tencent 主链路,向后兼容)
- 缓存白名单 / 配额计数逻辑保持原行为,只把 spark 加入允许缓存的引擎集合
|
2026-06-10 23:14:20 +08:00 |
|
Mavis
|
759eefabc3
|
style(web): 改暖色木色调,跟 Android 端对齐
Web 端原本是冷色蓝调(#2080f0 + #fafbfc),
Android 端已经是暖色木色(#8B6B45 + #F5E9D0),两端不一致。
统一到暖色木色调(跟 logo "D" 木质方块的视觉延伸):
style.css 重写:
- CSS 变量: --color-primary #8B6B45, --color-bg #F5E9D0 等
- 字体栈: serif 用于标题(Georgia / Songti), sans-serif 用于正文
- 卡片: 圆角 12px, 边框 + 浅阴影, hover 浮起 1px
- 滚动条: 木色风格
- 按钮 / 分页 / TopBar 主题色统一
Feed.vue:
- 中文标题字体 18px -> 20px, 字重 600 -> 700
- 原标题字号 13, 颜色淡木色
- 插图圆角 4px -> 8px, 高度自适应 max 280px
- 评论钩子: 淡木色背景 + 木色左边框 3px(与 Android 一致)
- 标签全部用 round
ArticleDetail.vue:
- 中文主标题大字号, 衬线粗体
- 原标题灰色辅助
- 操作按钮全部 round
- 卡片标题统一用 serif
Login.vue:
- 登录卡 圆角 16px, 木色渐变背景
- 标题用 serif, 按钮 round
未提交 Android 端改动 — 在 D:/selftools/diary-news-android/ 独立目录,
会重新 build APK 后单独交付。
|
2026-06-10 18:12:05 +08:00 |
|
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 |
|
Mavis
|
764de4e85c
|
fix(worker): enrichment_loop 改并发 + 加大 batch
之前每轮只跑 3 篇串行,587 篇待 enrich 队列要 4.7h 才清完。
改动:
- ENRICHMENT_BATCH_SIZE: 3 -> 8
- ENRICHMENT_INTERVAL_SEC: 5 -> 2
- 处理 todo_ids 改 asyncio.gather 并发 3 篇
- LlmClient 内部 interval_sec 限速不变,这里只加并发上限
效果:每分钟 ~7 篇 -> 587 篇约 84 分钟清完。
排查过程中还发现根因: llm_settings 表空行导致 enrichment 静默跳过,
已手动 INSERT 默认 LlmSetting(id=1, enabled=true) 触发循环。
|
2026-06-10 17:20:53 +08:00 |
|
Mavis
|
02f0260dfc
|
docs(android): 完整方案 + logo 资源 + 启动屏
新增 docs/android/ 目录:
- README.md 总入口(快速上手 + 决策摘要 + 数据流)
- 01-architecture.md 模块划分 + 数据流 + 选型理由
- 02-api-contract.md 每个接口的请求/响应 + DTO 字段映射
- 03-build-run.md Gradle/SDK/网络安全白名单/真机调试
- 04-milestones.md 7 天里程碑 + DoD + E2E 测试场景
新增 assets/:
- logo/: 主图标 master + adaptive icon + 5 DPI launcher (方/圆)
- splash/: 启动屏 logo + 完整背景预览 + 5 DPI 资源
- android_resources/: 集成所需的 XML(adaptive icon/主题/颜色/字符串/drawable/layout)
- INTEGRATION.md 集成指南
- logo.svg + _make_logo.py 设计源
设计风格:参考用户提供的木质方块字母积木图,米色木纹底 +
深棕色字母 D,代表 'Diary',温暖私人日记感。
服务器体检:所有容器/API/DB/翻译主链路正常,TMT 本月已用 0.37%。
MaaS 备用通道上次已验证可用。
|
2026-06-10 14:11:43 +08:00 |
|
Mavis
|
81c83ced8d
|
feat(feed): 列表展示翻译正文摘要 + 页码分页
首页 Feed.vue 改造:
- 卡片在中文标题下直接展示 body_zh_text(前 220 字)
用户不进详情就能看到译文正文,提升阅读效率
- 配图(image_ai_url 或 image_url)也直接显示在卡片中
- 把原标题作为副标题(灰色,辅助参考)
分页从 cursor 无限滚动换成 page + page_size:
- 后端 /articles 加 page/page_size 参数,返回 total/total_pages
- 干掉 _encode_cursor/_decode_cursor
- 前端用 n-pagination,显示 1,2,3,4,5 + 快速跳转
- 筛选/搜索变化自动回到第 1 页
- 切页自动滚到顶部
|
2026-06-10 12:07:04 +08:00 |
|
Mavis
|
3e56fed541
|
feat(translate): 接入腾讯 MaaS u2 作为 TMT 备用翻译通道
新通道:腾讯 MaaS u2 模型(云知声),OpenAI 兼容协议
- 端点:https://maas-api.hivoice.cn/v1
- 模型:u2(翻译专用,实测 + 锁定 prompt 后译文质量稳定)
- 备用链路:TMT 配额耗尽 / TMT 失败时自动降级到 MaaS
关键 prompt 工程(锁定):
- 必须用 user 提供的固定中文 prompt,否则 u2 会把译文放进 reasoning_content 而 content 返乱码
- 限定只接 EN/JA → ZH
- 中文输入固定返回拒绝文案
新增/改动:
- backend/app/services/translation/tencent_maas.py: 新建
- backend/app/services/translation/service.py: 备用链 maas → local,初始化失败友好降级
- backend/app/config.py: 加 tencent_maas_* 4 个配置
- .env.example: 文档化
|
2026-06-09 17:33:45 +08:00 |
|
Mavis
|
a5bfb7d49a
|
fix(worker): enrichment_loop 永远只扫老文章(已 enrich),新文章被排到最后
bug 复现:
- order_by translated_at asc nullslast
- 老文章已 enrich,translated_at 有值,排前
- 新文章 translated_at=NULL,nullslast 排最后,limit5 永远拿不到
修复:
- order_by 改为 Article.id.asc()(新文章 id 大)
- ENRICHMENT_BATCH_SIZE 1→3(并发候选)
- 文章间 sleep 0.5→0.2s
效果:enrichment_loop 现在会持续 enrich 新进文章,首页列表会逐步有分类/评论
|
2026-06-09 17:07:07 +08:00 |
|
Mavis
|
474299baf9
|
feat(feed): 首页列表展示分类标签 + LLM 评论预览
- 后端 ArticleListItem schema 加 commentary / commentary_status / image_ai_url
- 后端 articles.list 接口把以上字段写入响应
- 前端 API 类型同步
- 前端 Feed.vue 卡片:
* 分类 tag(逗号分隔,多 tag)
* 评论预览(蓝色引线块,140 字截断,带状态点)
* 用户点进详情页前就能看到 LLM 点评钩子
|
2026-06-09 15:59:48 +08:00 |
|
Mavis
|
76e95908e8
|
fix(llm): _safe_format 防 ValueError,模板里示例 JSON 也能正常 format
bug: classify_prompt 默认值里含示例 JSON {\\"categories\\": [...]},str.format
看到花括号就试图解析为 placeholder/format spec,遇到 \\" 时抛:
ValueError: Invalid format specifier ' [\\"时政\\"]' for object of type 'str'
修复:
- 引入 placeholder_re 提取所有合法 {varname} 占位符,stash 成 sentinel
- 剩余的 { / } 一律 escape 成 {{ / }},str.format 自然还原
- 用户显式写的 {{ / }}(标准转义)单独 stash,不被重复 escape
- 极端情况(KeyError/IndexError/ValueError)兜底:按原文返回,记录 warn
8 个本地单测全过(含示例 JSON 模板 / 老 prompt 缺变量 / 用户显式 {{ 场景)
|
2026-06-09 15:14:53 +08:00 |
|