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 端点
This commit is contained in:
xiaji
2026-06-14 16:15:21 +08:00
parent 07534eb144
commit e274246056
10 changed files with 677 additions and 51 deletions

View File

@@ -156,9 +156,13 @@ function commentaryState(status?: string | null, content?: string | null): Comme
return 'waiting'
}
// 正文摘要(取 body_zh_text 前 N 字;没有就 fallback 到 summary_zh)
function bodyExcerpt(text?: string | null, max = 200): string {
// 正文摘要:长新闻截前 200 字(把多空白合并),短新闻保留原始换行不截取
function bodyExcerpt(text?: string | null, max = 200, keepNewlines = false): string {
if (!text) return ''
if (keepNewlines) {
// 短新闻:不去空白,保留 \n 让前端 white-space: pre-wrap 换行
return text.length > max ? text.slice(0, max) + '…' : text
}
const trimmed = text.replace(/\s+/g, ' ').trim()
return trimmed.length > max ? trimmed.slice(0, max) + '…' : trimmed
}
@@ -209,7 +213,10 @@ onMounted(async () => {
v-for="a in items"
:key="a.id"
class="article-card"
:class="{ 'article-card-read': a.is_read }"
:class="{
'article-card-read': a.is_read,
'short-card': a.is_short_news,
}"
hoverable
@click="open(a)"
>
@@ -235,6 +242,17 @@ onMounted(async () => {
>
{{ c }}
</NTag>
<!-- 短新闻(API Push)角标:固定显示 -->
<NTag
v-if="a.is_short_news"
size="tiny"
type="info"
:bordered="false"
round
class="feed-short-tag"
>
📰 短讯
</NTag>
<!-- 已读/未读小标签 -->
<NTag
v-if="a.is_read"
@@ -273,9 +291,9 @@ onMounted(async () => {
{{ a.title }}
</div>
<!-- AI 插图(若有) -->
<!-- AI 插图(若有;短新闻不显示) -->
<img
v-if="a.image_ai_url || a.image_url"
v-if="!a.is_short_news && (a.image_ai_url || a.image_url)"
:src="a.image_ai_url || a.image_url || ''"
style="
display: block;
@@ -290,9 +308,14 @@ onMounted(async () => {
loading="lazy"
/>
<!-- 翻译后正文摘要 -->
<!--
正文摘要:
- 长新闻:body_zh_text 截前 200 (去多余空白)
- 短新闻:body_zh_text(=body_text)完整展示,保留换行
-->
<div
v-if="a.body_zh_text || a.summary_zh"
:class="{ 'short-body': a.is_short_news }"
style="
margin-top: 4px;
color: var(--color-letter);
@@ -300,7 +323,11 @@ onMounted(async () => {
line-height: 1.75;
"
>
{{ bodyExcerpt(a.body_zh_text || a.summary_zh, 220) }}
{{
a.is_short_news
? bodyExcerpt(a.body_zh_text || a.summary_zh || '', 5000, true)
: bodyExcerpt(a.body_zh_text || a.summary_zh, 200)
}}
</div>
<!-- 评论钩子( provider:Angel + 美团,三态显式显示:有内容 / 等待中 / 失败) -->
@@ -590,6 +617,36 @@ onMounted(async () => {
color: var(--color-text-faint);
}
/* === 短新闻(API Push)卡片差异化 ===
长新闻:卡片色调不变
短新闻:淡蓝底 + 左侧 3px 蓝色竖线条,便于一眼区分
*/
.article-card.short-card {
background: #f6f9fc;
border-left: 3px solid #4f9eff;
}
.article-card.short-card:hover {
background: #eef4fb;
}
/* 已读 + 短新闻:已读底色优先,左边色条仍保留作为"短讯"标识 */
.article-card.short-card.article-card-read {
background: #fafafa;
border-left-color: #4f9eff;
border-left-width: 3px;
}
/* 短新闻正文:保留换行 */
.short-body {
white-space: pre-wrap;
font-size: 13.5px;
line-height: 1.65;
}
/* 短讯角标 */
.feed-short-tag {
font-size: 11px;
}
/* === 底部操作栏(浮在卡片右下角)=== */
.feed-actions {
display: flex;