mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-07 22:01:18 +08:00
feat: 系统提示词预设功能优化
This commit is contained in:
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user