diff --git a/frontend/src/api/articles.ts b/frontend/src/api/articles.ts index 617f44e..47a35f9 100644 --- a/frontend/src/api/articles.ts +++ b/frontend/src/api/articles.ts @@ -44,15 +44,36 @@ export interface ArticleDetail extends ArticleListItem { body_text: string body_zh_html?: string | null body_zh_text?: string | null + body_zh_formatted?: string | null // LLM 排版后 HTML author?: string | null + image_ai_url?: string | null // LLM 生成的插图 translation_engine?: string | null translated_at?: string | null + // === LLM 增强状态 === + format_status?: string | null + classify_status?: string | null + image_ai_status?: string | null + commentary_status?: string | null + // === LLM 内容 === commentary?: string | null entities?: Record | null sentiment?: number | null duplicate_of?: number | null } +export interface LlmSetting { + format_prompt?: string | null + classify_prompt?: string | null + commentary_prompt?: string | null + image_prompt_template?: string | null + image_size: string + chat_model: string + image_model: string + interval_sec: number + enabled: boolean + updated_at?: string | null +} + export const articlesApi = { list(params: Record = {}) { return http.get('/articles', { params }).then((r) => r.data) @@ -108,4 +129,24 @@ export const adminApi = { health() { return http.get('/admin/health').then((r) => r.data) }, + // === LLM 设置 === + getLlmSettings() { + return http.get('/admin/llm/settings').then((r) => r.data) + }, + updateLlmSettings(body: Partial) { + return http.put('/admin/llm/settings', body).then((r) => r.data) + }, + resetLlmSettings() { + return http.post<{ reset: boolean; detail: string }>('/admin/llm/settings/reset').then((r) => r.data) + }, + testLlmConnection() { + return http.post<{ ok: boolean; detail: string; configured: boolean }>( + '/admin/llm/settings/test' + ).then((r) => r.data) + }, + triggerEnrich(articleId: number) { + return http.post<{ triggered: boolean; detail: string; results: Record | null }>( + `/admin/llm/enrich/${articleId}` + ).then((r) => r.data) + }, } diff --git a/frontend/src/components/AppLayout.vue b/frontend/src/components/AppLayout.vue index 3ad8a05..137342c 100644 --- a/frontend/src/components/AppLayout.vue +++ b/frontend/src/components/AppLayout.vue @@ -20,7 +20,10 @@ const menu = computed(() => [ { key: '/', label: '24h 列表', icon: () => '📰' }, { key: '/sources', label: '采集源', icon: () => '📡' }, { key: '/bookmarks', label: '收藏', icon: () => '⭐' }, - ...(auth.isOwner ? [{ key: '/admin/sources', label: '源管理(Admin)', icon: () => '🛠' }] : []), + ...(auth.isOwner ? [ + { key: '/admin/sources', label: '源管理(Admin)', icon: () => '🛠' }, + { key: '/admin/llm', label: 'LLM 智能增强', icon: () => '🤖' }, + ] : []), ]) const userMenu = computed(() => [ diff --git a/frontend/src/router.ts b/frontend/src/router.ts index 696dcc1..f1481da 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -13,6 +13,7 @@ const routes: RouteRecordRaw[] = [ { path: 'sources', component: () => import('@/views/Sources.vue') }, { path: 'bookmarks', component: () => import('@/views/Bookmarks.vue') }, { path: 'admin/sources', component: () => import('@/views/AdminSources.vue'), meta: { ownerOnly: true } }, + { path: 'admin/llm', component: () => import('@/views/AdminLlmSettings.vue'), meta: { ownerOnly: true } }, ], }, ] diff --git a/frontend/src/views/AdminLlmSettings.vue b/frontend/src/views/AdminLlmSettings.vue new file mode 100644 index 0000000..7f8eb70 --- /dev/null +++ b/frontend/src/views/AdminLlmSettings.vue @@ -0,0 +1,183 @@ + + + diff --git a/frontend/src/views/ArticleDetail.vue b/frontend/src/views/ArticleDetail.vue index 73f0c18..7720ed8 100644 --- a/frontend/src/views/ArticleDetail.vue +++ b/frontend/src/views/ArticleDetail.vue @@ -2,9 +2,9 @@ import { onMounted, ref, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { - NCard, NSpace, NTag, NText, NButton, NSpin, NEmpty, NDivider, NAlert, NSkeleton, useMessage, + NCard, NSpace, NTag, NText, NButton, NSpin, NEmpty, NDivider, NAlert, NSkeleton, NImage, useMessage, } from 'naive-ui' -import { articlesApi, type ArticleDetail } from '@/api/articles' +import { articlesApi, adminApi, type ArticleDetail } from '@/api/articles' import { http } from '@/api/client' import { useAuthStore } from '@/stores/auth' import dayjs from 'dayjs' @@ -21,6 +21,8 @@ const loading = ref(true) const starred = ref(false) const showOriginal = ref(true) const showTranslation = ref(true) +const showFormatted = ref(true) +const enriching = ref(false) async function load() { loading.value = true @@ -60,6 +62,7 @@ function fmtTime(s?: string | null) { const publishedAt = computed(() => article.value?.published_at || article.value?.fetched_at) const isOwner = computed(() => auth.isOwner) +const categories = computed(() => (article.value?.category || '').split(',').filter(Boolean)) async function rerunTranslation() { if (!article.value) return @@ -72,6 +75,30 @@ async function rerunTranslation() { } } +async function triggerEnrich() { + if (!article.value) return + enriching.value = true + try { + const out = await adminApi.triggerEnrich(article.value.id) + const r = out.results || {} + const ok = Object.values(r).filter((v) => v === 'ok').length + const fail = Object.values(r).filter((v) => v.startsWith('failed')).length + message.info(`完成 ${ok} 项,失败 ${fail} 项`) + await load() + } catch (e: any) { + message.error(e?.response?.data?.title || '触发失败') + } finally { + enriching.value = false + } +} + +function statusTagType(s?: string | null): 'success' | 'warning' | 'error' | 'default' { + if (s === 'ok') return 'success' + if (s === 'failed') return 'error' + if (s === 'pending') return 'warning' + return 'default' +} + onMounted(load) @@ -93,6 +120,7 @@ onMounted(load) {{ article.translation_engine }} + {{ c }}

{{ article.title }}

{{ article.title_zh }}

@@ -109,8 +137,18 @@ onMounted(load) 重译 + + 跑 LLM 增强 + 原文链接 ↗ + + LLM 状态: + 排版:{{ article.format_status || 'n/a' }} + 分类:{{ article.classify_status || 'n/a' }} + 插图:{{ article.image_ai_status || 'n/a' }} + 点评:{{ article.commentary_status || 'n/a' }} + @@ -118,15 +156,25 @@ onMounted(load) 本条翻译失败,可点 "重译" 重试,或查看后端日志。 -
- -
-
{{ article.body_text }}
+
+ +
- + + +
+ +
{{ article.body_zh_text }} @@ -135,15 +183,22 @@ onMounted(load)
- -
- 点评: - {{ article.commentary }} -
-
- 实体: - {{ JSON.stringify(article.entities) }} -
+
+ +
+
{{ article.body_text }}
+ +
+ + + +

{{ article.commentary }}

+
+ + + {{ JSON.stringify(article.entities) }}