mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-01 18:02:05 +08:00
feat: 移除旧版提示词系统
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
type Usage as PiUsage,
|
||||
} from '@mariozechner/pi-ai'
|
||||
|
||||
import type { AgentConfig, AgentStreamChunk, AgentResult, PromptConfig, TokenUsage, SkillContext } from './types'
|
||||
import type { AgentConfig, AgentStreamChunk, AgentResult, TokenUsage, SkillContext } from './types'
|
||||
import type { AssistantConfig } from '../assistant/types'
|
||||
import { buildSystemPrompt } from './prompt-builder'
|
||||
import { extractThinkingContent, stripToolCallTags } from './content-parser'
|
||||
@@ -26,7 +26,7 @@ import { AgentEventHandler } from './event-handler'
|
||||
type SimpleHistoryMessage = { role: 'user' | 'assistant'; content: string }
|
||||
|
||||
// Re-export types for external consumers
|
||||
export type { AgentConfig, AgentStreamChunk, AgentResult, PromptConfig, TokenUsage, AgentRuntimeStatus } from './types'
|
||||
export type { AgentConfig, AgentStreamChunk, AgentResult, TokenUsage, AgentRuntimeStatus } from './types'
|
||||
|
||||
/**
|
||||
* Agent 执行器类
|
||||
@@ -39,7 +39,6 @@ export class Agent {
|
||||
private apiKey: string
|
||||
private abortSignal?: AbortSignal
|
||||
private chatType: 'group' | 'private' = 'group'
|
||||
private promptConfig?: PromptConfig
|
||||
private assistantConfig?: AssistantConfig
|
||||
private skillCtx?: SkillContext
|
||||
private locale: string = 'zh-CN'
|
||||
@@ -50,7 +49,6 @@ export class Agent {
|
||||
apiKey: string,
|
||||
config: AgentConfig = {},
|
||||
chatType: 'group' | 'private' = 'group',
|
||||
promptConfig?: PromptConfig,
|
||||
locale: string = 'zh-CN',
|
||||
assistantConfig?: AssistantConfig,
|
||||
skillCtx?: SkillContext
|
||||
@@ -60,7 +58,6 @@ export class Agent {
|
||||
this.apiKey = apiKey
|
||||
this.abortSignal = config.abortSignal
|
||||
this.chatType = chatType
|
||||
this.promptConfig = promptConfig
|
||||
this.assistantConfig = assistantConfig
|
||||
this.skillCtx = skillCtx
|
||||
this.locale = locale
|
||||
@@ -83,13 +80,9 @@ export class Agent {
|
||||
|
||||
const maxToolRounds = Math.max(0, this.config.maxToolRounds ?? 0)
|
||||
|
||||
const effectivePromptConfig: PromptConfig | undefined = this.assistantConfig
|
||||
? { systemPrompt: this.assistantConfig.systemPrompt }
|
||||
: this.promptConfig
|
||||
|
||||
const systemPrompt = buildSystemPrompt(
|
||||
this.chatType,
|
||||
effectivePromptConfig,
|
||||
this.assistantConfig?.systemPrompt,
|
||||
this.context.ownerInfo,
|
||||
this.locale,
|
||||
this.skillCtx,
|
||||
@@ -312,7 +305,6 @@ export async function runAgent(
|
||||
context: ToolContext,
|
||||
config?: AgentConfig,
|
||||
chatType?: 'group' | 'private',
|
||||
promptConfig?: PromptConfig,
|
||||
locale?: string,
|
||||
assistantConfig?: AssistantConfig,
|
||||
skillCtx?: SkillContext
|
||||
@@ -320,17 +312,7 @@ export async function runAgent(
|
||||
const activeConfig = getActiveConfig()
|
||||
if (!activeConfig) throw new Error('LLM service not configured')
|
||||
const piModel = buildPiModel(activeConfig)
|
||||
const agent = new Agent(
|
||||
context,
|
||||
piModel,
|
||||
activeConfig.apiKey,
|
||||
config,
|
||||
chatType,
|
||||
promptConfig,
|
||||
locale,
|
||||
assistantConfig,
|
||||
skillCtx
|
||||
)
|
||||
const agent = new Agent(context, piModel, activeConfig.apiKey, config, chatType, locale, assistantConfig, skillCtx)
|
||||
return agent.execute(userMessage)
|
||||
}
|
||||
|
||||
@@ -343,7 +325,6 @@ export async function runAgentStream(
|
||||
onChunk: (chunk: AgentStreamChunk) => void,
|
||||
config?: AgentConfig,
|
||||
chatType?: 'group' | 'private',
|
||||
promptConfig?: PromptConfig,
|
||||
locale?: string,
|
||||
assistantConfig?: AssistantConfig,
|
||||
skillCtx?: SkillContext
|
||||
@@ -351,16 +332,6 @@ export async function runAgentStream(
|
||||
const activeConfig = getActiveConfig()
|
||||
if (!activeConfig) throw new Error('LLM service not configured')
|
||||
const piModel = buildPiModel(activeConfig)
|
||||
const agent = new Agent(
|
||||
context,
|
||||
piModel,
|
||||
activeConfig.apiKey,
|
||||
config,
|
||||
chatType,
|
||||
promptConfig,
|
||||
locale,
|
||||
assistantConfig,
|
||||
skillCtx
|
||||
)
|
||||
const agent = new Agent(context, piModel, activeConfig.apiKey, config, chatType, locale, assistantConfig, skillCtx)
|
||||
return agent.executeStream(userMessage, onChunk)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { t as i18nT } from '../../i18n'
|
||||
import type { OwnerInfo } from '../tools/types'
|
||||
import type { PromptConfig, SkillContext } from './types'
|
||||
import type { SkillContext } from './types'
|
||||
import type { ToolContext } from '../tools/types'
|
||||
|
||||
function agentT(key: string, locale: string, options?: Record<string, unknown>): string {
|
||||
@@ -80,20 +80,19 @@ function getFallbackRoleDefinition(chatType: 'group' | 'private', locale: string
|
||||
/**
|
||||
* 构建完整的系统提示词
|
||||
*
|
||||
* 提示词配置主要来自前端 src/config/prompts.ts,通过 promptConfig 参数传递。
|
||||
* Fallback 仅在前端未传递配置时使用。
|
||||
* 系统提示词优先使用当前助手配置;若助手不存在,再退回内置默认角色定义。
|
||||
*
|
||||
* 最终格式:{用户系统提示词}\n\n{系统锁定段(日期/owner/时间参数/通用指引)}
|
||||
* 最终格式:{助手系统提示词}\n\n{系统锁定段(日期/owner/时间参数/通用指引)}
|
||||
*/
|
||||
export function buildSystemPrompt(
|
||||
chatType: 'group' | 'private' = 'group',
|
||||
promptConfig?: PromptConfig,
|
||||
assistantSystemPrompt?: string,
|
||||
ownerInfo?: OwnerInfo,
|
||||
locale: string = 'zh-CN',
|
||||
skillCtx?: SkillContext,
|
||||
mentionedMembers?: ToolContext['mentionedMembers']
|
||||
): string {
|
||||
const systemPrompt = promptConfig?.systemPrompt || getFallbackRoleDefinition(chatType, locale)
|
||||
const systemPrompt = assistantSystemPrompt || getFallbackRoleDefinition(chatType, locale)
|
||||
const lockedSection = getLockedPromptSection(chatType, ownerInfo, locale, mentionedMembers)
|
||||
|
||||
let skillSection = ''
|
||||
|
||||
@@ -60,14 +60,6 @@ export interface AgentResult {
|
||||
totalUsage?: TokenUsage
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户自定义提示词配置
|
||||
*/
|
||||
export interface PromptConfig {
|
||||
/** 系统提示词(角色定义 + 回答要求,统一为单一字段) */
|
||||
systemPrompt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 技能上下文(传递给 prompt-builder)
|
||||
* 手动选择和 AI 自选两种模式互斥
|
||||
|
||||
@@ -15,6 +15,5 @@ export {
|
||||
getBuiltinCatalog,
|
||||
importAssistant,
|
||||
reimportAssistant,
|
||||
backupOldPromptPresets,
|
||||
} from './manager'
|
||||
export { getBuiltinSqlToolCatalog, getBuiltinTsToolNames } from './builtinSqlTools'
|
||||
|
||||
@@ -341,39 +341,6 @@ export function resetAssistant(id: string): AssistantSaveResult {
|
||||
return saveAssistantToDisk(resetConfig)
|
||||
}
|
||||
|
||||
// ==================== 提示词预设迁移 ====================
|
||||
|
||||
/**
|
||||
* 备份旧的提示词预设数据到 data/backup 目录
|
||||
*/
|
||||
export function backupOldPromptPresets(data: {
|
||||
customPresets?: unknown[]
|
||||
builtinOverrides?: Record<string, unknown>
|
||||
remotePresetIds?: string[]
|
||||
}): { success: boolean; filePath?: string; error?: string } {
|
||||
try {
|
||||
const backupDir = path.join(getAiDataDir(), '..', 'backup')
|
||||
ensureDir(backupDir)
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
||||
const filePath = path.join(backupDir, `prompt-presets-${timestamp}.json`)
|
||||
|
||||
const backupContent = {
|
||||
backupTime: new Date().toISOString(),
|
||||
description: 'ChatLab 旧提示词预设系统备份(已被多助手系统替代)',
|
||||
...data,
|
||||
}
|
||||
|
||||
writeJsonFile(filePath, backupContent)
|
||||
aiLogger.info('AssistantManager', 'Old prompt presets backed up', { filePath })
|
||||
|
||||
return { success: true, filePath }
|
||||
} catch (error) {
|
||||
aiLogger.error('AssistantManager', 'Failed to backup prompt presets', { error: String(error) })
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 内部工具函数 ====================
|
||||
|
||||
function ensureInitialized(): void {
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as llm from '../ai/llm'
|
||||
import * as rag from '../ai/rag'
|
||||
import { aiLogger, setDebugMode } from '../ai/logger'
|
||||
import { getLogsDir } from '../paths'
|
||||
import { Agent, type AgentStreamChunk, type PromptConfig, type SkillContext } from '../ai/agent'
|
||||
import { Agent, type AgentStreamChunk, type SkillContext } from '../ai/agent'
|
||||
import { getActiveConfig, buildPiModel } from '../ai/llm'
|
||||
import * as assistantManager from '../ai/assistant'
|
||||
import type { AssistantConfig } from '../ai/assistant/types'
|
||||
@@ -679,21 +679,6 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'assistant:backupOldPresets',
|
||||
async (
|
||||
_,
|
||||
data: { customPresets?: unknown[]; builtinOverrides?: Record<string, unknown>; remotePresetIds?: string[] }
|
||||
) => {
|
||||
try {
|
||||
return assistantManager.backupOldPromptPresets(data)
|
||||
} catch (error) {
|
||||
console.error('Failed to backup old presets:', error)
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ==================== 技能管理 API ====================
|
||||
|
||||
ipcMain.handle('skill:getAll', async () => {
|
||||
@@ -775,7 +760,6 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
* Agent 会自动调用工具并返回最终结果
|
||||
* Agent 通过 context.conversationId 从 SQLite 读取对话历史(数据流倒置)
|
||||
* @param chatType 聊天类型('group' | 'private')
|
||||
* @param promptConfig 用户自定义提示词配置(可选)
|
||||
* @param locale 语言设置(可选,默认 'zh-CN')
|
||||
* @param maxHistoryRounds 前端用户配置的最大历史轮数(可选,每轮 = user + assistant = 2 条)
|
||||
* @param assistantId 助手 ID(可选,传入时从 AssistantManager 获取配置)
|
||||
@@ -788,7 +772,6 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
userMessage: string,
|
||||
context: ToolContext,
|
||||
chatType?: 'group' | 'private',
|
||||
promptConfig?: PromptConfig,
|
||||
locale?: string,
|
||||
maxHistoryRounds?: number,
|
||||
assistantId?: string,
|
||||
@@ -800,7 +783,6 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
sessionId: context.sessionId,
|
||||
conversationId: context.conversationId,
|
||||
chatType: chatType ?? 'group',
|
||||
hasPromptConfig: !!promptConfig,
|
||||
assistantId: assistantId ?? '(none)',
|
||||
skillId: skillId ?? '(none)',
|
||||
enableAutoSkill: enableAutoSkill ?? false,
|
||||
@@ -826,7 +808,6 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
maxHistoryRounds: maxHistoryRounds ?? '(default)',
|
||||
maxMessagesLimit: context.maxMessagesLimit,
|
||||
hasTimeFilter: !!context.timeFilter,
|
||||
hasCustomPrompt: !!promptConfig,
|
||||
mentionedMembersCount: context.mentionedMembers?.length ?? 0,
|
||||
preprocess: pp
|
||||
? {
|
||||
@@ -839,13 +820,15 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
: '(disabled)',
|
||||
})
|
||||
|
||||
// 如果指定了 assistantId,从 AssistantManager 加载助手配置
|
||||
let assistantConfig: AssistantConfig | undefined
|
||||
if (assistantId) {
|
||||
assistantConfig = assistantManager.getAssistantConfig(assistantId) ?? undefined
|
||||
if (!assistantConfig) {
|
||||
aiLogger.warn('IPC', `Assistant not found: ${assistantId}, falling back to default`)
|
||||
}
|
||||
// 提示词系统已退场,主流程统一从助手配置获取 systemPrompt。
|
||||
// 若前端没有显式传 assistantId,则退回 general 助手兜底。
|
||||
let resolvedAssistantId = assistantId || 'general'
|
||||
let assistantConfig: AssistantConfig | undefined =
|
||||
assistantManager.getAssistantConfig(resolvedAssistantId) ?? undefined
|
||||
if (!assistantConfig && resolvedAssistantId !== 'general') {
|
||||
aiLogger.warn('IPC', `Assistant not found: ${resolvedAssistantId}, falling back to general`)
|
||||
resolvedAssistantId = 'general'
|
||||
assistantConfig = assistantManager.getAssistantConfig('general') ?? undefined
|
||||
}
|
||||
|
||||
// 构建技能上下文
|
||||
@@ -872,7 +855,6 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
activeAIConfig.apiKey,
|
||||
{ abortSignal: abortController.signal, contextHistoryLimit },
|
||||
chatType ?? 'group',
|
||||
promptConfig,
|
||||
locale ?? 'zh-CN',
|
||||
assistantConfig,
|
||||
skillCtx
|
||||
|
||||
@@ -182,11 +182,6 @@ export interface EmbeddingServiceConfigDisplay {
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
// 用户自定义提示词配置
|
||||
export interface PromptConfig {
|
||||
systemPrompt: string
|
||||
}
|
||||
|
||||
// ==================== AI API ====================
|
||||
|
||||
export const aiApi = {
|
||||
@@ -759,14 +754,6 @@ export const assistantApi = {
|
||||
reimportAssistant: (id: string): Promise<{ success: boolean; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:reimport', id)
|
||||
},
|
||||
|
||||
backupOldPresets: (data: {
|
||||
customPresets?: unknown[]
|
||||
builtinOverrides?: Record<string, unknown>
|
||||
remotePresetIds?: string[]
|
||||
}): Promise<{ success: boolean; filePath?: string; error?: string }> => {
|
||||
return ipcRenderer.invoke('assistant:backupOldPresets', data)
|
||||
},
|
||||
}
|
||||
|
||||
// ==================== Skill API ====================
|
||||
@@ -838,7 +825,6 @@ export const agentApi = {
|
||||
* 执行 Agent 对话(流式)
|
||||
* Agent 通过 context.conversationId 从后端 SQLite 读取对话历史
|
||||
* @param chatType 聊天类型('group' | 'private')
|
||||
* @param promptConfig 用户自定义提示词配置(可选)
|
||||
* @param locale 语言设置(可选,默认 'zh-CN')
|
||||
* @param maxHistoryRounds 最大历史轮数(可选,每轮 = user + assistant = 2 条)
|
||||
* @returns 返回 { requestId, promise },requestId 可用于中止请求
|
||||
@@ -848,7 +834,6 @@ export const agentApi = {
|
||||
context: ToolContext,
|
||||
onChunk?: (chunk: AgentStreamChunk) => void,
|
||||
chatType?: 'group' | 'private',
|
||||
promptConfig?: PromptConfig,
|
||||
locale?: string,
|
||||
maxHistoryRounds?: number,
|
||||
assistantId?: string,
|
||||
@@ -893,9 +878,7 @@ export const agentApi = {
|
||||
'conversationId:',
|
||||
sanitizedContext.conversationId ?? 'none',
|
||||
'chatType:',
|
||||
chatType ?? 'group',
|
||||
'hasPromptConfig:',
|
||||
!!promptConfig
|
||||
chatType ?? 'group'
|
||||
)
|
||||
|
||||
const promise = new Promise<{ success: boolean; result?: AgentResult; error?: string }>((resolve) => {
|
||||
@@ -939,7 +922,6 @@ export const agentApi = {
|
||||
userMessage,
|
||||
sanitizedContext,
|
||||
chatType,
|
||||
promptConfig,
|
||||
locale,
|
||||
maxHistoryRounds,
|
||||
assistantId,
|
||||
|
||||
12
electron/preload/index.d.ts
vendored
12
electron/preload/index.d.ts
vendored
@@ -666,18 +666,12 @@ interface ToolContext {
|
||||
preprocessConfig?: PreprocessConfig
|
||||
}
|
||||
|
||||
// 用户自定义提示词配置
|
||||
interface PromptConfig {
|
||||
systemPrompt: string
|
||||
}
|
||||
|
||||
interface AgentApi {
|
||||
runStream: (
|
||||
userMessage: string,
|
||||
context: ToolContext,
|
||||
onChunk?: (chunk: AgentStreamChunk) => void,
|
||||
chatType?: 'group' | 'private',
|
||||
promptConfig?: PromptConfig,
|
||||
locale?: string,
|
||||
maxHistoryRounds?: number,
|
||||
assistantId?: string,
|
||||
@@ -745,11 +739,6 @@ interface AssistantApi {
|
||||
getBuiltinTsToolNames: () => Promise<string[]>
|
||||
importAssistant: (builtinId: string) => Promise<{ success: boolean; error?: string }>
|
||||
reimportAssistant: (id: string) => Promise<{ success: boolean; error?: string }>
|
||||
backupOldPresets: (data: {
|
||||
customPresets?: unknown[]
|
||||
builtinOverrides?: Record<string, unknown>
|
||||
remotePresetIds?: string[]
|
||||
}) => Promise<{ success: boolean; filePath?: string; error?: string }>
|
||||
}
|
||||
|
||||
// ==================== 技能管理 ====================
|
||||
@@ -1029,7 +1018,6 @@ export {
|
||||
ToolContext,
|
||||
DesensitizeRule,
|
||||
PreprocessConfig,
|
||||
PromptConfig,
|
||||
TokenUsage,
|
||||
CacheDirectoryInfo,
|
||||
CacheInfo,
|
||||
|
||||
@@ -601,11 +601,8 @@ watch(
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<ChatStatusBar
|
||||
:chat-type="currentChatType"
|
||||
:session-token-usage="sessionTokenUsage"
|
||||
:agent-status="agentStatus"
|
||||
:has-l-l-m-config="hasLLMConfig"
|
||||
:is-checking-config="isCheckingConfig"
|
||||
:current-conversation-id="currentConversationId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@ onMounted(async () => {
|
||||
if (!isLoaded.value) {
|
||||
await assistantStore.loadAssistants()
|
||||
}
|
||||
assistantStore.migrateOldPromptPresets()
|
||||
})
|
||||
|
||||
function handleSelect(id: string) {
|
||||
|
||||
@@ -9,40 +9,24 @@ import { useLLMStore } from '@/stores/llm'
|
||||
import { exportConversation, type ExportFormat } from '@/utils/conversationExport'
|
||||
import type { AgentRuntimeStatus } from '@electron/shared/types'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
chatType: 'group' | 'private'
|
||||
sessionTokenUsage: { totalTokens: number }
|
||||
agentStatus?: AgentRuntimeStatus | null
|
||||
hasLLMConfig: boolean
|
||||
isCheckingConfig: boolean
|
||||
currentConversationId?: string | null
|
||||
}>()
|
||||
|
||||
// Store
|
||||
const promptStore = usePromptStore()
|
||||
const llmStore = useLLMStore()
|
||||
const { aiPromptSettings, activePreset, aiGlobalSettings } = storeToRefs(promptStore)
|
||||
const { aiGlobalSettings } = storeToRefs(promptStore)
|
||||
const { configs, activeConfig, isLoading: isLoadingLLM } = storeToRefs(llmStore)
|
||||
|
||||
// 当前类型对应的预设列表(根据 applicableTo 过滤)
|
||||
const currentPresets = computed(() => promptStore.getPresetsForChatType(props.chatType))
|
||||
|
||||
// 当前激活的预设 ID
|
||||
const currentActivePresetId = computed(() => aiPromptSettings.value.activePresetId)
|
||||
|
||||
// 当前激活的预设(如果当前激活的预设不适用于当前类型,使用第一个可用预设)
|
||||
const currentActivePreset = computed(() => {
|
||||
const activeInList = currentPresets.value.find((p) => p.id === currentActivePresetId.value)
|
||||
return activeInList || activePreset.value
|
||||
})
|
||||
|
||||
// 下拉菜单状态
|
||||
const isPresetPopoverOpen = ref(false)
|
||||
const isModelPopoverOpen = ref(false)
|
||||
const isOpeningLog = ref(false)
|
||||
|
||||
@@ -82,11 +66,6 @@ function formatNumber(value: number): string {
|
||||
return new Intl.NumberFormat().format(Math.max(0, Math.round(value)))
|
||||
}
|
||||
|
||||
const contextTokensText = computed(() => {
|
||||
if (!props.agentStatus) return '--'
|
||||
return formatCompactNumber(props.agentStatus.contextTokens)
|
||||
})
|
||||
|
||||
function formatCompactNumber(value: number): string {
|
||||
const num = Math.max(0, Math.round(value))
|
||||
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
|
||||
@@ -106,17 +85,6 @@ const agentCompactTitle = computed(() => {
|
||||
].join('\n')
|
||||
})
|
||||
|
||||
// 设置激活预设
|
||||
function setActivePreset(presetId: string) {
|
||||
promptStore.setActivePreset(presetId)
|
||||
isPresetPopoverOpen.value = false
|
||||
}
|
||||
|
||||
function openPresetSettings() {
|
||||
isPresetPopoverOpen.value = false
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'preset' } })
|
||||
}
|
||||
|
||||
function openChatSettings() {
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'chat' } })
|
||||
}
|
||||
@@ -250,66 +218,11 @@ async function openAiLogFile() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- 左侧:预设选择器 + 模型切换器 -->
|
||||
<!-- 抬高状态栏与模型下拉层级,避免被输入框上方的快捷提示遮住。 -->
|
||||
<div class="relative z-20 flex items-center justify-between">
|
||||
<!-- 左侧:模型切换器 -->
|
||||
<div class="flex items-center gap-1">
|
||||
<UPopover v-model:open="isPresetPopoverOpen" :ui="{ content: 'p-0' }">
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300"
|
||||
>
|
||||
<UIcon name="i-heroicons-chat-bubble-bottom-center-text" class="h-3.5 w-3.5" />
|
||||
<span class="max-w-[120px] truncate">
|
||||
{{ currentActivePreset?.name || t('ai.chat.statusBar.preset.default') }}
|
||||
</span>
|
||||
<UIcon name="i-heroicons-chevron-down" class="h-3 w-3" />
|
||||
</button>
|
||||
<template #content>
|
||||
<div class="w-48 py-1">
|
||||
<div class="px-3 py-1.5 text-xs font-medium text-gray-400 dark:text-gray-500">
|
||||
{{
|
||||
chatType === 'group'
|
||||
? t('ai.chat.statusBar.preset.groupTitle')
|
||||
: t('ai.chat.statusBar.preset.privateTitle')
|
||||
}}
|
||||
</div>
|
||||
<button
|
||||
v-for="preset in currentPresets"
|
||||
:key="preset.id"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
:class="[
|
||||
preset.id === currentActivePresetId
|
||||
? 'text-pink-600 dark:text-pink-400'
|
||||
: 'text-gray-700 dark:text-gray-300',
|
||||
]"
|
||||
@click="setActivePreset(preset.id)"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
preset.id === currentActivePresetId ? 'i-heroicons-check-circle-solid' : 'i-heroicons-document-text'
|
||||
"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:class="[preset.id === currentActivePresetId ? 'text-pink-500' : 'text-gray-400']"
|
||||
/>
|
||||
<span class="truncate">{{ preset.name }}</span>
|
||||
</button>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<!-- 管理预设按钮 -->
|
||||
<button
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300"
|
||||
@click="openPresetSettings"
|
||||
>
|
||||
<UIcon name="i-heroicons-cog-6-tooth" class="h-4 w-4 shrink-0" />
|
||||
<span>{{ t('ai.chat.statusBar.preset.manage') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
|
||||
<!-- 模型切换器 -->
|
||||
<UPopover v-model:open="isModelPopoverOpen" :ui="{ content: 'p-0' }">
|
||||
<UPopover v-model:open="isModelPopoverOpen" :ui="{ content: 'z-[80] p-0' }">
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300"
|
||||
:disabled="isLoadingLLM"
|
||||
@@ -373,14 +286,13 @@ async function openAiLogFile() {
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
v-if="agentStatus"
|
||||
class="hidden shrink-0 items-center gap-1 rounded-md border border-gray-200 bg-white px-1.5 py-1 text-xs dark:border-gray-700 dark:bg-gray-900 lg:flex"
|
||||
class="hidden shrink-0 items-center gap-1 rounded-lg bg-gray-50/90 px-1.5 py-1 text-xs shadow-[inset_0_0_0_1px_rgba(255,255,255,0.35)] dark:bg-gray-800/70 dark:shadow-[inset_0_0_0_1px_rgba(255,255,255,0.04)] lg:flex"
|
||||
:title="agentCompactTitle"
|
||||
>
|
||||
<!-- 主栏只展示阶段,context token 放进 tooltip,避免和累计 token 混淆。 -->
|
||||
<span class="rounded px-1 py-0.5 text-[10px] font-medium" :class="agentPhaseClass">
|
||||
{{ agentPhaseShortText }}
|
||||
</span>
|
||||
<UIcon name="i-heroicons-document-text" class="h-3 w-3 text-gray-400 dark:text-gray-500" />
|
||||
<span class="text-[10px] text-gray-500 dark:text-gray-400">{{ contextTokensText }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"aiConfig": "Chat Model",
|
||||
"aiRAG": "Vector Model",
|
||||
"aiPrompt": "Chat Config",
|
||||
"aiPreset": "Prompts",
|
||||
"aiPreset": "Legacy Prompts",
|
||||
"aiPreprocess": "Preprocess",
|
||||
"dataManage": "Data Management",
|
||||
"storage": "Storage",
|
||||
@@ -257,6 +257,27 @@
|
||||
"noDescription": "No description",
|
||||
"fetchingContent": "Loading content...",
|
||||
"fetchError": "Failed to load content"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "Legacy Prompts",
|
||||
"description": "The old prompt system no longer affects AI conversations. This section is kept only for viewing and copying legacy configs during the transition.",
|
||||
"copyJson": "Copy JSON",
|
||||
"emptyTitle": "No legacy prompt data found",
|
||||
"emptyDescription": "There is no legacy prompt config available to view on this device.",
|
||||
"activePreset": "Previously Active Preset",
|
||||
"notConfigured": "Not recorded",
|
||||
"customPresetCount": "Custom Presets",
|
||||
"remotePresetCount": "Imported Remote Presets",
|
||||
"parseError": "Failed to parse legacy prompt data, but you can still copy the raw JSON first.",
|
||||
"customPresetList": "Custom Presets",
|
||||
"noPromptContent": "No prompt content recorded",
|
||||
"rawJsonTitle": "Raw Config JSON",
|
||||
"rawJsonHint": "View and copy only",
|
||||
"copySuccess": "JSON copied to clipboard",
|
||||
"copyFailed": "Copy failed",
|
||||
"groupOnly": "Group Only",
|
||||
"privateOnly": "Private Only",
|
||||
"common": "Common"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"aiConfig": "チャットモデル",
|
||||
"aiRAG": "埋め込みモデル",
|
||||
"aiPrompt": "チャット設定",
|
||||
"aiPreset": "プロンプト設定",
|
||||
"aiPreset": "旧版プロンプト",
|
||||
"aiPreprocess": "前処理",
|
||||
"dataManage": "データ管理",
|
||||
"storage": "ストレージ管理",
|
||||
@@ -257,6 +257,27 @@
|
||||
"noDescription": "説明がありません",
|
||||
"fetchingContent": "コンテンツを読み込み中...",
|
||||
"fetchError": "コンテンツの読み込みに失敗しました"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "旧版プロンプト",
|
||||
"description": "旧プロンプトシステムはすでに AI 会話の実行には使われません。移行期間中に旧設定を確認・コピーするためだけの入口です。",
|
||||
"copyJson": "JSON をコピー",
|
||||
"emptyTitle": "旧版プロンプトデータが見つかりません",
|
||||
"emptyDescription": "この端末には確認できる旧版プロンプト設定がありません。",
|
||||
"activePreset": "以前の有効プリセット",
|
||||
"notConfigured": "記録なし",
|
||||
"customPresetCount": "カスタムプリセット数",
|
||||
"remotePresetCount": "リモート導入数",
|
||||
"parseError": "旧版プロンプトデータの解析に失敗しましたが、先に生の JSON をコピーできます。",
|
||||
"customPresetList": "カスタムプリセット",
|
||||
"noPromptContent": "プロンプト内容の記録がありません",
|
||||
"rawJsonTitle": "元の設定 JSON",
|
||||
"rawJsonHint": "閲覧とコピー専用",
|
||||
"copySuccess": "JSON をクリップボードにコピーしました",
|
||||
"copyFailed": "コピーに失敗しました",
|
||||
"groupOnly": "グループのみ",
|
||||
"privateOnly": "個人のみ",
|
||||
"common": "共通"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"aiConfig": "对话模型",
|
||||
"aiRAG": "向量模型",
|
||||
"aiPrompt": "对话配置",
|
||||
"aiPreset": "提示词配置",
|
||||
"aiPreset": "旧版提示词",
|
||||
"aiPreprocess": "预处理",
|
||||
"dataManage": "数据管理",
|
||||
"storage": "存储管理",
|
||||
@@ -257,6 +257,27 @@
|
||||
"noDescription": "暂无描述",
|
||||
"fetchingContent": "正在加载内容...",
|
||||
"fetchError": "加载内容失败"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "旧版提示词",
|
||||
"description": "提示词系统已不再参与 AI 对话运行。这里仅保留旧配置的查看与复制入口,方便你在过渡期手动迁移。",
|
||||
"copyJson": "复制 JSON",
|
||||
"emptyTitle": "未发现旧版提示词数据",
|
||||
"emptyDescription": "当前设备上没有可查看的旧版提示词配置。",
|
||||
"activePreset": "历史激活预设",
|
||||
"notConfigured": "未记录",
|
||||
"customPresetCount": "自定义预设数",
|
||||
"remotePresetCount": "远程导入数",
|
||||
"parseError": "旧版提示词数据解析失败,但你仍然可以先复制原始 JSON。",
|
||||
"customPresetList": "自定义预设",
|
||||
"noPromptContent": "未记录提示词内容",
|
||||
"rawJsonTitle": "原始配置 JSON",
|
||||
"rawJsonHint": "仅供查看与复制",
|
||||
"copySuccess": "JSON 已复制到剪贴板",
|
||||
"copyFailed": "复制失败",
|
||||
"groupOnly": "仅群聊",
|
||||
"privateOnly": "仅私聊",
|
||||
"common": "通用"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"aiConfig": "對話模型",
|
||||
"aiRAG": "向量模型",
|
||||
"aiPrompt": "聊天設定",
|
||||
"aiPreset": "提示詞設定",
|
||||
"aiPreset": "舊版提示詞",
|
||||
"aiPreprocess": "前處理",
|
||||
"dataManage": "資料管理",
|
||||
"storage": "儲存管理",
|
||||
@@ -257,6 +257,27 @@
|
||||
"noDescription": "暫無描述",
|
||||
"fetchingContent": "正在載入內容...",
|
||||
"fetchError": "載入內容失敗"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "舊版提示詞",
|
||||
"description": "提示詞系統已不再參與 AI 對話執行。這裡僅保留舊設定的查看與複製入口,方便你在過渡期手動遷移。",
|
||||
"copyJson": "複製 JSON",
|
||||
"emptyTitle": "未發現舊版提示詞資料",
|
||||
"emptyDescription": "目前這台裝置上沒有可查看的舊版提示詞設定。",
|
||||
"activePreset": "歷史啟用預設",
|
||||
"notConfigured": "未記錄",
|
||||
"customPresetCount": "自訂預設數",
|
||||
"remotePresetCount": "遠端匯入數",
|
||||
"parseError": "舊版提示詞資料解析失敗,但你仍可先複製原始 JSON。",
|
||||
"customPresetList": "自訂預設",
|
||||
"noPromptContent": "未記錄提示詞內容",
|
||||
"rawJsonTitle": "原始設定 JSON",
|
||||
"rawJsonHint": "僅供查看與複製",
|
||||
"copySuccess": "JSON 已複製到剪貼簿",
|
||||
"copyFailed": "複製失敗",
|
||||
"groupOnly": "僅群聊",
|
||||
"privateOnly": "僅私聊",
|
||||
"common": "通用"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { PromptPreset, PresetApplicableType } from '@/types/ai'
|
||||
import {
|
||||
getDefaultSystemPrompt,
|
||||
getLockedPromptSectionPreview,
|
||||
getOriginalBuiltinPreset,
|
||||
type LocaleType,
|
||||
} from '@/config/prompts'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
mode: 'add' | 'edit'
|
||||
preset: PromptPreset | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
saved: []
|
||||
}>()
|
||||
|
||||
const promptStore = usePromptStore()
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
systemPrompt: '',
|
||||
supportGroup: true,
|
||||
supportPrivate: true,
|
||||
})
|
||||
|
||||
const isBuiltIn = computed(() => props.preset?.isBuiltIn ?? false)
|
||||
const isEditMode = computed(() => props.mode === 'edit')
|
||||
const isModified = computed(() => {
|
||||
if (!isBuiltIn.value || !props.preset) return false
|
||||
return promptStore.isBuiltinPresetModified(props.preset.id)
|
||||
})
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (isBuiltIn.value) return t('settings.aiPrompt.modal.editBuiltin')
|
||||
return isEditMode.value ? t('settings.aiPrompt.modal.editCustom') : t('settings.aiPrompt.modal.addCustom')
|
||||
})
|
||||
|
||||
const canSave = computed(() => {
|
||||
return formData.value.name.trim() && formData.value.systemPrompt.trim()
|
||||
})
|
||||
|
||||
function applicableToCheckboxes(applicableTo?: PresetApplicableType): { group: boolean; private: boolean } {
|
||||
if (!applicableTo || applicableTo === 'common') {
|
||||
return { group: true, private: true }
|
||||
}
|
||||
return {
|
||||
group: applicableTo === 'group',
|
||||
private: applicableTo === 'private',
|
||||
}
|
||||
}
|
||||
|
||||
function checkboxesToApplicableTo(group: boolean, private_: boolean): PresetApplicableType {
|
||||
if (group && private_) return 'common'
|
||||
if (group) return 'group'
|
||||
if (private_) return 'private'
|
||||
return 'common'
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.preset) {
|
||||
const checkboxes = applicableToCheckboxes(props.preset.applicableTo)
|
||||
formData.value = {
|
||||
name: props.preset.name,
|
||||
systemPrompt: props.preset.systemPrompt,
|
||||
supportGroup: checkboxes.group,
|
||||
supportPrivate: checkboxes.private,
|
||||
}
|
||||
} else {
|
||||
formData.value = {
|
||||
name: '',
|
||||
systemPrompt: getDefaultSystemPrompt(locale.value as LocaleType),
|
||||
supportGroup: true,
|
||||
supportPrivate: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function closeModal() {
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
if (!canSave.value) return
|
||||
|
||||
const applicableTo = checkboxesToApplicableTo(formData.value.supportGroup, formData.value.supportPrivate)
|
||||
|
||||
if (isEditMode.value && props.preset) {
|
||||
const updates: {
|
||||
name: string
|
||||
systemPrompt: string
|
||||
applicableTo?: PresetApplicableType
|
||||
} = {
|
||||
name: formData.value.name.trim(),
|
||||
systemPrompt: formData.value.systemPrompt.trim(),
|
||||
}
|
||||
if (!isBuiltIn.value) {
|
||||
updates.applicableTo = applicableTo
|
||||
}
|
||||
promptStore.updatePromptPreset(props.preset.id, updates)
|
||||
} else {
|
||||
promptStore.addPromptPreset({
|
||||
name: formData.value.name.trim(),
|
||||
systemPrompt: formData.value.systemPrompt.trim(),
|
||||
applicableTo,
|
||||
})
|
||||
}
|
||||
|
||||
emit('saved')
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
if (!props.preset || !isBuiltIn.value) return
|
||||
|
||||
const original = getOriginalBuiltinPreset(props.preset.id, locale.value as LocaleType)
|
||||
if (original) {
|
||||
formData.value = {
|
||||
name: original.name,
|
||||
systemPrompt: original.systemPrompt,
|
||||
supportGroup: true,
|
||||
supportPrivate: true,
|
||||
}
|
||||
promptStore.resetBuiltinPreset(props.preset.id)
|
||||
}
|
||||
}
|
||||
|
||||
const previewContent = computed(() => {
|
||||
const lockedSection = getLockedPromptSectionPreview('group', undefined, locale.value as LocaleType)
|
||||
return `${formData.value.systemPrompt}
|
||||
|
||||
${lockedSection}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" :ui="{ content: 'md:w-full max-w-2xl' }" @update:open="emit('update:open', $event)">
|
||||
<template #content>
|
||||
<div class="p-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ modalTitle }}</h2>
|
||||
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="closeModal" />
|
||||
</div>
|
||||
|
||||
<!-- 表单 -->
|
||||
<div class="max-h-[500px] space-y-4 overflow-y-auto pr-1">
|
||||
<!-- 预设名称 -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.aiPrompt.modal.presetName') }}
|
||||
</label>
|
||||
<UInput
|
||||
v-model="formData.name"
|
||||
:placeholder="t('settings.aiPrompt.modal.presetNamePlaceholder')"
|
||||
class="w-60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 适用场景(仅自定义预设显示) -->
|
||||
<div v-if="!isBuiltIn">
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.aiPrompt.modal.applicableTo') }}
|
||||
<span class="font-normal text-gray-500">{{ t('settings.aiPrompt.modal.applicableToHint') }}</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
v-model="formData.supportGroup"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.aiPrompt.modal.groupChat') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
v-model="formData.supportPrivate"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||
/>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.aiPrompt.modal.privateChat') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统提示词 -->
|
||||
<div>
|
||||
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('settings.aiPrompt.modal.systemPrompt') }}
|
||||
</label>
|
||||
<UTextarea
|
||||
v-model="formData.systemPrompt"
|
||||
:rows="12"
|
||||
:placeholder="t('settings.aiPrompt.modal.systemPromptPlaceholder')"
|
||||
class="w-120 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 完整提示词预览 -->
|
||||
<div>
|
||||
<label class="mb-1.5 flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<UIcon name="i-heroicons-eye" class="h-4 w-4 text-violet-500" />
|
||||
{{ t('settings.aiPrompt.modal.preview') }}
|
||||
<span class="font-normal text-gray-500">{{ t('settings.aiPrompt.modal.previewHint') }}</span>
|
||||
</label>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<pre class="whitespace-pre-wrap text-sm text-gray-700 dark:text-gray-300">{{ previewContent }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<UButton v-if="isBuiltIn && isModified" variant="outline" color="warning" @click="handleReset">
|
||||
<UIcon name="i-heroicons-arrow-path" class="mr-1 h-4 w-4" />
|
||||
{{ t('settings.aiPrompt.modal.resetToDefault') }}
|
||||
</UButton>
|
||||
<UButton variant="ghost" @click="closeModal">{{ t('common.cancel') }}</UButton>
|
||||
<UButton color="primary" :disabled="!canSave" @click="handleSave">
|
||||
{{ isEditMode ? t('settings.aiPrompt.modal.saveChanges') : t('settings.aiPrompt.modal.addPreset') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -1,183 +1,217 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { PromptPreset } from '@/types/ai'
|
||||
import AIPromptEditModal from './AIPromptEditModal.vue'
|
||||
import ImportPresetModal from './ImportPresetModal.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
|
||||
|
||||
interface LegacyPromptStoreData {
|
||||
customPromptPresets?: Array<{
|
||||
id?: string
|
||||
name?: string
|
||||
systemPrompt?: string
|
||||
applicableTo?: 'common' | 'group' | 'private'
|
||||
}>
|
||||
builtinPresetOverrides?: Record<string, { name?: string; systemPrompt?: string }>
|
||||
fetchedRemotePresetIds?: string[]
|
||||
aiPromptSettings?: { activePresetId?: string }
|
||||
activeGroupPresetId?: string
|
||||
activePrivatePresetId?: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// Store
|
||||
const promptStore = usePromptStore()
|
||||
const { allPromptPresets, aiPromptSettings } = storeToRefs(promptStore)
|
||||
const rawPromptStore = ref<LegacyPromptStoreData | null>(null)
|
||||
const rawPromptText = ref('')
|
||||
const parseError = ref('')
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'config-changed': []
|
||||
}>()
|
||||
/**
|
||||
* 旧版提示词已经不再参与运行,这里只保留原始数据查看与复制能力。
|
||||
*/
|
||||
function loadLegacyPromptStore() {
|
||||
const raw = localStorage.getItem('prompt')
|
||||
rawPromptText.value = raw || ''
|
||||
parseError.value = ''
|
||||
rawPromptStore.value = null
|
||||
|
||||
// 弹窗状态
|
||||
const showEditModal = ref(false)
|
||||
const showImportModal = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editingPreset = ref<PromptPreset | null>(null)
|
||||
if (!raw) return
|
||||
|
||||
/** 打开新增预设弹窗 */
|
||||
function openAddModal() {
|
||||
editMode.value = 'add'
|
||||
editingPreset.value = null
|
||||
showEditModal.value = true
|
||||
try {
|
||||
rawPromptStore.value = JSON.parse(raw) as LegacyPromptStoreData
|
||||
} catch (error) {
|
||||
parseError.value = String(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开编辑预设弹窗 */
|
||||
function openEditModal(preset: PromptPreset) {
|
||||
editMode.value = 'edit'
|
||||
editingPreset.value = preset
|
||||
showEditModal.value = true
|
||||
const hasLegacyPromptStore = computed(() => rawPromptText.value.trim().length > 0)
|
||||
|
||||
const customPromptPresets = computed(() => {
|
||||
return Array.isArray(rawPromptStore.value?.customPromptPresets) ? rawPromptStore.value!.customPromptPresets : []
|
||||
})
|
||||
|
||||
const remotePresetIds = computed(() => {
|
||||
return Array.isArray(rawPromptStore.value?.fetchedRemotePresetIds) ? rawPromptStore.value!.fetchedRemotePresetIds : []
|
||||
})
|
||||
|
||||
const activePresetId = computed(() => {
|
||||
const settings = rawPromptStore.value?.aiPromptSettings
|
||||
return (
|
||||
settings?.activePresetId ||
|
||||
rawPromptStore.value?.activeGroupPresetId ||
|
||||
rawPromptStore.value?.activePrivatePresetId ||
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
const formattedPromptStoreJson = computed(() => {
|
||||
if (!rawPromptText.value) return ''
|
||||
if (!rawPromptStore.value) return rawPromptText.value
|
||||
return JSON.stringify(rawPromptStore.value, null, 2)
|
||||
})
|
||||
|
||||
function getApplicableLabel(applicableTo?: 'common' | 'group' | 'private'): string {
|
||||
if (applicableTo === 'group') return t('settings.aiPrompt.legacyPrompt.groupOnly')
|
||||
if (applicableTo === 'private') return t('settings.aiPrompt.legacyPrompt.privateOnly')
|
||||
return t('settings.aiPrompt.legacyPrompt.common')
|
||||
}
|
||||
|
||||
/** 处理子弹窗保存后的回调 */
|
||||
function handleModalSaved() {
|
||||
emit('config-changed')
|
||||
async function handleCopyJson() {
|
||||
if (!formattedPromptStoreJson.value) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(formattedPromptStoreJson.value)
|
||||
toast.add({
|
||||
title: t('settings.aiPrompt.legacyPrompt.copySuccess'),
|
||||
color: 'primary',
|
||||
icon: 'i-heroicons-clipboard-document-check',
|
||||
duration: 2000,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: t('settings.aiPrompt.legacyPrompt.copyFailed'),
|
||||
description: String(error),
|
||||
color: 'error',
|
||||
icon: 'i-heroicons-x-circle',
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置当前激活的预设 */
|
||||
function setActivePreset(presetId: string) {
|
||||
promptStore.setActivePreset(presetId)
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
/** 复制选中的预设 */
|
||||
function duplicatePreset(presetId: string) {
|
||||
promptStore.duplicatePromptPreset(presetId)
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
/** 删除选中的预设 */
|
||||
function deletePreset(presetId: string) {
|
||||
promptStore.removePromptPreset(presetId)
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
/** 判断预设是否处于激活状态 */
|
||||
function isActivePreset(presetId: string): boolean {
|
||||
return aiPromptSettings.value.activePresetId === presetId
|
||||
}
|
||||
|
||||
/** 导入预设后的回调 */
|
||||
function handleImportPresetAdded() {
|
||||
emit('config-changed')
|
||||
}
|
||||
onMounted(() => {
|
||||
loadLegacyPromptStore()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 系统提示词标题和操作按钮 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-document-text" class="h-4 w-4 text-amber-500" />
|
||||
{{ t('settings.aiPrompt.presets.title') }}
|
||||
</h4>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton variant="ghost" color="gray" size="xs" @click="openAddModal">
|
||||
<UIcon name="i-heroicons-plus" class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t('settings.aiPrompt.presets.add') }}
|
||||
</UButton>
|
||||
<UButton variant="soft" color="primary" size="xs" @click="showImportModal = true">
|
||||
<UIcon name="i-heroicons-cloud-arrow-down" class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t('settings.aiPrompt.presets.import') }}
|
||||
</UButton>
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50/80 p-4 dark:border-amber-900/60 dark:bg-amber-950/20">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-100 text-amber-600 dark:bg-amber-900/40 dark:text-amber-300"
|
||||
>
|
||||
<UIcon name="i-heroicons-archive-box" class="h-4.5 w-4.5" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h4 class="text-sm font-semibold text-amber-900 dark:text-amber-100">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.title') }}
|
||||
</h4>
|
||||
<p class="mt-1 text-sm leading-6 text-amber-800 dark:text-amber-200">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预设列表 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton color="primary" size="xs" :disabled="!hasLegacyPromptStore" @click="handleCopyJson">
|
||||
<UIcon name="i-heroicons-document-duplicate" class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t('settings.aiPrompt.legacyPrompt.copyJson') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!hasLegacyPromptStore"
|
||||
class="rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-700 dark:bg-gray-800/50"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.emptyTitle') }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.emptyDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 这里只保留最必要的旧数据摘要,避免遗留信息继续分散注意力。 -->
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.aiPrompt.legacyPrompt.activePreset') }}</p>
|
||||
<p class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ activePresetId || t('settings.aiPrompt.legacyPrompt.notConfigured') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.customPresetCount') }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ customPromptPresets.length }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.remotePresetCount') }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ remotePresetIds.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="preset in allPromptPresets"
|
||||
:key="preset.id"
|
||||
class="group flex cursor-pointer items-center justify-between rounded-lg border p-2.5 transition-colors"
|
||||
:class="[
|
||||
isActivePreset(preset.id)
|
||||
? 'border-primary-300 bg-primary-50 dark:border-primary-700 dark:bg-primary-900/20'
|
||||
: 'border-gray-200 bg-white hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-900 dark:hover:bg-gray-800',
|
||||
]"
|
||||
@click="setActivePreset(preset.id)"
|
||||
v-if="parseError"
|
||||
class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-900/60 dark:bg-red-950/20"
|
||||
>
|
||||
<!-- 预设信息 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full"
|
||||
:class="[
|
||||
isActivePreset(preset.id)
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-200 text-gray-500 dark:bg-gray-700 dark:text-gray-400',
|
||||
]"
|
||||
>
|
||||
<UIcon
|
||||
:name="isActivePreset(preset.id) ? 'i-heroicons-check' : 'i-heroicons-document-text'"
|
||||
class="h-3 w-3"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-xs font-medium text-gray-900 dark:text-white">{{ preset.name }}</span>
|
||||
<UBadge v-if="preset.isBuiltIn" color="gray" variant="soft" size="xs">
|
||||
{{ t('settings.aiPrompt.preset.builtIn') }}
|
||||
</UBadge>
|
||||
<!-- 适用场景标签(仅非通用预设显示) -->
|
||||
<UBadge
|
||||
v-if="!preset.isBuiltIn && preset.applicableTo && preset.applicableTo !== 'common'"
|
||||
:color="preset.applicableTo === 'group' ? 'violet' : 'blue'"
|
||||
variant="soft"
|
||||
size="xs"
|
||||
>
|
||||
{{
|
||||
preset.applicableTo === 'group'
|
||||
? t('settings.aiPrompt.preset.groupOnly')
|
||||
: t('settings.aiPrompt.preset.privateOnly')
|
||||
}}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.parseError') }}
|
||||
</p>
|
||||
<p class="mt-1 break-all text-xs text-red-600 dark:text-red-400">{{ parseError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100" @click.stop>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
:icon="preset.isBuiltIn ? 'i-heroicons-eye' : 'i-heroicons-pencil-square'"
|
||||
@click="openEditModal(preset)"
|
||||
/>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-heroicons-document-duplicate"
|
||||
@click="duplicatePreset(preset.id)"
|
||||
/>
|
||||
<UButton
|
||||
v-if="!preset.isBuiltIn"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-heroicons-trash"
|
||||
@click="deletePreset(preset.id)"
|
||||
/>
|
||||
<div v-if="customPromptPresets.length > 0" class="space-y-3">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.customPresetList') }}
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="preset in customPromptPresets"
|
||||
:key="preset.id || preset.name || preset.systemPrompt"
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ preset.name || '-' }}</p>
|
||||
<UBadge color="gray" variant="soft" size="xs">
|
||||
{{ getApplicableLabel(preset.applicableTo) }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 whitespace-pre-wrap text-xs leading-6 text-gray-500 dark:text-gray-400">
|
||||
{{ preset.systemPrompt || t('settings.aiPrompt.legacyPrompt.noPromptContent') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 说明文字 -->
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.presets.description') }}
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.rawJsonTitle') }}
|
||||
</h4>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.rawJsonHint') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/70">
|
||||
<pre
|
||||
class="max-h-[360px] overflow-auto whitespace-pre-wrap break-all text-xs leading-6 text-gray-600 dark:text-gray-300"
|
||||
>{{ formattedPromptStoreJson }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 编辑/添加弹窗 -->
|
||||
<AIPromptEditModal v-model:open="showEditModal" :mode="editMode" :preset="editingPreset" @saved="handleModalSaved" />
|
||||
|
||||
<!-- 导入预设弹窗 -->
|
||||
<ImportPresetModal v-model:open="showImportModal" @preset-added="handleImportPresetAdded" />
|
||||
</template>
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePromptStore, type RemotePresetData } from '@/stores/prompt'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const promptStore = usePromptStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
'preset-added': []
|
||||
}>()
|
||||
|
||||
// 状态
|
||||
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(() => [
|
||||
{
|
||||
key: 'common',
|
||||
icon: 'i-heroicons-squares-2x2',
|
||||
iconColor: 'text-emerald-500',
|
||||
titleKey: 'settings.aiPrompt.importPreset.commonPresets',
|
||||
presets: remotePresets.value.filter((p) => p.chatType === 'common' || !p.chatType),
|
||||
},
|
||||
{
|
||||
key: 'group',
|
||||
icon: 'i-heroicons-chat-bubble-left-right',
|
||||
iconColor: 'text-violet-500',
|
||||
titleKey: 'settings.aiPrompt.importPreset.groupPresets',
|
||||
presets: remotePresets.value.filter((p) => p.chatType === 'group'),
|
||||
},
|
||||
{
|
||||
key: 'private',
|
||||
icon: 'i-heroicons-user',
|
||||
iconColor: 'text-blue-500',
|
||||
titleKey: 'settings.aiPrompt.importPreset.privatePresets',
|
||||
presets: remotePresets.value.filter((p) => p.chatType === 'private'),
|
||||
},
|
||||
])
|
||||
|
||||
// 加载远程预设索引(不下载内容)
|
||||
async function loadRemotePresets() {
|
||||
isLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const presets = await promptStore.fetchRemotePresets(locale.value)
|
||||
remotePresets.value = presets
|
||||
|
||||
if (presets.length === 0) {
|
||||
error.value = t('settings.aiPrompt.importPreset.noPresets')
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = t('settings.aiPrompt.importPreset.loadError')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 预览预设(按需下载内容)
|
||||
async function handlePreview(preset: RemotePresetData) {
|
||||
previewError.value = ''
|
||||
|
||||
if (preset.systemPrompt) {
|
||||
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('settings.aiPrompt.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.systemPrompt) {
|
||||
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()
|
||||
}
|
||||
|
||||
// 监听打开状态,打开时加载数据
|
||||
watch(
|
||||
() => props.open,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
loadRemotePresets()
|
||||
} else {
|
||||
closePreview()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" :ui="{ content: 'md:w-full max-w-lg' }" @update:open="emit('update:open', $event)">
|
||||
<template #content>
|
||||
<div class="p-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-cloud-arrow-down" class="h-5 w-5 text-primary-500" />
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.importPreset.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="closeModal" />
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.importPreset.description') }}
|
||||
</p>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="max-h-[400px] overflow-y-auto">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center py-12">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-8 w-8 animate-spin text-primary-500" />
|
||||
<p class="mt-2 text-sm text-gray-500">{{ t('settings.aiPrompt.importPreset.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="flex flex-col items-center justify-center py-12">
|
||||
<UIcon name="i-heroicons-exclamation-circle" class="h-8 w-8 text-red-500" />
|
||||
<p class="mt-2 text-sm text-gray-500">{{ error }}</p>
|
||||
<UButton variant="soft" size="sm" class="mt-4" @click="loadRemotePresets">
|
||||
{{ t('common.retry') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- 预设列表 -->
|
||||
<div v-else class="space-y-4">
|
||||
<!-- 预设分组 -->
|
||||
<div v-for="group in presetGroups" :key="group.key">
|
||||
<div v-if="group.presets.length > 0">
|
||||
<h4 class="mb-2 flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<UIcon :name="group.icon" class="h-4 w-4" :class="group.iconColor" />
|
||||
{{ t(group.titleKey) }}
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="preset in group.presets"
|
||||
:key="preset.id"
|
||||
class="flex items-center justify-between rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<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-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ preset.description || t('settings.aiPrompt.importPreset.noDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 ml-2 shrink-0">
|
||||
<!-- 预览按钮 -->
|
||||
<UButton color="gray" size="xs" @click="handlePreview(preset)">
|
||||
<UIcon name="i-heroicons-eye" class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t('settings.aiPrompt.importPreset.preview') }}
|
||||
</UButton>
|
||||
<!-- 添加按钮 -->
|
||||
<UButton
|
||||
v-if="promptStore.isRemotePresetAdded(preset.id)"
|
||||
variant="soft"
|
||||
color="gray"
|
||||
size="xs"
|
||||
disabled
|
||||
>
|
||||
<UIcon name="i-heroicons-check" class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t('settings.aiPrompt.importPreset.added') }}
|
||||
</UButton>
|
||||
<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('settings.aiPrompt.importPreset.add') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
<UModal :open="!!previewPreset" :ui="{ content: 'md:w-full max-w-2xl' }" @update:open="closePreview">
|
||||
<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('settings.aiPrompt.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?.systemPrompt" 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('settings.aiPrompt.importPreset.systemPrompt') }}
|
||||
</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.systemPrompt }}
|
||||
</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('common.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('settings.aiPrompt.importPreset.add') }}
|
||||
</UButton>
|
||||
<UButton v-else variant="soft" color="gray" disabled>
|
||||
<UIcon name="i-heroicons-check" class="mr-1 h-4 w-4" />
|
||||
{{ t('settings.aiPrompt.importPreset.added') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -95,9 +95,9 @@ void aiModelConfigRef.value
|
||||
<!-- 分隔线 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<!-- 提示词配置 -->
|
||||
<!-- 旧版提示词查看 -->
|
||||
<div :ref="(el) => setSectionRef('preset', el as HTMLElement)">
|
||||
<AIPromptPresetTab @config-changed="handleAIConfigChanged" />
|
||||
<AIPromptPresetTab />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,5 +2,4 @@ export { default as AISettingsTab } from './AISettingsTab.vue'
|
||||
export { default as AIModelConfigTab } from './AI/AIModelConfigTab.vue'
|
||||
export { default as AIModelEditModal } from './AI/AIModelEditModal.vue'
|
||||
export { default as AIPromptConfigTab } from './AI/AIPromptConfigTab.vue'
|
||||
export { default as AIPromptEditModal } from './AI/AIPromptEditModal.vue'
|
||||
export { default as CacheManageTab } from './CacheManageTab.vue'
|
||||
|
||||
@@ -221,7 +221,7 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
const settingsStore = useSettingsStore()
|
||||
const assistantStore = useAssistantStore()
|
||||
const skillStore = useSkillStore()
|
||||
const { activePreset, aiGlobalSettings } = storeToRefs(promptStore)
|
||||
const { aiGlobalSettings } = storeToRefs(promptStore)
|
||||
|
||||
function generateId(prefix: string): string {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
@@ -759,7 +759,6 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
preprocessConfig: serializablePreprocessConfig,
|
||||
}
|
||||
|
||||
const currentPromptConfig = { systemPrompt: activePreset.value.systemPrompt }
|
||||
const { requestId: agentReqId, promise: agentPromise } = window.agentApi.runStream(
|
||||
content,
|
||||
context,
|
||||
@@ -846,7 +845,6 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
}
|
||||
},
|
||||
state.chatType,
|
||||
currentAssistantId ? undefined : { systemPrompt: currentPromptConfig.systemPrompt },
|
||||
state.locale,
|
||||
maxHistoryRounds,
|
||||
currentAssistantId,
|
||||
|
||||
@@ -246,48 +246,6 @@ export const useAssistantStore = defineStore('assistant', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const promptMigrationDone = ref(false)
|
||||
|
||||
async function migrateOldPromptPresets(): Promise<void> {
|
||||
if (promptMigrationDone.value) return
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem('prompt')
|
||||
if (!raw) {
|
||||
promptMigrationDone.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const data = JSON.parse(raw)
|
||||
const hasCustomPresets = Array.isArray(data.customPromptPresets) && data.customPromptPresets.length > 0
|
||||
const hasOverrides = data.builtinPresetOverrides && Object.keys(data.builtinPresetOverrides).length > 0
|
||||
const hasRemoteIds = Array.isArray(data.fetchedRemotePresetIds) && data.fetchedRemotePresetIds.length > 0
|
||||
|
||||
if (!hasCustomPresets && !hasOverrides && !hasRemoteIds) {
|
||||
promptMigrationDone.value = true
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[AssistantStore] Backing up old prompt presets...')
|
||||
const result = await window.assistantApi.backupOldPresets({
|
||||
customPresets: data.customPromptPresets,
|
||||
builtinOverrides: data.builtinPresetOverrides,
|
||||
remotePresetIds: data.fetchedRemotePresetIds,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
console.log('[AssistantStore] Backup saved to:', result.filePath)
|
||||
} else {
|
||||
console.warn('[AssistantStore] Backup failed:', result.error)
|
||||
}
|
||||
|
||||
promptMigrationDone.value = true
|
||||
} catch (error) {
|
||||
console.error('[AssistantStore] Migration check failed:', error)
|
||||
promptMigrationDone.value = true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
assistants,
|
||||
selectedAssistantId,
|
||||
@@ -302,7 +260,6 @@ export const useAssistantStore = defineStore('assistant', () => {
|
||||
defaultAssistants,
|
||||
moreAssistants,
|
||||
hasMoreAssistants,
|
||||
promptMigrationDone,
|
||||
loadAssistants,
|
||||
loadBuiltinCatalog,
|
||||
loadBuiltinSqlTools,
|
||||
@@ -318,6 +275,5 @@ export const useAssistantStore = defineStore('assistant', () => {
|
||||
importAssistant,
|
||||
reimportAssistant,
|
||||
deleteAssistant,
|
||||
migrateOldPromptPresets,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user