mavis
|
db4fd8699b
|
fix(search): ts_stat 改单参(text),避免 'a' mask 静默 0 行
zhparser 不标 A 权重(也不标 B/C/D),传 'a' mask 给 ts_stat(text, weights) 会过滤掉所有词
但不报错,静默 0 行。改成 ts_stat(text) 单参(等价 mask='abcd',聚合所有权重)。
修:
- 0010 迁移里 refresh_search_keywords() 改用单参 ts_stat
- 0010 迁移 downgrade 部分同步修
- 0009 迁移 refresh_search_keywords() 同步修
- services/search.py _fallback_keywords 改用 chinese_zh + 单参 ts_stat
|
2026-06-15 19:19:19 +08:00 |
|
mavis
|
e85a27f69d
|
fix(postgres): zhparser 正确仓库 amutu/zhparser(zhparser/zhparser 404 不存在)
|
2026-06-15 18:59:09 +08:00 |
|
mavis
|
21c0b5559a
|
fix(postgres): 手编 libscws 绕开 automake 不兼容(老项目 Makefile.am 跟新 automake 冲突)
|
2026-06-15 18:57:48 +08:00 |
|
mavis
|
1fe986e96c
|
fix(postgres): scws 需先 ./acprep 生成 configure
|
2026-06-15 18:51:23 +08:00 |
|
mavis
|
96a463858c
|
fix(postgres): 源码编译 scws + zhparser(全链无 apt 包)
|
2026-06-15 18:49:34 +08:00 |
|
mavis
|
363fd42f80
|
fix(postgres): zhparser 从源码编译(无现成 apt 包)
|
2026-06-15 18:48:19 +08:00 |
|
mavis
|
557b7a708e
|
feat(search): 装 zhparser 中文分词 + 0010 迁移修正
- Dockerfile.postgres: 从 alpine 切到 debian bookworm,apt 装 postgresql-16-zhparser
- docker-compose.yml: postgres 改用 build 指向 Dockerfile.postgres
- 0010 迁移: CREATE EXTENSION zhparser + 建 chinese_zh text search config +
重建 articles.title_zh_tsv 用 chinese_zh + 重写 refresh_search_keywords()
|
2026-06-15 18:46:09 +08:00 |
|
mavis
|
2b94be2048
|
chore: add deploy_search_suggestions.sh (端到端部署+测试脚本)
|
2026-06-15 18:30:19 +08:00 |
|
mavis
|
c3aa0f0cb6
|
feat(search): 智能搜索建议 - 固化候选词表 (search_keywords + search_title_suggestions)
后端:
- alembic 0009: 两张固化表 + GIN prefix_keys 索引 + articles trigger
- /api/v1/search/suggestions: 混合 A(高频词 ts_stat) + B(真实标题) + 冷启动 fallback
- worker 每日 03:00 + 启动时刷新 search_keywords
- 顺便填 commit 11 TODO: articles.title_zh_tsv + GIN 索引(未来 FTS 基础)
前端:
- NInput -> NAutoComplete + debounce 250ms
- 选标题 -> 跳详情;选关键词 -> 填入 + 触发搜索
- AbortController 防 race condition
性能: prefix_keys @> ARRAY[prefix] 走 GIN 亚毫秒,100w 行也稳
|
2026-06-15 18:26:35 +08:00 |
|
xiaji
|
b674fb4b22
|
feat(search): 搜索结果关键字高亮(标题/正文/评论)
Feed.vue 搜索 q 时,命中的关键字在卡片标题/正文/双 provider
评论预览里 <mark> 包裹高亮(暖黄底 + 加粗)。
实现:
- 新增 escapeHtml(text) — 防止 XSS(content 来自外部 RSS/ingest,
不可信;先 escape 再 replace,确保 <mark> 之外不会有任何原始
HTML 进入 DOM)
- 新增 highlightHtml(text, q) — 不区分大小写匹配,正则元字符
(.*+?^${}()|[]\\) 自动转义(避免用户搜 "*.x" 时被当 regex)
q 为空时返回纯 escape 文本(行为与原来 {{ }} 插值一致)
- 改造 previewCommentary(text, max, q) — 第三个参数 q 透传
highlightHtml
- 4 处渲染改 {{ }} -> v-html,传 highlightHtml(previewCommentary
(..., q)):
- 中文标题 + 原标题
- 正文摘要
- Angel 评论预览
- 美团评论预览
样式:
- .feed-list :deep(mark) 暖黄底 (#fff3a0) + inherit 父级文字色 +
padding 2px + 加粗
- :deep() 避免 Naive UI 组件 scoped 样式隔离问题
安全:
- 所有用户内容先 escapeHtml,再 replace
- <mark> 标签是 escape 之后才插入,不会引入新的 XSS 通道
- q 特殊字符转义,不构成 regex DoS
不影响:
- q 为空时(highlightHtml(text, '') = escapeHtml(text) 等价于
Vue 原生 {{ }} 自动 escape) - 非搜索场景行为完全不变
- waiting/failed 状态的评论不显示评论内容,不需高亮
- 短新闻正文也支持高亮(q 不空时,完整 5000 字都高亮匹配项)
无后端改动。
|
2026-06-15 07:32:39 +08:00 |
|
xiaji
|
8dfa302b96
|
fix(search): 搜索不限时间,搜全量历史
原 list_articles 默认套 24h 时间过滤(line 47-48):
if since is None and until is None:
since = _default_since_24h()
但用户搜索时(q 不为空)的意图是"找到含某关键字的文章",
跟时间无关;24h 默认会让搜索**找不到 24h 之前的文章**,
即使那篇文章确实包含关键字。
修法:仅在 q 为空时套 24h 默认;有 q 时不套。
显式传 since/until 仍生效(用户要限定时间就显式传)。
不影响:
- 普通 list(无 q)仍默认 24h(Feed 首页行为不变)
- 显式传 since/until 的 list 行为不变
- 搜索响应 schema 不变(前端无改动)
测试:open web -> Feed -> 搜索 "美联储" -> 看到 24h 之前的
相关文章也能命中。
|
2026-06-15 07:25:36 +08:00 |
|
xiaji
|
02d18a1157
|
feat(search): 搜索覆盖标题/正文/评论
原 /api/v1/articles?q=xxx 只搜 title + body_text(外文原文),
导致:
- 搜中文译文里出现的词搜不到
- 搜双 provider 评论里的"主题词"搜不到
- 搜摘要搜不到
扩展为搜索 7 个字段(命中任一即可):
- title (原标题,外文)
- body_text (原文,外文)
- title_zh (中文标题)
- body_zh_text (中文译文,纯文本)
- summary_zh (中文摘要)
- commentary (Angel 评论)
- commentary_meituan (美团评论)
不去搜 body_zh_formatted(HTML 包了 <p class="diary-para">),
避免 ilike 跟 HTML 标签字符串误匹配,纯文本走 body_zh_text 即可。
性能:
- ilike '%xxx%' 走不了 B-tree 索引,PG 会 seq scan
- 当前文章量级别(几千篇)下完全 OK,延迟 < 50ms
- 未来 10w+ 文章量时改 PG full-text search(to_tsvector + GIN)
或外部 ES;现阶段不做
无 schema/migration 改动;无前端改动(后端响应 schema 没变)。
|
2026-06-15 07:20:10 +08:00 |
|
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 |
|