feat(web): ArticleDetail 三段式(评论/译文/原文) + LLM 屏蔽词配置 + .diary-para 兜底

- types: Source / LlmSetting 加 blocklist_tags 字段
- AdminLlmSettings:
  - 新增 '全局屏蔽分类(命中即删文章)' 卡片(逗号/换行分隔,双向绑 blocklist_tags)
  - 分类 prompt 提示加 {blocklist} / drop 字段说明
- ArticleDetail 三段式:
  - 顶部:评论(LLM 点评)
  - 中部:文章译文(优先 LLM 排版版 / fallback 原始译文)
  - 底部:文章原文
  - AI 插图挂在译文卡片下作附属
- style.css: .diary-para 兜底规则(margin 0 0 1.5em 0 / line-height 1.7 / color #3e3e3e)
This commit is contained in:
Mavis
2026-06-09 14:38:29 +08:00
parent 728e8c9be3
commit 8dbc7c4ab2
4 changed files with 76 additions and 21 deletions

View File

@@ -15,6 +15,8 @@ export interface Source {
last_fetched_at?: string | null last_fetched_at?: string | null
last_status?: string | null last_status?: string | null
consecutive_failures: number consecutive_failures: number
// 源级屏蔽分类标签;与 llm_settings.blocklist_tags 合并后注入 classify prompt
blocklist_tags?: string[]
} }
export interface ArticleListItem { export interface ArticleListItem {
@@ -71,6 +73,8 @@ export interface LlmSetting {
image_model: string image_model: string
interval_sec: number interval_sec: number
enabled: boolean enabled: boolean
// 全局屏蔽分类标签;与 sources.blocklist_tags 合并后注入 classify prompt
blocklist_tags?: string[]
updated_at?: string | null updated_at?: string | null
} }

View File

@@ -45,3 +45,16 @@ img { max-width: 100%; }
.article-body p:last-child { .article-body p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
/* === 排版段落(.diary-para)兜底规则 ===
* 后端 enrichment._wrap_article_body 会内联 style 到 <p class="diary-para">;
* 这里做兜底,保证 v-html 渲染时一定有合理的行距和段距。
*/
.diary-para {
margin: 0 0 1.5em 0;
line-height: 1.7;
color: #3e3e3e;
}
.diary-para:last-child {
margin-bottom: 0;
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, watch } from 'vue' import { onMounted, ref, watch, computed } from 'vue'
import { import {
NCard, NSpace, NButton, NInput, NInputNumber, NSwitch, NAlert, useMessage, NSpin, NDivider, NText, NCode, NCard, NSpace, NButton, NInput, NInputNumber, NSwitch, NAlert, useMessage, NSpin, NDivider, NText, NCode,
} from 'naive-ui' } from 'naive-ui'
@@ -19,6 +19,19 @@ const setting = ref<LlmSetting>({
image_model: 'agnes-image-2.1-flash', image_model: 'agnes-image-2.1-flash',
interval_sec: 2.0, interval_sec: 2.0,
enabled: true, enabled: true,
blocklist_tags: [],
})
// === 屏蔽分类标签(文本框 ↔ 数组) ===
// UI 用逗号分隔,存到 setting.blocklist_tags(数组)
const blocklistText = computed({
get: () => (setting.value.blocklist_tags || []).join(' / '),
set: (v: string) => {
setting.value.blocklist_tags = v
.split(/[,,;\n]/)
.map((s) => s.trim())
.filter(Boolean)
},
}) })
const testResult = ref<{ ok: boolean; detail: string; configured: boolean } | null>(null) const testResult = ref<{ ok: boolean; detail: string; configured: boolean } | null>(null)
@@ -141,8 +154,8 @@ onMounted(load)
<NCard title="分类提示词" style="margin-top: 16px"> <NCard title="分类提示词" style="margin-top: 16px">
<NText depth="3" style="font-size: 12px"> <NText depth="3" style="font-size: 12px">
模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{summary}</NCode> = 摘要, <NCode>{body}</NCode> = 正文(节选)<br /> 模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{summary}</NCode> = 摘要, <NCode>{body}</NCode> = 正文(节选), <NCode>{blocklist}</NCode> = 屏蔽词列表<br />
期望返回 JSON(多标签,2-5 ),形如 <NCode>{`{"categories": ["时政", "国际", "经济"]}`}</NCode> 期望返回 JSON,形如 <NCode>{`{"categories": ["时政", "国际", "经济"], "drop": false}`}</NCode>;若新闻属于屏蔽词之一,务必将 <NCode>drop</NCode> 设为 true
</NText> </NText>
<NInput <NInput
v-model:value="setting.classify_prompt" v-model:value="setting.classify_prompt"
@@ -153,6 +166,24 @@ onMounted(load)
/> />
</NCard> </NCard>
<NCard title="全局屏蔽分类(命中即删文章)" style="margin-top: 16px">
<NText depth="3" style="font-size: 12px">
逗号或换行分隔,与各源级 <NCode>blocklist_tags</NCode> 合并去重后注入 classify prompt <NCode>{blocklist}</NCode> 变量<br />
匹配规则:LLM 返回的 categories 中任一项等于屏蔽词 整篇文章删除(且不再做后续 排版 / 插图 / 点评)<br />
: <NCode>体育 / 娱乐 / 游戏</NCode> 即可屏蔽这三类文章
</NText>
<NInput
v-model:value="blocklistText"
type="textarea"
:autosize="{ minRows: 2, maxRows: 6 }"
placeholder="体育 / 娱乐 / 游戏"
style="margin-top: 8px"
/>
<NText v-if="(setting.blocklist_tags || []).length === 0" depth="3" style="font-size: 12px; margin-top: 4px; display: block">
留空 = 不做屏蔽
</NText>
</NCard>
<NCard title="点评提示词" style="margin-top: 16px"> <NCard title="点评提示词" style="margin-top: 16px">
<NText depth="3" style="font-size: 12px"> <NText depth="3" style="font-size: 12px">
模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{body}</NCode> = 译文正文 模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{body}</NCode> = 译文正文

