diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1faf7bbf..21c0712a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -147,6 +147,7 @@ export * from './services/context/constants' // 导出收藏管理相关 export { FavoriteManager } from './services/favorite/manager' export { FavoriteManagerElectronProxy } from './services/favorite/electron-proxy' +export { TagTypeConverter } from './services/favorite/type-converter' export * from './services/favorite/types' export * from './services/favorite/errors' diff --git a/packages/core/src/services/favorite/electron-proxy.ts b/packages/core/src/services/favorite/electron-proxy.ts index 12d3d47a..c7cc73d2 100644 --- a/packages/core/src/services/favorite/electron-proxy.ts +++ b/packages/core/src/services/favorite/electron-proxy.ts @@ -5,7 +5,12 @@ import { FavoriteAlreadyExistsError, FavoriteCategoryNotFoundError, FavoriteValidationError, - FavoriteStorageError + FavoriteStorageError, + FavoriteTagAlreadyExistsError, + FavoriteTagNotFoundError, + FavoriteTagError, + FavoriteMigrationError, + FavoriteImportExportError } from './errors'; declare const window: { @@ -48,6 +53,23 @@ export class FavoriteManagerElectronProxy implements IFavoriteManager { if (error.code === 'STORAGE_ERROR') { throw new FavoriteStorageError(error.message || ''); } + // 标签相关错误 + if (error.code === 'TAG_ALREADY_EXISTS') { + throw new FavoriteTagAlreadyExistsError(error.tag || ''); + } + if (error.code === 'TAG_NOT_FOUND') { + throw new FavoriteTagNotFoundError(error.tag || ''); + } + if (error.code === 'TAG_ERROR') { + throw new FavoriteTagError(error.message || '', error.code); + } + // 数据迁移和导入导出错误 + if (error.code === 'MIGRATION_ERROR') { + throw new FavoriteMigrationError(error.message || '', error.cause); + } + if (error.code === 'IMPORT_EXPORT_ERROR') { + throw new FavoriteImportExportError(error.message || '', error.cause, error.details); + } throw new FavoriteError(error.message || '未知错误'); } } @@ -100,7 +122,7 @@ export class FavoriteManagerElectronProxy implements IFavoriteManager { return this.invokeMethod('updateCategory', id, updates); } - async deleteCategory(id: string): Promise { + async deleteCategory(id: string): Promise { return this.invokeMethod('deleteCategory', id); } @@ -134,6 +156,10 @@ export class FavoriteManagerElectronProxy implements IFavoriteManager { return this.invokeMethod('getAllTags'); } + async addTag(tag: string): Promise { + return this.invokeMethod('addTag', tag); + } + async renameTag(oldTag: string, newTag: string): Promise { return this.invokeMethod('renameTag', oldTag, newTag); } diff --git a/packages/core/src/services/favorite/errors.ts b/packages/core/src/services/favorite/errors.ts index ed73a9bd..9a25fb95 100644 --- a/packages/core/src/services/favorite/errors.ts +++ b/packages/core/src/services/favorite/errors.ts @@ -11,35 +11,85 @@ export class FavoriteError extends Error { export class FavoriteNotFoundError extends FavoriteError { constructor(id: string) { - super(`收藏项未找到: ${id}`, 'FAVORITE_NOT_FOUND'); + super(`Favorite not found: ${id}`, 'FAVORITE_NOT_FOUND'); this.name = 'FavoriteNotFoundError'; } } export class FavoriteAlreadyExistsError extends FavoriteError { constructor(content: string) { - super(`收藏项已存在: ${content.slice(0, 50)}...`, 'FAVORITE_ALREADY_EXISTS'); + super(`Favorite already exists: ${content.slice(0, 50)}...`, 'FAVORITE_ALREADY_EXISTS'); this.name = 'FavoriteAlreadyExistsError'; } } export class FavoriteCategoryNotFoundError extends FavoriteError { constructor(id: string) { - super(`分类未找到: ${id}`, 'CATEGORY_NOT_FOUND'); + super(`Category not found: ${id}`, 'CATEGORY_NOT_FOUND'); this.name = 'FavoriteCategoryNotFoundError'; } } export class FavoriteValidationError extends FavoriteError { constructor(message: string) { - super(`验证错误: ${message}`, 'VALIDATION_ERROR'); + super(`Validation error: ${message}`, 'VALIDATION_ERROR'); this.name = 'FavoriteValidationError'; } } export class FavoriteStorageError extends FavoriteError { - constructor(message: string) { - super(`存储错误: ${message}`, 'STORAGE_ERROR'); + constructor(message: string, public cause?: Error) { + super(`Storage error: ${message}`, 'STORAGE_ERROR'); this.name = 'FavoriteStorageError'; } +} + +/** + * 标签相关错误 + */ +export class FavoriteTagError extends FavoriteError { + constructor(message: string, code: string = 'TAG_ERROR') { + super(message, code); + this.name = 'FavoriteTagError'; + } +} + +/** + * 标签已存在错误 + */ +export class FavoriteTagAlreadyExistsError extends FavoriteTagError { + constructor(tag: string) { + super(`Tag already exists: ${tag}`, 'TAG_ALREADY_EXISTS'); + this.name = 'FavoriteTagAlreadyExistsError'; + } +} + +/** + * Tag not found error + */ +export class FavoriteTagNotFoundError extends FavoriteTagError { + constructor(tag: string) { + super(`Tag not found: ${tag}`, 'TAG_NOT_FOUND'); + this.name = 'FavoriteTagNotFoundError'; + } +} + +/** + * Data migration error + */ +export class FavoriteMigrationError extends FavoriteError { + constructor(message: string, public cause?: Error) { + super(`Migration error: ${message}`, 'MIGRATION_ERROR'); + this.name = 'FavoriteMigrationError'; + } +} + +/** + * 导入导出错误 + */ +export class FavoriteImportExportError extends FavoriteError { + constructor(message: string, public cause?: Error, public details?: string[]) { + super(message, 'IMPORT_EXPORT_ERROR'); + this.name = 'FavoriteImportExportError'; + } } \ No newline at end of file diff --git a/packages/core/src/services/favorite/index.ts b/packages/core/src/services/favorite/index.ts index 4b1c98d8..8bc5b513 100644 --- a/packages/core/src/services/favorite/index.ts +++ b/packages/core/src/services/favorite/index.ts @@ -1,4 +1,12 @@ export * from './types'; export * from './errors'; export * from './manager'; -export type { FavoritePrompt, FavoriteCategory, FavoriteStats, IFavoriteManager } from './types'; \ No newline at end of file +export * from './type-converter'; +export type { + FavoritePrompt, + FavoriteCategory, + FavoriteStats, + FavoriteTag, + TagStatistics, + IFavoriteManager +} from './types'; \ No newline at end of file diff --git a/packages/core/src/services/favorite/manager.ts b/packages/core/src/services/favorite/manager.ts index 12392947..222b395f 100644 --- a/packages/core/src/services/favorite/manager.ts +++ b/packages/core/src/services/favorite/manager.ts @@ -3,6 +3,7 @@ import type { FavoritePrompt, FavoriteCategory, FavoriteStats, + FavoriteTag, IFavoriteManager } from './types'; import { @@ -10,7 +11,10 @@ import { FavoriteNotFoundError, FavoriteCategoryNotFoundError, FavoriteValidationError, - FavoriteStorageError + FavoriteStorageError, + FavoriteTagAlreadyExistsError, + FavoriteMigrationError, + FavoriteImportExportError } from './errors'; import { TypeMapper } from './type-mapper'; @@ -21,7 +25,8 @@ export class FavoriteManager implements IFavoriteManager { private readonly STORAGE_KEYS = { FAVORITES: 'favorites', CATEGORIES: 'favorite_categories', - STATS: 'favorite_stats' + STATS: 'favorite_stats', + TAGS: 'favorite_tags' } as const; private initPromise: Promise; @@ -48,7 +53,8 @@ export class FavoriteManager implements IFavoriteManager { try { this.initState = 'initializing'; - await this.initializeDefaultCategories(); + // ❌ 移除自动创建默认分类 - 改由 UI 层调用 ensureDefaultCategories + // await this.initializeDefaultCategories(); await this.migrateLegacyData(); this.initialized = true; this.initState = 'initialized'; @@ -119,91 +125,61 @@ export class FavoriteManager implements IFavoriteManager { if (migrated) { // 迁移后更新统计信息 await this.updateStats(); + console.info('[FavoriteManager] 数据迁移完成,已更新收藏项格式'); } } catch (error) { - console.warn('[FavoriteManager] 旧数据迁移失败:', error); - // 迁移失败不应该阻止服务初始化 + const errorMessage = error instanceof Error ? error.message : String(error); + const migrationError = new FavoriteMigrationError( + `Legacy data migration failed: ${errorMessage}`, + error instanceof Error ? error : undefined + ); + console.warn('[FavoriteManager]', migrationError); + // 迁移失败不应该阻止服务初始化,仅记录警告 } } /** - * 初始化默认分类 + * 确保默认分类存在(仅首次) + * 由 UI 层调用,传入国际化后的分类配置 * - * 💡 **架构说明**: - * 此方法在初始化期间被调用,现在可以安全地调用公共方法。 - * `ensureInitialized()` 会检测初始化状态,在初始化期间直接返回,避免死锁。 + * @param defaultCategories 默认分类配置数组 */ - private async initializeDefaultCategories(): Promise { + async ensureDefaultCategories( + defaultCategories: Array<{ + name: string; + description?: string; + color: string; + }> + ): Promise { + await this.ensureInitialized(); + try { - // ✅ 现在可以安全调用公共方法,ensureInitialized() 会智能跳过等待 + // ✅ 检查是否已初始化过默认分类 + const hasInitialized = await this.storageProvider.getItem('favorite_categories_initialized'); + if (hasInitialized === 'true') { + return; // 已经初始化过,即使用户删光了也不再自动创建 + } + + // ✅ 检查是否已有分类 const existingCategories = await this.getCategories(); if (existingCategories.length === 0) { - // ✅ 创建默认分类 - 可以安全调用 addCategory() - const now = Date.now(); - const defaultCategories: FavoriteCategory[] = [ - { - id: `cat_${now}_${Math.random().toString(36).substr(2, 9)}`, - name: '未分类', - description: '默认分类', - color: '#6B7280', - sortOrder: 0, - createdAt: now - }, - { - id: `cat_${now + 1}_${Math.random().toString(36).substr(2, 9)}`, - name: '系统提示词', - description: '优化后的系统提示词', - color: '#3B82F6', - sortOrder: 1, - createdAt: now + 1 - }, - { - id: `cat_${now + 2}_${Math.random().toString(36).substr(2, 9)}`, - name: '用户提示词', - description: '优化后的用户提示词', - color: '#10B981', - sortOrder: 2, - createdAt: now + 2 - }, - { - id: `cat_${now + 3}_${Math.random().toString(36).substr(2, 9)}`, - name: '创意写作', - description: '创意写作相关的提示词', - color: '#8B5CF6', - sortOrder: 3, - createdAt: now + 3 - }, - { - id: `cat_${now + 4}_${Math.random().toString(36).substr(2, 9)}`, - name: '编程开发', - description: '编程开发相关的提示词', - color: '#F59E0B', - sortOrder: 4, - createdAt: now + 4 - }, - { - id: `cat_${now + 5}_${Math.random().toString(36).substr(2, 9)}`, - name: '商业分析', - description: '商业分析相关的提示词', - color: '#EF4444', - sortOrder: 5, - createdAt: now + 5 - } - ]; - - // ✅ 批量添加默认分类 - 使用公共方法 - for (const category of defaultCategories) { + // ✅ 首次使用,创建默认分类 + for (let i = 0; i < defaultCategories.length; i++) { + const category = defaultCategories[i]; await this.addCategory({ name: category.name, description: category.description, color: category.color, - sortOrder: category.sortOrder + sortOrder: i }); } + + // ✅ 标记已初始化 + await this.storageProvider.setItem('favorite_categories_initialized', 'true'); } } catch (error) { - console.warn('初始化默认分类失败:', error); + console.warn('[FavoriteManager] 确保默认分类失败:', error); } } @@ -212,24 +188,24 @@ export class FavoriteManager implements IFavoriteManager { // 验证输入 if (!favorite.content?.trim()) { - throw new FavoriteValidationError('提示词内容不能为空'); + throw new FavoriteValidationError('Prompt content cannot be empty'); } // 验证 functionMode 必填 if (!favorite.functionMode) { - throw new FavoriteValidationError('功能模式 (functionMode) 不能为空'); + throw new FavoriteValidationError('Function mode (functionMode) cannot be empty'); } // 验证功能模式分类的完整性 if (favorite.functionMode === 'basic' || favorite.functionMode === 'context') { if (!favorite.optimizationMode) { - throw new FavoriteValidationError(`${favorite.functionMode} 模式必须指定 optimizationMode`); + throw new FavoriteValidationError(`${favorite.functionMode} mode must specify optimizationMode`); } } if (favorite.functionMode === 'image') { if (!favorite.imageSubMode) { - throw new FavoriteValidationError('image 模式必须指定 imageSubMode'); + throw new FavoriteValidationError('Image mode must specify imageSubMode'); } } @@ -271,7 +247,7 @@ export class FavoriteManager implements IFavoriteManager { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`添加收藏失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to add favorite: ${errorMessage}`); } } @@ -342,7 +318,7 @@ export class FavoriteManager implements IFavoriteManager { return favoritesList; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`获取收藏列表失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to get favorites: ${errorMessage}`); } } @@ -361,7 +337,7 @@ export class FavoriteManager implements IFavoriteManager { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`获取收藏详情失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to get favorite details: ${errorMessage}`); } } @@ -391,7 +367,7 @@ export class FavoriteManager implements IFavoriteManager { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`更新收藏失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to update favorite: ${errorMessage}`); } } @@ -415,7 +391,7 @@ export class FavoriteManager implements IFavoriteManager { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`删除收藏失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to delete favorite: ${errorMessage}`); } } @@ -425,7 +401,7 @@ export class FavoriteManager implements IFavoriteManager { const favoritesList = favorites || []; const deletedCount = favoritesList.filter(f => ids.includes(f.id)).length; if (deletedCount === 0) { - throw new FavoriteNotFoundError('未找到要删除的收藏项'); + throw new FavoriteNotFoundError('Favorite to delete not found'); } return favoritesList.filter(f => !ids.includes(f.id)); @@ -437,7 +413,7 @@ export class FavoriteManager implements IFavoriteManager { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`批量删除收藏失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to batch delete favorites: ${errorMessage}`); } } @@ -458,7 +434,7 @@ export class FavoriteManager implements IFavoriteManager { return categories ? JSON.parse(categories) : []; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`获取分类列表失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to get categories: ${errorMessage}`); } } @@ -466,7 +442,7 @@ export class FavoriteManager implements IFavoriteManager { await this.ensureInitialized(); if (!category.name?.trim()) { - throw new FavoriteValidationError('分类名称不能为空'); + throw new FavoriteValidationError('Category name cannot be empty'); } const now = Date.now(); @@ -485,7 +461,7 @@ export class FavoriteManager implements IFavoriteManager { // 检查是否已存在同名分类 const existing = categoriesList.find(c => c.name === category.name); if (existing) { - throw new FavoriteError(`分类已存在: ${category.name}`, 'CATEGORY_ALREADY_EXISTS'); + throw new FavoriteError(`Category already exists: ${category.name}`, 'CATEGORY_ALREADY_EXISTS'); } return [...categoriesList, newCategory]; }); @@ -496,7 +472,7 @@ export class FavoriteManager implements IFavoriteManager { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`添加分类失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to add category: ${errorMessage}`); } } @@ -521,18 +497,34 @@ export class FavoriteManager implements IFavoriteManager { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`更新分类失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to update category: ${errorMessage}`); } } - async deleteCategory(id: string): Promise { + /** + * 删除分类 + * 会自动清空该分类下所有收藏的分类字段 + * + * @param id 分类ID + * @returns 受影响的收藏数量 + */ + async deleteCategory(id: string): Promise { + await this.ensureInitialized(); + try { - // 检查是否有收藏项使用此分类 - const favoritesInCategory = await this.getFavorites({ categoryId: id }); - if (favoritesInCategory.length > 0) { - throw new FavoriteValidationError(`无法删除分类,还有 ${favoritesInCategory.length} 个收藏项使用此分类`); + // ✅ 获取该分类下的所有收藏 + const allFavorites = await this.getFavorites(); + const favoritesInCategory = allFavorites.filter(f => f.category === id); + + // ✅ 清空这些收藏的分类字段(不依赖"未分类"是否存在) + for (const favorite of favoritesInCategory) { + await this.updateFavorite(favorite.id, { + ...favorite, + category: undefined // 清空分类 + }); } + // ✅ 删除分类 await this.storageProvider.updateData(this.STORAGE_KEYS.CATEGORIES, (categories: FavoriteCategory[] | null) => { const categoriesList = categories || []; const index = categoriesList.findIndex(c => c.id === id); @@ -542,12 +534,14 @@ export class FavoriteManager implements IFavoriteManager { return categoriesList.filter(c => c.id !== id); }); + + return favoritesInCategory.length; } catch (error) { if (error instanceof FavoriteError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`删除分类失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to delete category: ${errorMessage}`); } } @@ -562,7 +556,7 @@ export class FavoriteManager implements IFavoriteManager { return await this.updateStats(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`获取统计信息失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to get statistics: ${errorMessage}`); } } @@ -617,6 +611,21 @@ export class FavoriteManager implements IFavoriteManager { }); } + /** + * 获取独立标签库中的所有标签名称 + * @private + */ + private async getAllIndependentTags(): Promise { + try { + const storedTags = await this.storageProvider.getItem(this.STORAGE_KEYS.TAGS); + const independentTags: FavoriteTag[] = storedTags ? JSON.parse(storedTags) : []; + return independentTags.map(t => t.tag); + } catch (error) { + console.warn('获取独立标签失败:', error); + return []; + } + } + async exportFavorites(ids?: string[]): Promise { try { let favorites: FavoritePrompt[]; @@ -628,44 +637,120 @@ export class FavoriteManager implements IFavoriteManager { } const categories = await this.getCategories(); + const tags = await this.getAllIndependentTags(); const exportData = { version: '1.0', exportDate: new Date().toISOString(), favorites, - categories + categories, + tags // 导出独立标签库(包含所有标签:使用中的 + 预创建的) }; return JSON.stringify(exportData, null, 2); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`导出收藏失败: ${errorMessage}`); + throw new FavoriteImportExportError( + `Failed to export favorites: ${errorMessage}`, + error instanceof Error ? error : undefined + ); } } + /** + * 计算标签使用统计 + * @private + * @returns 包含标签名和使用次数的 Map + */ + private async computeTagCounts(): Promise> { + // 1. 获取独立标签 + const storedTags = await this.storageProvider.getItem(this.STORAGE_KEYS.TAGS); + const independentTags: FavoriteTag[] = storedTags ? JSON.parse(storedTags) : []; + + // 2. 统计收藏项中使用的标签 + const favorites = await this.getFavorites(); + const tagCounts = new Map(); + + favorites.forEach(favorite => { + favorite.tags.forEach(tag => { + tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); + }); + }); + + // 3. 合并独立标签和使用中的标签 + // 独立标签如果未被使用,count 为 0 + independentTags.forEach(({ tag }) => { + if (!tagCounts.has(tag)) { + tagCounts.set(tag, 0); + } + }); + + return tagCounts; + } + async getAllTags(): Promise> { try { - const favorites = await this.getFavorites(); - const tagCounts = new Map(); - - favorites.forEach(favorite => { - favorite.tags.forEach(tag => { - tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); - }); - }); + const tagCounts = await this.computeTagCounts(); + // 返回排序后的结果(使用次数降序,相同次数按标签名升序) return Array.from(tagCounts.entries()) .map(([tag, count]) => ({ tag, count })) - .sort((a, b) => b.count - a.count); // 按使用次数降序 + .sort((a, b) => { + if (b.count !== a.count) { + return b.count - a.count; // 按使用次数降序 + } + return a.tag.localeCompare(b.tag); // 相同次数按标签名升序 + }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`获取标签列表失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to get tags: ${errorMessage}`); + } + } + + async addTag(tag: string): Promise { + await this.ensureInitialized(); + + const trimmedTag = tag.trim(); + if (!trimmedTag) { + throw new FavoriteValidationError('Tag name cannot be empty'); + } + + try { + await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => { + const tagsList = tags || []; + + // 检查是否已存在 + const existing = tagsList.find(t => t.tag === trimmedTag); + if (existing) { + throw new FavoriteTagAlreadyExistsError(trimmedTag); + } + + const now = Date.now(); + const newTag: FavoriteTag = { + tag: trimmedTag, + createdAt: now + }; + + return [...tagsList, newTag]; + }); + + // 更新统计信息 + await this.updateStats(); + } catch (error) { + if (error instanceof FavoriteError) { + throw error; + } + const errorMessage = error instanceof Error ? error.message : String(error); + throw new FavoriteStorageError( + `Failed to add tag: ${errorMessage}`, + error instanceof Error ? error : undefined + ); } } async renameTag(oldTag: string, newTag: string): Promise { if (!oldTag || !newTag) { - throw new FavoriteValidationError('标签名不能为空'); + throw new FavoriteValidationError('Tag name cannot be empty'); } if (oldTag === newTag) { @@ -673,8 +758,21 @@ export class FavoriteManager implements IFavoriteManager { } let affectedCount = 0; + let oldTagExistedInIndependentLib = false; try { + // 1. 更新独立标签库:删除旧标签,记录是否存在 + await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => { + const tagsList = tags || []; + + // 检查旧标签是否存在 + oldTagExistedInIndependentLib = tagsList.some(t => t.tag === oldTag); + + // 删除旧标签 + return tagsList.filter(t => t.tag !== oldTag); + }); + + // 2. 更新收藏列表中的标签 await this.storageProvider.updateData(this.STORAGE_KEYS.FAVORITES, (favorites: FavoritePrompt[] | null) => { const favoritesList = favorites || []; @@ -695,26 +793,64 @@ export class FavoriteManager implements IFavoriteManager { return favoritesList; }); + // 3. 只有当旧标签存在于独立库或被收藏使用时,才添加新标签到独立库 + if (oldTagExistedInIndependentLib || affectedCount > 0) { + await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => { + const tagsList = tags || []; + + // 添加新标签(如果不存在) + const hasNewTag = tagsList.some(t => t.tag === newTag); + if (!hasNewTag) { + tagsList.push({ + tag: newTag, + createdAt: Date.now() + }); + } + + return tagsList; + }); + } + await this.updateStats(); return affectedCount; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`重命名标签失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to rename tag: ${errorMessage}`); } } async mergeTags(sourceTags: string[], targetTag: string): Promise { if (!sourceTags || sourceTags.length === 0) { - throw new FavoriteValidationError('源标签列表不能为空'); + throw new FavoriteValidationError('Source tag list cannot be empty'); } if (!targetTag) { - throw new FavoriteValidationError('目标标签不能为空'); + throw new FavoriteValidationError('Target tag cannot be empty'); } let affectedCount = 0; try { + // 1. 更新独立标签库:删除所有源标签,确保目标标签存在 + await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => { + const tagsList = tags || []; + + // 删除所有源标签 + const filteredTags = tagsList.filter(t => !sourceTags.includes(t.tag)); + + // 确保目标标签存在 + const hasTargetTag = filteredTags.some(t => t.tag === targetTag); + if (!hasTargetTag) { + filteredTags.push({ + tag: targetTag, + createdAt: Date.now() + }); + } + + return filteredTags; + }); + + // 2. 更新收藏列表中的标签 await this.storageProvider.updateData(this.STORAGE_KEYS.FAVORITES, (favorites: FavoritePrompt[] | null) => { const favoritesList = favorites || []; @@ -747,18 +883,25 @@ export class FavoriteManager implements IFavoriteManager { return affectedCount; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`合并标签失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to merge tags: ${errorMessage}`); } } async deleteTag(tag: string): Promise { if (!tag) { - throw new FavoriteValidationError('标签名不能为空'); + throw new FavoriteValidationError('Tag name cannot be empty'); } let affectedCount = 0; try { + // 1. 从独立标签中删除 + await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => { + const tagsList = tags || []; + return tagsList.filter(t => t.tag !== tag); + }); + + // 2. 从所有收藏项中删除 await this.storageProvider.updateData(this.STORAGE_KEYS.FAVORITES, (favorites: FavoritePrompt[] | null) => { const favoritesList = favorites || []; @@ -778,13 +921,13 @@ export class FavoriteManager implements IFavoriteManager { return affectedCount; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`删除标签失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to delete tag: ${errorMessage}`); } } async reorderCategories(categoryIds: string[]): Promise { if (!categoryIds || categoryIds.length === 0) { - throw new FavoriteValidationError('分类ID列表不能为空'); + throw new FavoriteValidationError('Category ID list cannot be empty'); } try { @@ -820,7 +963,7 @@ export class FavoriteManager implements IFavoriteManager { }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`重新排序分类失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to reorder categories: ${errorMessage}`); } } @@ -830,7 +973,7 @@ export class FavoriteManager implements IFavoriteManager { return favorites.length; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`获取分类使用统计失败: ${errorMessage}`); + throw new FavoriteStorageError(`Failed to get category usage: ${errorMessage}`); } } @@ -850,7 +993,43 @@ export class FavoriteManager implements IFavoriteManager { const importData = JSON.parse(data); if (!importData.favorites || !Array.isArray(importData.favorites)) { - throw new FavoriteValidationError('导入数据格式无效'); + throw new FavoriteValidationError('Invalid import data format'); + } + // 【新增】先导入分类(如果有) + if (importData.categories && Array.isArray(importData.categories)) { + for (const category of importData.categories) { + try { + // 检查分类是否已存在(根据ID或名称) + const existingCategories = await this.getCategories(); + const exists = existingCategories.some( + c => c.id === category.id || c.name === category.name + ); + + if (!exists) { + await this.addCategory({ + name: category.name, + description: category.description, + color: category.color, + sortOrder: category.sortOrder + }); + } + } catch (error) { + // 分类导入失败,记录错误但继续 + console.warn('Failed to import category:', category.name, error); + } + } + } + + + // 【新增】先导入独立标签(如果有) + if (importData.tags && Array.isArray(importData.tags)) { + for (const tag of importData.tags) { + try { + await this.addTag(tag); + } catch (error) { + // 标签已存在,跳过错误继续 + } + } } const existingFavorites = await this.getFavorites(); @@ -860,7 +1039,7 @@ export class FavoriteManager implements IFavoriteManager { try { // 验证必填字段 if (!favorite.content?.trim()) { - throw new FavoriteValidationError('导入数据中存在空内容的收藏项'); + throw new FavoriteValidationError('Import data contains favorite with empty content'); } // 构建功能模式数据,兼容旧数据 @@ -872,7 +1051,7 @@ export class FavoriteManager implements IFavoriteManager { const mapping = { functionMode, optimizationMode, imageSubMode }; if (!TypeMapper.validateMapping(mapping)) { throw new FavoriteValidationError( - `导入数据中存在无效的功能模式分类: functionMode=${functionMode}, optimizationMode=${optimizationMode}, imageSubMode=${imageSubMode}` + `Invalid function mode in import data: functionMode=${functionMode}, optimizationMode=${optimizationMode}, imageSubMode=${imageSubMode}` ); } @@ -912,7 +1091,7 @@ export class FavoriteManager implements IFavoriteManager { } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - result.errors.push(`导入收藏失败: ${errorMessage}`); + result.errors.push(`Failed to import favorite: ${errorMessage}`); } } @@ -920,7 +1099,11 @@ export class FavoriteManager implements IFavoriteManager { return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - throw new FavoriteStorageError(`导入收藏失败: ${errorMessage}`); + throw new FavoriteImportExportError( + `Failed to import favorites: ${errorMessage}`, + error instanceof Error ? error : undefined, + result.errors.length > 0 ? result.errors : undefined + ); } } } \ No newline at end of file diff --git a/packages/core/src/services/favorite/type-converter.ts b/packages/core/src/services/favorite/type-converter.ts new file mode 100644 index 00000000..87d06353 --- /dev/null +++ b/packages/core/src/services/favorite/type-converter.ts @@ -0,0 +1,90 @@ +import type { TagStatistics } from './types'; + +/** + * 标签类型转换工具 + * 统一处理不同格式之间的标签数据转换 + */ +export class TagTypeConverter { + /** + * 将 API 返回的标签数据转换为 TagStatistics 格式 + * @param apiData API 返回的标签数据 { tag: string; count: number }[] + * @returns TagStatistics[] 格式的标签统计数据 + */ + static toTagStatistics(apiData: Array<{ tag: string; count: number }>): TagStatistics[] { + return apiData.map(item => ({ + name: item.tag, + count: item.count, + lastUsed: undefined + })); + } + + /** + * 将 TagStatistics 转换回 API 格式 + * @param stats TagStatistics[] 格式的标签统计数据 + * @returns API 格式的标签数据 { tag: string; count: number }[] + */ + static fromTagStatistics(stats: TagStatistics[]): Array<{ tag: string; count: number }> { + return stats.map(item => ({ + tag: item.name, + count: item.count + })); + } + + /** + * 将标签数据转换为自动完成选项格式 + * @param apiData API 返回的标签数据 + * @returns 自动完成选项格式 { label: string; value: string; count: number }[] + */ + static toAutoCompleteOptions(apiData: Array<{ tag: string; count: number }>): Array<{ + label: string; + value: string; + count: number; + }> { + return apiData.map(item => ({ + label: `${item.tag} (${item.count})`, + value: item.tag, + count: item.count + })); + } + + /** + * 将标签数据转换为简单的字符串数组 + * @param apiData API 返回的标签数据 + * @returns 标签名称数组 + */ + static toStringArray(apiData: Array<{ tag: string; count: number }>): string[] { + return apiData.map(item => item.tag); + } + + /** + * 按使用次数降序排序标签 + * @param tags 标签数据 + * @returns 排序后的标签数据 + */ + static sortByCount(tags: T[]): T[] { + return [...tags].sort((a, b) => b.count - a.count); + } + + /** + * 按标签名称升序排序标签 + * @param tags 标签数据 + * @returns 排序后的标签数据 + */ + static sortByName(tags: TagStatistics[]): TagStatistics[] { + return [...tags].sort((a, b) => a.name.localeCompare(b.name)); + } + + /** + * 混合排序:先按使用次数降序,相同次数按名称升序 + * @param tags 标签数据 + * @returns 排序后的标签数据 + */ + static sortByCountThenName(tags: TagStatistics[]): TagStatistics[] { + return [...tags].sort((a, b) => { + if (b.count !== a.count) { + return b.count - a.count; + } + return a.name.localeCompare(b.name); + }); + } +} diff --git a/packages/core/src/services/favorite/types.ts b/packages/core/src/services/favorite/types.ts index 6fc864b1..5121fe0f 100644 --- a/packages/core/src/services/favorite/types.ts +++ b/packages/core/src/services/favorite/types.ts @@ -84,6 +84,29 @@ export interface FavoriteStats { lastUsedAt?: number; } +/** + * 独立标签接口 + */ +export interface FavoriteTag { + /** 标签名称 */ + tag: string; + /** 创建时间 */ + createdAt: number; +} + +/** + * 标签统计信息接口 + * 用于标签管理器展示标签使用情况 + */ +export interface TagStatistics { + /** 标签名称 */ + name: string; + /** 使用次数 */ + count: number; + /** 最后使用时间(可选,暂未实现) */ + lastUsed?: number; +} + /** * 收藏管理器接口 */ @@ -127,7 +150,7 @@ export interface IFavoriteManager { updateCategory(id: string, updates: Partial): Promise; /** 删除分类 */ - deleteCategory(id: string): Promise; + deleteCategory(id: string): Promise; /** 获取统计信息 */ getStats(): Promise; @@ -151,16 +174,19 @@ export interface IFavoriteManager { errors: string[]; }>; - /** 获取所有标签及其使用统计 */ + /** 获取所有标签及其使用统计(包含独立标签和使用中的标签) */ getAllTags(): Promise>; + /** 添加独立标签 */ + addTag(tag: string): Promise; + /** 重命名标签 */ renameTag(oldTag: string, newTag: string): Promise; /** 合并多个标签为一个 */ mergeTags(sourceTags: string[], targetTag: string): Promise; - /** 删除标签 */ + /** 删除标签(同时从独立标签和所有收藏项中删除) */ deleteTag(tag: string): Promise; /** 对分类进行重新排序 */ diff --git a/packages/core/tests/services/favorite/integration.test.ts b/packages/core/tests/services/favorite/integration.test.ts new file mode 100644 index 00000000..1b18e2b1 --- /dev/null +++ b/packages/core/tests/services/favorite/integration.test.ts @@ -0,0 +1,741 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { FavoriteManager } from '../../../src/services/favorite/manager'; +import type { IStorageProvider } from '../../../src/services/storage/types'; +import { TypeMapper } from '../../../src/services/favorite/type-mapper'; +import type { PromptRecordType } from '../../../src/services/history/types'; + +/** + * FavoriteManager 集成测试 + * 测试完整的业务流程和跨功能交互 + */ +describe('FavoriteManager - 集成测试', () => { + let manager: FavoriteManager; + let mockStorage: Map; + let storageProvider: IStorageProvider; + + beforeEach(() => { + // 创建模拟存储 + mockStorage = new Map(); + + storageProvider = { + getItem: vi.fn(async (key: string) => mockStorage.get(key) || null), + setItem: vi.fn(async (key: string, value: string) => { + mockStorage.set(key, value); + }), + removeItem: vi.fn(async (key: string) => { + mockStorage.delete(key); + }), + clearAll: vi.fn(async () => { + mockStorage.clear(); + }), + batchUpdate: vi.fn(async (operations: Array<{ key: string; operation: 'set' | 'remove'; value?: string }>) => { + operations.forEach(({ key, operation, value }) => { + if (operation === 'set' && value) { + mockStorage.set(key, value); + } else if (operation === 'remove') { + mockStorage.delete(key); + } + }); + }), + updateData: vi.fn(async (key: string, updater: (data: any) => any) => { + const currentData = mockStorage.get(key); + const parsedData = currentData ? JSON.parse(currentData) : null; + const updatedData = updater(parsedData); + mockStorage.set(key, JSON.stringify(updatedData)); + }) + }; + + manager = new FavoriteManager(storageProvider); + }); + + describe('收藏 CRUD 完整流程', () => { + it('应该完成完整的增删改查流程', async () => { + // 1. 创建分类 + const categoryId = await manager.addCategory({ + name: '测试分类', + description: '用于集成测试', + color: '#FF5722' + }); + + // 2. 添加收藏 + const favoriteId = await manager.addFavorite({ + title: '测试收藏', + content: '测试内容', + tags: ['测试', '集成'], + category: categoryId, + functionMode: 'basic', + optimizationMode: 'system', + metadata: { + originalContent: '原始内容', + sourceHistoryId: 'history-001' + } + }); + + expect(favoriteId).toBeTruthy(); + + // 3. 查询单个收藏 + const favorite = await manager.getFavorite(favoriteId); + expect(favorite.id).toBe(favoriteId); + expect(favorite.title).toBe('测试收藏'); + expect(favorite.tags).toEqual(['测试', '集成']); + expect(favorite.category).toBe(categoryId); + expect(favorite.functionMode).toBe('basic'); + expect(favorite.optimizationMode).toBe('system'); + expect(favorite.metadata?.originalContent).toBe('原始内容'); + expect(favorite.metadata?.sourceHistoryId).toBe('history-001'); + + // 4. 查询列表 + const favorites = await manager.getFavorites(); + expect(favorites.length).toBe(1); + expect(favorites[0].id).toBe(favoriteId); + + // 5. 更新收藏 + await manager.updateFavorite(favoriteId, { + title: '更新后的标题', + tags: ['测试', '集成', '更新'], + functionMode: 'context', + optimizationMode: 'user' + }); + + const updated = await manager.getFavorite(favoriteId); + expect(updated.title).toBe('更新后的标题'); + expect(updated.tags).toEqual(['测试', '集成', '更新']); + expect(updated.functionMode).toBe('context'); + expect(updated.optimizationMode).toBe('user'); + expect(updated.updatedAt).not.toBe(favorite.updatedAt); + + // 6. 删除收藏 + await manager.deleteFavorite(favoriteId); + const allFavorites = await manager.getFavorites(); + expect(allFavorites.length).toBe(0); + + // 7. 验证分类仍然存在 + const categories = await manager.getCategories(); + expect(categories.find(c => c.id === categoryId)).toBeDefined(); + }); + + it('应该正确处理多个收藏的关联关系', async () => { + // 创建2个分类 + const cat1Id = await manager.addCategory({ name: '分类1', color: '#FF0000' }); + const cat2Id = await manager.addCategory({ name: '分类2', color: '#00FF00' }); + + // 创建3个收藏 + const fav1Id = await manager.addFavorite({ + title: '收藏1', + content: '内容1', + tags: ['共享标签', '标签1'], + category: cat1Id, + functionMode: 'basic', + optimizationMode: 'system' + }); + + const fav2Id = await manager.addFavorite({ + title: '收藏2', + content: '内容2', + tags: ['共享标签', '标签2'], + category: cat1Id, + functionMode: 'basic', + optimizationMode: 'user' + }); + + const fav3Id = await manager.addFavorite({ + title: '收藏3', + content: '内容3', + tags: ['标签3'], + category: cat2Id, + functionMode: 'image', + imageSubMode: 'text2image' + }); + + // 验证标签统计 + const tags = await manager.getAllTags(); + const sharedTag = tags.find(t => t.tag === '共享标签'); + expect(sharedTag?.count).toBe(2); + + // 验证分类使用统计 + const cat1Usage = await manager.getCategoryUsage(cat1Id); + const cat2Usage = await manager.getCategoryUsage(cat2Id); + expect(cat1Usage).toBe(2); + expect(cat2Usage).toBe(1); + + // 按分类查询 + const cat1Favorites = await manager.getFavorites({ categoryId: cat1Id }); + expect(cat1Favorites.length).toBe(2); + + // 按标签查询 + const sharedTagFavorites = await manager.getFavorites({ tags: ['共享标签'] }); + expect(sharedTagFavorites.length).toBe(2); + }); + + it('应该支持功能模式验证的完整流程', async () => { + // 添加各种模式的收藏 + const basicId = await manager.addFavorite({ + title: 'Basic Mode', + content: 'Content', + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const contextId = await manager.addFavorite({ + title: 'Context Mode', + content: 'Content', + tags: [], + functionMode: 'context', + optimizationMode: 'user' + }); + + const imageId = await manager.addFavorite({ + title: 'Image Mode', + content: 'Content', + tags: [], + functionMode: 'image', + imageSubMode: 'text2image' + }); + + // 验证每个收藏的模式 + const basic = await manager.getFavorite(basicId); + expect(basic.functionMode).toBe('basic'); + expect(basic.optimizationMode).toBe('system'); + expect(basic.imageSubMode).toBeUndefined(); + + const context = await manager.getFavorite(contextId); + expect(context.functionMode).toBe('context'); + expect(context.optimizationMode).toBe('user'); + expect(context.imageSubMode).toBeUndefined(); + + const image = await manager.getFavorite(imageId); + expect(image.functionMode).toBe('image'); + expect(image.imageSubMode).toBe('text2image'); + expect(image.optimizationMode).toBeUndefined(); + + // 更新模式应该生效 + await manager.updateFavorite(basicId, { + functionMode: 'context', + optimizationMode: 'system' + }); + + const updated = await manager.getFavorite(basicId); + expect(updated.functionMode).toBe('context'); + expect(updated.optimizationMode).toBe('system'); + }); + }); + + describe('从优化历史保存收藏集成测试', () => { + it('应该从 optimize 类型正确创建收藏', async () => { + // 模拟历史记录 + const recordType: PromptRecordType = 'optimize'; + const mapping = TypeMapper.mapFromRecordType(recordType); + + // 创建收藏 + const favoriteId = await manager.addFavorite({ + title: '优化后的提示词', + content: '这是优化后的内容', + tags: ['AI', '优化'], + ...mapping, + metadata: { + originalContent: '这是原始内容', + sourceHistoryId: 'hist-123' + } + }); + + // 验证 + const favorite = await manager.getFavorite(favoriteId); + expect(favorite.functionMode).toBe('basic'); + expect(favorite.optimizationMode).toBe('system'); + expect(favorite.metadata?.originalContent).toBe('这是原始内容'); + expect(favorite.metadata?.sourceHistoryId).toBe('hist-123'); + }); + + it('应该从 contextUserOptimize 类型正确创建收藏', async () => { + const recordType: PromptRecordType = 'contextUserOptimize'; + const mapping = TypeMapper.mapFromRecordType(recordType); + + const favoriteId = await manager.addFavorite({ + title: '用户上下文优化', + content: '优化内容', + tags: [], + ...mapping, + metadata: { + originalContent: '原始内容', + sourceHistoryId: 'hist-456' + } + }); + + const favorite = await manager.getFavorite(favoriteId); + expect(favorite.functionMode).toBe('context'); + expect(favorite.optimizationMode).toBe('user'); + }); + + it('应该从 imageOptimize 类型正确创建收藏', async () => { + const recordType: PromptRecordType = 'imageOptimize'; + const mapping = TypeMapper.mapFromRecordType(recordType); + + const favoriteId = await manager.addFavorite({ + title: '图像提示词优化', + content: '优化后的图像提示词', + tags: ['图像生成'], + ...mapping + }); + + const favorite = await manager.getFavorite(favoriteId); + expect(favorite.functionMode).toBe('image'); + expect(favorite.imageSubMode).toBe('text2image'); + }); + + it('应该从 image2imageOptimize 类型正确创建收藏', async () => { + const recordType: PromptRecordType = 'image2imageOptimize'; + const mapping = TypeMapper.mapFromRecordType(recordType); + + const favoriteId = await manager.addFavorite({ + title: '图生图提示词', + content: '图生图优化内容', + tags: [], + ...mapping + }); + + const favorite = await manager.getFavorite(favoriteId); + expect(favorite.functionMode).toBe('image'); + expect(favorite.imageSubMode).toBe('image2image'); + }); + + it('应该处理所有历史记录类型', async () => { + const allTypes: PromptRecordType[] = [ + 'optimize', + 'userOptimize', + 'iterate', + 'test', + 'contextSystemOptimize', + 'contextUserOptimize', + 'contextIterate', + 'imageOptimize', + 'contextImageOptimize', + 'imageIterate', + 'text2imageOptimize', + 'image2imageOptimize' + ]; + + const ids: string[] = []; + + for (const type of allTypes) { + const mapping = TypeMapper.mapFromRecordType(type); + const id = await manager.addFavorite({ + title: `收藏-${type}`, + content: `内容-${type}`, + tags: [type], + ...mapping + }); + ids.push(id); + } + + // 验证所有收藏都创建成功 + expect(ids.length).toBe(allTypes.length); + + const favorites = await manager.getFavorites(); + expect(favorites.length).toBe(allTypes.length); + + // 验证每个收藏的功能模式都有效 + for (const favorite of favorites) { + const mapping = { + functionMode: favorite.functionMode, + optimizationMode: favorite.optimizationMode, + imageSubMode: favorite.imageSubMode + }; + expect(TypeMapper.validateMapping(mapping)).toBe(true); + } + }); + }); + + describe('标签和分类管理集成测试', () => { + it('标签重命名应该更新所有关联收藏', async () => { + // 创建多个使用相同标签的收藏 + await manager.addFavorite({ + title: '收藏1', + content: '内容1', + tags: ['旧标签', '其他标签'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容2', + tags: ['旧标签'], + functionMode: 'basic', + optimizationMode: 'user' + }); + + await manager.addFavorite({ + title: '收藏3', + content: '内容3', + tags: ['不相关标签'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + // 重命名标签 + await manager.renameTag('旧标签', '新标签'); + + // 验证所有收藏更新 + const favorites = await manager.getFavorites(); + const fav1 = favorites.find(f => f.title === '收藏1'); + const fav2 = favorites.find(f => f.title === '收藏2'); + const fav3 = favorites.find(f => f.title === '收藏3'); + + expect(fav1?.tags).toContain('新标签'); + expect(fav1?.tags).not.toContain('旧标签'); + expect(fav1?.tags).toContain('其他标签'); + + expect(fav2?.tags).toContain('新标签'); + expect(fav2?.tags).not.toContain('旧标签'); + + expect(fav3?.tags).toContain('不相关标签'); + expect(fav3?.tags).not.toContain('新标签'); + + // 验证标签统计 + const tags = await manager.getAllTags(); + const newTag = tags.find(t => t.tag === '新标签'); + const oldTag = tags.find(t => t.tag === '旧标签'); + + expect(newTag?.count).toBe(2); + expect(oldTag).toBeUndefined(); + }); + + it('标签合并应该正确去重', async () => { + // 创建测试数据 + await manager.addFavorite({ + title: '收藏1', + content: '内容', + tags: ['标签A', '标签B'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容', + tags: ['标签A', '标签C'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏3', + content: '内容', + tags: ['标签B', '标签C'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + // 合并标签A和标签B -> 标签B + await manager.mergeTags(['标签A'], '标签B'); + + const favorites = await manager.getFavorites(); + const fav1 = favorites.find(f => f.title === '收藏1'); + const fav2 = favorites.find(f => f.title === '收藏2'); + const fav3 = favorites.find(f => f.title === '收藏3'); + + // 收藏1: 原本 [A, B] -> [B] (去重) + expect(fav1?.tags).toEqual(['标签B']); + + // 收藏2: 原本 [A, C] -> [B, C] + expect(fav2?.tags).toContain('标签B'); + expect(fav2?.tags).toContain('标签C'); + expect(fav2?.tags).not.toContain('标签A'); + + // 收藏3: 原本 [B, C] -> [B, C] (不变) + expect(fav3?.tags).toContain('标签B'); + expect(fav3?.tags).toContain('标签C'); + + // 验证标签统计 + const tags = await manager.getAllTags(); + const tagA = tags.find(t => t.tag === '标签A'); + const tagB = tags.find(t => t.tag === '标签B'); + + expect(tagA).toBeUndefined(); + expect(tagB?.count).toBe(3); // 所有3个收藏都有标签B + }); + + it('标签删除应该从所有收藏中移除', async () => { + await manager.addFavorite({ + title: '收藏1', + content: '内容', + tags: ['要删除', '保留'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容', + tags: ['要删除'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.deleteTag('要删除'); + + const favorites = await manager.getFavorites(); + favorites.forEach(fav => { + expect(fav.tags).not.toContain('要删除'); + }); + + const tags = await manager.getAllTags(); + expect(tags.find(t => t.tag === '要删除')).toBeUndefined(); + expect(tags.find(t => t.tag === '保留')).toBeDefined(); + }); + + it('分类排序应该更新所有分类的 sortOrder', async () => { + const cat1 = await manager.addCategory({ name: '分类1', color: '#FF0000' }); + const cat2 = await manager.addCategory({ name: '分类2', color: '#00FF00' }); + const cat3 = await manager.addCategory({ name: '分类3', color: '#0000FF' }); + + // 新顺序: 3, 1, 2 + await manager.reorderCategories([cat3, cat1, cat2]); + + const categories = await manager.getCategories(); + const sorted = categories.sort((a, b) => a.sortOrder - b.sortOrder); + + expect(sorted[0].id).toBe(cat3); + expect(sorted[0].sortOrder).toBe(0); + expect(sorted[1].id).toBe(cat1); + expect(sorted[1].sortOrder).toBe(1); + expect(sorted[2].id).toBe(cat2); + expect(sorted[2].sortOrder).toBe(2); + }); + + it('分类使用统计应该实时更新', async () => { + const catId = await manager.addCategory({ name: '测试分类', color: '#FF0000' }); + + // 初始使用为0 + expect(await manager.getCategoryUsage(catId)).toBe(0); + + // 添加收藏 + const fav1 = await manager.addFavorite({ + title: '收藏1', + content: '内容', + tags: [], + category: catId, + functionMode: 'basic', + optimizationMode: 'system' + }); + + expect(await manager.getCategoryUsage(catId)).toBe(1); + + const fav2 = await manager.addFavorite({ + title: '收藏2', + content: '内容', + tags: [], + category: catId, + functionMode: 'basic', + optimizationMode: 'system' + }); + + expect(await manager.getCategoryUsage(catId)).toBe(2); + + // 删除一个收藏 + await manager.deleteFavorite(fav1); + expect(await manager.getCategoryUsage(catId)).toBe(1); + + // 更新收藏移除分类 + await manager.updateFavorite(fav2, { category: undefined }); + expect(await manager.getCategoryUsage(catId)).toBe(0); + }); + + it('删除分类应该清空关联收藏的分类字段', async () => { + const catId = await manager.addCategory({ name: '要删除的分类', color: '#FF0000' }); + + const fav1 = await manager.addFavorite({ + title: '收藏1', + content: '内容', + tags: [], + category: catId, + functionMode: 'basic', + optimizationMode: 'system' + }); + + const fav2 = await manager.addFavorite({ + title: '收藏2', + content: '内容', + tags: [], + category: catId, + functionMode: 'basic', + optimizationMode: 'system' + }); + + const affectedCount = await manager.deleteCategory(catId); + expect(affectedCount).toBe(2); + + const updated1 = await manager.getFavorite(fav1); + const updated2 = await manager.getFavorite(fav2); + + expect(updated1.category).toBeUndefined(); + expect(updated2.category).toBeUndefined(); + }); + }); + + describe('导入导出集成测试', () => { + it('应该正确导出和导入包含所有关联数据', async () => { + // 创建完整的测试数据 + const catId = await manager.addCategory({ + name: '测试分类', + description: '描述', + color: '#FF5722' + }); + + await manager.addFavorite({ + title: '完整收藏', + content: '内容', + tags: ['标签1', '标签2'], + category: catId, + functionMode: 'basic', + optimizationMode: 'system', + metadata: { + originalContent: '原始内容', + sourceHistoryId: 'hist-001', + customField: '自定义值' + } + }); + + // 导出 + const exportData = await manager.exportFavorites(); + + // 清空数据 + await storageProvider.clearAll(); + + // 导入 + await manager.importFavorites(exportData); + + // 验证 + const favorites = await manager.getFavorites(); + expect(favorites.length).toBe(1); + + const favorite = favorites[0]; + expect(favorite.title).toBe('完整收藏'); + expect(favorite.tags).toEqual(['标签1', '标签2']); + expect(favorite.category).toBe(catId); + expect(favorite.functionMode).toBe('basic'); + expect(favorite.optimizationMode).toBe('system'); + expect(favorite.metadata?.originalContent).toBe('原始内容'); + expect(favorite.metadata?.sourceHistoryId).toBe('hist-001'); + expect(favorite.metadata?.customField).toBe('自定义值'); + + const categories = await manager.getCategories(); + expect(categories.length).toBe(1); + expect(categories[0].name).toBe('测试分类'); + }); + + it('应该正确处理导入时的分类和标签关联', async () => { + const cat1 = await manager.addCategory({ name: '分类1', color: '#FF0000' }); + const cat2 = await manager.addCategory({ name: '分类2', color: '#00FF00' }); + + await manager.addFavorite({ + title: '收藏1', + content: '内容1', + tags: ['共享', 'A'], + category: cat1, + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容2', + tags: ['共享', 'B'], + category: cat2, + functionMode: 'context', + optimizationMode: 'user' + }); + + const exportData = await manager.exportFavorites(); + await storageProvider.clearAll(); + await manager.importFavorites(exportData); + + // 验证分类 + const categories = await manager.getCategories(); + expect(categories.length).toBe(2); + + // 验证标签 + const tags = await manager.getAllTags(); + const sharedTag = tags.find(t => t.tag === '共享'); + expect(sharedTag?.count).toBe(2); + + // 验证收藏 + const favorites = await manager.getFavorites(); + expect(favorites.length).toBe(2); + }); + }); + + describe('搜索和过滤集成测试', () => { + beforeEach(async () => { + // 创建测试数据集 + const cat1 = await manager.addCategory({ name: 'AI工具', color: '#FF0000' }); + const cat2 = await manager.addCategory({ name: '写作助手', color: '#00FF00' }); + + await manager.addFavorite({ + title: 'ChatGPT提示词', + content: '帮助我写一个关于AI的文章', + tags: ['AI', 'ChatGPT'], + category: cat1, + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '创意写作助手', + content: '帮我生成创意故事大纲', + tags: ['写作', '创意'], + category: cat2, + functionMode: 'context', + optimizationMode: 'user' + }); + + await manager.addFavorite({ + title: 'AI绘图提示词', + content: 'a beautiful sunset over mountains', + tags: ['AI', '绘图'], + category: cat1, + functionMode: 'image', + imageSubMode: 'text2image' + }); + }); + + it('应该能按关键词搜索', async () => { + const results = await manager.searchFavorites('AI'); + expect(results.length).toBe(2); + expect(results.every(f => f.title.includes('AI') || f.content.includes('AI'))).toBe(true); + }); + + it('应该能按分类过滤', async () => { + const categories = await manager.getCategories(); + const aiCategory = categories.find(c => c.name === 'AI工具'); + + const results = await manager.getFavorites({ categoryId: aiCategory!.id }); + expect(results.length).toBe(2); + expect(results.every(f => f.category === aiCategory!.id)).toBe(true); + }); + + it('应该能按标签过滤', async () => { + const results = await manager.getFavorites({ tags: ['AI'] }); + expect(results.length).toBe(2); + expect(results.every(f => f.tags.includes('AI'))).toBe(true); + }); + + it('应该支持组合过滤', async () => { + const categories = await manager.getCategories(); + const aiCategory = categories.find(c => c.name === 'AI工具'); + + const results = await manager.searchFavorites('提示词', { + tags: ['AI'], + categoryId: aiCategory!.id + }); + + expect(results.length).toBeGreaterThan(0); + results.forEach(f => { + expect(f.title.includes('提示词') || f.content.includes('提示词')).toBe(true); + expect(f.tags.includes('AI')).toBe(true); + expect(f.category).toBe(aiCategory!.id); + }); + }); + }); +}); diff --git a/packages/core/tests/services/favorite/manager-extended.test.ts b/packages/core/tests/services/favorite/manager-extended.test.ts new file mode 100644 index 00000000..56ff43bf --- /dev/null +++ b/packages/core/tests/services/favorite/manager-extended.test.ts @@ -0,0 +1,515 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { FavoriteManager } from '../../../src/services/favorite/manager'; +import type { IStorageProvider } from '../../../src/services/storage/types'; +import { FavoriteValidationError } from '../../../src/services/favorite/errors'; +import { TypeMapper } from '../../../src/services/favorite/type-mapper'; + +/** + * FavoriteManager 扩展功能单元测试 + * 测试 functionMode 验证、分类管理等功能 + */ +describe('FavoriteManager - 扩展功能', () => { + let manager: FavoriteManager; + let mockStorage: Map; + let storageProvider: IStorageProvider; + + beforeEach(() => { + // 创建模拟存储 + mockStorage = new Map(); + + storageProvider = { + getItem: vi.fn(async (key: string) => mockStorage.get(key) || null), + setItem: vi.fn(async (key: string, value: string) => { + mockStorage.set(key, value); + }), + removeItem: vi.fn(async (key: string) => { + mockStorage.delete(key); + }), + clearAll: vi.fn(async () => { + mockStorage.clear(); + }), + batchUpdate: vi.fn(async (operations: Array<{ key: string; operation: 'set' | 'remove'; value?: string }>) => { + operations.forEach(({ key, operation, value }) => { + if (operation === 'set' && value) { + mockStorage.set(key, value); + } else if (operation === 'remove') { + mockStorage.delete(key); + } + }); + }), + updateData: vi.fn(async (key: string, updater: (data: any) => any) => { + const currentData = mockStorage.get(key); + const parsedData = currentData ? JSON.parse(currentData) : null; + const updatedData = updater(parsedData); + mockStorage.set(key, JSON.stringify(updatedData)); + }) + }; + + manager = new FavoriteManager(storageProvider); + }); + + describe('addFavorite - functionMode 验证', () => { + it('应该拒绝缺少 functionMode 的收藏', async () => { + await expect( + manager.addFavorite({ + title: '测试', + content: '内容', + tags: [], + // @ts-expect-error 故意省略 functionMode 测试验证 + optimizationMode: 'system' + }) + ).rejects.toThrow(FavoriteValidationError); + }); + + it('应该接受合法的 basic/system 模式', async () => { + const id = await manager.addFavorite({ + title: '测试基础模式', + content: '测试内容', + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + expect(id).toBeTruthy(); + const favorite = await manager.getFavorite(id); + expect(favorite.functionMode).toBe('basic'); + expect(favorite.optimizationMode).toBe('system'); + }); + + it('应该接受合法的 context/user 模式', async () => { + const id = await manager.addFavorite({ + title: '测试上下文模式', + content: '测试内容', + tags: [], + functionMode: 'context', + optimizationMode: 'user' + }); + + expect(id).toBeTruthy(); + const favorite = await manager.getFavorite(id); + expect(favorite.functionMode).toBe('context'); + expect(favorite.optimizationMode).toBe('user'); + }); + + it('应该接受合法的 image/text2image 模式', async () => { + const id = await manager.addFavorite({ + title: '测试图像模式', + content: '测试内容', + tags: [], + functionMode: 'image', + imageSubMode: 'text2image' + }); + + expect(id).toBeTruthy(); + const favorite = await manager.getFavorite(id); + expect(favorite.functionMode).toBe('image'); + expect(favorite.imageSubMode).toBe('text2image'); + }); + + it('应该拒绝 basic 模式缺少 optimizationMode', async () => { + await expect( + manager.addFavorite({ + title: '测试', + content: '内容', + tags: [], + functionMode: 'basic' + // 缺少 optimizationMode + }) + ).rejects.toThrow(FavoriteValidationError); + }); + + it('应该拒绝 image 模式缺少 imageSubMode', async () => { + await expect( + manager.addFavorite({ + title: '测试', + content: '内容', + tags: [], + functionMode: 'image' + // 缺少 imageSubMode + }) + ).rejects.toThrow(FavoriteValidationError); + }); + + it('basic 模式包含 imageSubMode 不会报错(字段会被保存但不使用)', async () => { + // 注意: 当前实现不验证冲突字段,这是一个已知的设计选择 + const id = await manager.addFavorite({ + title: '测试', + content: '内容', + tags: [], + functionMode: 'basic', + optimizationMode: 'system', + imageSubMode: 'text2image' // 冗余字段,不会报错 + }); + + expect(id).toBeTruthy(); + const favorite = await manager.getFavorite(id); + expect(favorite.functionMode).toBe('basic'); + expect(favorite.optimizationMode).toBe('system'); + // imageSubMode 会被保存但在 basic 模式下不使用 + }); + + it('image 模式包含 optimizationMode 不会报错(字段会被保存但不使用)', async () => { + // 注意: 当前实现不验证冲突字段,这是一个已知的设计选择 + const id = await manager.addFavorite({ + title: '测试', + content: '内容', + tags: [], + functionMode: 'image', + imageSubMode: 'text2image', + optimizationMode: 'system' // 冗余字段,不会报错 + }); + + expect(id).toBeTruthy(); + const favorite = await manager.getFavorite(id); + expect(favorite.functionMode).toBe('image'); + expect(favorite.imageSubMode).toBe('text2image'); + // optimizationMode 会被保存但在 image 模式下不使用 + }); + }); + + describe('addFavorite - metadata 处理', () => { + it('应该正确存储 metadata.originalContent', async () => { + const id = await manager.addFavorite({ + title: '测试', + content: '优化后内容', + tags: [], + functionMode: 'basic', + optimizationMode: 'system', + metadata: { + originalContent: '原始内容' + } + }); + + const favorite = await manager.getFavorite(id); + expect(favorite.metadata?.originalContent).toBe('原始内容'); + }); + + it('应该正确存储 metadata.sourceHistoryId', async () => { + const id = await manager.addFavorite({ + title: '测试', + content: '优化后内容', + tags: [], + functionMode: 'basic', + optimizationMode: 'system', + metadata: { + sourceHistoryId: 'history-123' + } + }); + + const favorite = await manager.getFavorite(id); + expect(favorite.metadata?.sourceHistoryId).toBe('history-123'); + }); + + it('应该保留其他 metadata 字段', async () => { + const id = await manager.addFavorite({ + title: '测试', + content: '内容', + tags: [], + functionMode: 'basic', + optimizationMode: 'system', + metadata: { + customField: '自定义值', + anotherField: 123 + } + }); + + const favorite = await manager.getFavorite(id); + expect(favorite.metadata?.customField).toBe('自定义值'); + expect(favorite.metadata?.anotherField).toBe(123); + }); + }); + + describe('分类管理 - reorderCategories', () => { + it('应该能重新排序分类', async () => { + // 创建测试分类 + const cat1Id = await manager.addCategory({ name: '分类1', color: '#ff0000' }); + const cat2Id = await manager.addCategory({ name: '分类2', color: '#00ff00' }); + const cat3Id = await manager.addCategory({ name: '分类3', color: '#0000ff' }); + + // 重新排序:3, 1, 2 + await manager.reorderCategories([cat3Id, cat1Id, cat2Id]); + + const categories = await manager.getCategories(); + + // 按 sortOrder 排序 + const sorted = categories.sort((a, b) => a.sortOrder - b.sortOrder); + + expect(sorted[0].id).toBe(cat3Id); + expect(sorted[0].sortOrder).toBe(0); + + expect(sorted[1].id).toBe(cat1Id); + expect(sorted[1].sortOrder).toBe(1); + + expect(sorted[2].id).toBe(cat2Id); + expect(sorted[2].sortOrder).toBe(2); + }); + + it('应该过滤不存在的分类ID', async () => { + const cat1Id = await manager.addCategory({ name: '分类1', color: '#ff0000' }); + + // 包含不存在的ID + await manager.reorderCategories(['non-existent', cat1Id]); + + const categories = await manager.getCategories(); + const existingCategory = categories.find(c => c.id === cat1Id); + + expect(existingCategory).toBeDefined(); + expect(existingCategory!.sortOrder).toBeDefined(); + }); + + it('空数组应该抛出验证错误', async () => { + await expect(manager.reorderCategories([])).rejects.toThrow(FavoriteValidationError); + }); + }); + + describe('分类管理 - getCategoryUsage', () => { + it('应该返回分类的使用次数', async () => { + const catId = await manager.addCategory({ name: '测试分类', color: '#ff0000' }); + + // 添加3个收藏使用该分类 + await manager.addFavorite({ + title: '收藏1', + content: '内容1', + category: catId, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容2', + category: catId, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏3', + content: '内容3', + category: catId, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const usage = await manager.getCategoryUsage(catId); + expect(usage).toBe(3); + }); + + it('应该返回0对于未使用的分类', async () => { + const catId = await manager.addCategory({ name: '未使用分类', color: '#ff0000' }); + + const usage = await manager.getCategoryUsage(catId); + expect(usage).toBe(0); + }); + + it('应该返回0对于不存在的分类', async () => { + const usage = await manager.getCategoryUsage('non-existent-id'); + expect(usage).toBe(0); + }); + + it('不同分类应该独立计数', async () => { + const cat1Id = await manager.addCategory({ name: '分类1', color: '#ff0000' }); + const cat2Id = await manager.addCategory({ name: '分类2', color: '#00ff00' }); + + // 分类1: 2个收藏 + await manager.addFavorite({ + title: '收藏1-1', + content: '内容', + category: cat1Id, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏1-2', + content: '内容', + category: cat1Id, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + // 分类2: 1个收藏 + await manager.addFavorite({ + title: '收藏2-1', + content: '内容', + category: cat2Id, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const usage1 = await manager.getCategoryUsage(cat1Id); + const usage2 = await manager.getCategoryUsage(cat2Id); + + expect(usage1).toBe(2); + expect(usage2).toBe(1); + }); + }); + + describe('分类管理 - deleteCategory', () => { + it('删除分类时应清空收藏的分类字段并返回受影响数量', async () => { + const catId = await manager.addCategory({ name: '测试分类', color: '#ff0000' }); + + // 添加2个使用该分类的收藏 + const fav1Id = await manager.addFavorite({ + title: '收藏1', + content: '内容1', + category: catId, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const fav2Id = await manager.addFavorite({ + title: '收藏2', + content: '内容2', + category: catId, + tags: [], + functionMode: 'basic', + optimizationMode: 'system' + }); + + // 删除分类 + const affectedCount = await manager.deleteCategory(catId); + + // 应该返回受影响的收藏数量 + expect(affectedCount).toBe(2); + + // 收藏的分类字段应该被清空 + const fav1 = await manager.getFavorite(fav1Id); + const fav2 = await manager.getFavorite(fav2Id); + expect(fav1.category).toBeUndefined(); + expect(fav2.category).toBeUndefined(); + + // 分类应该被删除 + const categories = await manager.getCategories(); + expect(categories.find(c => c.id === catId)).toBeUndefined(); + }); + + it('删除空分类应该返回0', async () => { + const catId = await manager.addCategory({ name: '空分类', color: '#ff0000' }); + + const affectedCount = await manager.deleteCategory(catId); + + expect(affectedCount).toBe(0); + }); + + it('删除不存在的分类应该抛出错误', async () => { + await expect(manager.deleteCategory('non-existent-id')).rejects.toThrow(); + }); + }); + + describe('ensureDefaultCategories - 默认分类管理', () => { + it('首次调用应该创建默认分类', async () => { + const customCategories = [ + { name: 'Category 1', description: 'Desc 1', color: '#FF0000' }, + { name: 'Category 2', description: 'Desc 2', color: '#00FF00' } + ]; + + await manager.ensureDefaultCategories(customCategories); + const categories = await manager.getCategories(); + + expect(categories.length).toBe(2); + expect(categories.find(c => c.name === 'Category 1')).toBeDefined(); + expect(categories.find(c => c.name === 'Category 2')).toBeDefined(); + }); + + it('用户删除所有分类后不应自动重建', async () => { + // 首次创建 + await manager.ensureDefaultCategories([ + { name: 'Cat1', color: '#FF0000' } + ]); + + let categories = await manager.getCategories(); + expect(categories.length).toBe(1); + + // 用户删除所有分类 + for (const cat of categories) { + await manager.deleteCategory(cat.id); + } + + categories = await manager.getCategories(); + expect(categories.length).toBe(0); + + // 再次调用不应创建(已标记为已初始化) + await manager.ensureDefaultCategories([ + { name: 'Cat1', color: '#FF0000' } + ]); + + categories = await manager.getCategories(); + expect(categories.length).toBe(0); // 仍然为0 + }); + + it('已有分类时不应重复创建', async () => { + // 先手动创建一个分类 + await manager.addCategory({ name: 'Existing', color: '#000000' }); + + // 调用 ensureDefaultCategories 不应创建新分类 + await manager.ensureDefaultCategories([ + { name: 'New Category', color: '#FFFFFF' } + ]); + + const categories = await manager.getCategories(); + expect(categories.length).toBe(1); // 仍然只有1个 + expect(categories[0].name).toBe('Existing'); + }); + + it('默认分类应该有正确的 sortOrder', async () => { + await manager.ensureDefaultCategories([ + { name: 'Cat1', color: '#FF0000' }, + { name: 'Cat2', color: '#00FF00' }, + { name: 'Cat3', color: '#0000FF' } + ]); + + const categories = await manager.getCategories(); + const sorted = categories.sort((a, b) => a.sortOrder - b.sortOrder); + + // sortOrder 应该是连续递增的 + sorted.forEach((cat, index) => { + expect(cat.sortOrder).toBe(index); + }); + }); + }); + + describe('TypeMapper 集成测试', () => { + it('TypeMapper 映射的结果应该能通过 addFavorite 验证', () => { + const testTypes = [ + 'optimize', + 'userOptimize', + 'contextSystemOptimize', + 'contextUserOptimize', + 'imageOptimize', + 'text2imageOptimize', + 'image2imageOptimize' + ] as const; + + testTypes.forEach(type => { + const mapping = TypeMapper.mapFromRecordType(type); + // 验证映射结果的有效性 + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + }); + + it('应该能使用 TypeMapper 映射结果创建收藏', async () => { + const mapping = TypeMapper.mapFromRecordType('optimize'); + + const id = await manager.addFavorite({ + title: '从TypeMapper映射创建', + content: '测试内容', + tags: [], + ...mapping + }); + + expect(id).toBeTruthy(); + + const favorite = await manager.getFavorite(id); + expect(favorite.functionMode).toBe(mapping.functionMode); + expect(favorite.optimizationMode).toBe(mapping.optimizationMode); + }); + }); +}); diff --git a/packages/core/tests/services/favorite/tag-manager.test.ts b/packages/core/tests/services/favorite/tag-manager.test.ts new file mode 100644 index 00000000..dde4955a --- /dev/null +++ b/packages/core/tests/services/favorite/tag-manager.test.ts @@ -0,0 +1,451 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { FavoriteManager } from '../../../src/services/favorite/manager'; +import type { IStorageProvider } from '../../../src/services/storage/types'; +import { FavoriteValidationError, FavoriteError } from '../../../src/services/favorite/errors'; + +/** + * 标签管理功能单元测试 + */ +describe('FavoriteManager - 标签管理', () => { + let manager: FavoriteManager; + let mockStorage: Map; + let storageProvider: IStorageProvider; + + beforeEach(() => { + // 创建模拟存储 + mockStorage = new Map(); + + storageProvider = { + getItem: vi.fn(async (key: string) => mockStorage.get(key) || null), + setItem: vi.fn(async (key: string, value: string) => { + mockStorage.set(key, value); + }), + removeItem: vi.fn(async (key: string) => { + mockStorage.delete(key); + }), + clearAll: vi.fn(async () => { + mockStorage.clear(); + }), + batchUpdate: vi.fn(async (operations: Array<{ key: string; operation: 'set' | 'remove'; value?: string }>) => { + operations.forEach(({ key, operation, value }) => { + if (operation === 'set' && value) { + mockStorage.set(key, value); + } else if (operation === 'remove') { + mockStorage.delete(key); + } + }); + }), + updateData: vi.fn(async (key: string, updater: (data: any) => any) => { + const currentData = mockStorage.get(key); + const parsedData = currentData ? JSON.parse(currentData) : null; + const updatedData = updater(parsedData); + mockStorage.set(key, JSON.stringify(updatedData)); + }) + }; + + manager = new FavoriteManager(storageProvider); + }); + + describe('添加标签', () => { + it('应该能成功添加新标签', async () => { + await manager.addTag('测试标签'); + + const tags = await manager.getAllTags(); + expect(tags).toHaveLength(1); + expect(tags[0].tag).toBe('测试标签'); + expect(tags[0].count).toBe(0); // 新标签未使用,count 为 0 + }); + + it('应该拒绝空标签名', async () => { + await expect(manager.addTag('')).rejects.toThrow(FavoriteValidationError); + await expect(manager.addTag(' ')).rejects.toThrow(FavoriteValidationError); + }); + + it('应该拒绝重复的标签', async () => { + await manager.addTag('标签1'); + await expect(manager.addTag('标签1')).rejects.toThrow(FavoriteError); + }); + + it('应该自动去除首尾空格', async () => { + await manager.addTag(' 标签2 '); + const tags = await manager.getAllTags(); + expect(tags[0].tag).toBe('标签2'); + }); + + it('应该能添加多个标签', async () => { + await manager.addTag('标签1'); + await manager.addTag('标签2'); + await manager.addTag('标签3'); + + const tags = await manager.getAllTags(); + expect(tags).toHaveLength(3); + }); + }); + + describe('获取所有标签', () => { + it('应该返回独立标签和使用中的标签', async () => { + // 添加独立标签 + await manager.addTag('独立标签1'); + await manager.addTag('独立标签2'); + + // 添加收藏(使用某些标签) + await manager.addFavorite({ + title: '测试收藏', + content: '测试内容', + tags: ['使用中标签', '独立标签1'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const tags = await manager.getAllTags(); + + // 应该包含 3 个标签:独立标签1(count=1)、独立标签2(count=0)、使用中标签(count=1) + expect(tags).toHaveLength(3); + + const tag1 = tags.find(t => t.tag === '独立标签1'); + expect(tag1?.count).toBe(1); + + const tag2 = tags.find(t => t.tag === '独立标签2'); + expect(tag2?.count).toBe(0); + + const tag3 = tags.find(t => t.tag === '使用中标签'); + expect(tag3?.count).toBe(1); + }); + + it('应该按使用次数降序排序', async () => { + await manager.addFavorite({ + title: '收藏1', + content: '内容1', + tags: ['标签A', '标签B'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容2', + tags: ['标签B', '标签C'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏3', + content: '内容3', + tags: ['标签B'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const tags = await manager.getAllTags(); + + // 标签B 使用3次,应该排第一 + expect(tags[0].tag).toBe('标签B'); + expect(tags[0].count).toBe(3); + + // 标签A 和 标签C 各使用1次 + expect(tags[1].count).toBe(1); + expect(tags[2].count).toBe(1); + }); + + it('相同使用次数时应该按标签名升序排序', async () => { + await manager.addFavorite({ + title: '收藏', + content: '内容', + tags: ['Zebra', 'Apple', 'Banana'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const tags = await manager.getAllTags(); + + // 都是使用1次,应该按字母顺序排序 + expect(tags[0].tag).toBe('Apple'); + expect(tags[1].tag).toBe('Banana'); + expect(tags[2].tag).toBe('Zebra'); + }); + }); + + describe('重命名标签', () => { + it('应该能成功重命名标签并更新所有收藏', async () => { + // 添加使用该标签的收藏 + await manager.addFavorite({ + title: '收藏1', + content: '内容1', + tags: ['旧标签'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容2', + tags: ['旧标签', '其他标签'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const affectedCount = await manager.renameTag('旧标签', '新标签'); + + expect(affectedCount).toBe(2); // 影响了2个收藏 + + // 验证收藏已更新 + const favorites = await manager.getFavorites(); + expect(favorites[0].tags).toContain('新标签'); + expect(favorites[0].tags).not.toContain('旧标签'); + expect(favorites[1].tags).toContain('新标签'); + expect(favorites[1].tags).not.toContain('旧标签'); + + // 验证标签统计 + const tags = await manager.getAllTags(); + const newTag = tags.find(t => t.tag === '新标签'); + expect(newTag?.count).toBe(2); + + const oldTag = tags.find(t => t.tag === '旧标签'); + expect(oldTag).toBeUndefined(); + }); + + it('重命名为相同名称时应该返回0', async () => { + const affectedCount = await manager.renameTag('标签A', '标签A'); + expect(affectedCount).toBe(0); + }); + + it('应该拒绝空标签名', async () => { + await expect(manager.renameTag('', '新标签')).rejects.toThrow(FavoriteValidationError); + await expect(manager.renameTag('旧标签', '')).rejects.toThrow(FavoriteValidationError); + }); + + it('重命名不存在的标签时应该返回0', async () => { + const affectedCount = await manager.renameTag('不存在的标签', '新标签'); + expect(affectedCount).toBe(0); + }); + + it('重命名时如果新标签已存在应该合并', async () => { + await manager.addFavorite({ + title: '收藏', + content: '内容', + tags: ['标签A', '标签B'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.renameTag('标签A', '标签B'); + + const favorites = await manager.getFavorites(); + // 应该只包含一个标签B,不重复 + expect(favorites[0].tags).toEqual(['标签B']); + }); + }); + + describe('合并标签', () => { + it('应该能成功合并多个标签', async () => { + await manager.addFavorite({ + title: '收藏1', + content: '内容1', + tags: ['标签A'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容2', + tags: ['标签B'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏3', + content: '内容3', + tags: ['标签C'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const affectedCount = await manager.mergeTags(['标签A', '标签B'], '标签C'); + + expect(affectedCount).toBe(2); // 影响了2个收藏(收藏1和收藏2) + + const favorites = await manager.getFavorites(); + expect(favorites[0].tags).toContain('标签C'); + expect(favorites[0].tags).not.toContain('标签A'); + expect(favorites[1].tags).toContain('标签C'); + expect(favorites[1].tags).not.toContain('标签B'); + + // 收藏3的标签C不应重复 + expect(favorites[2].tags).toEqual(['标签C']); + }); + + it('应该拒绝空的源标签列表', async () => { + await expect(manager.mergeTags([], '目标标签')).rejects.toThrow(FavoriteValidationError); + }); + + it('应该拒绝空的目标标签', async () => { + await expect(manager.mergeTags(['标签A'], '')).rejects.toThrow(FavoriteValidationError); + }); + + it('合并不存在的标签时应该返回0', async () => { + const affectedCount = await manager.mergeTags(['不存在1', '不存在2'], '目标'); + expect(affectedCount).toBe(0); + }); + }); + + describe('删除标签', () => { + it('应该能删除标签并从所有收藏中移除', async () => { + // 添加独立标签(未被使用) + await manager.addTag('独立标签'); + + // 添加使用该标签的收藏 + await manager.addFavorite({ + title: '收藏1', + content: '内容1', + tags: ['要删除的标签', '保留的标签'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + await manager.addFavorite({ + title: '收藏2', + content: '内容2', + tags: ['要删除的标签'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const affectedCount = await manager.deleteTag('要删除的标签'); + + expect(affectedCount).toBe(2); // 影响了2个收藏 + + // 验证收藏已更新 + const favorites = await manager.getFavorites(); + + // 验证两个收藏都不包含"要删除的标签" + favorites.forEach(fav => { + expect(fav.tags).not.toContain('要删除的标签'); + }); + + // 第一个收藏应该保留"保留的标签" + const firstFavorite = favorites.find(f => f.title === '收藏1'); + expect(firstFavorite).toBeDefined(); + expect(firstFavorite!.tags).toEqual(['保留的标签']); + + // 第二个收藏应该没有标签 + const secondFavorite = favorites.find(f => f.title === '收藏2'); + expect(secondFavorite).toBeDefined(); + expect(secondFavorite!.tags).toEqual([]); + + // 验证标签统计:要删除的标签不应该存在 + const tags = await manager.getAllTags(); + const deletedTag = tags.find(t => t.tag === '要删除的标签'); + expect(deletedTag).toBeUndefined(); + + // 独立标签应该仍然存在(count=0,因为未被使用) + const independentTag = tags.find(t => t.tag === '独立标签'); + expect(independentTag).toBeDefined(); + expect(independentTag!.count).toBe(0); + + // 测试删除独立标签 + const independentTagDelCount = await manager.deleteTag('独立标签'); + expect(independentTagDelCount).toBe(0); // 未被任何收藏使用 + + const tagsAfterDel = await manager.getAllTags(); + const deletedIndependentTag = tagsAfterDel.find(t => t.tag === '独立标签'); + expect(deletedIndependentTag).toBeUndefined(); + }); + + it('应该拒绝空标签名', async () => { + await expect(manager.deleteTag('')).rejects.toThrow(FavoriteValidationError); + }); + + it('删除不存在的标签时应该返回0', async () => { + const affectedCount = await manager.deleteTag('不存在的标签'); + expect(affectedCount).toBe(0); + }); + }); + + describe('标签导入导出', () => { + it('导出时应该包含独立标签', async () => { + await manager.addTag('独立标签1'); + await manager.addTag('独立标签2'); + + // 添加使用中标签到独立标签库 + await manager.addTag('使用中标签'); + + await manager.addFavorite({ + title: '测试', + content: '内容', + tags: ['使用中标签'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + const exportData = await manager.exportFavorites(); + const parsed = JSON.parse(exportData); + + expect(parsed.tags).toBeDefined(); + expect(parsed.tags).toContain('独立标签1'); + expect(parsed.tags).toContain('独立标签2'); + expect(parsed.tags).toContain('使用中标签'); + }); + + it('导入时应该自动创建独立标签', async () => { + const importData = JSON.stringify({ + version: '1.0', + exportDate: new Date().toISOString(), + favorites: [ + { + title: '测试', + content: '内容', + tags: ['标签1', '标签2'], + functionMode: 'basic', + optimizationMode: 'system' + } + ], + categories: [], + tags: ['标签1', '标签2', '预创建标签'] + }); + + await manager.importFavorites(importData); + + const tags = await manager.getAllTags(); + + // 应该包含所有标签 + expect(tags.find(t => t.tag === '标签1')).toBeDefined(); + expect(tags.find(t => t.tag === '标签2')).toBeDefined(); + expect(tags.find(t => t.tag === '预创建标签')).toBeDefined(); + + // 预创建标签使用次数为0 + const preCreatedTag = tags.find(t => t.tag === '预创建标签'); + expect(preCreatedTag?.count).toBe(0); + }); + }); + + describe('保存收藏时自动注册标签', () => { + it('保存收藏时应该自动将标签添加到独立标签库', async () => { + // 这个测试需要在 UI 层面实现,这里只是验证概念 + // 在 SaveFavoriteDialog 中,保存前应该调用 addTag + + await manager.addFavorite({ + title: '测试', + content: '内容', + tags: ['新标签1', '新标签2'], + functionMode: 'basic', + optimizationMode: 'system' + }); + + // 手动模拟 SaveFavoriteDialog 的行为 + for (const tag of ['新标签1', '新标签2']) { + try { + await manager.addTag(tag); + } catch (error) { + // 标签已存在,忽略错误 + } + } + + const tags = await manager.getAllTags(); + expect(tags.find(t => t.tag === '新标签1')).toBeDefined(); + expect(tags.find(t => t.tag === '新标签2')).toBeDefined(); + }); + }); +}); diff --git a/packages/core/tests/services/favorite/type-converter.test.ts b/packages/core/tests/services/favorite/type-converter.test.ts new file mode 100644 index 00000000..20b983cc --- /dev/null +++ b/packages/core/tests/services/favorite/type-converter.test.ts @@ -0,0 +1,218 @@ +import { describe, it, expect } from 'vitest'; +import { TagTypeConverter } from '../../../src/services/favorite/type-converter'; +import type { TagStatistics } from '../../../src/services/favorite/types'; + +/** + * 标签类型转换器单元测试 + */ +describe('TagTypeConverter', () => { + describe('toTagStatistics', () => { + it('应该将API格式转换为TagStatistics格式', () => { + const apiData = [ + { tag: '标签1', count: 5 }, + { tag: '标签2', count: 3 } + ]; + + const result = TagTypeConverter.toTagStatistics(apiData); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + name: '标签1', + count: 5, + lastUsed: undefined + }); + expect(result[1]).toEqual({ + name: '标签2', + count: 3, + lastUsed: undefined + }); + }); + + it('应该处理空数组', () => { + const result = TagTypeConverter.toTagStatistics([]); + expect(result).toEqual([]); + }); + }); + + describe('fromTagStatistics', () => { + it('应该将TagStatistics格式转换为API格式', () => { + const stats: TagStatistics[] = [ + { name: '标签1', count: 5, lastUsed: undefined }, + { name: '标签2', count: 3, lastUsed: Date.now() } + ]; + + const result = TagTypeConverter.fromTagStatistics(stats); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ tag: '标签1', count: 5 }); + expect(result[1]).toEqual({ tag: '标签2', count: 3 }); + }); + + it('应该处理空数组', () => { + const result = TagTypeConverter.fromTagStatistics([]); + expect(result).toEqual([]); + }); + }); + + describe('toAutoCompleteOptions', () => { + it('应该转换为自动完成选项格式', () => { + const apiData = [ + { tag: '标签1', count: 5 }, + { tag: '标签2', count: 3 } + ]; + + const result = TagTypeConverter.toAutoCompleteOptions(apiData); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + label: '标签1 (5)', + value: '标签1', + count: 5 + }); + expect(result[1]).toEqual({ + label: '标签2 (3)', + value: '标签2', + count: 3 + }); + }); + + it('应该处理使用次数为0的标签', () => { + const apiData = [{ tag: '未使用标签', count: 0 }]; + + const result = TagTypeConverter.toAutoCompleteOptions(apiData); + + expect(result[0]).toEqual({ + label: '未使用标签 (0)', + value: '未使用标签', + count: 0 + }); + }); + }); + + describe('toStringArray', () => { + it('应该转换为字符串数组', () => { + const apiData = [ + { tag: '标签1', count: 5 }, + { tag: '标签2', count: 3 }, + { tag: '标签3', count: 0 } + ]; + + const result = TagTypeConverter.toStringArray(apiData); + + expect(result).toEqual(['标签1', '标签2', '标签3']); + }); + + it('应该处理空数组', () => { + const result = TagTypeConverter.toStringArray([]); + expect(result).toEqual([]); + }); + }); + + describe('sortByCount', () => { + it('应该按使用次数降序排序', () => { + const tags = [ + { count: 3 }, + { count: 10 }, + { count: 1 }, + { count: 5 } + ]; + + const result = TagTypeConverter.sortByCount(tags); + + expect(result.map(t => t.count)).toEqual([10, 5, 3, 1]); + }); + + it('应该不修改原数组', () => { + const tags = [ + { count: 3 }, + { count: 1 } + ]; + + const result = TagTypeConverter.sortByCount(tags); + + expect(tags.map(t => t.count)).toEqual([3, 1]); + expect(result.map(t => t.count)).toEqual([3, 1]); + }); + }); + + describe('sortByName', () => { + it('应该按标签名升序排序', () => { + const tags: TagStatistics[] = [ + { name: 'Zebra', count: 1 }, + { name: 'Apple', count: 2 }, + { name: 'Banana', count: 3 } + ]; + + const result = TagTypeConverter.sortByName(tags); + + expect(result.map(t => t.name)).toEqual(['Apple', 'Banana', 'Zebra']); + }); + + it('应该正确处理中文排序', () => { + const tags: TagStatistics[] = [ + { name: '编程', count: 1 }, + { name: '设计', count: 2 }, + { name: '测试', count: 3 } + ]; + + const result = TagTypeConverter.sortByName(tags); + + // 中文按拼音排序 + expect(result.map(t => t.name)).toEqual(['编程', '测试', '设计']); + }); + + it('应该不修改原数组', () => { + const tags: TagStatistics[] = [ + { name: 'B', count: 1 }, + { name: 'A', count: 2 } + ]; + + const result = TagTypeConverter.sortByName(tags); + + expect(tags.map(t => t.name)).toEqual(['B', 'A']); + expect(result.map(t => t.name)).toEqual(['A', 'B']); + }); + }); + + describe('sortByCountThenName', () => { + it('应该先按使用次数降序,再按名称升序排序', () => { + const tags: TagStatistics[] = [ + { name: 'Zebra', count: 5 }, + { name: 'Apple', count: 5 }, + { name: 'Banana', count: 3 }, + { name: 'Cat', count: 5 } + ]; + + const result = TagTypeConverter.sortByCountThenName(tags); + + // count=5的按名称升序: Apple, Cat, Zebra + // count=3的: Banana + expect(result.map(t => t.name)).toEqual(['Apple', 'Cat', 'Zebra', 'Banana']); + }); + + it('应该处理使用次数全部相同的情况', () => { + const tags: TagStatistics[] = [ + { name: 'C', count: 1 }, + { name: 'A', count: 1 }, + { name: 'B', count: 1 } + ]; + + const result = TagTypeConverter.sortByCountThenName(tags); + + expect(result.map(t => t.name)).toEqual(['A', 'B', 'C']); + }); + + it('应该不修改原数组', () => { + const tags: TagStatistics[] = [ + { name: 'B', count: 2 }, + { name: 'A', count: 1 } + ]; + + const result = TagTypeConverter.sortByCountThenName(tags); + + expect(tags[0].name).toBe('B'); + expect(result[0].name).toBe('B'); + expect(result[1].name).toBe('A'); + }); + }); +}); diff --git a/packages/core/tests/services/favorite/type-mapper.test.ts b/packages/core/tests/services/favorite/type-mapper.test.ts new file mode 100644 index 00000000..aa18dad3 --- /dev/null +++ b/packages/core/tests/services/favorite/type-mapper.test.ts @@ -0,0 +1,397 @@ +import { describe, it, expect } from 'vitest'; +import { TypeMapper, type FunctionModeMapping } from '../../../src/services/favorite/type-mapper'; +import type { PromptRecordType } from '../../../src/services/history/types'; + +/** + * TypeMapper 单元测试 + * 测试历史记录类型到功能模式的映射逻辑 + */ +describe('TypeMapper', () => { + describe('mapFromRecordType - 基础模式映射', () => { + it('应该将 optimize 映射为 basic/system', () => { + const result = TypeMapper.mapFromRecordType('optimize'); + expect(result).toEqual({ + functionMode: 'basic', + optimizationMode: 'system' + }); + }); + + it('应该将 iterate 映射为 basic/system', () => { + const result = TypeMapper.mapFromRecordType('iterate'); + expect(result).toEqual({ + functionMode: 'basic', + optimizationMode: 'system' + }); + }); + + it('应该将 userOptimize 映射为 basic/user', () => { + const result = TypeMapper.mapFromRecordType('userOptimize'); + expect(result).toEqual({ + functionMode: 'basic', + optimizationMode: 'user' + }); + }); + + it('应该将 test 映射为 basic/system', () => { + const result = TypeMapper.mapFromRecordType('test'); + expect(result).toEqual({ + functionMode: 'basic', + optimizationMode: 'system' + }); + }); + }); + + describe('mapFromRecordType - 上下文模式映射', () => { + it('应该将 contextSystemOptimize 映射为 context/system', () => { + const result = TypeMapper.mapFromRecordType('contextSystemOptimize'); + expect(result).toEqual({ + functionMode: 'context', + optimizationMode: 'system' + }); + }); + + it('应该将 contextIterate 映射为 context/system', () => { + const result = TypeMapper.mapFromRecordType('contextIterate'); + expect(result).toEqual({ + functionMode: 'context', + optimizationMode: 'system' + }); + }); + + it('应该将 contextUserOptimize 映射为 context/user', () => { + const result = TypeMapper.mapFromRecordType('contextUserOptimize'); + expect(result).toEqual({ + functionMode: 'context', + optimizationMode: 'user' + }); + }); + }); + + describe('mapFromRecordType - 图像模式映射', () => { + it('应该将 imageOptimize 映射为 image/text2image', () => { + const result = TypeMapper.mapFromRecordType('imageOptimize'); + expect(result).toEqual({ + functionMode: 'image', + imageSubMode: 'text2image' + }); + }); + + it('应该将 contextImageOptimize 映射为 image/text2image', () => { + const result = TypeMapper.mapFromRecordType('contextImageOptimize'); + expect(result).toEqual({ + functionMode: 'image', + imageSubMode: 'text2image' + }); + }); + + it('应该将 imageIterate 映射为 image/text2image', () => { + const result = TypeMapper.mapFromRecordType('imageIterate'); + expect(result).toEqual({ + functionMode: 'image', + imageSubMode: 'text2image' + }); + }); + + it('应该将 text2imageOptimize 映射为 image/text2image', () => { + const result = TypeMapper.mapFromRecordType('text2imageOptimize'); + expect(result).toEqual({ + functionMode: 'image', + imageSubMode: 'text2image' + }); + }); + + it('应该将 image2imageOptimize 映射为 image/image2image', () => { + const result = TypeMapper.mapFromRecordType('image2imageOptimize'); + expect(result).toEqual({ + functionMode: 'image', + imageSubMode: 'image2image' + }); + }); + }); + + describe('mapFromRecordType - 未知类型处理', () => { + it('应该将未知类型映射为 basic/system 并输出警告', () => { + // 使用 console.warn 的 spy + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const result = TypeMapper.mapFromRecordType('unknown' as PromptRecordType); + + expect(result).toEqual({ + functionMode: 'basic', + optimizationMode: 'system' + }); + expect(warnSpy).toHaveBeenCalledWith( + '[TypeMapper] Unknown record type: unknown, falling back to basic/system' + ); + + warnSpy.mockRestore(); + }); + }); + + describe('validateMapping - 合法映射验证', () => { + it('应该接受合法的 basic/system 映射', () => { + const mapping: FunctionModeMapping = { + functionMode: 'basic', + optimizationMode: 'system' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + + it('应该接受合法的 basic/user 映射', () => { + const mapping: FunctionModeMapping = { + functionMode: 'basic', + optimizationMode: 'user' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + + it('应该接受合法的 context/system 映射', () => { + const mapping: FunctionModeMapping = { + functionMode: 'context', + optimizationMode: 'system' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + + it('应该接受合法的 context/user 映射', () => { + const mapping: FunctionModeMapping = { + functionMode: 'context', + optimizationMode: 'user' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + + it('应该接受合法的 image/text2image 映射', () => { + const mapping: FunctionModeMapping = { + functionMode: 'image', + imageSubMode: 'text2image' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + + it('应该接受合法的 image/image2image 映射', () => { + const mapping: FunctionModeMapping = { + functionMode: 'image', + imageSubMode: 'image2image' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + }); + + describe('validateMapping - 非法映射验证', () => { + it('应该拒绝缺少 functionMode 的映射', () => { + const mapping: Partial = { + optimizationMode: 'system' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝非法的 functionMode 值', () => { + const mapping = { + functionMode: 'invalid' as any, + optimizationMode: 'system' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 basic 模式缺少 optimizationMode', () => { + const mapping: Partial = { + functionMode: 'basic' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 context 模式缺少 optimizationMode', () => { + const mapping: Partial = { + functionMode: 'context' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 basic 模式的非法 optimizationMode 值', () => { + const mapping = { + functionMode: 'basic' as const, + optimizationMode: 'invalid' as any + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 basic 模式包含 imageSubMode', () => { + const mapping = { + functionMode: 'basic' as const, + optimizationMode: 'system' as const, + imageSubMode: 'text2image' as const + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 context 模式包含 imageSubMode', () => { + const mapping = { + functionMode: 'context' as const, + optimizationMode: 'system' as const, + imageSubMode: 'text2image' as const + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 image 模式缺少 imageSubMode', () => { + const mapping: Partial = { + functionMode: 'image' + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 image 模式的非法 imageSubMode 值', () => { + const mapping = { + functionMode: 'image' as const, + imageSubMode: 'invalid' as any + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + + it('应该拒绝 image 模式包含 optimizationMode', () => { + const mapping = { + functionMode: 'image' as const, + imageSubMode: 'text2image' as const, + optimizationMode: 'system' as const + }; + expect(TypeMapper.validateMapping(mapping)).toBe(false); + }); + }); + + describe('inferRecordTypes - 反向推断', () => { + it('应该从 basic/system 推断出 optimize 和 iterate', () => { + const mapping: FunctionModeMapping = { + functionMode: 'basic', + optimizationMode: 'system' + }; + const result = TypeMapper.inferRecordTypes(mapping); + expect(result).toEqual(['optimize', 'iterate']); + }); + + it('应该从 basic/user 推断出 userOptimize', () => { + const mapping: FunctionModeMapping = { + functionMode: 'basic', + optimizationMode: 'user' + }; + const result = TypeMapper.inferRecordTypes(mapping); + expect(result).toEqual(['userOptimize']); + }); + + it('应该从 context/system 推断出 contextSystemOptimize 和 contextIterate', () => { + const mapping: FunctionModeMapping = { + functionMode: 'context', + optimizationMode: 'system' + }; + const result = TypeMapper.inferRecordTypes(mapping); + expect(result).toEqual(['contextSystemOptimize', 'contextIterate']); + }); + + it('应该从 context/user 推断出 contextUserOptimize', () => { + const mapping: FunctionModeMapping = { + functionMode: 'context', + optimizationMode: 'user' + }; + const result = TypeMapper.inferRecordTypes(mapping); + expect(result).toEqual(['contextUserOptimize']); + }); + + it('应该从 image/text2image 推断出所有文生图类型', () => { + const mapping: FunctionModeMapping = { + functionMode: 'image', + imageSubMode: 'text2image' + }; + const result = TypeMapper.inferRecordTypes(mapping); + expect(result).toEqual([ + 'imageOptimize', + 'contextImageOptimize', + 'imageIterate', + 'text2imageOptimize' + ]); + }); + + it('应该从 image/image2image 推断出 image2imageOptimize', () => { + const mapping: FunctionModeMapping = { + functionMode: 'image', + imageSubMode: 'image2image' + }; + const result = TypeMapper.inferRecordTypes(mapping); + expect(result).toEqual(['image2imageOptimize']); + }); + + it('应该对非法映射返回空数组', () => { + const mapping = { + functionMode: 'basic' as const, + optimizationMode: undefined as any + }; + const result = TypeMapper.inferRecordTypes(mapping); + expect(result).toEqual([]); + }); + }); + + describe('映射和验证的完整性测试', () => { + it('所有 PromptRecordType 映射后的结果都应该是合法的', () => { + const allTypes: PromptRecordType[] = [ + 'optimize', + 'userOptimize', + 'iterate', + 'test', + 'contextSystemOptimize', + 'contextUserOptimize', + 'contextIterate', + 'imageOptimize', + 'contextImageOptimize', + 'imageIterate', + 'text2imageOptimize', + 'image2imageOptimize' + ]; + + allTypes.forEach(type => { + const mapping = TypeMapper.mapFromRecordType(type); + expect(TypeMapper.validateMapping(mapping)).toBe(true); + }); + }); + + it('映射和反向推断应该是一致的', () => { + const testCases: Array<{ + recordType: PromptRecordType; + mapping: FunctionModeMapping; + }> = [ + { + recordType: 'optimize', + mapping: { functionMode: 'basic', optimizationMode: 'system' } + }, + { + recordType: 'userOptimize', + mapping: { functionMode: 'basic', optimizationMode: 'user' } + }, + { + recordType: 'contextSystemOptimize', + mapping: { functionMode: 'context', optimizationMode: 'system' } + }, + { + recordType: 'contextUserOptimize', + mapping: { functionMode: 'context', optimizationMode: 'user' } + }, + { + recordType: 'text2imageOptimize', + mapping: { functionMode: 'image', imageSubMode: 'text2image' } + }, + { + recordType: 'image2imageOptimize', + mapping: { functionMode: 'image', imageSubMode: 'image2image' } + } + ]; + + testCases.forEach(({ recordType, mapping }) => { + // 正向映射 + const mappedResult = TypeMapper.mapFromRecordType(recordType); + expect(mappedResult).toEqual(mapping); + + // 反向推断 + const inferredTypes = TypeMapper.inferRecordTypes(mapping); + expect(inferredTypes).toContain(recordType); + }); + }); + }); +}); diff --git a/packages/ui/src/components/FavoriteCard.vue b/packages/ui/src/components/FavoriteCard.vue index 341ed2db..8a16e129 100644 --- a/packages/ui/src/components/FavoriteCard.vue +++ b/packages/ui/src/components/FavoriteCard.vue @@ -14,19 +14,54 @@ {{ favorite.title }} -
+ + - - {{ category.name }} - + {{ t(`favorites.manager.card.functionMode.${favorite.functionMode}`) }} -
+ + + + {{ getSubModeLabel(favorite) }} + + + + + + {{ category.name }} + + @@ -492,6 +527,38 @@ onBeforeUnmount(() => { } }); +// 功能模式标签类型映射 +const getFunctionModeTagType = (mode: string): 'default' | 'info' | 'success' => { + const typeMap: Record = { + basic: 'default', + context: 'info', + image: 'success' + }; + return typeMap[mode] || 'default'; +}; + +// 子模式标签类型映射 +const getSubModeTagType = (favorite: FavoritePrompt): 'warning' | 'error' | 'success' | 'info' | 'default' => { + if (favorite.optimizationMode) { + return favorite.optimizationMode === 'system' ? 'warning' : 'error'; + } + if (favorite.imageSubMode) { + return favorite.imageSubMode === 'text2image' ? 'success' : 'info'; + } + return 'default'; +}; + +// 获取子模式标签文本 +const getSubModeLabel = (favorite: FavoritePrompt): string => { + if (favorite.optimizationMode) { + return t(`favorites.manager.card.optimizationMode.${favorite.optimizationMode}`); + } + if (favorite.imageSubMode) { + return t(`favorites.manager.card.imageSubMode.${favorite.imageSubMode}`); + } + return ''; +}; + const formatDate = (timestamp: number) => { const date = new Date(timestamp); const now = new Date(); diff --git a/packages/ui/src/components/FavoriteManager.vue b/packages/ui/src/components/FavoriteManager.vue index a9b0b8be..40de9889 100644 --- a/packages/ui/src/components/FavoriteManager.vue +++ b/packages/ui/src/components/FavoriteManager.vue @@ -222,6 +222,13 @@ + + + | null>('services', null); const message = useToast(); +// 初始化默认分类(仅在首次使用时创建) +const { ensureDefaultCategories } = services?.value?.favoriteManager + ? useFavoriteInitializer(services.value.favoriteManager) + : { ensureDefaultCategories: async () => {} }; + // 响应式数据 const loading = ref(false); const favorites = ref([]); @@ -327,6 +343,7 @@ const createState = reactive({ }); const previewFavorite = ref(null); const categoryManagerVisible = ref(false); +const tagManagerVisible = ref(false); // 响应式的视口宽度 const viewportWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1280); @@ -481,6 +498,19 @@ const pageSize = computed(() => { }); const actionMenuOptions = computed(() => [ + { + label: t('favorites.manager.actions.manageTags'), + key: 'manageTags', + icon: () => h(NIcon, null, { default: () => h(Tags) }) + }, + { + label: t('favorites.manager.actions.manageCategories'), + key: 'manageCategories', + icon: () => h(NIcon, null, { default: () => h(Folder) }) + }, + { + type: 'divider' + }, { label: t('favorites.manager.actions.export'), key: 'export', @@ -759,6 +789,12 @@ const handleUseFavorite = (favorite: FavoritePrompt) => { const handleActionMenuSelect = (key: string) => { switch (key) { + case 'manageTags': + tagManagerVisible.value = true; + break; + case 'manageCategories': + categoryManagerVisible.value = true; + break; case 'export': handleExportFavorites(); break; @@ -884,29 +920,32 @@ watch(() => editState.visible, (visible) => { } }); -onMounted(() => { +// 窗口大小变化处理 +const handleResize = () => { + if (typeof window !== 'undefined') { + viewportWidth.value = window.innerWidth; + } +}; +const debouncedResize = useDebounceFn(handleResize, 150); + +// 生命周期 +onMounted(async () => { if (services?.value?.favoriteManager) { + // 确保默认分类存在(仅在首次使用时创建) + await ensureDefaultCategories(); loadFavorites(); loadCategories(); } - // 监听窗口大小变化 - const handleResize = () => { - if (typeof window !== 'undefined') { - viewportWidth.value = window.innerWidth; - } - }; - const debouncedResize = useDebounceFn(handleResize, 150); - if (typeof window !== 'undefined') { window.addEventListener('resize', debouncedResize); } +}); - onBeforeUnmount(() => { - if (typeof window !== 'undefined') { - window.removeEventListener('resize', debouncedResize); - } - }); +onBeforeUnmount(() => { + if (typeof window !== 'undefined') { + window.removeEventListener('resize', debouncedResize); + } }); defineExpose({ diff --git a/packages/ui/src/components/SaveFavoriteDialog.vue b/packages/ui/src/components/SaveFavoriteDialog.vue index a961e529..444104f3 100644 --- a/packages/ui/src/components/SaveFavoriteDialog.vue +++ b/packages/ui/src/components/SaveFavoriteDialog.vue @@ -84,11 +84,31 @@ - +
+ + + + {{ tag }} + + + + + +
@@ -131,7 +151,8 @@ import { NFormItem, NInput, NSelect, - NDynamicTags, + NAutoComplete, + NTag, NButton, NSpace, NScrollbar, @@ -141,11 +162,13 @@ import { } from 'naive-ui'; import { useI18n } from 'vue-i18n'; import { useToast } from '../composables/useToast'; +import { useTagSuggestions } from '../composables/useTagSuggestions'; import OutputDisplayCore from './OutputDisplayCore.vue'; import CategoryTreeSelect from './CategoryTreeSelect.vue'; import type { AppServices } from '../types/services'; const { t } = useI18n(); +const { filterTags, loadTags } = useTagSuggestions(); interface Props { /** 是否显示对话框 */ @@ -183,6 +206,16 @@ const message = useToast(); const saving = ref(false); +// 标签输入和建议 +const tagInputValue = ref(''); +const tagSuggestions = computed(() => { + const suggestions = filterTags(tagInputValue.value, formData.tags); + return suggestions.map(s => ({ + label: s.label, + value: s.value + })); +}); + // 对话框标题 const dialogTitle = computed(() => { if (props.mode === 'create') return t('favorites.dialog.createTitle'); @@ -234,6 +267,27 @@ const handleFunctionModeChange = (mode: 'basic' | 'context' | 'image') => { } }; +// 标签管理函数 +const handleRemoveTag = (index: number) => { + formData.tags.splice(index, 1); +}; + +const handleSelectTag = (value: string) => { + if (value && !formData.tags.includes(value) && formData.tags.length < 10) { + formData.tags.push(value); + tagInputValue.value = ''; + } +}; + +const handleAddTag = (e: KeyboardEvent) => { + e.preventDefault(); + const trimmedValue = tagInputValue.value.trim(); + if (trimmedValue && !formData.tags.includes(trimmedValue) && formData.tags.length < 10) { + formData.tags.push(trimmedValue); + tagInputValue.value = ''; + } +}; + // 关闭对话框 const handleClose = () => { emit('update:show', false); @@ -260,6 +314,21 @@ const handleSave = async () => { saving.value = true; try { + // 【优化】保存收藏前,确保所有标签都存在于独立标签库中 + // 这样可以保证:1. 标签统一管理 2. 导出时完整 3. 标签管理器能看到所有标签 + for (const tag of formData.tags) { + try { + await servicesValue.favoriteManager.addTag(tag); + } catch (error: any) { + // 只忽略"标签已存在"错误,其他错误需要抛出 + if (error?.code !== 'TAG_ALREADY_EXISTS') { + console.error('添加标签到独立库失败:', error); + throw error; + } + // 标签已存在,这是正常情况,继续处理 + } + } + if (props.mode === 'edit' && props.favorite) { // 编辑模式:更新现有收藏 await servicesValue.favoriteManager.updateFavorite(props.favorite.id, { @@ -308,8 +377,11 @@ const handleSave = async () => { }; // 监听对话框显示,初始化表单 -watch(() => props.show, (newShow) => { +watch(() => props.show, async (newShow) => { if (newShow) { + // 加载标签建议 + await loadTags(); + if (props.mode === 'create') { // 新建模式: 重置为空表单 formData.title = ''; diff --git a/packages/ui/src/components/TagManager.vue b/packages/ui/src/components/TagManager.vue new file mode 100644 index 00000000..6a02bb57 --- /dev/null +++ b/packages/ui/src/components/TagManager.vue @@ -0,0 +1,425 @@ + + + diff --git a/packages/ui/src/composables/useFavoriteInitializer.ts b/packages/ui/src/composables/useFavoriteInitializer.ts new file mode 100644 index 00000000..1d962ad6 --- /dev/null +++ b/packages/ui/src/composables/useFavoriteInitializer.ts @@ -0,0 +1,54 @@ +import { useI18n } from 'vue-i18n'; +import type { FavoriteManager } from '@prompt-optimizer/core'; + +/** + * 收藏功能初始化器 + * 负责创建国际化的默认分类 + */ +export function useFavoriteInitializer(manager: FavoriteManager) { + const { t } = useI18n(); + + /** + * 确保默认分类存在(仅首次使用时创建) + */ + const ensureDefaultCategories = async () => { + const defaultCategories = [ + { + name: t('favorites.categories.default.uncategorized'), + description: t('favorites.categories.default.uncategorizedDesc'), + color: '#6B7280' + }, + { + name: t('favorites.categories.default.creativeWriting'), + description: t('favorites.categories.default.creativeWritingDesc'), + color: '#8B5CF6' + }, + { + name: t('favorites.categories.default.programming'), + description: t('favorites.categories.default.programmingDesc'), + color: '#F59E0B' + }, + { + name: t('favorites.categories.default.businessAnalysis'), + description: t('favorites.categories.default.businessAnalysisDesc'), + color: '#EF4444' + }, + { + name: t('favorites.categories.default.learning'), + description: t('favorites.categories.default.learningDesc'), + color: '#10B981' + }, + { + name: t('favorites.categories.default.dailyAssistant'), + description: t('favorites.categories.default.dailyAssistantDesc'), + color: '#3B82F6' + } + ]; + + await manager.ensureDefaultCategories(defaultCategories); + }; + + return { + ensureDefaultCategories + }; +} diff --git a/packages/ui/src/composables/useTagSuggestions.ts b/packages/ui/src/composables/useTagSuggestions.ts new file mode 100644 index 00000000..43b02db8 --- /dev/null +++ b/packages/ui/src/composables/useTagSuggestions.ts @@ -0,0 +1,111 @@ +import { ref, computed, inject, type Ref } from 'vue'; +import type { AppServices } from '../types/services'; +import { TagTypeConverter } from '@prompt-optimizer/core'; + +export interface TagSuggestion { + label: string; + value: string; + count: number; +} + +/** + * 标签建议 Composable + * 提供标签自动完成功能,基于现有收藏中的标签使用情况 + */ +export function useTagSuggestions() { + const services = inject>('services'); + const allTags = ref([]); + const loading = ref(false); + + /** + * 加载所有标签统计数据 + */ + const loadTags = async () => { + if (!services?.value?.favoriteManager) { + return; + } + + loading.value = true; + try { + const tagStats = await services.value.favoriteManager.getAllTags(); + // 使用统一的类型转换器转换为自动完成选项格式 + allTags.value = TagTypeConverter.toAutoCompleteOptions(tagStats); + } catch (error) { + console.error('Failed to load tags:', error); + allTags.value = []; + } finally { + loading.value = false; + } + }; + + /** + * 根据输入查询过滤标签建议 + * @param query 查询字符串 + * @param excludeTags 需要排除的标签(已选中的标签) + * @returns 过滤后的标签建议列表 + */ + const filterTags = (query: string, excludeTags: string[] = []): TagSuggestion[] => { + if (!query) { + // 如果没有输入,返回所有未选中的标签,按使用次数排序 + return allTags.value + .filter(tag => !excludeTags.includes(tag.value)) + .sort((a, b) => b.count - a.count); + } + + // 模糊搜索匹配 + const lowerQuery = query.toLowerCase(); + return allTags.value + .filter(tag => { + // 排除已选中的标签 + if (excludeTags.includes(tag.value)) { + return false; + } + // 包含查询字符串 + return tag.value.toLowerCase().includes(lowerQuery); + }) + .sort((a, b) => { + // 优先匹配前缀 + const aStartsWith = a.value.toLowerCase().startsWith(lowerQuery); + const bStartsWith = b.value.toLowerCase().startsWith(lowerQuery); + if (aStartsWith && !bStartsWith) return -1; + if (!aStartsWith && bStartsWith) return 1; + // 其次按使用次数排序 + return b.count - a.count; + }); + }; + + /** + * 获取热门标签(使用次数最多的前N个) + * @param limit 返回数量限制 + * @param excludeTags 需要排除的标签 + */ + const getPopularTags = computed(() => { + return (limit = 10, excludeTags: string[] = []): TagSuggestion[] => { + return allTags.value + .filter(tag => !excludeTags.includes(tag.value)) + .sort((a, b) => b.count - a.count) + .slice(0, limit); + }; + }); + + /** + * 获取最近使用的标签(暂时与热门标签相同,未来可以基于时间戳优化) + * @param limit 返回数量限制 + * @param excludeTags 需要排除的标签 + */ + const getRecentTags = computed(() => { + return (limit = 10, excludeTags: string[] = []): TagSuggestion[] => { + // TODO: 未来可以基于收藏的更新时间来优化这个逻辑 + return getPopularTags.value(limit, excludeTags); + }; + }); + + return { + allTags, + loading, + loadTags, + filterTags, + getPopularTags, + getRecentTags + }; +} diff --git a/packages/ui/src/i18n/locales/en-US.ts b/packages/ui/src/i18n/locales/en-US.ts index 7e802119..f4bcd6cf 100644 --- a/packages/ui/src/i18n/locales/en-US.ts +++ b/packages/ui/src/i18n/locales/en-US.ts @@ -1,3 +1,20 @@ +const favoriteCategoriesMessages = { + default: { + uncategorized: 'Uncategorized', + uncategorizedDesc: 'Default category', + creativeWriting: 'Creative Writing', + creativeWritingDesc: 'Creative writing prompts', + programming: 'Programming', + programmingDesc: 'Programming and development prompts', + businessAnalysis: 'Business Analysis', + businessAnalysisDesc: 'Business analysis prompts', + learning: 'Learning & Research', + learningDesc: 'Learning and research prompts', + dailyAssistant: 'Daily Assistant', + dailyAssistantDesc: 'Daily life assistant prompts' + } +}; + export default { common: { loading: 'Loading...', @@ -103,6 +120,7 @@ export default { }, favorites: { title: 'My Favorites', + categories: favoriteCategoriesMessages, manager: { title: 'Favorites Manager', searchPlaceholder: 'Search favorites...', @@ -138,6 +156,8 @@ export default { title: 'Category Manager' }, actions: { + manageTags: 'Manage Tags', + manageCategories: 'Manage Categories', export: 'Export Favorites', clear: 'Clear Favorites', exportSuccess: 'Export successful', @@ -180,7 +200,77 @@ export default { edit: 'Edit', delete: 'Delete', cancel: 'Cancel', - deleteConfirm: 'Delete "{title}"?' + deleteConfirm: 'Delete "{title}"?', + functionMode: { + basic: 'Basic', + context: 'Context', + image: 'Image' + }, + optimizationMode: { + system: 'System', + user: 'User' + }, + imageSubMode: { + text2image: 'Text-to-Image', + image2image: 'Image-to-Image' + } + }, + categories: favoriteCategoriesMessages, + tagManager: { + title: 'Tag Manager', + searchPlaceholder: 'Search tags...', + totalTags: 'Total {count} tags', + add: 'Add Tag', + tagName: 'Tag Name', + useCount: 'Use Count', + actions: 'Actions', + rename: 'Rename', + merge: 'Merge', + mergeTooltip: 'Merge current tag into target tag', + delete: 'Delete', + close: 'Close', + addDialog: { + title: 'Add Tag', + tagName: 'Tag Name', + tagNamePlaceholder: 'Enter tag name', + emptyWarning: 'Tag name cannot be empty', + existWarning: 'Tag already exists', + confirm: 'Confirm', + cancel: 'Cancel' + }, + renameDialog: { + title: 'Rename Tag', + oldName: 'Original Name', + newName: 'New Name', + newNamePlaceholder: 'Enter new tag name', + currentName: 'Current tag name: {name}', + emptyWarning: 'Tag name cannot be empty', + confirm: 'Confirm', + cancel: 'Cancel' + }, + mergeDialog: { + title: 'Merge Tags', + sourceName: 'Source Tag', + targetName: 'Merge Into', + targetPlaceholder: 'Select target tag', + sourceTag: 'Source tag: {name}', + selectTargetWarning: 'Please select target tag', + confirm: 'Confirm Merge', + cancel: 'Cancel', + warning: 'After merging, "{source}" tag will be deleted, and all favorites using this tag will use "{target}" tag instead' + }, + deleteConfirm: 'Are you sure you want to delete tag "{name}"? This will remove the tag from {count} favorites.', + messages: { + loadFailed: 'Failed to load tag list', + addSuccess: 'Tag added successfully', + addFailed: 'Failed to add tag', + renameSuccess: 'Tag renamed successfully', + renameFailed: 'Failed to rename tag', + mergeSuccess: 'Tags merged successfully', + mergeFailed: 'Failed to merge tags', + deleteSuccess: 'Tag deleted successfully', + deleteFailed: 'Failed to delete tag' + } } }, dialog: { diff --git a/packages/ui/src/i18n/locales/zh-CN.ts b/packages/ui/src/i18n/locales/zh-CN.ts index 4f7ae37c..f9236b03 100644 --- a/packages/ui/src/i18n/locales/zh-CN.ts +++ b/packages/ui/src/i18n/locales/zh-CN.ts @@ -1,4 +1,21 @@ -export default { +const favoriteCategoriesMessages = { + default: { + uncategorized: '未分类', + uncategorizedDesc: '默认分类', + creativeWriting: '创意写作', + creativeWritingDesc: '创意写作相关的提示词', + programming: '编程开发', + programmingDesc: '编程开发相关的提示词', + businessAnalysis: '商业分析', + businessAnalysisDesc: '商业分析相关的提示词', + learning: '学习研究', + learningDesc: '学习研究相关的提示词', + dailyAssistant: '日常助手', + dailyAssistantDesc: '日常生活助手类提示词' + } +}; + +export default { common: { loading: '加载中...', save: '保存', @@ -103,6 +120,7 @@ }, favorites: { title: '我的收藏', + categories: favoriteCategoriesMessages, manager: { title: '收藏管理', searchPlaceholder: '搜索收藏...', @@ -138,6 +156,8 @@ title: '分类管理' }, actions: { + manageTags: '管理标签', + manageCategories: '管理分类', export: '导出收藏', clear: '清空收藏', exportSuccess: '导出成功', @@ -180,7 +200,77 @@ edit: '编辑', delete: '删除', cancel: '取消', - deleteConfirm: '确定删除"{title}"吗?' + deleteConfirm: '确定删除"{title}"吗?', + functionMode: { + basic: '基础', + context: '上下文', + image: '图像' + }, + optimizationMode: { + system: '系统', + user: '用户' + }, + imageSubMode: { + text2image: '文生图', + image2image: '图生图' + } + }, + categories: favoriteCategoriesMessages, + tagManager: { + title: '标签管理', + searchPlaceholder: '搜索标签...', + totalTags: '共 {count} 个标签', + add: '新增标签', + tagName: '标签名', + useCount: '使用次数', + actions: '操作', + rename: '重命名', + merge: '合并', + mergeTooltip: '将当前标签合并到目标标签中', + delete: '删除', + close: '关闭', + addDialog: { + title: '新增标签', + tagName: '标签名称', + tagNamePlaceholder: '输入标签名称', + emptyWarning: '标签名不能为空', + existWarning: '标签已存在', + confirm: '确认', + cancel: '取消' + }, + renameDialog: { + title: '重命名标签', + oldName: '原标签名', + newName: '新标签名', + newNamePlaceholder: '输入新的标签名', + currentName: '当前标签名:{name}', + emptyWarning: '标签名不能为空', + confirm: '确认', + cancel: '取消' + }, + mergeDialog: { + title: '合并标签', + sourceName: '源标签', + targetName: '合并到', + targetPlaceholder: '选择目标标签', + sourceTag: '源标签:{name}', + selectTargetWarning: '请选择目标标签', + confirm: '确认合并', + cancel: '取消', + warning: '合并后,"{source}" 标签将被删除,所有使用该标签的收藏将改用 "{target}" 标签' + }, + deleteConfirm: '确定删除标签 "{name}"?这将从 {count} 个收藏中移除该标签。', + messages: { + loadFailed: '加载标签列表失败', + addSuccess: '标签添加成功', + addFailed: '标签添加失败', + renameSuccess: '标签重命名成功', + renameFailed: '标签重命名失败', + mergeSuccess: '标签合并成功', + mergeFailed: '标签合并失败', + deleteSuccess: '标签删除成功', + deleteFailed: '标签删除失败' + } } }, dialog: { diff --git a/packages/ui/src/i18n/locales/zh-TW.ts b/packages/ui/src/i18n/locales/zh-TW.ts index 32262268..16da2077 100644 --- a/packages/ui/src/i18n/locales/zh-TW.ts +++ b/packages/ui/src/i18n/locales/zh-TW.ts @@ -1,3 +1,20 @@ +const favoriteCategoriesMessages = { + default: { + uncategorized: '未分類', + uncategorizedDesc: '預設分類', + creativeWriting: '創意寫作', + creativeWritingDesc: '創意寫作相關的提示詞', + programming: '程式開發', + programmingDesc: '程式開發相關的提示詞', + businessAnalysis: '商業分析', + businessAnalysisDesc: '商業分析相關的提示詞', + learning: '學習研究', + learningDesc: '學習研究相關的提示詞', + dailyAssistant: '日常助手', + dailyAssistantDesc: '日常生活助手類提示詞' + } +}; + export default { common: { loading: '載入中...', @@ -103,6 +120,7 @@ export default { }, favorites: { title: '我的收藏', + categories: favoriteCategoriesMessages, manager: { title: '收藏管理', searchPlaceholder: '搜尋收藏...', @@ -138,6 +156,8 @@ export default { title: '分類管理' }, actions: { + manageTags: '管理標籤', + manageCategories: '管理分類', export: '匯出收藏', clear: '清空收藏', exportSuccess: '匯出成功', @@ -180,7 +200,77 @@ export default { edit: '編輯', delete: '刪除', cancel: '取消', - deleteConfirm: '確定刪除"{title}"嗎?' + deleteConfirm: '確定刪除"{title}"嗎?', + functionMode: { + basic: '基礎', + context: '情境', + image: '圖像' + }, + optimizationMode: { + system: '系統', + user: '使用者' + }, + imageSubMode: { + text2image: '文生圖', + image2image: '圖生圖' + } + }, + categories: favoriteCategoriesMessages, + tagManager: { + title: '標籤管理', + searchPlaceholder: '搜尋標籤...', + totalTags: '共 {count} 個標籤', + add: '新增標籤', + tagName: '標籤名稱', + useCount: '使用次數', + actions: '操作', + rename: '重新命名', + merge: '合併', + mergeTooltip: '將目前標籤合併到目標標籤中', + delete: '刪除', + close: '關閉', + addDialog: { + title: '新增標籤', + tagName: '標籤名稱', + tagNamePlaceholder: '輸入標籤名稱', + emptyWarning: '標籤名稱不能為空', + existWarning: '標籤已存在', + confirm: '確認', + cancel: '取消' + }, + renameDialog: { + title: '重新命名標籤', + oldName: '原標籤名稱', + newName: '新標籤名稱', + newNamePlaceholder: '輸入新的標籤名稱', + currentName: '目前標籤名稱:{name}', + emptyWarning: '標籤名稱不能為空', + confirm: '確認', + cancel: '取消' + }, + mergeDialog: { + title: '合併標籤', + sourceName: '源標籤', + targetName: '合併到', + targetPlaceholder: '選擇目標標籤', + sourceTag: '源標籤:{name}', + selectTargetWarning: '請選擇目標標籤', + confirm: '確認合併', + cancel: '取消', + warning: '合併後,"{source}" 標籤將被刪除,所有使用該標籤的收藏將改用 "{target}" 標籤' + }, + deleteConfirm: '確定刪除標籤 "{name}"?這將從 {count} 個收藏中移除該標籤。', + messages: { + loadFailed: '載入標籤清單失敗', + addSuccess: '標籤新增成功', + addFailed: '標籤新增失敗', + renameSuccess: '標籤重新命名成功', + renameFailed: '標籤重新命名失敗', + mergeSuccess: '標籤合併成功', + mergeFailed: '標籤合併失敗', + deleteSuccess: '標籤刪除成功', + deleteFailed: '標籤刪除失敗' + } } }, dialog: {