feat: 系统提示词预设功能优化

This commit is contained in:
digua
2026-01-14 00:05:38 +08:00
parent 0bb245d679
commit 1fb456e122
3 changed files with 266 additions and 54 deletions
+15 -4
View File
@@ -57,7 +57,7 @@ export function registerWindowHandlers(ctx: IpcContext): void {
ipcMain.on('window:setThemeSource', (_, mode: 'system' | 'light' | 'dark') => {
const { nativeTheme } = require('electron')
nativeTheme.themeSource = mode
// Windows 上动态更新图标颜色以匹配主题
if (process.platform === 'win32' && win) {
const isDark = nativeTheme.shouldUseDarkColors
@@ -74,12 +74,23 @@ export function registerWindowHandlers(ctx: IpcContext): void {
return app.getVersion()
})
// 获取动态文案配置
// 获取远程配置(支持 JSON 和纯文本/Markdown
ipcMain.handle('app:fetchRemoteConfig', async (_, url: string) => {
try {
const response = await fetch(url)
const data = await response.json()
return { success: true, data }
const contentType = response.headers.get('content-type') || ''
// 根据 Content-Type 或 URL 后缀决定解析方式
const isJson = contentType.includes('application/json') || url.endsWith('.json')
if (isJson) {
const data = await response.json()
return { success: true, data }
} else {
// 纯文本/Markdown 等其他格式
const data = await response.text()
return { success: true, data }
}
} catch (error) {
return { success: false, error: String(error) }
}
@@ -22,6 +22,14 @@ const isLoading = ref(false)
const error = ref('')
const remotePresets = ref<RemotePresetData[]>([])
// 预览状态
const previewPreset = ref<RemotePresetData | null>(null)
const isPreviewLoading = ref(false)
const previewError = ref('')
// 添加状态(跟踪正在添加的预设 ID)
const addingPresetId = ref<string | null>(null)
// 预设分组配置
const presetGroups = computed(() => [
{
@@ -47,7 +55,7 @@ const presetGroups = computed(() => [
},
])
// 加载远程预设
// 加载远程预设索引(不下载内容)
async function loadRemotePresets() {
isLoading.value = true
error.value = ''
@@ -66,17 +74,78 @@ async function loadRemotePresets() {
}
}
// 添加预设
function handleAddPreset(preset: RemotePresetData) {
const success = promptStore.addRemotePreset(preset)
// 预览预设(按需下载内容)
async function handlePreview(preset: RemotePresetData) {
previewError.value = ''
// 如果已有内容,直接显示
if (preset.roleDefinition && preset.responseRules) {
previewPreset.value = preset
return
}
isPreviewLoading.value = true
previewPreset.value = preset // 先显示基本信息
const fullPreset = await promptStore.fetchPresetContent(preset)
if (fullPreset) {
previewPreset.value = fullPreset
// 更新列表中的缓存
const index = remotePresets.value.findIndex((p) => p.id === preset.id)
if (index !== -1) {
remotePresets.value[index] = fullPreset
}
} else {
previewError.value = t('importPreset.fetchError')
}
isPreviewLoading.value = false
}
// 关闭预览
function closePreview() {
previewPreset.value = null
previewError.value = ''
}
// 添加预设(按需下载后再添加)
async function handleAddPreset(preset: RemotePresetData) {
addingPresetId.value = preset.id
// 如果还没有内容,先下载
let fullPreset = preset
if (!preset.roleDefinition || !preset.responseRules) {
const fetched = await promptStore.fetchPresetContent(preset)
if (!fetched) {
addingPresetId.value = null
return
}
fullPreset = fetched
// 更新列表中的缓存
const index = remotePresets.value.findIndex((p) => p.id === preset.id)
if (index !== -1) {
remotePresets.value[index] = fullPreset
}
}
const success = promptStore.addRemotePreset(fullPreset)
if (success) {
emit('preset-added')
}
addingPresetId.value = null
}
// 从预览弹窗添加预设(添加后自动关闭预览)
async function handleAddPresetFromPreview() {
if (!previewPreset.value) return
await handleAddPreset(previewPreset.value)
closePreview()
}
// 关闭弹窗
function closeModal() {
emit('update:open', false)
closePreview()
}
// 监听打开状态,打开时加载数据
@@ -85,6 +154,8 @@ watch(
(newVal) => {
if (newVal) {
loadRemotePresets()
} else {
closePreview()
}
}
)
@@ -140,38 +211,16 @@ watch(
>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ preset.name }}</p>
<p class="mt-0.5 line-clamp-1 text-xs text-gray-500 dark:text-gray-400">
{{ preset.roleDefinition.slice(0, 50) }}...
<p class="mt-0.5 line-clamp-2 text-xs text-gray-500 dark:text-gray-400">
{{ preset.description || t('importPreset.noDescription') }}
</p>
</div>
<div class="flex items-center gap-1.5 ml-2 shrink-0">
<!-- 预览按钮 -->
<UPopover :popper="{ placement: 'left' }">
<UButton color="gray" size="xs">
<UIcon name="i-heroicons-eye" class="mr-1 h-3.5 w-3.5" />
{{ t('importPreset.preview') }}
</UButton>
<template #content>
<div class="w-80 max-h-96 overflow-y-auto p-3">
<div class="mb-3">
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-300">
{{ t('importPreset.roleDefinition') }}
</p>
<p class="whitespace-pre-wrap text-xs text-gray-600 dark:text-gray-400">
{{ preset.roleDefinition }}
</p>
</div>
<div>
<p class="mb-1 text-xs font-semibold text-gray-600 dark:text-gray-300">
{{ t('importPreset.responseRules') }}
</p>
<p class="whitespace-pre-wrap text-xs text-gray-600 dark:text-gray-400">
{{ preset.responseRules }}
</p>
</div>
</div>
</template>
</UPopover>
<UButton color="gray" size="xs" @click="handlePreview(preset)">
<UIcon name="i-heroicons-eye" class="mr-1 h-3.5 w-3.5" />
{{ t('importPreset.preview') }}
</UButton>
<!-- 添加按钮 -->
<UButton
v-if="promptStore.isRemotePresetAdded(preset.id)"
@@ -183,8 +232,15 @@ watch(
<UIcon name="i-heroicons-check" class="mr-1 h-3.5 w-3.5" />
{{ t('importPreset.added') }}
</UButton>
<UButton v-else variant="soft" color="primary" size="xs" @click="handleAddPreset(preset)">
<UIcon name="i-heroicons-plus" class="mr-1 h-3.5 w-3.5" />
<UButton
v-else
variant="soft"
color="primary"
size="xs"
:loading="addingPresetId === preset.id"
@click="handleAddPreset(preset)"
>
<UIcon v-if="addingPresetId !== preset.id" name="i-heroicons-plus" class="mr-1 h-3.5 w-3.5" />
{{ t('importPreset.add') }}
</UButton>
</div>
@@ -197,6 +253,79 @@ watch(
</div>
</template>
</UModal>
<!-- 预览弹窗 -->
<UModal :open="!!previewPreset" @update:open="closePreview" :ui="{ content: 'md:w-full max-w-2xl' }">
<template #content>
<div class="p-6">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ previewPreset?.name }}
</h3>
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="closePreview" />
</div>
<!-- 加载中 -->
<div v-if="isPreviewLoading" class="flex items-center justify-center py-8">
<UIcon name="i-heroicons-arrow-path" class="h-6 w-6 animate-spin text-primary-500" />
<span class="ml-2 text-sm text-gray-500">{{ t('importPreset.fetchingContent') }}</span>
</div>
<!-- 错误 -->
<div v-else-if="previewError" class="text-center py-8">
<UIcon name="i-heroicons-exclamation-circle" class="h-8 w-8 text-red-500 mx-auto" />
<p class="mt-2 text-sm text-gray-500">{{ previewError }}</p>
</div>
<!-- 内容 -->
<div v-else-if="previewPreset?.roleDefinition" class="max-h-[60vh] overflow-y-auto space-y-4">
<div>
<p class="mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ t('importPreset.roleDefinition') }}
</p>
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
<p class="whitespace-pre-wrap text-sm text-gray-600 dark:text-gray-400">
{{ previewPreset.roleDefinition }}
</p>
</div>
</div>
<div>
<p class="mb-2 text-sm font-semibold text-gray-700 dark:text-gray-300">
{{ t('importPreset.responseRules') }}
</p>
<div class="rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
<p class="whitespace-pre-wrap text-sm text-gray-600 dark:text-gray-400">
{{ previewPreset.responseRules }}
</p>
</div>
</div>
</div>
<!-- 底部按钮 -->
<div
v-if="previewPreset && !isPreviewLoading && !previewError"
class="mt-4 flex justify-end gap-2 border-t border-gray-200 pt-4 dark:border-gray-700"
>
<UButton variant="ghost" color="gray" @click="closePreview">
{{ t('importPreset.close') }}
</UButton>
<UButton
v-if="!promptStore.isRemotePresetAdded(previewPreset.id)"
color="primary"
:loading="addingPresetId === previewPreset.id"
@click="handleAddPresetFromPreview"
>
<UIcon v-if="addingPresetId !== previewPreset.id" name="i-heroicons-plus" class="mr-1 h-4 w-4" />
{{ t('importPreset.add') }}
</UButton>
<UButton v-else variant="soft" color="gray" disabled>
<UIcon name="i-heroicons-check" class="mr-1 h-4 w-4" />
{{ t('importPreset.added') }}
</UButton>
</div>
</div>
</template>
</UModal>
</template>
<i18n>
@@ -216,7 +345,11 @@ watch(
"added": "已添加",
"preview": "预览",
"roleDefinition": "角色定义",
"responseRules": "回复规则"
"responseRules": "回复规则",
"noDescription": "暂无描述",
"fetchingContent": "正在加载内容...",
"fetchError": "加载内容失败",
"close": "关闭"
}
},
"en-US": {
@@ -234,7 +367,11 @@ watch(
"added": "Added",
"preview": "Preview",
"roleDefinition": "Role Definition",
"responseRules": "Response Rules"
"responseRules": "Response Rules",
"noDescription": "No description",
"fetchingContent": "Loading content...",
"fetchError": "Failed to load content",
"close": "Close"
}
}
}
+78 -14
View File
@@ -14,8 +14,14 @@ const REMOTE_PRESET_BASE_URL = 'https://chatlab.fun'
export interface RemotePresetData {
id: string
name: string
roleDefinition: string
responseRules: string
/** Markdown 文件绝对路径(如 /cn/system-prompt/xxx.md */
path: string
/** 简短描述(索引中提供,用于列表展示) */
description?: string
/** 角色定义(从 Markdown 文件解析后填充) */
roleDefinition?: string
/** 回复规则(从 Markdown 文件解析后填充) */
responseRules?: string
/** 适用场景:common(通用)、group(仅群聊)、private(仅私聊) */
chatType?: 'common' | 'group' | 'private'
}
@@ -275,34 +281,91 @@ export const usePromptStore = defineStore(
}
/**
* 从远程获取预设列表(仅获取,不自动添加)
* 解析 Markdown 文件内容,使用 `---` 分隔 roleDefinition 和 responseRules
* @param content Markdown 文件内容
* @returns { roleDefinition, responseRules }
*/
function parseMarkdownContent(content: string): { roleDefinition: string; responseRules: string } {
// 使用 `---` 独立成行作为分隔符
const separator = /\n---\n/
const parts = content.split(separator)
if (parts.length >= 2) {
return {
roleDefinition: parts[0].trim(),
responseRules: parts.slice(1).join('\n---\n').trim(),
}
}
// 如果没有分隔符,整个内容作为 roleDefinition
return {
roleDefinition: content.trim(),
responseRules: '',
}
}
/**
* 从远程获取预设索引列表(不下载 Markdown 内容,节省流量)
* @param locale 当前语言设置 (如 'zh-CN', 'en-US')
* @returns 远程预设列表,获取失败返回空数组
* @returns 远程预设索引列表,获取失败返回空数组
*/
async function fetchRemotePresets(locale: string): Promise<RemotePresetData[]> {
const langPath = locale === 'zh-CN' ? 'cn' : 'en'
const url = `${REMOTE_PRESET_BASE_URL}/${langPath}/system-prompt.json`
const indexUrl = `${REMOTE_PRESET_BASE_URL}/${langPath}/system-prompt.json`
try {
const result = await window.api.app.fetchRemoteConfig(url)
const result = await window.api.app.fetchRemoteConfig(indexUrl)
if (!result.success || !result.data) {
return []
}
const remotePresets = result.data as RemotePresetData[]
if (!Array.isArray(remotePresets)) {
const presetIndex = result.data as RemotePresetData[]
if (!Array.isArray(presetIndex)) {
return []
}
// 过滤无效数据
return remotePresets.filter(
(preset) => preset.id && preset.name && preset.roleDefinition && preset.responseRules
)
// 过滤有效的索引项(必须有 id、name、path)
return presetIndex.filter((p) => p.id && p.name && p.path)
} catch {
return []
}
}
/**
* 按需下载单个预设的 Markdown 内容
* @param preset 预设索引数据
* @returns 包含完整内容的预设数据,失败返回 null
*/
async function fetchPresetContent(
preset: RemotePresetData
): Promise<(RemotePresetData & { roleDefinition: string; responseRules: string }) | null> {
// 如果已经有内容,直接返回
if (preset.roleDefinition && preset.responseRules) {
return preset as RemotePresetData & { roleDefinition: string; responseRules: string }
}
const mdUrl = `${REMOTE_PRESET_BASE_URL}${preset.path}`
try {
const mdResult = await window.api.app.fetchRemoteConfig(mdUrl)
if (!mdResult.success || typeof mdResult.data !== 'string') {
return null
}
const { roleDefinition, responseRules } = parseMarkdownContent(mdResult.data)
if (!roleDefinition || !responseRules) {
return null
}
return {
...preset,
roleDefinition,
responseRules,
}
} catch {
return null
}
}
/**
* 添加远程预设到自定义预设列表
* @param preset 远程预设数据
@@ -321,8 +384,8 @@ export const usePromptStore = defineStore(
const newPreset: PromptPreset = {
id: preset.id,
name: preset.name,
roleDefinition: preset.roleDefinition,
responseRules: preset.responseRules,
roleDefinition: preset.roleDefinition || '',
responseRules: preset.responseRules || '',
isBuiltIn: false,
applicableTo,
createdAt: now,
@@ -413,6 +476,7 @@ export const usePromptStore = defineStore(
getActivePresetForChatType,
getPresetsForChatType,
fetchRemotePresets,
fetchPresetContent,
addRemotePreset,
isRemotePresetAdded,
}