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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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> = 译文正文
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user