feat: Enhance history management with versioning and chain tracking

- Implement comprehensive history management with record chains
- Add support for tracking prompt optimization and iteration versions
- Introduce new history types and interfaces for better record management
- Update HistoryDrawer to display versioned records with detailed information
- Integrate UUID for generating unique record and chain identifiers
- Improve history persistence and retrieval mechanisms
- Refactor history-related services and components to support new versioning system
This commit is contained in:
linshen committed 2025-02-11 23:25:17 +08:00
1 parent fc0d8955fc
commit 2e72d2613e
11 files changed
+995 -293

No files matched your search

+44 -4
View File
@@ -16,6 +16,7 @@
"element-plus": "^2.9.3",
"js-yaml": "^4.1.0",
"langchain": "^0.3.12",
"uuid": "^11.0.5",
"vue": "^3.5.13"
},
"devDependencies": {
@@ -830,6 +831,19 @@
"node": ">=18"
}
},
"node_modules/@langchain/core/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@langchain/google-genai": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@langchain/google-genai/-/google-genai-0.1.6.tgz",
@@ -3024,6 +3038,19 @@
}
}
},
"node_modules/langchain/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/langsmith": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.2.tgz",
@@ -3047,6 +3074,19 @@
}
}
},
"node_modules/langsmith/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -4467,16 +4507,16 @@
"license": "MIT"
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
+1
View File
@@ -21,6 +21,7 @@
"element-plus": "^2.9.3",
"js-yaml": "^4.1.0",
"langchain": "^0.3.12",
"uuid": "^11.0.5",
"vue": "^3.5.13"
},
"devDependencies": {
+134 -18
View File
@@ -72,8 +72,11 @@
v-model:optimized-prompt="optimizedPrompt"
:is-iterating="isIterating"
v-model:selected-iterate-template="selectedIterateTemplate"
:versions="currentVersions"
:current-version-id="currentVersionId"
@iterate="handleIteratePrompt"
@openTemplateManager="openTemplateManager"
@switchVersion="handleSwitchVersion"
/>
</div>
</div>
@@ -137,12 +140,13 @@
/>
</Teleport>
<!-- 历史记录抽屉 -->
<HistoryDrawer
<!-- 历史记录弹窗 -->
<HistoryModal
:show="showHistory"
:history="history"
@close="showHistory = false"
@reuse="handleSelectHistory"
@clear="handleClearHistory"
/>
<!-- 全局提示 -->
@@ -159,12 +163,14 @@ import { templateManager } from './services/template/manager'
import ModelManager from './components/ModelManager.vue'
import TemplateManager from './components/TemplateManager.vue'
import Toast from './components/Toast.vue'
import HistoryDrawer from './components/HistoryDrawer.vue'
import HistoryModal from './components/HistoryDrawer.vue'
import PromptPanel from './components/PromptPanel.vue'
import InputPanel from './components/InputPanel.vue'
import OutputPanel from './components/OutputPanel.vue'
import { useToast } from './composables/useToast'
import TemplateSelect from './components/TemplateSelect.vue'
import { historyManager } from './services/history/manager'
import { v4 as uuidv4 } from 'uuid'
// 初始化服务
const llmService = createLLMService(modelManager)
@@ -196,6 +202,9 @@ const selectedIterateTemplate = ref(null)
const history = ref([])
const models = ref([])
const outputPanelRef = ref(null)
const currentChainId = ref('')
const currentVersions = ref([])
const currentVersionId = ref('')
// 流式输出处理器
const streamHandler = {
@@ -270,17 +279,42 @@ const handleOptimizePrompt = async () => {
})
// 使用流式调用
await promptService.optimizePromptStream(
const result = await promptService.optimizePromptStream(
prompt.value,
optimizeModel.value,
selectedOptimizeTemplate.value.content, // 直接使用当前选中的提示词内容
selectedOptimizeTemplate.value.content,
{
onToken: (token) => {
optimizedPrompt.value += token
},
onComplete: () => {
// 更新历史记录
history.value = promptService.getHistory()
// 创建新记录链
console.log('创建新的历史记录链...')
const newRecord = historyManager.createNewChain({
id: uuidv4(),
originalPrompt: prompt.value,
optimizedPrompt: optimizedPrompt.value,
type: 'optimize',
modelKey: optimizeModel.value,
templateId: selectedOptimizeTemplate.value.id,
timestamp: Date.now(),
metadata: {}
});
console.log('新记录链创建成功:', {
chainId: newRecord.chainId,
record: newRecord
})
currentChainId.value = newRecord.chainId;
// 更新版本信息
currentVersions.value = newRecord.versions;
currentVersionId.value = newRecord.currentRecord.id;
history.value = historyManager.getAllChains();
console.log('历史记录已更新:', {
totalChains: history.value?.length
})
toast.success('优化成功')
isOptimizing.value = false
},
@@ -316,8 +350,7 @@ const handleIteratePrompt = async ({ originalPrompt, iterateInput }) => {
try {
// 使用流式调用
const template = selectedIterateTemplate.value;
await promptService.iteratePromptStream(
const result = await promptService.iteratePromptStream(
originalPrompt,
iterateInput,
optimizeModel.value,
@@ -326,15 +359,41 @@ const handleIteratePrompt = async ({ originalPrompt, iterateInput }) => {
optimizedPrompt.value += token; // 直接更新到 optimizedPrompt
},
onComplete: () => {
// 更新历史记录
history.value = promptService.getHistory()
// 添加迭代记录
console.log('添加迭代记录...', {
chainId: currentChainId.value,
originalPrompt,
iterateInput
})
const updatedChain = historyManager.addIteration({
chainId: currentChainId.value,
originalPrompt: originalPrompt,
optimizedPrompt: optimizedPrompt.value,
iterationNote: iterateInput,
modelKey: optimizeModel.value,
templateId: selectedIterateTemplate.value.id
});
console.log('迭代记录添加成功:', {
chain: updatedChain
})
// 更新版本信息
currentVersions.value = updatedChain.versions;
currentVersionId.value = updatedChain.currentRecord.id;
history.value = historyManager.getAllChains();
console.log('历史记录已更新:', {
totalChains: history.value?.length
})
toast.success('迭代优化成功')
},
onError: (error) => {
toast.error(error.message || '迭代优化失败')
}
},
template
selectedIterateTemplate.value
);
} catch (error) {
console.error('迭代优化失败:', error)
@@ -392,9 +451,38 @@ const handleTest = async () => {
};
const handleSelectHistory = (item) => {
prompt.value = item.prompt
optimizedPrompt.value = item.result
showHistory.value = false
// 如果是迭代记录,需要找到原始的提示词
if (item.type === 'iterate') {
// 从历史记录中找到对应的链
const chain = history.value.find(c => c.chainId === item.chainId);
if (chain) {
prompt.value = chain.rootRecord.originalPrompt;
currentVersions.value = chain.versions;
currentVersionId.value = item.id;
}
} else {
prompt.value = item.originalPrompt;
const chain = history.value.find(c => c.chainId === item.chainId);
if (chain) {
currentVersions.value = chain.versions;
currentVersionId.value = item.id;
}
}
optimizedPrompt.value = item.optimizedPrompt;
showHistory.value = false;
}
// 添加清空历史记录的处理函数
const handleClearHistory = async () => {
try {
await historyManager.clearHistory()
history.value = []
console.log('历史记录已清空')
toast.success('历史记录已清空')
} catch (error) {
console.error('清空历史记录失败:', error)
toast.error('清空历史记录失败')
}
}
// 保存提示词选择到本地存储
@@ -424,14 +512,34 @@ const openTemplateManager = (type = 'optimize') => {
showTemplates.value = true
}
// 添加版本切换处理函数
const handleSwitchVersion = (version) => {
optimizedPrompt.value = version.optimizedPrompt;
currentVersionId.value = version.id;
}
// 生命周期钩子
onMounted(async () => {
await initServices()
loadModels()
// 加载历史记录
if (promptService) {
history.value = promptService.getHistory()
// 初始化历史记录管理器
try {
console.log('初始化历史记录管理器...')
await historyManager.init()
history.value = historyManager.getAllChains()
console.log('历史记录加载完成:', {
recordCount: history.value?.length,
chains: history.value?.map(chain => ({
chainId: chain.chainId,
versionsCount: chain.versions.length,
rootRecord: chain.rootRecord,
currentRecord: chain.currentRecord
}))
})
} catch (error) {
console.error('加载历史记录失败:', error)
toast.error('加载历史记录失败')
}
// 初始化提示词选择
@@ -487,6 +595,14 @@ watch(showConfig, (newVal) => {
loadModels()
}
})
// 添加历史记录显示状态监听
watch(showHistory, (newVal) => {
console.log('历史记录显示状态变更:', {
show: newVal,
currentHistory: history.value?.length
})
})
</script>
<style>
+178 -39
View File
@@ -1,73 +1,212 @@
<template>
<div
v-if="show"
class="fixed inset-0 bg-black bg-opacity-50 z-45"
class="fixed inset-0 bg-black bg-opacity-50 z-45 flex items-center justify-center"
@click="$emit('close')"
>
<div
class="fixed right-0 top-[60px] sm:top-[68px] bottom-0 w-full max-w-md bg-gray-900/90 backdrop-blur-sm border-l border-purple-700/50 shadow-xl transition-transform duration-300 transform"
class="w-full max-w-4xl h-[90vh] bg-gray-900/90 backdrop-blur-sm border border-purple-700/50 shadow-xl rounded-lg transform transition-all duration-300 ease-in-out"
:class="show ? 'scale-100 opacity-100' : 'scale-95 opacity-0'"
@click.stop
>
<div class="h-full flex flex-col">
<div class="sticky top-0 z-10 p-4 sm:p-6 border-b border-purple-700/50 flex items-center justify-between bg-gray-900/95 backdrop-blur-sm">
<h2 class="text-lg font-semibold text-white/90">历史记录</h2>
<div class="flex-none p-4 sm:p-6 border-b border-purple-700/50 flex items-center justify-between bg-gray-900/95 backdrop-blur-sm rounded-t-lg">
<div class="flex items-center gap-4">
<h2 class="text-lg font-semibold text-white/90">历史记录</h2>
<button
v-if="history && history.length > 0"
@click.stop="handleClear"
class="text-sm text-white/60 hover:text-white/90 transition-colors px-2 py-1 rounded border border-white/20 hover:border-white/40"
>
清空
</button>
</div>
<button
@click="$emit('close')"
@click.stop="$emit('close')"
class="text-white/60 hover:text-white/90 transition-colors text-xl"
>
×
</button>
</div>
<div class="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4">
<div
v-for="item in history"
:key="item.id"
class="bg-black/20 rounded-xl border border-purple-600/50 p-4 space-y-3 hover:border-purple-500/70 transition-colors"
>
<div class="text-sm text-white/50">
{{ new Date(item.timestamp).toLocaleString() }}
</div>
<div class="space-y-2">
<div class="text-sm font-medium text-white/90">原始提示词:</div>
<div class="text-sm text-white/70 bg-black/20 rounded-lg p-3 border border-purple-600/20">
{{ item.prompt }}
</div>
</div>
<div class="space-y-2">
<div class="text-sm font-medium text-white/90">优化结果:</div>
<div class="text-sm text-white/70 bg-black/20 rounded-lg p-3 border border-purple-600/20 whitespace-pre-wrap">
{{ item.result }}
</div>
</div>
<div class="flex justify-end">
<button
@click="reuse(item)"
class="px-4 py-1.5 text-sm rounded-lg bg-purple-600/20 text-purple-300 hover:bg-purple-600/30 transition-colors"
<div class="flex-1 overflow-y-auto p-4 sm:p-6">
<template v-if="history && history.length > 0">
<div class="space-y-4">
<div
v-for="chain in history"
:key="chain.chainId"
class="bg-black/20 rounded-xl border border-purple-600/50 overflow-hidden"
>
重新使用
</button>
<!-- 历史记录头部信息 -->
<div class="p-4 border-b border-purple-600/30">
<div class="flex items-center justify-between mb-2">
<div class="text-sm text-white/50">
创建于 {{ formatDate(chain.rootRecord.timestamp) }}
</div>
</div>
<div class="text-sm text-white/90 break-all">
{{ chain.rootRecord.originalPrompt }}
</div>
</div>
<!-- 版本列表 -->
<div class="divide-y divide-purple-600/20">
<div
v-for="record in chain.versions.slice().reverse()"
:key="record.id"
class="relative"
>
<!-- 版本标题栏 -->
<div
class="p-3 flex items-center justify-between cursor-pointer hover:bg-purple-600/10 transition-colors"
@click="toggleVersion(record.id)"
>
<div class="flex items-center gap-3 overflow-hidden">
<span class="text-sm font-medium text-purple-300 flex-none">V{{ record.version }}</span>
<span class="text-xs text-white/50 flex-none">{{ formatDate(record.timestamp) }}</span>
<span v-if="record.type === 'iterate' && record.iterationNote" class="text-xs text-white/60 truncate">
- {{ truncateText(record.iterationNote, 30) }}
</span>
</div>
<div class="flex items-center gap-2 flex-none">
<span v-if="record.type === 'iterate'" class="text-xs text-purple-400 px-2 py-0.5 rounded-full bg-purple-400/10">迭代</span>
<button class="text-purple-300 hover:text-purple-100 transition-colors text-sm">
{{ expandedVersions[record.id] ? '收起' : '展开' }}
</button>
</div>
</div>
<!-- 版本详细内容 -->
<div
v-show="expandedVersions[record.id]"
class="p-4 bg-black/20 space-y-3"
>
<!-- 迭代说明 -->
<div v-if="record.iterationNote" class="text-xs text-purple-300">
迭代说明: <span class="text-white/60">{{ record.iterationNote }}</span>
</div>
<!-- 优化后的提示词 -->
<div class="space-y-1">
<div class="text-xs text-white/50">优化后:</div>
<div class="text-sm text-white/70 whitespace-pre-wrap">{{ record.optimizedPrompt }}</div>
</div>
<!-- 使用按钮 -->
<div class="flex justify-end">
<button
@click="reuse(record)"
class="text-xs text-purple-300 hover:text-purple-100 transition-colors px-3 py-1 rounded border border-purple-300/20 hover:border-purple-300/40"
>
使用此版本
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div class="flex flex-col items-center justify-center h-full py-12 text-white/60">
<div class="text-4xl mb-4">📜</div>
<div class="text-sm">暂无历史记录</div>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import type { PropType } from 'vue'
import type { PromptRecord, PromptRecordChain } from '../services/history/types'
import { useToast } from '../composables/useToast'
const props = defineProps({
show: Boolean,
history: {
type: Array,
type: Array as PropType<PromptRecordChain[]>,
default: () => []
}
})
const emit = defineEmits(['close', 'reuse'])
// 添加日志
console.log('HistoryDrawer props:', {
show: props.show,
historyLength: props.history?.length
})
const reuse = (item) => {
const emit = defineEmits<{
(e: 'close'): void
(e: 'reuse', item: PromptRecord): void
(e: 'clear'): void
}>()
const toast = useToast()
const expandedVersions = ref<Record<string, boolean>>({})
// 切换版本展开/收起状态
const toggleVersion = (recordId: string) => {
expandedVersions.value = {
...expandedVersions.value,
[recordId]: !expandedVersions.value[recordId]
}
}
// 清空历史记录
const handleClear = async () => {
if (confirm('确定要清空所有历史记录吗?此操作不可恢复。')) {
emit('clear')
toast.success('历史记录已清空')
}
}
// 监听历史记录变化
watch(() => props.history, (newHistory) => {
console.log('History updated:', {
length: newHistory?.length,
chains: newHistory?.map(chain => ({
chainId: chain.chainId,
versionsCount: chain.versions.length
}))
})
// 重置所有版本的展开状态为 false
expandedVersions.value = {}
console.log('展开状态已重置:', expandedVersions.value)
}, { immediate: true, deep: true })
// 监听显示状态变化
watch(() => props.show, (newShow) => {
console.log('HistoryDrawer visibility changed:', newShow)
if (!newShow) {
// 关闭时重置所有展开状态
expandedVersions.value = {}
}
})
onMounted(() => {
console.log('HistoryDrawer mounted')
})
const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleString()
}
const reuse = (item: PromptRecord) => {
emit('reuse', item)
emit('close')
}
</script>
// 添加文本截断函数
const truncateText = (text: string, maxLength: number) => {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...'
}
</script>
<style scoped>
/* 删除之前的样式,使用Tailwind的transform和transition类 */
</style>
+39 -2
View File
@@ -2,7 +2,24 @@
<div class="flex flex-col h-full">
<!-- 标题和按钮区域 -->
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-0 mb-3 flex-none">
<h3 class="text-lg font-semibold text-white/90">优化后的提示词</h3>
<div class="flex items-center gap-3">
<h3 class="text-lg font-semibold text-white/90">优化后的提示词</h3>
<div v-if="versions && versions.length > 0" class="flex items-center gap-1">
<button
v-for="version in versions.slice().reverse()"
:key="version.id"
@click="switchVersion(version)"
class="px-2 py-1 text-xs rounded transition-colors"
:class="[
currentVersionId === version.id
? 'bg-purple-600/30 text-purple-300 font-medium'
: 'text-white/50 hover:text-white/70'
]"
>
V{{ version.version }}
</button>
</div>
</div>
<div class="flex items-center space-x-4">
<button
v-if="optimizedPrompt"
@@ -106,10 +123,24 @@ const props = defineProps({
selectedIterateTemplate: {
type: Object,
default: null
},
versions: {
type: Array,
default: () => []
},
currentVersionId: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:optimizedPrompt', 'iterate', 'openTemplateManager', 'update:selectedIterateTemplate'])
const emit = defineEmits([
'update:optimizedPrompt',
'iterate',
'openTemplateManager',
'update:selectedIterateTemplate',
'switchVersion'
])
const showIterateInput = ref(false)
const iterateInput = ref('')
@@ -172,6 +203,12 @@ const submitIterate = () => {
iterateInput.value = ''
showIterateInput.value = false
}
// 添加版本切换函数
const switchVersion = (version) => {
if (version.id === props.currentVersionId) return
emit('switchVersion', version)
}
</script>
<style scoped>
+101 -5
View File
@@ -1,5 +1,6 @@
import { IHistoryManager, PromptRecord } from './types';
import { IHistoryManager, PromptRecord, PromptRecordChain } from './types';
import { HistoryError, RecordNotFoundError, StorageError, RecordValidationError } from './errors';
import { v4 as uuidv4 } from 'uuid';
/**
* 历史记录管理器实现
@@ -91,7 +92,7 @@ export class HistoryManager implements IHistoryManager {
}
records.splice(index, 1);
try {
this.saveToStorage(records);
localStorage.setItem(this.storageKey, JSON.stringify(records));
} catch (error) {
throw new StorageError('删除记录失败', 'delete');
}
@@ -112,7 +113,7 @@ export class HistoryManager implements IHistoryManager {
if (!record) break;
chain.unshift(record);
currentId = record.parentId ?? '';
currentId = record.previousId ?? '';
}
return chain;
@@ -142,9 +143,11 @@ export class HistoryManager implements IHistoryManager {
const errors: string[] = [];
if (!record.id) errors.push('缺少记录ID');
if (!record.prompt) errors.push('缺少提示词');
if (!record.result) errors.push('缺少结果');
if (!record.originalPrompt) errors.push('缺少原始提示词');
if (!record.optimizedPrompt) errors.push('缺少优化后的提示词');
if (!record.type) errors.push('缺少记录类型');
if (!record.chainId) errors.push('缺少记录链ID');
if (!record.version) errors.push('缺少版本号');
if (!record.timestamp) errors.push('缺少时间戳');
if (!record.modelKey) errors.push('缺少模型标识');
if (!record.templateId) errors.push('缺少提示词标识');
@@ -164,6 +167,99 @@ export class HistoryManager implements IHistoryManager {
throw new StorageError('保存历史记录失败', 'write');
}
}
/**
* 创建新的记录链(初始优化)
*/
createNewChain(record: Omit<PromptRecord, 'chainId' | 'version' | 'previousId'>): PromptRecordChain {
const chainId = uuidv4();
const newRecord: PromptRecord = {
...record,
chainId,
version: 1,
previousId: undefined
};
this.addRecord(newRecord);
return this.getChain(chainId);
}
/**
* 添加迭代记录
*/
addIteration(params: {
chainId: string;
originalPrompt: string;
optimizedPrompt: string;
iterationNote?: string;
modelKey: string;
templateId: string;
}): PromptRecordChain {
const chain = this.getChain(params.chainId);
const latest = chain.currentRecord;
const newRecord: PromptRecord = {
...params,
id: uuidv4(),
chainId: params.chainId,
type: 'iterate',
version: latest.version + 1,
previousId: latest.id,
timestamp: Date.now()
};
this.addRecord(newRecord);
return this.getChain(params.chainId);
}
/**
* 获取完整记录链
*/
getChain(chainId: string): PromptRecordChain {
const allRecords = this.getRecords();
const chainRecords = allRecords.filter(r => r.chainId === chainId);
if (chainRecords.length === 0) {
throw new RecordNotFoundError('记录链不存在', chainId);
}
const sorted = chainRecords.sort((a, b) => a.version - b.version);
return {
chainId,
rootRecord: sorted[0],
currentRecord: sorted[sorted.length - 1],
versions: sorted
};
}
/**
* 获取所有记录链
*/
getAllChains(): PromptRecordChain[] {
const chains = new Map<string, PromptRecord[]>();
const allRecords = this.getRecords();
// 按chainId分组
for (const record of allRecords) {
const chain = chains.get(record.chainId) || [];
chain.push(record);
chains.set(record.chainId, chain);
}
// 先按chainId排序,再按version排序
return Array.from(chains.entries())
.sort(([chainId1], [chainId2]) => chainId1.localeCompare(chainId2))
.map(([_, records]) => {
const sorted = records.sort((a, b) => a.version - b.version);
return {
chainId: sorted[0].chainId,
rootRecord: sorted[0],
currentRecord: sorted[sorted.length - 1],
versions: sorted
};
});
}
}
// 导出单例实例
+20 -6
View File
@@ -1,7 +1,7 @@
/**
* 提示词记录类型
*/
export type PromptRecordType = 'optimize' | 'iterate' | 'test';
export type PromptRecordType = 'optimize' | 'iterate';
/**
* 提示词记录接口
@@ -10,19 +10,25 @@ export interface PromptRecord {
/** 记录ID */
id: string;
/** 原始提示词 */
prompt: string;
/** 结果 */
result: string;
originalPrompt: string;
/** 优化/迭代后的提示词 */
optimizedPrompt: string;
/** 记录类型 */
type: PromptRecordType;
/** 父记录ID(用于迭代链) */
parentId?: string;
/** 所属的提示词链ID */
chainId: string;
/** 在链中的版本号 */
version: number;
/** 前一个版本ID */
previousId?: string;
/** 时间戳 */
timestamp: number;
/** 使用的模型key */
modelKey: string;
/** 使用的提示词ID */
templateId: string;
/** 迭代时的修改说明 */
iterationNote?: string;
/** 元数据 */
metadata?: Record<string, any>;
}
@@ -45,4 +51,12 @@ export interface IHistoryManager {
getIterationChain(recordId: string): PromptRecord[];
/** 清除所有记录 */
clearHistory(): void;
}
// 新增历史记录链类型
export interface PromptRecordChain {
chainId: string;
rootRecord: PromptRecord;
currentRecord: PromptRecord;
versions: PromptRecord[];
}
+28 -59
View File
@@ -100,13 +100,13 @@ export class PromptService implements IPromptService {
throw new OptimizationError(`优化失败: ${error.message}`, prompt);
}
if (!template?.template) {
if (!template?.content) {
throw new OptimizationError('优化失败: 提示词不存在或无效', prompt);
}
// 构建消息
const messages: Message[] = [
{ role: 'system', content: template.template },
{ role: 'system', content: template.content },
{ role: 'user', content: prompt }
];
@@ -122,9 +122,11 @@ export class PromptService implements IPromptService {
// 保存历史记录
this.historyManager.addRecord({
id: Date.now().toString(),
prompt,
result,
originalPrompt: prompt,
optimizedPrompt: result,
type: 'optimize',
chainId: Date.now().toString(),
version: 1,
timestamp: Date.now(),
modelKey,
templateId: 'optimize'
@@ -167,13 +169,13 @@ export class PromptService implements IPromptService {
throw new IterationError(`迭代失败: ${error.message}`, originalPrompt, iterateInput);
}
if (!template?.template) {
if (!template?.content) {
throw new IterationError('迭代失败: 提示词不存在或无效', originalPrompt, iterateInput);
}
// 构建消息
const messages: Message[] = [
{ role: 'system', content: template.template },
{ role: 'system', content: template.content },
{ role: 'user', content: `原始提示词:${originalPrompt}\n\n优化需求:${iterateInput}` }
];
@@ -186,10 +188,12 @@ export class PromptService implements IPromptService {
// 保存历史记录
this.historyManager.addRecord({
id: Date.now().toString(),
prompt: iterateInput,
result,
originalPrompt: iterateInput,
optimizedPrompt: result,
type: 'iterate',
parentId: originalPrompt,
chainId: originalPrompt,
version: 1,
previousId: originalPrompt,
timestamp: Date.now(),
modelKey,
templateId: 'iterate'
@@ -238,10 +242,11 @@ export class PromptService implements IPromptService {
// 保存历史记录
this.historyManager.addRecord({
id: Date.now().toString(),
prompt: testInput,
result,
type: 'test',
parentId: prompt,
originalPrompt: prompt,
optimizedPrompt: result,
type: 'optimize',
chainId: prompt,
version: 1,
timestamp: Date.now(),
modelKey,
templateId: 'test'
@@ -296,18 +301,8 @@ export class PromptService implements IPromptService {
];
await this.llmService.sendMessageStream(messages, modelKey, callbacks);
// 完成后保存历史记录
this.historyManager.addRecord({
id: Date.now().toString(),
prompt: testInput,
result: '流式响应', // 可以考虑在回调中收集完整响应
type: 'test',
parentId: prompt,
timestamp: Date.now(),
modelKey,
templateId: 'test'
});
// 移除历史记录相关操作
} catch (error) {
if (error instanceof TestError) {
throw error;
@@ -365,18 +360,6 @@ export class PromptService implements IPromptService {
onComplete: () => {
// 验证响应
this.validateResponse(result, prompt);
// 保存历史记录
this.historyManager.addRecord({
id: Date.now().toString(),
prompt,
result,
type: 'optimize',
timestamp: Date.now(),
modelKey,
templateId: 'optimize'
});
callbacks.onComplete();
},
onError: callbacks.onError
@@ -404,7 +387,7 @@ export class PromptService implements IPromptService {
iterateInput: string,
modelKey: string,
handlers: StreamHandlers,
template?: string
template: { content: string } | string
): Promise<void> {
try {
// 获取模型配置
@@ -414,22 +397,20 @@ export class PromptService implements IPromptService {
}
// 获取迭代提示词
let templateToUse;
if (template) {
templateToUse = template;
let templateContent: string;
if (typeof template === 'string') {
templateContent = template;
} else if (template && typeof template.content === 'string') {
templateContent = template.content;
} else {
templateToUse = await this.templateManager.getTemplate('iterate');
}
if (!templateToUse?.content) {
throw new IterationError('迭代失败: 提示词不存在或无效', originalPrompt, iterateInput);
throw new IterationError('迭代失败: 未提供有效的提示词模板', originalPrompt, iterateInput);
}
// 构建消息
const messages: Message[] = [
{
role: 'system',
content: templateToUse.content
content: templateContent
},
{ role: 'user', content: `原始提示词:${originalPrompt}\n\n优化需求:${iterateInput}` }
];
@@ -445,18 +426,6 @@ export class PromptService implements IPromptService {
handlers.onToken(token);
},
onComplete: () => {
// 保存历史记录
this.historyManager.addRecord({
id: Date.now().toString(),
prompt: iterateInput,
result,
type: 'iterate',
parentId: originalPrompt,
timestamp: Date.now(),
modelKey,
templateId: 'iterate'
});
handlers.onComplete();
},
onError: handlers.onError
+330 -63
View File
@@ -9,6 +9,8 @@ import { templateManager } from '../../src/services/template/manager'
import ElementPlus from 'element-plus'
import { createTestingPinia } from '@pinia/testing'
import { ElInput, ElButton, ElSelect, ElOption } from 'element-plus'
import { useToast } from '../../src/composables/useToast'
import { historyManager } from '../../src/services/history/manager'
// Mock toast functions
const mockSuccess = vi.fn()
@@ -43,12 +45,49 @@ const mockLLMService = {
const mockPromptService = {
optimizePrompt: vi.fn(() => Promise.resolve('Optimized prompt')),
optimizePromptStream: vi.fn(() => Promise.resolve()),
optimizePromptStream: vi.fn().mockImplementation(async (prompt, model, template, handlers) => {
handlers.onToken('优化后的')
handlers.onToken('提示词')
handlers.onComplete()
return {
chainId: 'test-chain-1',
currentRecord: {
id: '1',
originalPrompt: '测试提示词',
optimizedPrompt: '优化后的提示词',
type: 'optimize',
chainId: 'test-chain-1',
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
}
}
}),
iteratePrompt: vi.fn(() => Promise.resolve('Iterated prompt')),
iteratePromptStream: vi.fn(() => Promise.resolve()),
iteratePromptStream: vi.fn().mockImplementation(async (prompt, input, model, handlers) => {
handlers.onToken('迭代后的')
handlers.onToken('提示词')
handlers.onComplete()
return {
chainId: 'test-chain-1',
currentRecord: {
id: '2',
originalPrompt: prompt,
optimizedPrompt: '迭代后的提示词',
type: 'iterate',
chainId: 'test-chain-1',
version: 2,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-2',
previousId: '1'
}
}
}),
testPrompt: vi.fn(() => Promise.resolve('Test response')),
testPromptStream: vi.fn(() => Promise.resolve()),
getHistory: vi.fn(() => [])
getHistory: vi.fn().mockReturnValue([])
}
vi.mock('../../src/services/llm/service', () => ({
@@ -67,6 +106,17 @@ vi.mock('../../src/services/model/manager', () => ({
}
}))
// Mock historyManager
vi.mock('../../src/services/history/manager', () => ({
historyManager: {
init: vi.fn(),
clearHistory: vi.fn(),
createNewChain: vi.fn(),
addIteration: vi.fn(),
getAllChains: vi.fn()
}
}))
// Mock components
const createComponent = (name, template = '<div><slot></slot></div>') => ({
name,
@@ -92,6 +142,7 @@ const createComponent = (name, template = '<div><slot></slot></div>') => ({
describe('App.vue', () => {
let wrapper
let mockPromptService
beforeEach(async () => {
// 重置所有mock
@@ -103,9 +154,86 @@ describe('App.vue', () => {
mockStreamHandler.onComplete.mockClear()
mockStreamHandler.onError.mockClear()
// 初始化提示词管理器
// 初始化提示词管理器和历史记录管理器
await templateManager.init()
await historyManager.init() // 先初始化
await historyManager.clearHistory() // 再清理历史记录
mockPromptService = {
optimizePromptStream: vi.fn().mockImplementation(async (prompt, model, template, handlers) => {
handlers.onToken('优化后的')
handlers.onToken('提示词')
handlers.onComplete()
return {
chainId: 'test-chain-1',
currentRecord: {
id: '1',
originalPrompt: '测试提示词',
optimizedPrompt: '优化后的提示词',
type: 'optimize',
chainId: 'test-chain-1',
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
}
}
}),
iteratePromptStream: vi.fn().mockImplementation(async (prompt, input, model, handlers) => {
handlers.onToken('迭代后的')
handlers.onToken('提示词')
handlers.onComplete()
return {
chainId: 'test-chain-1',
currentRecord: {
id: '2',
originalPrompt: prompt,
optimizedPrompt: '迭代后的提示词',
type: 'iterate',
chainId: 'test-chain-1',
version: 2,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-2',
previousId: '1'
}
}
}),
testPromptStream: vi.fn().mockImplementation(async (prompt, input, model, handlers) => {
handlers.onToken('测试')
handlers.onToken('结果')
handlers.onComplete()
return {
chainId: 'test-chain-1',
currentRecord: {
id: '3',
currentPrompt: input,
optimizedPrompt: '测试结果',
type: 'test',
chainId: prompt,
version: 1,
timestamp: Date.now(),
modelKey: model,
templateId: 'test'
}
}
}),
getHistory: vi.fn().mockReturnValue([
{
id: '1',
currentPrompt: '测试提示词',
optimizedPrompt: '优化后的提示词',
type: 'optimize',
chainId: 'test-chain-1',
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
}
])
}
createPromptService.mockResolvedValue(mockPromptService)
// 设置组件
wrapper = mount(App, {
global: {
@@ -146,68 +274,79 @@ describe('App.vue', () => {
it('应该正确处理提示词优化', async () => {
const testPrompt = '测试提示词'
const optimizedPrompt = '优化后的提示词'
const mockHistory = [{ id: '1', prompt: testPrompt, result: optimizedPrompt }]
// 设置提示词和提示词
wrapper.vm.prompt = testPrompt
wrapper.vm.selectedTemplate = {
id: 'optimize', // 使用正确的提示词ID
name: '提示词优化',
content: 'template content'
const chainId = 'test-chain-1'
const recordId = '1'
const mockRecord = {
id: recordId,
originalPrompt: testPrompt,
optimizedPrompt: optimizedPrompt,
type: 'optimize',
chainId,
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
}
// 设置模拟返回值
wrapper.vm.prompt = testPrompt
wrapper.vm.optimizeModel = 'test-model'
wrapper.vm.selectedOptimizeTemplate = {
id: 'template-1',
content: '优化模板'
}
// Mock historyManager
historyManager.createNewChain.mockReturnValue({
chainId,
rootRecord: mockRecord,
currentRecord: mockRecord,
versions: [mockRecord]
})
historyManager.getAllChains.mockReturnValue([{
chainId,
rootRecord: mockRecord,
currentRecord: mockRecord,
versions: [mockRecord]
}])
mockPromptService.optimizePromptStream.mockImplementation(async (prompt, model, template, handlers) => {
handlers.onToken(optimizedPrompt)
await nextTick()
handlers.onComplete()
return {
chainId,
currentRecord: mockRecord
}
})
mockPromptService.getHistory.mockReturnValue(mockHistory)
// 触发提交
const inputPanel = wrapper.findComponent({ name: 'InputPanel' })
await inputPanel.vm.$emit('submit')
await wrapper.vm.handleOptimizePrompt()
// 等待异步操作完成
await nextTick()
await nextTick()
// 验证服务调用
expect(mockPromptService.optimizePromptStream).toHaveBeenCalled()
// 验证结果更新
expect(wrapper.vm.optimizedPrompt).toBe(optimizedPrompt)
// 验证成功提示
expect(mockSuccess).toHaveBeenCalledWith('优化成功')
})
it('应该处理优化失败的情况', async () => {
const testPrompt = '测试提示词'
const errorMessage = '优化失败'
mockPromptService.optimizePromptStream.mockImplementationOnce(
async (prompt, model, template, handlers) => {
handlers.onError(new Error(errorMessage))
throw new Error(errorMessage)
}
)
// 设置提示词和提示词
wrapper.vm.prompt = testPrompt
wrapper.vm.selectedTemplate = {
id: 'optimize', // 使用正确的提示词ID
name: '提示词优化',
content: 'template content'
wrapper.vm.optimizeModel = 'test-model'
wrapper.vm.selectedOptimizeTemplate = {
id: 'template-1',
content: '优化模板'
}
// 设置模拟错误
mockPromptService.optimizePromptStream.mockImplementation(async (prompt, model, template, handlers) => {
handlers.onError(new Error(errorMessage))
})
// 触发提交
const inputPanel = wrapper.findComponent({ name: 'InputPanel' })
await inputPanel.vm.$emit('submit')
await wrapper.vm.handleOptimizePrompt()
// 等待异步操作
await nextTick()
// 验证错误状态
expect(wrapper.vm.isOptimizing).toBe(false)
expect(mockError).toHaveBeenCalledWith(errorMessage)
})
})
@@ -274,66 +413,194 @@ describe('App.vue', () => {
it('应该在优化成功后立即更新历史记录', async () => {
const testPrompt = '测试提示词'
const optimizedPrompt = '优化后的提示词'
const mockHistory = [{ id: '1', prompt: testPrompt, result: optimizedPrompt }]
// 设置提示词和提示词
const chainId = 'test-chain-1'
const recordId = '1'
const mockRecord = {
id: recordId,
originalPrompt: testPrompt,
optimizedPrompt: optimizedPrompt,
type: 'optimize',
chainId,
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
}
// 设置提示词和模板
wrapper.vm.prompt = testPrompt
wrapper.vm.selectedTemplate = {
id: 'optimize', // 使用正确的提示词ID
wrapper.vm.selectedOptimizeTemplate = {
id: 'template-1',
name: '提示词优化',
content: 'template content'
}
// Mock historyManager
historyManager.createNewChain.mockReturnValue({
chainId,
rootRecord: mockRecord,
currentRecord: mockRecord,
versions: [mockRecord]
})
historyManager.getAllChains.mockReturnValue([{
chainId,
rootRecord: mockRecord,
currentRecord: mockRecord,
versions: [mockRecord]
}])
// 设置模拟返回值
mockPromptService.optimizePromptStream.mockImplementation(async (prompt, model, template, handlers) => {
handlers.onToken(optimizedPrompt)
await nextTick()
handlers.onComplete()
return {
chainId,
currentRecord: mockRecord
}
})
mockPromptService.getHistory.mockReturnValue(mockHistory)
// 触发优化
const inputPanel = wrapper.findComponent({ name: 'InputPanel' })
await inputPanel.vm.$emit('submit')
await wrapper.vm.handleOptimizePrompt()
// 等待异步操作
await nextTick()
await nextTick()
// 验证历史记录更新
expect(mockPromptService.getHistory).toHaveBeenCalled()
expect(wrapper.vm.history).toEqual(mockHistory)
expect(historyManager.createNewChain).toHaveBeenCalled()
expect(historyManager.getAllChains).toHaveBeenCalled()
expect(wrapper.vm.history[0].chainId).toBe(chainId)
expect(wrapper.vm.history[0].currentRecord.id).toBe(recordId)
expect(wrapper.vm.history[0].currentRecord.originalPrompt).toBe(testPrompt)
expect(wrapper.vm.history[0].currentRecord.optimizedPrompt).toBe(optimizedPrompt)
})
it('应该在迭代优化成功后立即更新历史记录', async () => {
const originalPrompt = '测试提示词'
const iterateInput = '迭代输入'
const iteratedPrompt = '迭代后的提示词'
const mockHistory = [{ id: '1', prompt: originalPrompt, result: iteratedPrompt }]
const chainId = 'test-chain-1'
const recordId = '2'
const previousId = '1'
const mockRecord = {
id: recordId,
originalPrompt: originalPrompt,
optimizedPrompt: iteratedPrompt,
type: 'iterate',
chainId,
version: 2,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-2',
previousId
}
// 设置当前链ID和迭代模板
wrapper.vm.currentChainId = chainId
wrapper.vm.selectedIterateTemplate = {
id: 'template-2',
name: '迭代优化',
content: 'template content'
}
// Mock historyManager
historyManager.addIteration.mockReturnValue({
chainId,
rootRecord: {
id: previousId,
originalPrompt: originalPrompt,
optimizedPrompt: '优化后的提示词',
type: 'optimize',
chainId,
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
},
currentRecord: mockRecord,
versions: [
{
id: previousId,
originalPrompt: originalPrompt,
optimizedPrompt: '优化后的提示词',
type: 'optimize',
chainId,
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
},
mockRecord
]
})
historyManager.getAllChains.mockReturnValue([{
chainId,
rootRecord: {
id: previousId,
originalPrompt: originalPrompt,
optimizedPrompt: '优化后的提示词',
type: 'optimize',
chainId,
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
},
currentRecord: mockRecord,
versions: [
{
id: previousId,
originalPrompt: originalPrompt,
optimizedPrompt: '优化后的提示词',
type: 'optimize',
chainId,
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-1'
},
mockRecord
]
}])
mockPromptService.iteratePromptStream.mockImplementation(async (prompt, input, model, handlers) => {
handlers.onToken(iteratedPrompt)
handlers.onComplete()
return {
chainId,
currentRecord: mockRecord
}
})
mockPromptService.getHistory.mockReturnValue(mockHistory)
// 触发迭代
const promptPanel = wrapper.findComponent({ name: 'PromptPanel' })
await promptPanel.vm.$emit('iterate', { originalPrompt, iterateInput })
await wrapper.vm.handleIteratePrompt({ originalPrompt, iterateInput })
// 等待异步操作
await nextTick()
await nextTick()
// 验证历史记录更新
expect(mockPromptService.getHistory).toHaveBeenCalled()
expect(wrapper.vm.history).toEqual(mockHistory)
expect(historyManager.addIteration).toHaveBeenCalled()
expect(historyManager.getAllChains).toHaveBeenCalled()
expect(wrapper.vm.history[0].chainId).toBe(chainId)
expect(wrapper.vm.history[0].currentRecord.id).toBe(recordId)
expect(wrapper.vm.history[0].currentRecord.originalPrompt).toBe(originalPrompt)
expect(wrapper.vm.history[0].currentRecord.optimizedPrompt).toBe(iteratedPrompt)
expect(wrapper.vm.history[0].currentRecord.previousId).toBe(previousId)
})
})
// 清理
afterEach(async () => {
wrapper.unmount()
// 清理提示词管理器
templateManager.clearCache()
if (wrapper) {
wrapper.unmount()
}
// 清理提示词管理器和历史记录管理器
await templateManager.clearCache()
await historyManager.init() // 先初始化
await historyManager.clearHistory() // 再清理历史记录
})
})
+95 -76
View File
@@ -8,13 +8,28 @@ describe('HistoryManager', () => {
let mockStorage: Record<string, any> = {};
const mockRecord: PromptRecord = {
id: 'test-1',
prompt: 'Test prompt',
timestamp: Date.now(),
id: '1',
originalPrompt: 'test prompt',
optimizedPrompt: 'optimized prompt',
type: 'optimize',
result: 'Test result',
chainId: 'test-chain',
version: 1,
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'optimize'
templateId: 'template-1'
};
const mockIterationRecord: PromptRecord = {
id: '2',
originalPrompt: 'iteration input',
optimizedPrompt: 'iterated prompt',
type: 'iterate',
chainId: 'test-chain',
version: 2,
previousId: '1',
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'template-2'
};
beforeEach(async () => {
@@ -45,134 +60,138 @@ describe('HistoryManager', () => {
});
it('should throw error when adding invalid record', () => {
const invalidRecord = {
prompt: 'Test prompt',
timestamp: Date.now(),
type: 'optimize',
result: 'Test result',
modelKey: 'test-model',
templateId: 'optimize'
};
expect(() => manager.addRecord(invalidRecord as PromptRecord))
.toThrow(HistoryError);
const invalidRecord = { ...mockRecord, id: undefined };
expect(() => manager.addRecord(invalidRecord as any)).toThrow('记录验证失败');
});
it('should limit the number of records', () => {
const maxRecords = 50;
for (let i = 0; i < maxRecords + 10; i++) {
for (let i = 0; i < 60; i++) {
manager.addRecord({
...mockRecord,
id: `test-${i + 1}`
id: `${i}`,
timestamp: Date.now() + i
});
}
expect(manager.getRecords()).toHaveLength(maxRecords);
const records = manager.getRecords();
expect(records).toHaveLength(50);
});
it('should handle storage errors', () => {
vi.mocked(localStorage.setItem).mockImplementation(() => {
vi.spyOn(localStorage, 'setItem').mockImplementation(() => {
throw new Error('Storage error');
});
expect(() => manager.addRecord(mockRecord))
.toThrow(StorageError);
expect(() => manager.addRecord(mockRecord)).toThrow('保存历史记录失败');
});
});
describe('getRecord', () => {
beforeEach(() => {
manager.addRecord(mockRecord);
});
it('should get existing record', () => {
const record = manager.getRecord('test-1');
manager.addRecord(mockRecord);
const record = manager.getRecord(mockRecord.id);
expect(record).toEqual(mockRecord);
});
it('should throw error when record not found', () => {
expect(() => manager.getRecord('invalid'))
.toThrow(RecordNotFoundError);
expect(() => manager.getRecord('non-existent')).toThrow('记录不存在');
});
});
describe('deleteRecord', () => {
beforeEach(() => {
manager.addRecord(mockRecord);
});
it('should delete existing record', () => {
manager.deleteRecord('test-1');
expect(manager.getRecords()).toHaveLength(0);
manager.addRecord(mockRecord);
manager.deleteRecord(mockRecord.id);
expect(() => manager.getRecord(mockRecord.id)).toThrow('记录不存在');
});
it('should throw error when deleting non-existent record', () => {
expect(() => manager.deleteRecord('invalid'))
.toThrow(RecordNotFoundError);
expect(() => manager.deleteRecord('non-existent')).toThrow('记录不存在');
});
it('should handle storage errors', () => {
vi.mocked(localStorage.setItem).mockImplementation(() => {
// 先正常添加记录
manager.addRecord(mockRecord);
// 然后模拟删除时的存储错误
const setItemSpy = vi.spyOn(localStorage, 'setItem').mockImplementation(() => {
throw new Error('Storage error');
});
expect(() => manager.deleteRecord('test-1'))
.toThrow(StorageError);
expect(() => manager.deleteRecord(mockRecord.id)).toThrow('删除记录失败');
// 清理 spy
setItemSpy.mockRestore();
});
});
describe('getIterationChain', () => {
beforeEach(() => {
manager.addRecord({
...mockRecord,
id: 'record-1'
});
manager.addRecord({
...mockRecord,
id: 'record-2',
parentId: 'record-1'
});
manager.addRecord({
...mockRecord,
id: 'record-3',
parentId: 'record-2'
});
});
it('should get complete iteration chain', () => {
const chain = manager.getIterationChain('record-3');
expect(chain).toHaveLength(3);
expect(chain[0].id).toBe('record-1');
expect(chain[1].id).toBe('record-2');
expect(chain[2].id).toBe('record-3');
manager.addRecord(mockRecord);
manager.addRecord(mockIterationRecord);
const chain = manager.getIterationChain(mockIterationRecord.id);
expect(chain).toHaveLength(2);
expect(chain[0]).toEqual(mockRecord);
expect(chain[1]).toEqual(mockIterationRecord);
});
it('should handle broken chains gracefully', () => {
const chain = manager.getIterationChain('record-2');
expect(chain).toHaveLength(2);
expect(chain[0].id).toBe('record-1');
expect(chain[1].id).toBe('record-2');
manager.addRecord(mockIterationRecord);
const chain = manager.getIterationChain(mockIterationRecord.id);
expect(chain).toHaveLength(1);
expect(chain[0]).toEqual(mockIterationRecord);
});
it('should return single record for no parent', () => {
const chain = manager.getIterationChain('record-1');
it('should return single record for no previous', () => {
manager.addRecord(mockRecord);
const chain = manager.getIterationChain(mockRecord.id);
expect(chain).toHaveLength(1);
expect(chain[0].id).toBe('record-1');
expect(chain[0]).toEqual(mockRecord);
});
});
describe('clearHistory', () => {
beforeEach(() => {
manager.addRecord(mockRecord);
});
it('should clear all records', () => {
manager.addRecord(mockRecord);
manager.clearHistory();
expect(manager.getRecords()).toHaveLength(0);
});
it('should handle storage errors', () => {
vi.mocked(localStorage.removeItem).mockImplementation(() => {
vi.spyOn(localStorage, 'removeItem').mockImplementation(() => {
throw new Error('Storage error');
});
expect(() => manager.clearHistory())
.toThrow(StorageError);
expect(() => manager.clearHistory()).toThrow('清除历史记录失败');
});
});
describe('getChain', () => {
it('should get chain by chainId', () => {
manager.addRecord(mockRecord);
manager.addRecord(mockIterationRecord);
const chain = manager.getChain(mockRecord.chainId);
expect(chain.chainId).toBe(mockRecord.chainId);
expect(chain.rootRecord).toEqual(mockRecord);
expect(chain.currentRecord).toEqual(mockIterationRecord);
expect(chain.versions).toHaveLength(2);
});
it('should throw error when chain not found', () => {
expect(() => manager.getChain('non-existent')).toThrow('记录链不存在');
});
});
describe('getAllChains', () => {
it('should get all chains', () => {
manager.addRecord(mockRecord);
manager.addRecord(mockIterationRecord);
const chains = manager.getAllChains();
expect(chains).toHaveLength(1);
expect(chains[0].chainId).toBe(mockRecord.chainId);
expect(chains[0].versions).toHaveLength(2);
});
it('should return empty array when no chains exist', () => {
const chains = manager.getAllChains();
expect(chains).toHaveLength(0);
});
});
});
+25 -21
View File
@@ -9,6 +9,7 @@ import { Template } from '../../../../src/services/template/types';
import { PromptRecord } from '../../../../src/services/history/types';
import { LLMService } from '../../../../src/services/llm/service';
import { OptimizationError, IterationError, TestError } from '../../../../src/services/prompt/errors';
import { v4 as uuidv4 } from 'uuid';
// 模拟 fetch API
const mockFetch = vi.fn();
@@ -42,8 +43,7 @@ describe('PromptService', () => {
const mockTemplate: Template = {
id: 'test',
name: 'Test Template',
template: 'test template content',
description: 'test template',
content: 'test template content',
version: '1.0'
};
@@ -114,7 +114,7 @@ describe('PromptService', () => {
it('当提示词内容为空时应抛出错误', async () => {
const emptyTemplate = {
...mockTemplate,
template: ''
content: ''
};
vi.spyOn(templateManager, 'getTemplate').mockResolvedValue(emptyTemplate);
@@ -163,12 +163,14 @@ describe('PromptService', () => {
it('应该返回历史记录', () => {
const mockHistory: PromptRecord[] = [{
id: '1',
prompt: 'test prompt',
result: 'test result',
originalPrompt: 'test prompt',
optimizedPrompt: 'test result',
type: 'optimize',
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'test'
templateId: 'test',
chainId: 'test-chain',
version: 1
}];
vi.spyOn(historyManager, 'getRecords').mockReturnValue(mockHistory);
@@ -181,12 +183,14 @@ describe('PromptService', () => {
it('应该返回迭代链', () => {
const mockChain: PromptRecord[] = [{
id: '1',
prompt: 'test prompt',
result: 'test result',
originalPrompt: 'test prompt',
optimizedPrompt: 'test result',
type: 'iterate',
timestamp: Date.now(),
modelKey: 'test-model',
templateId: 'test'
templateId: 'test',
chainId: 'test-chain',
version: 1
}];
vi.spyOn(historyManager, 'getIterationChain').mockReturnValue(mockChain);
@@ -244,8 +248,8 @@ describe('PromptService', () => {
expect(addRecordSpy).toHaveBeenCalledWith(expect.objectContaining({
type: 'optimize',
prompt: 'test prompt',
result: '优化结果',
originalPrompt: 'test prompt',
optimizedPrompt: '优化结果',
modelKey: 'test-model'
}));
});
@@ -259,10 +263,11 @@ describe('PromptService', () => {
expect(addRecordSpy).toHaveBeenCalledWith(expect.objectContaining({
type: 'iterate',
prompt: 'iteration input',
result: '迭代结果',
originalPrompt: 'iteration input',
optimizedPrompt: '迭代结果',
modelKey: 'test-model',
parentId: 'original'
chainId: 'original',
previousId: 'original'
}));
});
@@ -273,11 +278,11 @@ describe('PromptService', () => {
await promptService.testPrompt('test prompt', 'test input', 'test-model');
expect(addRecordSpy).toHaveBeenCalledWith(expect.objectContaining({
type: 'test',
prompt: 'test input',
result: '测试结果',
type: 'optimize',
originalPrompt: 'test prompt',
optimizedPrompt: '测试结果',
modelKey: 'test-model',
parentId: 'test prompt'
chainId: 'test prompt'
}));
});
});
@@ -303,8 +308,7 @@ describe('PromptService 提示词管理器初始化测试', () => {
const mockTemplate: Template = {
id: 'test',
name: 'Test Template',
template: 'test template content',
description: 'test template',
content: 'test template content',
version: '1.0'
};
@@ -353,7 +357,7 @@ describe('PromptService 提示词管理器初始化测试', () => {
it('提示词管理器正确初始化但提示词内容为空时应抛出错误', async () => {
const emptyTemplate = {
...mockTemplate,
template: ''
content: ''
};
vi.spyOn(templateManager, 'getTemplate').mockResolvedValue(emptyTemplate);
promptService = new PromptService(modelManager, llmService, templateManager, historyManager);