View File

@@ -156,25 +156,31 @@ onMounted(load)
本条翻译失败,可点 "重译" 重试,或查看后端日志 本条翻译失败,可点 "重译" 重试,或查看后端日志
</NAlert> </NAlert>
<div v-if="article.image_ai_url" style="margin-top: 16px"> <!-- 三段式:评论(顶部) / 译文() / 原文() -->
<NCard title="🎨 AI 插图">
<NImage :src="article.image_ai_url" object-fit="cover" style="max-width: 100%; border-radius: 6px" />
</NCard>
</div>
<!-- 1) 评论(LLM 点评) -->
<NCard v-if="article.commentary" style="margin-top: 16px" title="💬 评论">
<template #header-extra>
<NTag size="tiny" :type="statusTagType(article.commentary_status)">{{ article.commentary_status || 'n/a' }}</NTag>
</template>
<p style="white-space: pre-wrap; line-height: 1.8; margin: 0">{{ article.commentary }}</p>
</NCard>
<!-- 2) 译文(优先 LLM 排版版,fallback 原始译文) -->
<div v-if="showTranslation" style="margin-top: 16px"> <div v-if="showTranslation" style="margin-top: 16px">
<NCard v-if="article.body_zh_formatted" title="译文(LLM 排版版)"> <NCard v-if="article.body_zh_formatted" title="📖 文章译文">
<template #header-extra> <template #header-extra>
<NSpace align="center"> <NSpace align="center">
<NTag size="tiny" :type="statusTagType(article.format_status)">{{ article.format_status || 'n/a' }}</NTag> <NTag size="tiny" :type="statusTagType(article.format_status)">排版:{{ article.format_status || 'n/a' }}</NTag>
<NButton text size="tiny" @click="showFormatted = !showFormatted"> <NButton text size="tiny" @click="showFormatted = !showFormatted">
{{ showFormatted ? '隐藏' : '显示' }} {{ showFormatted ? '隐藏排版' : '显示排版' }}
</NButton> </NButton>
</NSpace> </NSpace>
</template> </template>
<div v-if="showFormatted" v-html="article.body_zh_formatted" style="line-height: 1.8" /> <div v-if="showFormatted" v-html="article.body_zh_formatted" />
<NText v-else depth="3" style="font-size: 12px">已隐藏排版版(点击右上角显示)</NText>
</NCard> </NCard>
<NCard v-else title="译文(原始)" style="margin-top: 16px"> <NCard v-else title="📖 文章译文(原始)" style="margin-top: 16px">
<div v-if="article.body_zh_html" v-html="article.body_zh_html" style="line-height: 1.8" /> <div v-if="article.body_zh_html" v-html="article.body_zh_html" style="line-height: 1.8" />
<div v-else-if="article.body_zh_text" style="white-space: pre-wrap; line-height: 1.8"> <div v-else-if="article.body_zh_text" style="white-space: pre-wrap; line-height: 1.8">
{{ article.body_zh_text }} {{ article.body_zh_text }}
@@ -183,20 +189,21 @@ onMounted(load)
</NCard> </NCard>
</div> </div>
<!-- AI 插图(挂在译文卡片下,作附属) -->
<div v-if="article.image_ai_url" style="margin-top: 16px">
<NCard title="🎨 AI 插图">
<NImage :src="article.image_ai_url" object-fit="cover" style="max-width: 100%; border-radius: 6px" />
</NCard>
</div>
<!-- 3) 原文 -->
<div v-if="showOriginal" style="margin-top: 16px"> <div v-if="showOriginal" style="margin-top: 16px">
<NCard title="原文"> <NCard title="📄 文章原文">
<div v-if="article.body_html" v-html="article.body_html" style="line-height: 1.8" /> <div v-if="article.body_html" v-html="article.body_html" style="line-height: 1.8" />
<div v-else style="white-space: pre-wrap; line-height: 1.8">{{ article.body_text }}</div> <div v-else style="white-space: pre-wrap; line-height: 1.8">{{ article.body_text }}</div>
</NCard> </NCard>
</div> </div>
<NCard v-if="article.commentary" style="margin-top: 16px" title="💬 AI 点评">
<template #header-extra>
<NTag size="tiny" :type="statusTagType(article.commentary_status)">{{ article.commentary_status || 'n/a' }}</NTag>
</template>
<p style="white-space: pre-wrap; line-height: 1.8; margin: 0">{{ article.commentary }}</p>
</NCard>
<NCard v-if="article.entities" style="margin-top: 16px" title="🔍 实体(预留)"> <NCard v-if="article.entities" style="margin-top: 16px" title="🔍 实体(预留)">
<code style="font-size: 12px">{{ JSON.stringify(article.entities) }}</code> <code style="font-size: 12px">{{ JSON.stringify(article.entities) }}</code>
</NCard> </NCard>