xiaji
|
26f5a16530
|
feat(feed): 按分类批量已读 + 标已读后弹出分类提示条
后端:
- POST /me/reads/by-category:按 category + time window 批量标已读
- GET /me/reads/category-count:查多个分类的 24h 未读数
前端:
- 标已读后,查该文章前 2 个 category 的 24h 未读数
- 每个有未读的 category 显示一行提示(独立全部已读/稍后再说按钮)
- 全部已读走乐观更新,命中的 article 走累计 delay 滑出
- 过滤变化时清掉提示(上下文变了)
- 8 秒自动消失
|
2026-06-15 20:36:06 +08:00 |
|
mavis
|
85c05c19a7
|
refactor(search): 只展示 keyword 续接词,去掉 titles 段
产品决定:搜索建议只展示 ts_stat 高频词续接(如'美'→美国/美军/美国政府),
不要真实文章 id 提示(用户认为这种'文章#566871'是噪音,没连续性)。
改动:
- SearchSuggestionsResponse 去 title,只剩 query + keywords
- SearchService 只查 search_keywords,fallback 路径也只针对 keywords
- Feed.vue: 删掉 suggestTitles 状态 + SuggestTitleOption 类型联合,
renderSuggestion 简化成 '词' 标签 + 词文本 + 右侧 weight 数字
- 0011 迁移: 删 search_title_suggestions 表 + 3 索引 + trigger + 函数
(trigger 在每篇文章 INSERT/UPDATE 都会跑,删了能省掉无用性能损耗)
- 删除: app/models/search_title_suggestion.py + backfill_search_suggestions.py
替换成: app/scripts/refresh_search_keywords.py(只跑一次词频刷新)
|
2026-06-15 19:37:40 +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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
728e8c9be3
|
feat(api): LlmSetting/Source 暴露 blocklist_tags,admin 编辑入口就绪
- schemas/source.SourceOut/In/Update 加 blocklist_tags
- admin.create_source 透传 blocklist_tags
- admin.update_source 走 setattr 通用,自动支持新字段
- admin_llm.get_settings 在 row=None 分支返回默认值时补 blocklist_tags=[]
- update_settings 走 setattr 通用,自动支持新字段
|
2026-06-09 14:35:54 +08:00 |
|
Mavis
|
ba2298da0a
|
chore: 集成 LLM 增强 — config/main/articles schema/workers + .env.example 加 Agnes 配置
|
2026-06-08 14:24:23 +08:00 |
|
Mavis
|
ffd667f0dc
|
feat(llm): 新增 LLM 智能增强服务(Agnes client + 4 项 enrichment 任务 + admin API + migration)
|
2026-06-08 14:24:00 +08:00 |
|
Mavis
|
e79cfaa5f7
|
fix: articles.py get_article 链式 await coroutine 报错(.first())
|
2026-06-08 00:19:03 +08:00 |
|
Mavis
|
ce903ac58e
|
fix: 修剩余的 (await ...)).scalar_one_or_none() 链式 + bookmark
|
2026-06-07 23:25:53 +08:00 |
|
Mavis
|
5109d6f824
|
fix: API 全部改用显式两步走 await session.execute + result.scalars()
之前 (await ...).scalars() 链式在 SQLAlchemy 2.0 async 下报
'coroutine' has no attribute 'scalars' 错误。改为先 await 拿 result
再 .scalars(),这是 SQLAlchemy 2.0 推荐的 async 写法。
|
2026-06-07 23:22:56 +08:00 |
|
Mavis
|
60b062daf2
|
feat: initial MVP - FastAPI backend + Vue3 frontend + docker-compose
- backend: FastAPI + SQLAlchemy 2.0(async) + asyncpg + Alembic
- 7 API routes: auth/me/articles/sources/bookmarks/subscriptions/admin
- models: User/Source/Article/Bookmark/Subscription/ApiToken
- services: RSS fetcher (feedparser) + Tencent TMT translator with quota + cache + local NLLB fallback
- workers: APScheduler + asyncio pipeline (fetch -> dedupe -> insert -> translate)
- seed scripts: create_user, seed_sources (5 RSS: Reuters/BBC/Al Jazeera/NHK/DW)
- frontend: Vue 3 + Vite + Naive UI + Pinia + vue-router
- pages: Login, Feed (24h), ArticleDetail, Sources, Bookmarks, AdminSources
- deploy: docker-compose (postgres/redis/api/worker/frontend/caddy)
- docs: README, DEPLOY, architecture, acceptance
|
2026-06-07 21:51:01 +08:00 |
|