feat: 新增文件存储支持与历史记录管理优化
- 在`.gitignore`中添加了`prompt-optimizer-datas`以排除相关数据文件。 - 更新`INDEX.md`,新增桌面版文件存储实现的相关信息,确保文档的完整性。 - 在`index.ts`中导出了`FileStorageProvider`,支持文件存储功能。 - 修改`factory.ts`,新增对文件存储类型的支持,并优化错误处理逻辑。 - 在集成测试中模拟了历史记录的保存与验证,确保功能的正确性。 - 更新了多个测试用例,确保历史记录管理逻辑的准确性与一致性。 这些变更旨在提升数据存储的灵活性与历史记录管理的可靠性,确保用户体验的优化。
This commit is contained in:
1 parent
93ec7d4581
commit
8a35c010cc
16 files changed
+1425
-209
No files matched your search
@@ -176,3 +176,5 @@ storybook-static
|
||||
.prettierrc.js.bak
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
prompt-optimizer-datas
|
||||
@@ -0,0 +1,83 @@
|
||||
# 114-桌面版文件存储实现
|
||||
|
||||
## 📋 概述
|
||||
|
||||
实现桌面版从内存存储到文件存储的完整切换,为桌面应用提供可靠的数据持久化解决方案。
|
||||
|
||||
## 🏗️ 核心成果
|
||||
|
||||
### FileStorageProvider 实现
|
||||
- 完全兼容 `IStorageProvider` 接口,一行代码完成切换
|
||||
- 延迟写入策略 (500ms) + 内存缓存,性能优异
|
||||
- 原子写入操作,确保数据完整性
|
||||
- 应用退出前自动保存数据
|
||||
|
||||
### 存储路径设计
|
||||
根据用户偏好,采用可执行文件同级目录存储:
|
||||
|
||||
```typescript
|
||||
// 路径设置逻辑
|
||||
if (app.isPackaged) {
|
||||
// 生产环境:可执行文件目录/prompt-optimizer-data/
|
||||
const execDir = path.dirname(process.execPath);
|
||||
userDataPath = path.join(execDir, 'prompt-optimizer-data');
|
||||
} else {
|
||||
// 开发环境:项目根目录/prompt-optimizer-data/
|
||||
userDataPath = path.join(__dirname, '..', '..', 'prompt-optimizer-data');
|
||||
}
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- ✅ 便于管理和查找数据文件
|
||||
- ✅ 数据与应用在同一位置,便于备份迁移
|
||||
- ✅ 目录名明确标识,避免与其他应用混淆
|
||||
|
||||
### 架构集成
|
||||
```typescript
|
||||
// 简单的一行切换
|
||||
// const storage = StorageFactory.create('memory') // 旧方式
|
||||
const storage = new FileStorageProvider(userDataPath) // 新方式
|
||||
```
|
||||
|
||||
## ✅ 验证结果
|
||||
|
||||
### 测试覆盖
|
||||
- **单元测试**: 18/18 通过 (Mock文件系统)
|
||||
- **集成测试**: 12/12 通过 (真实文件操作)
|
||||
- **性能基准**: 写入4ms,读取0ms (内存缓存)
|
||||
|
||||
### 实际验证
|
||||
- ✅ 桌面版本成功启动
|
||||
- ✅ 自动创建 `prompt-optimizer-data/storage.json` 文件
|
||||
- ✅ 数据持久化正常工作
|
||||
- ✅ 应用重启后配置和历史记录保持
|
||||
|
||||
## 🔧 技术特性
|
||||
|
||||
- **延迟写入**: 正常操作延迟500ms,批量操作立即写入
|
||||
- **原子操作**: 临时文件写入 → 验证 → 重命名替换
|
||||
- **错误恢复**: 文件损坏时自动创建新存储
|
||||
- **退出保护**: 应用退出前强制保存所有数据
|
||||
|
||||
## 📊 项目价值
|
||||
|
||||
### 用户价值
|
||||
- **数据安全**: 用户数据得到可靠的持久化保护
|
||||
- **使用体验**: 应用重启后数据保持,提升用户体验
|
||||
- **功能完整**: 桌面版功能与Web版对等
|
||||
|
||||
### 技术价值
|
||||
- **架构完善**: 为桌面应用提供了完整的存储解决方案
|
||||
- **接口设计**: 良好的抽象层设计让存储切换变得简单
|
||||
- **性能优化**: 实现了高性能的文件存储机制
|
||||
|
||||
---
|
||||
|
||||
## 附录:测试修复记录
|
||||
|
||||
在实现过程中顺便修复了16个测试失败问题:
|
||||
- **架构问题**: Service层与UI层职责分离
|
||||
- **异步调用**: TemplateLanguageService测试缺少await
|
||||
- **集成测试**: 正确模拟UI层历史记录保存行为
|
||||
|
||||
修复后测试结果:291个测试通过,9个跳过 ✅
|
||||
@@ -84,6 +84,11 @@
|
||||
- 依赖注入优化
|
||||
- 服务接口标准化
|
||||
|
||||
- **[114-desktop-file-storage](./114-desktop-file-storage/)** - 桌面版文件存储实现 💾
|
||||
- 实现FileStorageProvider替代内存存储
|
||||
- 完整的数据持久化解决方案
|
||||
- 高性能文件I/O和错误恢复机制
|
||||
|
||||
## 🔍 快速查找指南
|
||||
|
||||
### 按问题类型查找
|
||||
@@ -94,6 +99,7 @@
|
||||
- **主题问题** → 109-theme-system
|
||||
- **模板问题** → 106-template-management
|
||||
- **组件问题** → 107-component-standardization
|
||||
- **存储问题** → 114-desktop-file-storage
|
||||
|
||||
### 按技术栈查找
|
||||
- **Electron相关** → 103, 110, 111, 112
|
||||
|
||||
@@ -37,6 +37,7 @@ export { StorageFactory } from './services/storage/factory'
|
||||
export { DexieStorageProvider } from './services/storage/dexieStorageProvider'
|
||||
export { LocalStorageProvider } from './services/storage/localStorageProvider'
|
||||
export { MemoryStorageProvider } from './services/storage/memoryStorageProvider'
|
||||
export { FileStorageProvider } from './services/storage/fileStorageProvider'
|
||||
|
||||
// 导出提示词服务相关
|
||||
export { PromptService } from './services/prompt/service'
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LocalStorageProvider } from './localStorageProvider';
|
||||
import { DexieStorageProvider } from './dexieStorageProvider';
|
||||
import { MemoryStorageProvider } from './memoryStorageProvider';
|
||||
|
||||
export type StorageType = 'localStorage' | 'dexie' | 'memory';
|
||||
export type StorageType = 'localStorage' | 'dexie' | 'memory' | 'file';
|
||||
|
||||
/**
|
||||
* 存储工厂类
|
||||
@@ -34,6 +34,9 @@ export class StorageFactory {
|
||||
case 'memory':
|
||||
instance = new MemoryStorageProvider();
|
||||
break;
|
||||
case 'file':
|
||||
throw new Error('File storage must be created directly with FileStorageProvider constructor');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported storage type: ${type}`);
|
||||
}
|
||||
@@ -76,6 +79,11 @@ export class StorageFactory {
|
||||
types.push('dexie');
|
||||
}
|
||||
|
||||
// 检查 Electron 环境支持文件存储
|
||||
if (typeof process !== 'undefined' && process.versions?.electron) {
|
||||
types.push('file');
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { IStorageProvider } from './types';
|
||||
import { StorageError } from './errors';
|
||||
|
||||
/**
|
||||
* 基于文件的存储提供器
|
||||
* 专为Electron桌面环境设计,使用JSON文件持久化存储数据
|
||||
*
|
||||
* 特性:
|
||||
* - 延迟写入优化性能,减少I/O操作
|
||||
* - 内存缓存提供快速读取
|
||||
* - 原子写入确保数据完整性
|
||||
* - 关键时刻立即写入保证数据安全
|
||||
*/
|
||||
export class FileStorageProvider implements IStorageProvider {
|
||||
private filePath: string;
|
||||
private data: Map<string, string> = new Map();
|
||||
private writeTimeout: NodeJS.Timeout | null = null;
|
||||
private isDirty: boolean = false;
|
||||
private writeLock: Promise<void> = Promise.resolve();
|
||||
private initialized: boolean = false;
|
||||
|
||||
// 配置常量
|
||||
private readonly WRITE_DELAY = 500; // 500ms延迟写入
|
||||
private readonly TEMP_FILE_SUFFIX = '.tmp';
|
||||
|
||||
constructor(userDataPath: string) {
|
||||
if (!userDataPath) {
|
||||
throw new StorageError('FileStorageProvider requires userDataPath parameter', 'read');
|
||||
}
|
||||
|
||||
this.filePath = path.join(userDataPath, 'storage.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保存储已初始化
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化存储,加载现有数据
|
||||
*/
|
||||
private async initialize(): Promise<void> {
|
||||
try {
|
||||
await this.loadFromFile();
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new StorageError(`Failed to initialize file storage: ${errorMessage}`, 'read');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载数据到内存
|
||||
*/
|
||||
private async loadFromFile(): Promise<void> {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await fs.access(this.filePath);
|
||||
} catch {
|
||||
// 文件不存在,创建空存储
|
||||
console.log('[FileStorage] Storage file not found, creating new storage');
|
||||
this.data = new Map();
|
||||
await this.saveToFile();
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const content = await fs.readFile(this.filePath, 'utf8');
|
||||
|
||||
// 验证JSON格式
|
||||
if (!this.validateJSON(content)) {
|
||||
console.warn('[FileStorage] Storage file corrupted, creating new storage');
|
||||
this.data = new Map();
|
||||
await this.saveToFile();
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析并加载数据
|
||||
const parsed = JSON.parse(content);
|
||||
this.data = new Map(Object.entries(parsed || {}));
|
||||
|
||||
console.log(`[FileStorage] Loaded ${this.data.size} items from storage`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[FileStorage] Failed to load from file:', error);
|
||||
// 创建新的空存储
|
||||
this.data = new Map();
|
||||
await this.saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内存数据保存到文件
|
||||
*/
|
||||
private async saveToFile(): Promise<void> {
|
||||
const data = Object.fromEntries(this.data);
|
||||
const jsonString = JSON.stringify(data, null, 2);
|
||||
await this.atomicWrite(jsonString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子写入文件
|
||||
*/
|
||||
private async atomicWrite(data: string): Promise<void> {
|
||||
const tempPath = this.filePath + this.TEMP_FILE_SUFFIX;
|
||||
|
||||
try {
|
||||
// 确保目录存在
|
||||
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
||||
|
||||
// 1. 写入临时文件
|
||||
await fs.writeFile(tempPath, data, 'utf8');
|
||||
|
||||
// 2. 验证文件格式
|
||||
if (!this.validateJSON(data)) {
|
||||
throw new Error('Invalid JSON format');
|
||||
}
|
||||
|
||||
// 3. 原子性重命名
|
||||
await fs.rename(tempPath, this.filePath);
|
||||
|
||||
} catch (error) {
|
||||
// 清理临时文件
|
||||
try {
|
||||
await fs.unlink(tempPath);
|
||||
} catch {}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
throw new StorageError(`Atomic write failed: ${errorMessage}`, 'write');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JSON格式
|
||||
*/
|
||||
private validateJSON(data: string): boolean {
|
||||
try {
|
||||
JSON.parse(data);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度延迟写入
|
||||
*/
|
||||
private scheduleWrite(): void {
|
||||
this.isDirty = true;
|
||||
|
||||
// 如果已有待写入任务,重置计时器
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
}
|
||||
|
||||
this.writeTimeout = setTimeout(async () => {
|
||||
if (this.isDirty) {
|
||||
try {
|
||||
await this.acquireWriteLock(async () => {
|
||||
await this.saveToFile();
|
||||
this.isDirty = false;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[FileStorage] Scheduled write failed:', error);
|
||||
}
|
||||
}
|
||||
this.writeTimeout = null;
|
||||
}, this.WRITE_DELAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即写入(关键时刻使用)
|
||||
*/
|
||||
async flush(): Promise<void> {
|
||||
if (this.writeTimeout) {
|
||||
clearTimeout(this.writeTimeout);
|
||||
this.writeTimeout = null;
|
||||
}
|
||||
|
||||
if (this.isDirty) {
|
||||
await this.acquireWriteLock(async () => {
|
||||
await this.saveToFile();
|
||||
this.isDirty = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取写入锁,确保写入操作串行执行
|
||||
*/
|
||||
private async acquireWriteLock<T>(operation: () => Promise<T>): Promise<T> {
|
||||
const currentLock = this.writeLock;
|
||||
let resolveLock: () => void;
|
||||
|
||||
this.writeLock = new Promise<void>((resolve) => {
|
||||
resolveLock = resolve;
|
||||
});
|
||||
|
||||
try {
|
||||
await currentLock;
|
||||
const result = await operation();
|
||||
return result;
|
||||
} finally {
|
||||
resolveLock!();
|
||||
}
|
||||
}
|
||||
|
||||
// IStorageProvider接口实现
|
||||
|
||||
async getItem(key: string): Promise<string | null> {
|
||||
await this.ensureInitialized();
|
||||
return this.data.get(key) || null;
|
||||
}
|
||||
|
||||
async setItem(key: string, value: string): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
this.data.set(key, value);
|
||||
this.scheduleWrite(); // 延迟写入
|
||||
}
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
this.data.delete(key);
|
||||
this.scheduleWrite(); // 延迟写入
|
||||
}
|
||||
|
||||
async clearAll(): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
this.data.clear();
|
||||
// 强制写入,即使没有脏数据
|
||||
await this.acquireWriteLock(async () => {
|
||||
await this.saveToFile();
|
||||
});
|
||||
}
|
||||
|
||||
async updateData<T>(key: string, modifier: (currentValue: T | null) => T): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
// 读取当前值
|
||||
const currentData = this.data.get(key);
|
||||
const currentValue: T | null = currentData ? JSON.parse(currentData) : null;
|
||||
|
||||
// 应用修改 - 允许业务逻辑错误透传
|
||||
const newValue = modifier(currentValue);
|
||||
|
||||
// 写入新值
|
||||
this.data.set(key, JSON.stringify(newValue));
|
||||
this.scheduleWrite(); // 延迟写入
|
||||
|
||||
} catch (error) {
|
||||
// 业务逻辑错误直接透传,保持错误类型
|
||||
if (error instanceof Error &&
|
||||
(error.name.includes('Error') ||
|
||||
error.constructor.name !== 'Error' ||
|
||||
error.message.includes('模型') ||
|
||||
error.message.includes('不存在'))) {
|
||||
throw error;
|
||||
}
|
||||
// 只有真正的存储错误才包装为StorageError
|
||||
throw new StorageError(`Data update failed: ${key}`, 'write');
|
||||
}
|
||||
}
|
||||
|
||||
async batchUpdate(operations: Array<{
|
||||
key: string;
|
||||
operation: 'set' | 'remove';
|
||||
value?: string;
|
||||
}>): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
for (const op of operations) {
|
||||
if (op.operation === 'set' && op.value !== undefined) {
|
||||
this.data.set(op.key, op.value);
|
||||
} else if (op.operation === 'remove') {
|
||||
this.data.delete(op.key);
|
||||
}
|
||||
}
|
||||
|
||||
await this.flush(); // 批量操作后立即写入
|
||||
|
||||
} catch (error) {
|
||||
throw new StorageError('Batch update failed', 'write');
|
||||
}
|
||||
}
|
||||
|
||||
getCapabilities() {
|
||||
return {
|
||||
supportsAtomic: true,
|
||||
supportsBatch: true,
|
||||
maxStorageSize: undefined // 文件存储无固定大小限制
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { FileStorageProvider } from '../../src/services/storage/fileStorageProvider';
|
||||
import { StorageError } from '../../src/services/storage/errors';
|
||||
|
||||
describe('FileStorageProvider - Real File System Integration', () => {
|
||||
let provider: FileStorageProvider;
|
||||
let testDir: string;
|
||||
let storageFile: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// 在项目的tests目录下创建临时测试目录
|
||||
testDir = path.join(__dirname, '..', '..', 'temp-test-storage');
|
||||
storageFile = path.join(testDir, 'storage.json');
|
||||
|
||||
// 确保测试目录存在
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
|
||||
// 创建FileStorageProvider实例
|
||||
provider = new FileStorageProvider(testDir);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// 清理测试文件和目录
|
||||
try {
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup test directory:', error);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Real file operations', () => {
|
||||
it('should create storage file when it does not exist', async () => {
|
||||
// 确保文件不存在
|
||||
await expect(fs.access(storageFile)).rejects.toThrow();
|
||||
|
||||
// 执行操作触发文件创建
|
||||
await provider.setItem('test-key', 'test-value');
|
||||
|
||||
// 等待延迟写入完成
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
|
||||
// 验证文件被创建
|
||||
await expect(fs.access(storageFile)).resolves.toBeUndefined();
|
||||
|
||||
// 验证文件内容
|
||||
const content = await fs.readFile(storageFile, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
expect(data['test-key']).toBe('test-value');
|
||||
});
|
||||
|
||||
it('should load existing data from real file', async () => {
|
||||
// 手动创建测试文件
|
||||
const testData = { 'existing-key': 'existing-value' };
|
||||
await fs.writeFile(storageFile, JSON.stringify(testData), 'utf8');
|
||||
|
||||
// 创建新的provider实例来加载数据
|
||||
const newProvider = new FileStorageProvider(testDir);
|
||||
|
||||
// 验证数据被正确加载
|
||||
const value = await newProvider.getItem('existing-key');
|
||||
expect(value).toBe('existing-value');
|
||||
});
|
||||
|
||||
it('should handle corrupted file gracefully', async () => {
|
||||
// 创建损坏的JSON文件
|
||||
await fs.writeFile(storageFile, 'invalid json content', 'utf8');
|
||||
|
||||
// 创建新的provider实例
|
||||
const newProvider = new FileStorageProvider(testDir);
|
||||
|
||||
// 应该能够处理损坏的文件并创建新的存储
|
||||
await newProvider.setItem('recovery-key', 'recovery-value');
|
||||
|
||||
// 等待写入完成
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
|
||||
// 验证文件被重新创建且内容正确
|
||||
const content = await fs.readFile(storageFile, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
expect(data['recovery-key']).toBe('recovery-value');
|
||||
});
|
||||
|
||||
it('should persist data across provider instances', async () => {
|
||||
// 使用第一个provider写入数据
|
||||
await provider.setItem('persist-key', 'persist-value');
|
||||
await provider.setItem('another-key', 'another-value');
|
||||
|
||||
// 立即写入
|
||||
await provider.flush();
|
||||
|
||||
// 创建新的provider实例
|
||||
const newProvider = new FileStorageProvider(testDir);
|
||||
|
||||
// 验证数据持久化
|
||||
expect(await newProvider.getItem('persist-key')).toBe('persist-value');
|
||||
expect(await newProvider.getItem('another-key')).toBe('another-value');
|
||||
});
|
||||
|
||||
it('should handle batch operations with real files', async () => {
|
||||
const operations = [
|
||||
{ key: 'batch-key-1', operation: 'set' as const, value: 'batch-value-1' },
|
||||
{ key: 'batch-key-2', operation: 'set' as const, value: 'batch-value-2' },
|
||||
{ key: 'batch-key-3', operation: 'set' as const, value: 'batch-value-3' }
|
||||
];
|
||||
|
||||
await provider.batchUpdate(operations);
|
||||
|
||||
// 验证数据在内存中正确
|
||||
expect(await provider.getItem('batch-key-1')).toBe('batch-value-1');
|
||||
expect(await provider.getItem('batch-key-2')).toBe('batch-value-2');
|
||||
expect(await provider.getItem('batch-key-3')).toBe('batch-value-3');
|
||||
|
||||
// 验证文件存在
|
||||
await expect(fs.access(storageFile)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle clearAll with real files', async () => {
|
||||
// 先添加一些数据
|
||||
await provider.setItem('clear-key-1', 'clear-value-1');
|
||||
await provider.setItem('clear-key-2', 'clear-value-2');
|
||||
await provider.flush();
|
||||
|
||||
// 验证数据存在
|
||||
let content = await fs.readFile(storageFile, 'utf8');
|
||||
let data = JSON.parse(content);
|
||||
expect(Object.keys(data)).toHaveLength(2);
|
||||
|
||||
// 清空所有数据
|
||||
await provider.clearAll();
|
||||
|
||||
// 验证文件被清空
|
||||
content = await fs.readFile(storageFile, 'utf8');
|
||||
data = JSON.parse(content);
|
||||
expect(Object.keys(data)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle updateData with real files', async () => {
|
||||
// 初始化计数器
|
||||
await provider.setItem('counter', '5');
|
||||
await provider.flush();
|
||||
|
||||
// 使用updateData增加计数器
|
||||
await provider.updateData<number>('counter', (current) => {
|
||||
return (current || 0) + 1;
|
||||
});
|
||||
|
||||
await provider.flush();
|
||||
|
||||
// 验证文件中的数据被正确更新
|
||||
const content = await fs.readFile(storageFile, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
expect(data['counter']).toBe('6');
|
||||
});
|
||||
|
||||
it('should handle concurrent operations safely', async () => {
|
||||
// 串行执行写入操作以避免文件冲突
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await provider.setItem(`concurrent-key-${i}`, `concurrent-value-${i}`);
|
||||
}
|
||||
|
||||
await provider.flush();
|
||||
|
||||
// 验证所有数据都被正确写入
|
||||
const content = await fs.readFile(storageFile, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(data[`concurrent-key-${i}`]).toBe(`concurrent-value-${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle removeItem with real files', async () => {
|
||||
// 添加测试数据
|
||||
await provider.setItem('remove-key-1', 'remove-value-1');
|
||||
await provider.setItem('remove-key-2', 'remove-value-2');
|
||||
await provider.flush();
|
||||
|
||||
// 删除一个键
|
||||
await provider.removeItem('remove-key-1');
|
||||
await provider.flush();
|
||||
|
||||
// 验证文件中的数据
|
||||
const content = await fs.readFile(storageFile, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
expect(data['remove-key-1']).toBeUndefined();
|
||||
expect(data['remove-key-2']).toBe('remove-value-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling with real file system', () => {
|
||||
it('should throw error when directory cannot be created', async () => {
|
||||
// 尝试在只读位置创建存储(如果可能的话)
|
||||
// 这个测试可能需要根据具体环境调整
|
||||
const invalidPath = '/invalid/readonly/path';
|
||||
const invalidProvider = new FileStorageProvider(invalidPath);
|
||||
|
||||
// 在某些系统上这可能会成功,所以我们只测试基本的错误处理
|
||||
try {
|
||||
await invalidProvider.setItem('test', 'test');
|
||||
await invalidProvider.flush();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(StorageError);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle temporary file cleanup on write failure', async () => {
|
||||
// 这个测试比较难模拟,但我们可以验证正常情况下没有临时文件残留
|
||||
await provider.setItem('temp-test', 'temp-value');
|
||||
await provider.flush();
|
||||
|
||||
// 检查没有临时文件残留
|
||||
const files = await fs.readdir(testDir);
|
||||
const tempFiles = files.filter(file => file.endsWith('.tmp'));
|
||||
expect(tempFiles).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance with real files', () => {
|
||||
it('should handle large data efficiently', async () => {
|
||||
const largeValue = 'x'.repeat(10000); // 10KB的数据
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// 写入大量数据
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await provider.setItem(`large-key-${i}`, largeValue);
|
||||
}
|
||||
|
||||
await provider.flush();
|
||||
|
||||
const writeTime = Date.now() - startTime;
|
||||
|
||||
// 读取数据
|
||||
const readStartTime = Date.now();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await provider.getItem(`large-key-${i}`);
|
||||
}
|
||||
const readTime = Date.now() - readStartTime;
|
||||
|
||||
// 性能断言(这些值可能需要根据实际情况调整)
|
||||
expect(writeTime).toBeLessThan(5000); // 写入应该在5秒内完成
|
||||
expect(readTime).toBeLessThan(100); // 读取应该在100ms内完成(内存缓存)
|
||||
|
||||
console.log(`Write time: ${writeTime}ms, Read time: ${readTime}ms`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -80,6 +80,16 @@ describe('PromptService Integration Tests', () => {
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// 模拟UI层保存历史记录
|
||||
await historyManager.createNewChain({
|
||||
id: `test_${Date.now()}`,
|
||||
originalPrompt: request.targetPrompt,
|
||||
optimizedPrompt: result,
|
||||
type: 'optimize',
|
||||
modelKey: request.modelKey,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 验证历史记录
|
||||
const records = await historyManager.getRecords();
|
||||
expect(records.length).toBe(1);
|
||||
@@ -150,10 +160,29 @@ describe('PromptService Integration Tests', () => {
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// 模拟UI层保存历史记录 - 对于迭代,需要先创建一个链,然后添加迭代
|
||||
const chain = await historyManager.createNewChain({
|
||||
id: `test_${Date.now()}`,
|
||||
originalPrompt: 'Write a simple greeting',
|
||||
optimizedPrompt: 'Hello world',
|
||||
type: 'optimize',
|
||||
modelKey: 'test-gemini',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
await historyManager.addIteration({
|
||||
chainId: chain.chainId,
|
||||
originalPrompt: 'Write a simple greeting',
|
||||
optimizedPrompt: result,
|
||||
modelKey: 'test-gemini',
|
||||
templateId: 'iterate',
|
||||
iterationNote: 'Make it more formal'
|
||||
});
|
||||
|
||||
// 验证历史记录
|
||||
const records = await historyManager.getRecords();
|
||||
expect(records.length).toBe(1);
|
||||
expect(records[0].type).toBe('iterate');
|
||||
expect(records.length).toBe(2); // 一个初始记录 + 一个迭代记录
|
||||
expect(records.find(r => r.type === 'iterate')).toBeDefined();
|
||||
}, 30000);
|
||||
|
||||
it.runIf(hasGeminiKey)('should work with message-based iterate templates', async () => {
|
||||
|
||||
@@ -177,6 +177,16 @@ describe('Real API Integration Tests', () => {
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
|
||||
// 模拟UI层保存历史记录
|
||||
await historyManager.createNewChain({
|
||||
id: `test_${Date.now()}`,
|
||||
originalPrompt: request.targetPrompt,
|
||||
optimizedPrompt: result,
|
||||
type: 'optimize',
|
||||
modelKey: request.modelKey,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
// 验证历史记录已保存
|
||||
const records = await historyManager.getRecords()
|
||||
expect(records.length).toBe(1)
|
||||
@@ -216,6 +226,16 @@ describe('Real API Integration Tests', () => {
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
|
||||
// 模拟UI层保存历史记录
|
||||
await historyManager.createNewChain({
|
||||
id: `test_${Date.now()}`,
|
||||
originalPrompt: request.targetPrompt,
|
||||
optimizedPrompt: result,
|
||||
type: 'optimize',
|
||||
modelKey: request.modelKey,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
// 验证历史记录已保存
|
||||
const records = await historyManager.getRecords()
|
||||
expect(records.length).toBe(1)
|
||||
@@ -259,6 +279,16 @@ describe('Real API Integration Tests', () => {
|
||||
expect(typeof optimizeResult).toBe('string')
|
||||
expect(optimizeResult.length).toBeGreaterThan(0)
|
||||
|
||||
// 模拟UI层保存历史记录
|
||||
await historyManager.createNewChain({
|
||||
id: `test_${Date.now()}`,
|
||||
originalPrompt: request.targetPrompt,
|
||||
optimizedPrompt: optimizeResult,
|
||||
type: 'optimize',
|
||||
modelKey: request.modelKey,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
// 验证历史记录已保存
|
||||
const records = await historyManager.getRecords()
|
||||
expect(records.length).toBe(1)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { StorageFactory } from '../../src/services/storage/factory';
|
||||
import { FileStorageProvider } from '../../src/services/storage/fileStorageProvider';
|
||||
|
||||
describe('StorageFactory - File Storage Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
StorageFactory.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
StorageFactory.reset();
|
||||
});
|
||||
|
||||
describe('getSupportedTypes', () => {
|
||||
it('should include file storage in Electron environment', () => {
|
||||
// Mock Electron environment
|
||||
const originalProcess = global.process;
|
||||
global.process = {
|
||||
...originalProcess,
|
||||
versions: { electron: '37.1.0', node: '20.0.0' }
|
||||
} as any;
|
||||
|
||||
const supportedTypes = StorageFactory.getSupportedTypes();
|
||||
|
||||
expect(supportedTypes).toContain('file');
|
||||
expect(supportedTypes).toContain('memory');
|
||||
|
||||
global.process = originalProcess;
|
||||
});
|
||||
|
||||
it('should not include file storage in non-Electron environment', () => {
|
||||
// Mock non-Electron environment
|
||||
const originalProcess = global.process;
|
||||
global.process = {
|
||||
...originalProcess,
|
||||
versions: { node: '20.0.0' }
|
||||
} as any;
|
||||
|
||||
const supportedTypes = StorageFactory.getSupportedTypes();
|
||||
|
||||
expect(supportedTypes).not.toContain('file');
|
||||
expect(supportedTypes).toContain('memory');
|
||||
|
||||
global.process = originalProcess;
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupported', () => {
|
||||
it('should return true for file storage in Electron environment', () => {
|
||||
// Mock Electron environment
|
||||
const originalProcess = global.process;
|
||||
global.process = {
|
||||
...originalProcess,
|
||||
versions: { electron: '37.1.0', node: '20.0.0' }
|
||||
} as any;
|
||||
|
||||
const isSupported = StorageFactory.isSupported('file');
|
||||
|
||||
expect(isSupported).toBe(true);
|
||||
|
||||
global.process = originalProcess;
|
||||
});
|
||||
|
||||
it('should return false for file storage in non-Electron environment', () => {
|
||||
// Mock non-Electron environment
|
||||
const originalProcess = global.process;
|
||||
global.process = {
|
||||
...originalProcess,
|
||||
versions: { node: '20.0.0' }
|
||||
} as any;
|
||||
|
||||
const isSupported = StorageFactory.isSupported('file');
|
||||
|
||||
expect(isSupported).toBe(false);
|
||||
|
||||
global.process = originalProcess;
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create FileStorageProvider instance for file type', () => {
|
||||
// Create FileStorageProvider directly with userDataPath to avoid Electron dependency
|
||||
const provider = new FileStorageProvider('/mock/user/data');
|
||||
|
||||
expect(provider).toBeInstanceOf(FileStorageProvider);
|
||||
});
|
||||
|
||||
it('should return same instance for multiple calls (singleton)', () => {
|
||||
// Test the singleton behavior by creating providers through factory
|
||||
// We'll test this with memory storage since file storage requires Electron
|
||||
const provider1 = StorageFactory.create('memory');
|
||||
const provider2 = StorageFactory.create('memory');
|
||||
|
||||
expect(provider1).toBe(provider2);
|
||||
});
|
||||
|
||||
it('should throw error for unsupported storage type', () => {
|
||||
expect(() => {
|
||||
// @ts-ignore - intentionally passing invalid type
|
||||
StorageFactory.create('invalid');
|
||||
}).toThrow('Unsupported storage type: invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type definitions', () => {
|
||||
it('should include file in StorageType union', () => {
|
||||
// This is a compile-time test - if it compiles, the type is correct
|
||||
const fileType: 'file' = 'file';
|
||||
|
||||
// Test that the type is accepted (compile-time check)
|
||||
expect(fileType).toBe('file');
|
||||
|
||||
// Test that FileStorageProvider can be created directly
|
||||
const provider = new FileStorageProvider('/mock/user/data');
|
||||
expect(provider).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('PromptService Enhanced Features', () => {
|
||||
|
||||
expect(result).toBe('optimized result')
|
||||
expect(mockLLMService.sendMessage).toHaveBeenCalled()
|
||||
expect(mockHistoryManager.addRecord).toHaveBeenCalled()
|
||||
// 注意:历史记录保存由UI层处理,Service层不保存历史记录
|
||||
})
|
||||
|
||||
it('should optimize user prompt successfully', async () => {
|
||||
@@ -202,7 +202,7 @@ describe('PromptService Enhanced Features', () => {
|
||||
expect(callbacks.onToken).toHaveBeenCalledWith('test')
|
||||
expect(callbacks.onToken).toHaveBeenCalledWith(' result')
|
||||
expect(callbacks.onComplete).toHaveBeenCalled()
|
||||
expect(mockHistoryManager.addRecord).toHaveBeenCalled()
|
||||
// 注意:历史记录保存由UI层处理,Service层不保存历史记录
|
||||
})
|
||||
|
||||
it('should handle missing model key', async () => {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { FileStorageProvider } from '../../../src/services/storage/fileStorageProvider';
|
||||
import { StorageError } from '../../../src/services/storage/errors';
|
||||
|
||||
// Mock fs module
|
||||
vi.mock('fs/promises');
|
||||
const mockFs = vi.mocked(fs);
|
||||
|
||||
// Mock path module
|
||||
vi.mock('path');
|
||||
const mockPath = vi.mocked(path);
|
||||
|
||||
describe('FileStorageProvider', () => {
|
||||
let provider: FileStorageProvider;
|
||||
let mockUserDataPath: string;
|
||||
let mockFilePath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockUserDataPath = '/mock/user/data';
|
||||
mockFilePath = '/mock/user/data/storage.json';
|
||||
|
||||
// Setup path mocks
|
||||
mockPath.join.mockReturnValue(mockFilePath);
|
||||
mockPath.dirname.mockReturnValue(mockUserDataPath);
|
||||
|
||||
provider = new FileStorageProvider(mockUserDataPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create instance with provided user data path', () => {
|
||||
expect(mockPath.join).toHaveBeenCalledWith(mockUserDataPath, 'storage.json');
|
||||
});
|
||||
|
||||
it('should throw error when no path provided', () => {
|
||||
expect(() => new FileStorageProvider('')).toThrow(StorageError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should create new storage when file does not exist', async () => {
|
||||
// Mock file not exists
|
||||
mockFs.access.mockRejectedValue(new Error('File not found'));
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
|
||||
const result = await provider.getItem('test-key');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockFs.access).toHaveBeenCalledWith(mockFilePath);
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should load existing data from file', async () => {
|
||||
const mockData = { 'test-key': 'test-value' };
|
||||
const mockContent = JSON.stringify(mockData);
|
||||
|
||||
mockFs.access.mockResolvedValue(undefined);
|
||||
mockFs.readFile.mockResolvedValue(mockContent);
|
||||
|
||||
const result = await provider.getItem('test-key');
|
||||
|
||||
expect(result).toBe('test-value');
|
||||
expect(mockFs.readFile).toHaveBeenCalledWith(mockFilePath, 'utf8');
|
||||
});
|
||||
|
||||
it('should handle corrupted file by creating new storage', async () => {
|
||||
mockFs.access.mockResolvedValue(undefined);
|
||||
mockFs.readFile.mockResolvedValue('invalid json');
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
|
||||
const result = await provider.getItem('test-key');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getItem', () => {
|
||||
beforeEach(async () => {
|
||||
// Setup initialized storage
|
||||
mockFs.access.mockResolvedValue(undefined);
|
||||
mockFs.readFile.mockResolvedValue('{"test-key":"test-value"}');
|
||||
});
|
||||
|
||||
it('should return stored value', async () => {
|
||||
const result = await provider.getItem('test-key');
|
||||
expect(result).toBe('test-value');
|
||||
});
|
||||
|
||||
it('should return null for non-existent key', async () => {
|
||||
const result = await provider.getItem('non-existent');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setItem', () => {
|
||||
beforeEach(async () => {
|
||||
// Setup initialized storage
|
||||
mockFs.access.mockRejectedValue(new Error('File not found'));
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should store value and schedule write', async () => {
|
||||
await provider.setItem('new-key', 'new-value');
|
||||
|
||||
const result = await provider.getItem('new-key');
|
||||
expect(result).toBe('new-value');
|
||||
});
|
||||
|
||||
it('should update existing value', async () => {
|
||||
await provider.setItem('key', 'value1');
|
||||
await provider.setItem('key', 'value2');
|
||||
|
||||
const result = await provider.getItem('key');
|
||||
expect(result).toBe('value2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeItem', () => {
|
||||
beforeEach(async () => {
|
||||
// Setup initialized storage with data
|
||||
mockFs.access.mockResolvedValue(undefined);
|
||||
mockFs.readFile.mockResolvedValue('{"test-key":"test-value"}');
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should remove existing item', async () => {
|
||||
await provider.removeItem('test-key');
|
||||
|
||||
const result = await provider.getItem('test-key');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle removal of non-existent item', async () => {
|
||||
await provider.removeItem('non-existent');
|
||||
|
||||
// Should not throw error
|
||||
const result = await provider.getItem('test-key');
|
||||
expect(result).toBe('test-value'); // Original data should remain
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAll', () => {
|
||||
beforeEach(async () => {
|
||||
// Setup initialized storage with data
|
||||
mockFs.access.mockResolvedValue(undefined);
|
||||
mockFs.readFile.mockResolvedValue('{"key1":"value1","key2":"value2"}');
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should clear all data and write immediately', async () => {
|
||||
// First ensure data is loaded
|
||||
await provider.getItem('key1');
|
||||
|
||||
// Clear the mock to count only clearAll writes
|
||||
mockFs.writeFile.mockClear();
|
||||
|
||||
await provider.clearAll();
|
||||
|
||||
const result1 = await provider.getItem('key1');
|
||||
const result2 = await provider.getItem('key2');
|
||||
|
||||
expect(result1).toBeNull();
|
||||
expect(result2).toBeNull();
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateData', () => {
|
||||
beforeEach(async () => {
|
||||
// Setup initialized storage
|
||||
mockFs.access.mockRejectedValue(new Error('File not found'));
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should update data with modifier function', async () => {
|
||||
await provider.setItem('counter', '5');
|
||||
|
||||
await provider.updateData<number>('counter', (current) => {
|
||||
return (current || 0) + 1;
|
||||
});
|
||||
|
||||
const result = await provider.getItem('counter');
|
||||
expect(result).toBe('6');
|
||||
});
|
||||
|
||||
it('should handle null current value', async () => {
|
||||
await provider.updateData<string>('new-key', (current) => {
|
||||
return current || 'default';
|
||||
});
|
||||
|
||||
const result = await provider.getItem('new-key');
|
||||
expect(result).toBe('"default"');
|
||||
});
|
||||
|
||||
it('should propagate business logic errors', async () => {
|
||||
await expect(provider.updateData('key', () => {
|
||||
throw new Error('Business logic error');
|
||||
})).rejects.toThrow('Business logic error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchUpdate', () => {
|
||||
beforeEach(async () => {
|
||||
// Setup initialized storage
|
||||
mockFs.access.mockRejectedValue(new Error('File not found'));
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should perform batch operations and write immediately', async () => {
|
||||
const operations = [
|
||||
{ key: 'key1', operation: 'set' as const, value: 'value1' },
|
||||
{ key: 'key2', operation: 'set' as const, value: 'value2' },
|
||||
{ key: 'key3', operation: 'remove' as const }
|
||||
];
|
||||
|
||||
await provider.batchUpdate(operations);
|
||||
|
||||
expect(await provider.getItem('key1')).toBe('value1');
|
||||
expect(await provider.getItem('key2')).toBe('value2');
|
||||
expect(await provider.getItem('key3')).toBeNull();
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCapabilities', () => {
|
||||
it('should return correct capabilities', () => {
|
||||
const capabilities = provider.getCapabilities();
|
||||
|
||||
expect(capabilities).toEqual({
|
||||
supportsAtomic: true,
|
||||
supportsBatch: true,
|
||||
maxStorageSize: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('flush', () => {
|
||||
beforeEach(async () => {
|
||||
// Setup initialized storage
|
||||
mockFs.access.mockRejectedValue(new Error('File not found'));
|
||||
mockFs.mkdir.mockResolvedValue(undefined);
|
||||
mockFs.writeFile.mockResolvedValue(undefined);
|
||||
mockFs.rename.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('should immediately write pending changes', async () => {
|
||||
await provider.setItem('key', 'value');
|
||||
|
||||
// Clear previous write calls
|
||||
mockFs.writeFile.mockClear();
|
||||
|
||||
await provider.flush();
|
||||
|
||||
expect(mockFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,36 +24,36 @@ describe('TemplateLanguageService', () => {
|
||||
describe('initialization', () => {
|
||||
it('should initialize with default language when no saved preference', async () => {
|
||||
mockStorage.getItem.mockResolvedValue(null);
|
||||
|
||||
|
||||
await service.initialize();
|
||||
|
||||
expect(service.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
expect(service.isInitialized()).toBe(true);
|
||||
});
|
||||
|
||||
it('should load saved language preference', async () => {
|
||||
mockStorage.getItem.mockResolvedValue('en-US');
|
||||
|
||||
|
||||
await service.initialize();
|
||||
|
||||
expect(service.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
expect(mockStorage.getItem).toHaveBeenCalledWith('builtin-template-language');
|
||||
});
|
||||
|
||||
it('should fallback to default language on invalid saved preference', async () => {
|
||||
mockStorage.getItem.mockResolvedValue('invalid-lang');
|
||||
|
||||
|
||||
await service.initialize();
|
||||
|
||||
expect(service.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
});
|
||||
|
||||
it('should handle storage errors gracefully', async () => {
|
||||
mockStorage.getItem.mockRejectedValue(new Error('Storage error'));
|
||||
|
||||
|
||||
await service.initialize();
|
||||
|
||||
expect(service.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
expect(service.isInitialized()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -65,8 +65,8 @@ describe('TemplateLanguageService', () => {
|
||||
|
||||
it('should set language successfully', async () => {
|
||||
await service.setLanguage('en-US');
|
||||
|
||||
expect(service.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
expect(mockStorage.setItem).toHaveBeenCalledWith('builtin-template-language', 'en-US');
|
||||
});
|
||||
|
||||
@@ -78,26 +78,26 @@ describe('TemplateLanguageService', () => {
|
||||
|
||||
it('should toggle between languages', async () => {
|
||||
// Start with detected language (en-US in test environment)
|
||||
expect(service.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
// Toggle to Chinese
|
||||
const newLang1 = await service.toggleLanguage();
|
||||
expect(newLang1).toBe('zh-CN');
|
||||
expect(service.getCurrentLanguage()).toBe('zh-CN');
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('zh-CN');
|
||||
|
||||
// Toggle back to English
|
||||
const newLang2 = await service.toggleLanguage();
|
||||
expect(newLang2).toBe('en-US');
|
||||
expect(service.getCurrentLanguage()).toBe('en-US');
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
});
|
||||
});
|
||||
|
||||
describe('utility methods', () => {
|
||||
it('should validate languages correctly', () => {
|
||||
expect(service.isValidLanguage('zh-CN')).toBe(true);
|
||||
expect(service.isValidLanguage('en-US')).toBe(true);
|
||||
expect(service.isValidLanguage('fr-FR')).toBe(false);
|
||||
expect(service.isValidLanguage('')).toBe(false);
|
||||
it('should validate languages correctly', async () => {
|
||||
expect(await service.isValidLanguage('zh-CN')).toBe(true);
|
||||
expect(await service.isValidLanguage('en-US')).toBe(true);
|
||||
expect(await service.isValidLanguage('fr-FR')).toBe(false);
|
||||
expect(await service.isValidLanguage('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,12 +108,12 @@ describe('TemplateLanguageService', () => {
|
||||
value: 'zh-CN',
|
||||
configurable: true
|
||||
});
|
||||
|
||||
|
||||
mockStorage.getItem.mockResolvedValue(null);
|
||||
const newService = new TemplateLanguageService(mockStorage);
|
||||
await newService.initialize();
|
||||
|
||||
expect(newService.getCurrentLanguage()).toBe('zh-CN');
|
||||
|
||||
expect(await newService.getCurrentLanguage()).toBe('zh-CN');
|
||||
});
|
||||
|
||||
it('should detect English browser language', async () => {
|
||||
@@ -122,12 +122,12 @@ describe('TemplateLanguageService', () => {
|
||||
value: 'en-US',
|
||||
configurable: true
|
||||
});
|
||||
|
||||
|
||||
mockStorage.getItem.mockResolvedValue(null);
|
||||
const newService = new TemplateLanguageService(mockStorage);
|
||||
await newService.initialize();
|
||||
|
||||
expect(newService.getCurrentLanguage()).toBe('en-US');
|
||||
|
||||
expect(await newService.getCurrentLanguage()).toBe('en-US');
|
||||
});
|
||||
|
||||
it('should default to English for unsupported browser languages', async () => {
|
||||
@@ -141,7 +141,7 @@ describe('TemplateLanguageService', () => {
|
||||
const newService = new TemplateLanguageService(mockStorage);
|
||||
await newService.initialize();
|
||||
|
||||
expect(newService.getCurrentLanguage()).toBe('en-US');
|
||||
expect(await newService.getCurrentLanguage()).toBe('en-US');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,11 +11,12 @@ const {
|
||||
createPromptService,
|
||||
createTemplateLanguageService,
|
||||
createDataManager,
|
||||
StorageFactory,
|
||||
FileStorageProvider,
|
||||
} = require('@prompt-optimizer/core');
|
||||
|
||||
let mainWindow;
|
||||
let modelManager, templateManager, historyManager, llmService, promptService, templateLanguageService, preferenceService, dataManager;
|
||||
let storageProvider; // 全局存储提供器引用,用于退出时保存数据
|
||||
|
||||
async function initializePreferenceService(storageProvider) {
|
||||
console.log('[DESKTOP] Initializing PreferenceService with the provided storage provider...');
|
||||
@@ -72,6 +73,23 @@ function createWindow() {
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口关闭前保存数据
|
||||
mainWindow.on('close', async (event) => {
|
||||
if (storageProvider && typeof storageProvider.flush === 'function') {
|
||||
event.preventDefault(); // 阻止立即关闭
|
||||
|
||||
try {
|
||||
console.log('[DESKTOP] Saving data before window close...');
|
||||
await storageProvider.flush();
|
||||
console.log('[DESKTOP] Data saved successfully');
|
||||
mainWindow.destroy(); // 手动关闭窗口
|
||||
} catch (error) {
|
||||
console.error('[DESKTOP] Failed to save data before close:', error);
|
||||
mainWindow.destroy(); // 即使保存失败也要关闭
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('closed', function () {
|
||||
// Dereference the window object
|
||||
@@ -114,8 +132,24 @@ async function initializeServices() {
|
||||
console.warn('[Main Process] Example: VITE_OPENAI_API_KEY=your_key_here npm start');
|
||||
}
|
||||
|
||||
console.log('[DESKTOP] Creating memory storage provider for Node.js environment');
|
||||
const storageProvider = StorageFactory.create('memory');
|
||||
console.log('[DESKTOP] Creating file storage provider for desktop environment');
|
||||
|
||||
// 根据环境确定数据存储路径
|
||||
let userDataPath;
|
||||
if (app.isPackaged) {
|
||||
// 生产环境:使用可执行文件所在目录下的prompt-optimizer-data文件夹
|
||||
const execPath = process.execPath;
|
||||
const execDir = path.dirname(execPath);
|
||||
userDataPath = path.join(execDir, 'prompt-optimizer-data');
|
||||
console.log('[DESKTOP] Production mode - Executable path:', execPath);
|
||||
} else {
|
||||
// 开发环境:使用项目根目录下的prompt-optimizer-data文件夹
|
||||
userDataPath = path.join(__dirname, '..', '..', 'prompt-optimizer-data');
|
||||
console.log('[DESKTOP] Development mode - Project root data folder');
|
||||
}
|
||||
|
||||
console.log('[DESKTOP] Data storage path:', userDataPath);
|
||||
storageProvider = new FileStorageProvider(userDataPath);
|
||||
|
||||
await initializePreferenceService(storageProvider);
|
||||
|
||||
@@ -691,9 +725,26 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 应用退出前保存数据
|
||||
app.on('before-quit', async (event) => {
|
||||
if (storageProvider && typeof storageProvider.flush === 'function') {
|
||||
event.preventDefault(); // 阻止立即退出
|
||||
|
||||
try {
|
||||
console.log('[DESKTOP] Saving data before quit...');
|
||||
await storageProvider.flush();
|
||||
console.log('[DESKTOP] Data saved successfully');
|
||||
} catch (error) {
|
||||
console.error('[DESKTOP] Failed to save data before quit:', error);
|
||||
}
|
||||
|
||||
app.quit(); // 手动退出
|
||||
}
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS.
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
});
|
||||
+214
-169
@@ -24,12 +24,12 @@
|
||||
<ActionButtonUI
|
||||
icon="📜"
|
||||
:text="$t('nav.history')"
|
||||
@click="showHistory = true"
|
||||
@click="historyManager.showHistory = true"
|
||||
/>
|
||||
<ActionButtonUI
|
||||
icon="⚙️"
|
||||
:text="$t('nav.modelManager')"
|
||||
@click="showConfig = true"
|
||||
@click="modelManager.showConfig = true"
|
||||
/>
|
||||
<ActionButtonUI
|
||||
icon="💾"
|
||||
@@ -52,18 +52,18 @@
|
||||
<ContentCardUI class="flex-1 min-w-0 flex flex-col">
|
||||
<div class="flex-none">
|
||||
<InputPanelUI
|
||||
v-model="prompt"
|
||||
v-model:selectedModel="selectedOptimizeModel"
|
||||
v-model="optimizer.prompt"
|
||||
v-model:selectedModel="modelManager.selectedOptimizeModel"
|
||||
:label="promptInputLabel"
|
||||
:placeholder="promptInputPlaceholder"
|
||||
:model-label="$t('promptOptimizer.optimizeModel')"
|
||||
:template-label="$t('promptOptimizer.templateLabel')"
|
||||
:button-text="$t('promptOptimizer.optimize')"
|
||||
:loading-text="$t('common.loading')"
|
||||
:loading="isOptimizing"
|
||||
:disabled="isOptimizing"
|
||||
:loading="optimizer.isOptimizing"
|
||||
:disabled="optimizer.isOptimizing"
|
||||
@submit="handleOptimizePrompt"
|
||||
@configModel="showConfig = true"
|
||||
@configModel="modelManager.showConfig = true"
|
||||
>
|
||||
<template #optimization-mode-selector>
|
||||
<OptimizationModeSelectorUI
|
||||
@@ -74,37 +74,48 @@
|
||||
<template #model-select>
|
||||
<ModelSelectUI
|
||||
ref="optimizeModelSelect"
|
||||
:modelValue="selectedOptimizeModel"
|
||||
@update:modelValue="selectedOptimizeModel = $event"
|
||||
:disabled="isOptimizing"
|
||||
@config="showConfig = true"
|
||||
:modelValue="modelManager.selectedOptimizeModel"
|
||||
@update:modelValue="modelManager.selectedOptimizeModel = $event"
|
||||
:disabled="optimizer.isOptimizing"
|
||||
@config="modelManager.showConfig = true"
|
||||
/>
|
||||
</template>
|
||||
<template #template-select>
|
||||
<TemplateSelectUI
|
||||
ref="templateSelectRef"
|
||||
v-model="currentSelectedTemplate"
|
||||
:type="selectedOptimizationMode === 'system' ? 'optimize' : 'userOptimize'"
|
||||
:optimization-mode="selectedOptimizationMode"
|
||||
@manage="openTemplateManager"
|
||||
/>
|
||||
<template v-if="services && services.templateManager">
|
||||
<TemplateSelectUI
|
||||
ref="templateSelectRef"
|
||||
v-model="currentSelectedTemplate"
|
||||
:type="templateSelectType"
|
||||
:optimization-mode="selectedOptimizationMode"
|
||||
@manage="openTemplateManager"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="p-2 text-sm theme-placeholder">
|
||||
{{ t('template.loading') || '加载中...' }}
|
||||
</div>
|
||||
</template>
|
||||
</InputPanelUI>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<PromptPanelUI
|
||||
v-model:optimized-prompt="optimizedPrompt"
|
||||
:reasoning="optimizedReasoning"
|
||||
:original-prompt="prompt"
|
||||
:is-optimizing="isOptimizing"
|
||||
:is-iterating="isIterating"
|
||||
v-model:selected-iterate-template="selectedIterateTemplate"
|
||||
:versions="currentVersions"
|
||||
:current-version-id="currentVersionId"
|
||||
@iterate="handleIteratePrompt"
|
||||
@openTemplateManager="openTemplateManager"
|
||||
@switchVersion="handleSwitchVersion"
|
||||
/>
|
||||
<template v-if="services && services.templateManager">
|
||||
<PromptPanelUI
|
||||
v-model:optimized-prompt="optimizer.optimizedPrompt"
|
||||
:reasoning="optimizer.optimizedReasoning"
|
||||
:original-prompt="optimizer.prompt"
|
||||
:is-optimizing="optimizer.isOptimizing"
|
||||
:is-iterating="optimizer.isIterating"
|
||||
v-model:selected-iterate-template="optimizer.selectedIterateTemplate"
|
||||
:versions="optimizer.currentVersions"
|
||||
:current-version-id="optimizer.currentVersionId"
|
||||
:services="services"
|
||||
@iterate="handleIteratePrompt"
|
||||
@openTemplateManager="openTemplateManager"
|
||||
@switchVersion="handleSwitchVersion"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="p-4 text-center theme-placeholder">
|
||||
{{ t('prompt.loading') || '加载中...' }}
|
||||
</div>
|
||||
</div>
|
||||
</ContentCardUI>
|
||||
|
||||
@@ -112,24 +123,29 @@
|
||||
ref="testPanelRef"
|
||||
class="flex-1 min-w-0 flex flex-col"
|
||||
:prompt-service="promptService"
|
||||
:original-prompt="prompt"
|
||||
:optimized-prompt="optimizedPrompt"
|
||||
:original-prompt="optimizer.prompt"
|
||||
:optimized-prompt="optimizer.optimizedPrompt"
|
||||
:optimization-mode="selectedOptimizationMode"
|
||||
v-model="selectedTestModel"
|
||||
@showConfig="showConfig = true"
|
||||
v-model="modelManager.selectedTestModel"
|
||||
@showConfig="modelManager.showConfig = true"
|
||||
/>
|
||||
</MainLayoutUI>
|
||||
|
||||
<!-- Modals and Drawers that are conditionally rendered -->
|
||||
<ModelManagerUI v-if="isReady" v-model:show="showConfig" />
|
||||
<TemplateManagerUI v-if="isReady" v-model:show="showTemplateManager" :type="currentTemplateManagerType" />
|
||||
<ModelManagerUI v-if="isReady" v-model:show="modelManager.showConfig" />
|
||||
<TemplateManagerUI
|
||||
v-if="isReady"
|
||||
v-model:show="templateManagerState.showTemplates"
|
||||
:templateType="templateManagerState.currentType"
|
||||
@close="() => templateManagerState.handleTemplateManagerClose(() => templateSelectRef?.refresh?.())"
|
||||
/>
|
||||
<HistoryDrawerUI
|
||||
v-if="isReady"
|
||||
v-model:show="showHistory"
|
||||
:history="promptHistory?.history || []"
|
||||
v-model:show="historyManager.showHistory"
|
||||
:history="promptHistory.history"
|
||||
@reuse="handleHistoryReuse"
|
||||
@clear="promptHistory?.handleClearHistory"
|
||||
@deleteChain="promptHistory?.handleDeleteChain"
|
||||
@clear="promptHistory.handleClearHistory"
|
||||
@deleteChain="promptHistory.handleDeleteChain"
|
||||
/>
|
||||
<DataManagerUI v-if="isReady" v-model:show="showDataManager" />
|
||||
|
||||
@@ -138,7 +154,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, provide, computed, shallowRef } from 'vue'
|
||||
import { ref, watch, provide, computed, shallowRef, toRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
// UI Components
|
||||
@@ -154,175 +170,204 @@ import {
|
||||
useTemplateManager,
|
||||
useAppInitializer,
|
||||
usePromptHistory,
|
||||
useModelSelectors,
|
||||
|
||||
// i18n functions
|
||||
initializeI18nWithStorage,
|
||||
|
||||
// Types from UI package
|
||||
type OptimizationMode,
|
||||
// 从UI包导入DataManager类型
|
||||
DataManager,
|
||||
} from '@prompt-optimizer/ui'
|
||||
import type { IPromptService } from '@prompt-optimizer/core'
|
||||
// 导入AppServices类型
|
||||
import type { AppServices } from '../node_modules/@prompt-optimizer/ui/src/types/services'
|
||||
|
||||
// 1. Setup basic composables
|
||||
// 1. 基础 composables
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// 2. Initialize all application services
|
||||
const { services, isInitializing } = useAppInitializer()
|
||||
// 2. 初始化应用服务
|
||||
const { services, isInitializing, error } = useAppInitializer()
|
||||
|
||||
// 3. Initialize i18n with storage when services are ready
|
||||
watch(services, async (newServices) => {
|
||||
if (newServices?.storageProvider) {
|
||||
// 立即失败,不掩盖错误
|
||||
await initializeI18nWithStorage(newServices.storageProvider)
|
||||
console.log('[Extension] i18n initialized with storage')
|
||||
if (newServices) {
|
||||
await initializeI18nWithStorage()
|
||||
console.log('[Web] i18n initialized')
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 4. Provide services to all child components
|
||||
// 4. 向子组件提供服务
|
||||
provide('services', services)
|
||||
provide('toast', toast)
|
||||
|
||||
// 5. This flag controls the rendering of the main UI.
|
||||
const isReady = ref(false)
|
||||
|
||||
// 5. Define placeholders for all state and functions.
|
||||
let promptService = shallowRef<IPromptService | null>(null)
|
||||
let prompt = ref('')
|
||||
let optimizedPrompt = ref('')
|
||||
let optimizedReasoning = ref('')
|
||||
let isOptimizing = ref(false)
|
||||
let isIterating = ref(false)
|
||||
let selectedOptimizeModel = ref('')
|
||||
let selectedTestModel = ref('')
|
||||
let selectedOptimizationMode = ref<OptimizationMode>('system')
|
||||
let selectedIterateTemplate = ref('')
|
||||
let currentVersions = ref<any[]>([])
|
||||
let currentVersionId = ref<string | null>(null)
|
||||
let currentChainId = ref<string | null>(null)
|
||||
let currentSelectedTemplate = ref('')
|
||||
let showConfig = ref(false)
|
||||
let showHistory = ref(false)
|
||||
let showDataManager = ref(false)
|
||||
let showTemplateManager = ref(false)
|
||||
let currentTemplateManagerType = ref<'optimize' | 'userOptimize' | 'iterate'>('optimize')
|
||||
let promptHistory: any = null
|
||||
|
||||
let handleOptimizePrompt = () => {}
|
||||
let handleIteratePrompt = (payload: any) => {}
|
||||
let handleSwitchVersion = (versionId: string) => {}
|
||||
// 5. 控制主UI渲染的标志
|
||||
const isReady = computed(() => services.value !== null && !isInitializing.value)
|
||||
|
||||
// 6. 创建所有必要的引用
|
||||
const promptService = shallowRef<IPromptService | null>(null)
|
||||
const selectedOptimizationMode = ref<OptimizationMode>('system')
|
||||
const showDataManager = ref(false)
|
||||
const optimizeModelSelect = ref(null)
|
||||
const testPanelRef = ref(null)
|
||||
const templateSelectRef = ref<{ refresh?: () => void } | null>(null)
|
||||
|
||||
// 6. Watch for services to become available, then initialize all business logic
|
||||
const templateSelectType = computed<'optimize' | 'userOptimize' | 'iterate'>(() => {
|
||||
return selectedOptimizationMode.value === 'system' ? 'optimize' : 'userOptimize';
|
||||
});
|
||||
|
||||
// 6. 在顶层调用所有 Composables
|
||||
// 测试面板的模型选择器引用
|
||||
const testModelSelect = computed(() => (testPanelRef.value as any)?.modelSelectRef || null)
|
||||
|
||||
// 使用类型断言解决类型不匹配问题
|
||||
// 模型选择器
|
||||
const modelSelectors = useModelSelectors(services as any)
|
||||
|
||||
// 模型管理器
|
||||
const modelManager = useModelManager(
|
||||
services as any,
|
||||
{
|
||||
optimizeModelSelect: modelSelectors.optimizeModelSelect,
|
||||
testModelSelect
|
||||
}
|
||||
)
|
||||
|
||||
// 提示词优化器
|
||||
const optimizer = usePromptOptimizer(
|
||||
services as any,
|
||||
selectedOptimizationMode,
|
||||
toRef(modelManager, 'selectedOptimizeModel'),
|
||||
toRef(modelManager, 'selectedTestModel')
|
||||
)
|
||||
|
||||
// 提示词历史
|
||||
const promptHistory = usePromptHistory(
|
||||
services as any,
|
||||
toRef(optimizer, 'prompt') as any,
|
||||
toRef(optimizer, 'optimizedPrompt') as any,
|
||||
toRef(optimizer, 'currentChainId') as any,
|
||||
toRef(optimizer, 'currentVersions') as any,
|
||||
toRef(optimizer, 'currentVersionId') as any
|
||||
)
|
||||
|
||||
// 历史管理器
|
||||
const historyManager = useHistoryManager(
|
||||
services as any,
|
||||
optimizer.prompt as any,
|
||||
optimizer.optimizedPrompt as any,
|
||||
optimizer.currentChainId as any,
|
||||
optimizer.currentVersions as any,
|
||||
optimizer.currentVersionId as any,
|
||||
promptHistory.handleSelectHistory,
|
||||
promptHistory.handleClearHistory,
|
||||
promptHistory.handleDeleteChain as any
|
||||
)
|
||||
|
||||
// 模板管理器
|
||||
const templateManagerState = useTemplateManager(
|
||||
services as any,
|
||||
{
|
||||
selectedOptimizeTemplate: toRef(optimizer, 'selectedOptimizeTemplate'),
|
||||
selectedUserOptimizeTemplate: toRef(optimizer, 'selectedUserOptimizeTemplate'),
|
||||
selectedIterateTemplate: toRef(optimizer, 'selectedIterateTemplate')
|
||||
}
|
||||
)
|
||||
|
||||
// 7. 监听服务初始化
|
||||
watch(services, (newServices) => {
|
||||
if (!newServices) return
|
||||
|
||||
promptService.value = newServices.promptService;
|
||||
// 设置服务引用
|
||||
promptService.value = newServices.promptService
|
||||
|
||||
const testModelSelect = computed(() => (testPanelRef.value as any)?.modelSelectRef || null)
|
||||
|
||||
const modelMgr = useModelManager({
|
||||
modelManager: newServices.modelManager,
|
||||
optimizeModelSelect: optimizeModelSelect,
|
||||
testModelSelect: testModelSelect
|
||||
});
|
||||
showConfig = modelMgr.showConfig
|
||||
selectedOptimizeModel = modelMgr.selectedOptimizeModel
|
||||
selectedTestModel = modelMgr.selectedTestModel
|
||||
|
||||
const optimizer = usePromptOptimizer(
|
||||
newServices.modelManager,
|
||||
newServices.templateManager,
|
||||
newServices.historyManager,
|
||||
newServices.promptService,
|
||||
selectedOptimizationMode,
|
||||
selectedOptimizeModel,
|
||||
selectedTestModel,
|
||||
);
|
||||
prompt = optimizer.prompt
|
||||
optimizedPrompt = optimizer.optimizedPrompt
|
||||
optimizedReasoning = optimizer.optimizedReasoning
|
||||
isOptimizing = optimizer.isOptimizing
|
||||
isIterating = optimizer.isIterating
|
||||
selectedIterateTemplate = optimizer.selectedIterateTemplate
|
||||
currentVersions = optimizer.currentVersions
|
||||
currentVersionId = optimizer.currentVersionId
|
||||
currentChainId = optimizer.currentChainId
|
||||
handleOptimizePrompt = optimizer.handleOptimizePrompt
|
||||
handleIteratePrompt = optimizer.handleIteratePrompt
|
||||
handleSwitchVersion = optimizer.handleSwitchVersion
|
||||
|
||||
promptHistory = usePromptHistory(
|
||||
newServices.historyManager,
|
||||
prompt,
|
||||
optimizedPrompt,
|
||||
currentChainId,
|
||||
currentVersions,
|
||||
currentVersionId
|
||||
)
|
||||
|
||||
const historyMgr = useHistoryManager(
|
||||
newServices.historyManager,
|
||||
prompt,
|
||||
optimizedPrompt,
|
||||
currentChainId,
|
||||
currentVersions,
|
||||
currentVersionId,
|
||||
promptHistory.handleSelectHistory,
|
||||
promptHistory.handleClearHistory,
|
||||
promptHistory.handleDeleteChain
|
||||
)
|
||||
showHistory = historyMgr.showHistory
|
||||
|
||||
const templateMgr = useTemplateManager(
|
||||
services as any,
|
||||
{
|
||||
selectedOptimizeTemplate: optimizer.selectedOptimizeTemplate,
|
||||
selectedUserOptimizeTemplate: optimizer.selectedUserOptimizeTemplate,
|
||||
selectedIterateTemplate: optimizer.selectedIterateTemplate
|
||||
}
|
||||
)
|
||||
showTemplateManager = templateMgr.showTemplates
|
||||
currentTemplateManagerType = templateMgr.currentType
|
||||
|
||||
currentSelectedTemplate = computed({
|
||||
get() {
|
||||
return selectedOptimizationMode.value === 'system'
|
||||
? optimizer.selectedOptimizeTemplate.value
|
||||
: optimizer.selectedUserOptimizeTemplate.value
|
||||
},
|
||||
set(newValue) {
|
||||
if (!newValue) return;
|
||||
if (selectedOptimizationMode.value === 'system') {
|
||||
optimizer.selectedOptimizeTemplate.value = newValue
|
||||
} else {
|
||||
optimizer.selectedUserOptimizeTemplate.value = newValue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
isReady.value = true
|
||||
console.log('All services and composables initialized.')
|
||||
})
|
||||
|
||||
// Helper functions for template
|
||||
// 8. 计算属性和方法
|
||||
const currentSelectedTemplate = computed({
|
||||
get() {
|
||||
return selectedOptimizationMode.value === 'system'
|
||||
? optimizer.selectedOptimizeTemplate
|
||||
: optimizer.selectedUserOptimizeTemplate
|
||||
},
|
||||
set(newValue) {
|
||||
if (!newValue) return
|
||||
if (selectedOptimizationMode.value === 'system') {
|
||||
optimizer.selectedOptimizeTemplate = newValue
|
||||
} else {
|
||||
optimizer.selectedUserOptimizeTemplate = newValue
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 处理优化提示词
|
||||
const handleOptimizePrompt = () => {
|
||||
optimizer.handleOptimizePrompt()
|
||||
}
|
||||
|
||||
// 处理迭代提示词
|
||||
const handleIteratePrompt = (payload: any) => {
|
||||
optimizer.handleIteratePrompt(payload)
|
||||
}
|
||||
|
||||
// 处理切换版本
|
||||
const handleSwitchVersion = (versionId: any) => {
|
||||
optimizer.handleSwitchVersion(versionId)
|
||||
}
|
||||
|
||||
// 打开GitHub仓库
|
||||
const openGithubRepo = () => {
|
||||
window.open('https://github.com/prompt-optimizer/prompt-optimizer', '_blank');
|
||||
window.open('https://github.com/prompt-optimizer/prompt-optimizer', '_blank')
|
||||
}
|
||||
const openTemplateManager = (templateType?: string) => {
|
||||
|
||||
// 打开模板管理器
|
||||
const openTemplateManager = (templateType?: 'optimize' | 'userOptimize' | 'iterate') => {
|
||||
// 如果传入了模板类型,直接使用;否则根据当前优化模式判断(向后兼容)
|
||||
currentTemplateManagerType.value = templateType || (selectedOptimizationMode.value === 'system' ? 'optimize' : 'userOptimize')
|
||||
showTemplateManager.value = true
|
||||
templateManagerState.currentType = templateType || (selectedOptimizationMode.value === 'system' ? 'optimize' : 'userOptimize')
|
||||
templateManagerState.showTemplates = true
|
||||
}
|
||||
|
||||
// 处理优化模式变更
|
||||
const handleOptimizationModeChange = (mode: OptimizationMode) => {
|
||||
selectedOptimizationMode.value = mode
|
||||
}
|
||||
|
||||
// 处理历史记录使用 - 智能模式切换
|
||||
const handleHistoryReuse = async (context: { record: any, chainId: string, rootPrompt: string, chain: any }) => {
|
||||
const { chain } = context
|
||||
|
||||
// 根据链条的根记录类型确定应该切换到的优化模式
|
||||
let targetMode: OptimizationMode
|
||||
if (chain.rootRecord.type === 'optimize') {
|
||||
targetMode = 'system'
|
||||
} else if (chain.rootRecord.type === 'userOptimize') {
|
||||
targetMode = 'user'
|
||||
} else {
|
||||
// 兜底:从根记录的 metadata 中获取优化模式
|
||||
targetMode = chain.rootRecord.metadata?.optimizationMode || 'system'
|
||||
}
|
||||
|
||||
// 如果目标模式与当前模式不同,自动切换
|
||||
if (targetMode !== selectedOptimizationMode.value) {
|
||||
selectedOptimizationMode.value = targetMode
|
||||
toast.info(t('toast.info.optimizationModeAutoSwitched', {
|
||||
mode: targetMode === 'system' ? t('common.system') : t('common.user')
|
||||
}))
|
||||
}
|
||||
|
||||
// 调用原有的历史记录处理逻辑
|
||||
await promptHistory.handleSelectHistory(context)
|
||||
}
|
||||
|
||||
// 提示词输入标签
|
||||
const promptInputLabel = computed(() => {
|
||||
return selectedOptimizationMode.value === 'system' ? t('promptOptimizer.originalPrompt') : t('promptOptimizer.userPrompt')
|
||||
return selectedOptimizationMode.value === 'system' ? t('promptOptimizer.originalPrompt') : t('promptOptimizer.userPromptInput')
|
||||
})
|
||||
|
||||
// 提示词输入占位符
|
||||
const promptInputPlaceholder = computed(() => {
|
||||
return selectedOptimizationMode.value === 'system' ? t('promptOptimizer.originalPromptPlaceholder') : t('promptOptimizer.userPromptPlaceholder')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"pref:theme-id": "\"dark\"",
|
||||
"pref:app:selected-optimize-model": "\"gemini\"",
|
||||
"pref:app:selected-optimize-template": "\"general-optimize\"",
|
||||
"pref:app:selected-user-optimize-template": "\"user-prompt-professional\"",
|
||||
"pref:app:selected-iterate-template": "\"iterate\"",
|
||||
"pref:app:selected-test-model": "\"gemini\"",
|
||||
"builtin-template-language": "zh-CN",
|
||||
"prompt_history": "[{\"id\":\"2ded8a19-3939-45dd-80b9-fed914af9ce4\",\"originalPrompt\":\"你是一个诗人\",\"optimizedPrompt\":\"# Role: 诗人\\n\\n## Profile\\n- language: 中文\\n- description: 你是一位富有创造力和想象力的诗人,擅长运用各种诗歌形式,捕捉生活中的美好、感悟和哲理,并将它们转化为优美动人的诗歌。\\n- background: 你可能经历了丰富的人生阅历,对自然、社会、人文等方面有着深刻的理解和感悟。你的作品可能受到古典诗歌、现代诗歌以及其他艺术形式的影响。\\n- personality: 敏感细腻、情感丰富、观察力强、充满想象力、富有创造力。\\n- expertise: 诗歌创作、诗歌鉴赏、文学理论、语言运用。\\n- target_audience: 诗歌爱好者、文学研究者、普通读者。\\n\\n## Skills\\n\\n1. 诗歌创作\\n - 格律诗创作: 能够熟练运用各种格律诗的创作技巧,如对仗、押韵、平仄等。\\n - 现代诗创作: 能够自由地表达情感和思想,运用意象、象征、隐喻等手法。\\n - 散文诗创作: 能够将散文的叙述性和诗歌的意境相结合。\\n - 主题提炼: 能够从生活体验中提炼出具有普遍意义的主题。\\n\\n2. 语言运用\\n - 词语选择: 能够精准地选择词语,表达细腻的情感和丰富的意境。\\n - 修辞手法: 能够灵活运用各种修辞手法,增强诗歌的表现力和感染力。\\n - 节奏韵律: 能够把握诗歌的节奏韵律,使诗歌更具音乐性。\\n - 意象构建: 能够创造出鲜明生动的意象,引发读者的联想和共鸣。\\n\\n## Rules\\n\\n1. 基本原则:\\n - 情感真挚: 诗歌应表达真挚的情感,避免空洞和虚假。\\n - 意象鲜明: 运用生动形象的意象,增强诗歌的感染力。\\n - 语言精炼: 运用简洁精炼的语言,表达深刻的思想。\\n - 主题深刻: 诗歌应具有一定的思想深度,引发读者的思考。\\n\\n2. 行为准则:\\n - 尊重原创: 不抄袭他人的作品,坚持原创。\\n - 不断学习: 不断学习诗歌理论和创作技巧,提升自身水平。\\n - 广泛阅读: 广泛阅读各种文学作品,拓展知识面。\\n - 虚心接受: 虚心接受他人的意见和建议,不断改进自己的作品。\\n\\n3. 限制条件:\\n - 避免政治敏感: 避免创作涉及政治敏感话题的诗歌。\\n - 避免低俗内容: 避免创作内容低俗、格调不高的诗歌。\\n - 避免过度晦涩: 避免使用过于晦涩难懂的语言,影响诗歌的传播。\\n - 避免过度堆砌辞藻: 避免过度使用华丽的辞藻,使诗歌显得空洞无物。\\n\\n## Workflows\\n\\n- 目标: 根据用户给定的主题或情境,创作一首高质量的诗歌。\\n- 步骤 1: 仔细理解用户提供的主题或情境,提炼出诗歌的核心情感和思想。\\n- 步骤 2: 构思诗歌的结构和布局,确定采用何种诗歌形式。\\n- 步骤 3: 选择恰当的意象和语言,运用修辞手法,进行诗歌的创作。\\n- 步骤 4: 对诗歌进行反复修改和润色,力求使其更加完美。\\n- 预期结果: 创作出一首主题鲜明、情感真挚、语言精炼、意象生动的诗歌,能够引发读者的共鸣和思考。\\n\\n## Initialization\\n作为诗人,你必须遵守上述Rules,按照Workflows执行任务。\\n\",\"type\":\"optimize\",\"modelKey\":\"gemini\",\"templateId\":\"general-optimize\",\"timestamp\":1751778053458,\"metadata\":{\"optimizationMode\":\"system\"},\"chainId\":\"8450c0bf-3c93-4920-a9bb-da9429a8a417\",\"version\":1,\"modelName\":\"Gemini\"}]",
|
||||
"user-templates": "[{\"id\":\"user-template-1751778165441-s6f1pd\",\"name\":\"通用优化 - 副本\",\"content\":\"你是一个专业的AI提示词优化专家。请帮我优化以下prompt,并按照以下格式返回:\\n\\n# Role: [角色名称]\\n\\n## Profile\\n- language: [语言]\\n- description: [详细的角色描述]\\n- background: [角色背景]\\n- personality: [性格特征]\\n- expertise: [专业领域]\\n- target_audience: [目标用户群]\\n\\n## Skills\\n\\n1. [核心技能类别]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n\\n2. [辅助技能类别]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n\\n## Rules\\n\\n1. [基本原则]:\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n\\n2. [行为准则]:\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n\\n3. [限制条件]:\\n - [具体限制]: [详细说明]\\n - [具体限制]: [详细说明]\\n - [具体限制]: [详细说明]\\n - [具体限制]: [详细说明]\\n\\n## Workflows\\n\\n- 目标: [明确目标]\\n- 步骤 1: [详细说明]\\n- 步骤 2: [详细说明]\\n- 步骤 3: [详细说明]\\n- 预期结果: [说明]\\n\\n\\n## Initialization\\n作为[角色名称],你必须遵守上述Rules,按照Workflows执行任务。\\n\\n\\n请基于以上模板,优化并扩展以下prompt,确保内容专业、完整且结构清晰,注意不要携带任何引导词或解释,不要使用代码块包围:\\n \",\"metadata\":{\"version\":\"1.0.0\",\"lastModified\":1751778165441,\"description\":\"通用优化提示词,适用于大多数场景\",\"author\":\"User\",\"templateType\":\"optimize\"},\"isBuiltin\":false}]"
|
||||
}
|
||||
Reference in new issue
Block a user