feat: 新增文件存储支持与历史记录管理优化

- 在`.gitignore`中添加了`prompt-optimizer-datas`以排除相关数据文件。
- 更新`INDEX.md`,新增桌面版文件存储实现的相关信息,确保文档的完整性。
- 在`index.ts`中导出了`FileStorageProvider`,支持文件存储功能。
- 修改`factory.ts`,新增对文件存储类型的支持,并优化错误处理逻辑。
- 在集成测试中模拟了历史记录的保存与验证,确保功能的正确性。
- 更新了多个测试用例,确保历史记录管理逻辑的准确性与一致性。

这些变更旨在提升数据存储的灵活性与历史记录管理的可靠性,确保用户体验的优化。
This commit is contained in:
linshen committed 2025-07-05 16:24:21 +08:00
1 parent 93ec7d4581
commit 8a35c010cc
16 files changed
+1425 -209

No files matched your search

+2
View File
@@ -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个跳过 ✅
+6
View File
@@ -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
+1
View File
@@ -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');
});
});
+55 -4
View File
@@ -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
View File
@@ -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')
})
+11
View File
@@ -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}]"
}