Files
diary-news/frontend/src/views/AdminLlmSettings.vue
Mavis 380e8b124e feat(llm): 排版容器固定CSS + 插图用正文第一段 + 适中尺寸
- enrichment._enrich_format:把排版好的段落包到带固定 CSS 的 <div class=article-body> 里
  (font: system-ui / 17px / line-height 1.7 / color #3e3e3e / p margin-bottom 1.5em)
  CSS 同时内联到 style 属性,前端 .article-body 全局类做兑底
- enrichment._enrich_image:prompt 改用 body_zh_text 的第一段(原为 title);
  新增 {body} 占位符,image_prompt_template 默认模板同步改写
- 插图尺寸写死为 768x512(适中);image_size 字段保留供用户手改但默认行为不依赖它
- 分类明确多标签(2-5 个),提示词加 {body} 变量,容错读 categories/tags 两种 key
- AdminLlmSettings.vue:placeholder / 变量说明同步更新
2026-06-08 20:53:21 +08:00

185 lines
6.3 KiB
Vue

<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import {
NCard, NSpace, NButton, NInput, NInputNumber, NSwitch, NAlert, useMessage, NSpin, NDivider, NText, NCode,
} from 'naive-ui'
import { adminApi, type LlmSetting } from '@/api/articles'
const message = useMessage()
const loading = ref(false)
const saving = ref(false)
const testing = ref(false)
const setting = ref<LlmSetting>({
format_prompt: '',
classify_prompt: '',
commentary_prompt: '',
image_prompt_template: '',
image_size: '768x512',
chat_model: 'agnes-2.0-flash',
image_model: 'agnes-image-2.1-flash',
interval_sec: 2.0,
enabled: true,
})
const testResult = ref<{ ok: boolean; detail: string; configured: boolean } | null>(null)
async function load() {
loading.value = true
try {
setting.value = await adminApi.getLlmSettings()
} catch (e: any) {
message.error(e?.response?.data?.title || '加载失败')
} finally {
loading.value = false
}
}
async function save() {
saving.value = true
try {
const updated = await adminApi.updateLlmSettings(setting.value)
setting.value = updated
message.success('已保存')
} catch (e: any) {
message.error(e?.response?.data?.title || '保存失败')
} finally {
saving.value = false
}
}
async function reset() {
if (!confirm('恢复所有提示词为默认值?(模型名 / 限速 / enabled 保留)')) return
try {
const out = await adminApi.resetLlmSettings()
message.success(out.detail || '已重置')
await load()
} catch (e: any) {
message.error(e?.response?.data?.title || '重置失败')
}
}
async function test() {
testing.value = true
testResult.value = null
try {
testResult.value = await adminApi.testLlmConnection()
if (testResult.value.ok) message.success('连接 OK')
else message.warning('连接失败')
} catch (e: any) {
testResult.value = { ok: false, detail: e?.message || '请求失败', configured: true }
message.error('测试失败')
} finally {
testing.value = false
}
}
onMounted(load)
</script>
<template>
<NSpace vertical>
<NSpace justify="space-between" align="center">
<div>
<h2 style="margin: 0">🤖 LLM 智能增强设置</h2>
<NText depth="3" style="font-size: 12px">
翻译完成后,自动调用 Agnes LLM 4 项任务:排版 / 分类 / 插图 / 点评
</NText>
</div>
<NSpace>
<NButton :loading="testing" @click="test">测连接</NButton>
<NButton @click="reset">重置默认提示词</NButton>
<NButton type="primary" :loading="saving" :disabled="loading" @click="save">保存</NButton>
</NSpace>
</NSpace>
<NAlert v-if="testResult" :type="testResult.ok ? 'success' : 'warning'" :show-icon="false">
<NText strong>测连接结果:</NText>
<NText>{{ testResult.detail }}</NText>
</NAlert>
<NSpin :show="loading">
<NCard title="总开关 + 模型">
<NSpace vertical>
<NSpace align="center">
<NText>启用 LLM 增强:</NText>
<NSwitch v-model:value="setting.enabled" />
<NText v-if="!setting.enabled" depth="3" style="font-size: 12px">(关闭后翻译后不再调 LLM)</NText>
</NSpace>
<NSpace>
<NText>文生文模型:</NText>
<NInput v-model:value="setting.chat_model" placeholder="agnes-2.0-flash" style="width: 240px" />
</NSpace>
<NSpace>
<NText>文生图模型:</NText>
<NInput v-model:value="setting.image_model" placeholder="agnes-image-2.1-flash" style="width: 240px" />
</NSpace>
<NSpace>
<NText>插图尺寸:</NText>
<NInput v-model:value="setting.image_size" placeholder="768x512" style="width: 160px" />
<NText depth="3" style="font-size: 12px">(格式: WIDTHxHEIGHT, 768x512;后端默认固定用 768x512)</NText>
</NSpace>
<NSpace>
<NText>LLM 调用间隔():</NText>
<NInputNumber v-model:value="setting.interval_sec" :min="0" :max="60" :step="0.5" />
<NText depth="3" style="font-size: 12px">(chat + image 1 个串行,每次调用后等这么久)</NText>
</NSpace>
</NSpace>
</NCard>
<NCard title="排版提示词" style="margin-top: 16px">
<NText depth="3" style="font-size: 12px">
模板变量: <NCode>{body}</NCode> = 译文正文
</NText>
<NInput
v-model:value="setting.format_prompt"
type="textarea"
:autosize="{ minRows: 6, maxRows: 20 }"
placeholder="留空用默认"
style="margin-top: 8px"
/>
</NCard>
<NCard title="分类提示词" style="margin-top: 16px">
<NText depth="3" style="font-size: 12px">
模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{summary}</NCode> = 摘要, <NCode>{body}</NCode> = 正文(节选)<br />
期望返回 JSON(多标签,2-5 ),形如 <NCode>{`{"categories": ["时政", "国际", "经济"]}`}</NCode>
</NText>
<NInput
v-model:value="setting.classify_prompt"
type="textarea"
:autosize="{ minRows: 4, maxRows: 12 }"
placeholder="留空用默认"
style="margin-top: 8px"
/>
</NCard>
<NCard title="点评提示词" style="margin-top: 16px">
<NText depth="3" style="font-size: 12px">
模板变量: <NCode>{title}</NCode> = 译后标题, <NCode>{body}</NCode> = 译文正文
</NText>
<NInput
v-model:value="setting.commentary_prompt"
type="textarea"
:autosize="{ minRows: 4, maxRows: 12 }"
placeholder="留空用默认"
style="margin-top: 8px"
/>
</NCard>
<NCard title="插图 prompt 模板" style="margin-top: 16px">
<NText depth="3" style="font-size: 12px">
模板变量: <NCode>{body}</NCode> = 译文正文第一段(主要), <NCode>{title}</NCode> = 标题(fallback)<br />
最终 prompt 会拼成英文描述发给文生图模型
</NText>
<NInput
v-model:value="setting.image_prompt_template"
type="textarea"
:autosize="{ minRows: 3, maxRows: 8 }"
placeholder="留空用默认"
style="margin-top: 8px"
/>
</NCard>
</NSpin>
</NSpace>
</template>