Skip to content

Commit 2a0151a

Browse files
committed
feat(favorite): 实现收藏功能核心服务与IPC通信
- 新增 ensureDefaultCategories 方法确保默认分类存在 - 优化标签添加逻辑,实现幂等性处理- 移除重复的 FavoriteTagAlreadyExistsError 错误类型 - 仅在新增标签时更新统计信息,避免重复计算 - 在桌面端主进程新增完整的收藏管理 IPC 处理器 - 在预加载脚本中暴露收藏管理器接口给渲染进程 - 优化保存收藏对话框的标签处理逻辑 - 新增标签去重与类型安全处理 - 在分类管理组件中引入 watch API - 完善收藏功能的错误处理与数据序列化机制
1 parent 253101d commit 2a0151a

File tree

8 files changed

+421
-41
lines changed

8 files changed

+421
-41
lines changed

packages/core/src/services/favorite/electron-proxy.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,12 @@ export class FavoriteManagerElectronProxy implements IFavoriteManager {
179179
async getCategoryUsage(categoryId: string): Promise<number> {
180180
return this.invokeMethod('getCategoryUsage', categoryId);
181181
}
182-
}
182+
183+
async ensureDefaultCategories(defaultCategories: Array<{
184+
name: string;
185+
description?: string;
186+
color: string;
187+
}>): Promise<void> {
188+
return this.invokeMethod('ensureDefaultCategories', defaultCategories);
189+
}
190+
}

packages/core/src/services/favorite/manager.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
FavoriteCategoryNotFoundError,
1313
FavoriteValidationError,
1414
FavoriteStorageError,
15-
FavoriteTagAlreadyExistsError,
1615
FavoriteMigrationError,
1716
FavoriteImportExportError
1817
} from './errors';
@@ -716,14 +715,17 @@ export class FavoriteManager implements IFavoriteManager {
716715
throw new FavoriteValidationError('Tag name cannot be empty');
717716
}
718717

718+
let added = false;
719+
719720
try {
720721
await this.storageProvider.updateData(this.STORAGE_KEYS.TAGS, (tags: FavoriteTag[] | null) => {
721722
const tagsList = tags || [];
722723

723724
// 检查是否已存在
724725
const existing = tagsList.find(t => t.tag === trimmedTag);
725726
if (existing) {
726-
throw new FavoriteTagAlreadyExistsError(trimmedTag);
727+
// 标签已存在,保持幂等,不再抛错
728+
return tagsList;
727729
}
728730

729731
const now = Date.now();
@@ -732,11 +734,14 @@ export class FavoriteManager implements IFavoriteManager {
732734
createdAt: now
733735
};
734736

737+
added = true;
735738
return [...tagsList, newTag];
736739
});
737740

738-
// 更新统计信息
739-
await this.updateStats();
741+
// 仅在新增标签时更新统计信息
742+
if (added) {
743+
await this.updateStats();
744+
}
740745
} catch (error) {
741746
if (error instanceof FavoriteError) {
742747
throw error;

packages/core/src/services/favorite/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,11 @@ export interface IFavoriteManager {
194194

195195
/** 获取分类使用统计 */
196196
getCategoryUsage(categoryId: string): Promise<number>;
197-
}
197+
198+
/** 确保默认分类存在(仅首次执行有效) */
199+
ensureDefaultCategories(defaultCategories: Array<{
200+
name: string;
201+
description?: string;
202+
color: string;
203+
}>): Promise<void>;
204+
}

packages/core/tests/services/favorite/tag-manager.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect, beforeEach, vi } from 'vitest';
22
import { FavoriteManager } from '../../../src/services/favorite/manager';
33
import type { IStorageProvider } from '../../../src/services/storage/types';
4-
import { FavoriteValidationError, FavoriteError } from '../../../src/services/favorite/errors';
4+
import { FavoriteValidationError } from '../../../src/services/favorite/errors';
55

66
/**
77
* 标签管理功能单元测试
@@ -61,9 +61,12 @@ describe('FavoriteManager - 标签管理', () => {
6161
await expect(manager.addTag(' ')).rejects.toThrow(FavoriteValidationError);
6262
});
6363

64-
it('应该拒绝重复的标签', async () => {
64+
it('重复添加标签应该幂等', async () => {
6565
await manager.addTag('标签1');
66-
await expect(manager.addTag('标签1')).rejects.toThrow(FavoriteError);
66+
await expect(manager.addTag('标签1')).resolves.toBeUndefined();
67+
68+
const tags = await manager.getAllTags();
69+
expect(tags.filter(tag => tag.tag === '标签1')).toHaveLength(1);
6770
});
6871

6972
it('应该自动去除首尾空格', async () => {

packages/desktop/main.js

Lines changed: 257 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const {
4747
createTemplateLanguageService,
4848
createDataManager,
4949
createContextRepo,
50+
FavoriteManager,
5051
FileStorageProvider,
5152
// 导入共享的环境变量扫描常量
5253
CUSTOM_API_PATTERN,
@@ -81,7 +82,7 @@ function safeSerialize(obj) {
8182
}
8283

8384
let mainWindow;
84-
let modelManager, templateManager, historyManager, llmService, promptService, templateLanguageService, preferenceService, dataManager, contextRepo;
85+
let modelManager, templateManager, historyManager, llmService, promptService, templateLanguageService, preferenceService, dataManager, contextRepo, favoriteManager;
8586
let imageModelManager, imageService;
8687
let imageAdapterRegistry; // 全局引用以供 IPC 处理器使用
8788
let storageProvider; // 全局存储提供器引用,用于退出时保存数据
@@ -509,6 +510,9 @@ async function initializeServices() {
509510

510511
console.log('[DESKTOP] Creating Data manager...');
511512
dataManager = createDataManager(modelManager, templateManager, historyManager, preferenceService, contextRepo);
513+
514+
console.log('[DESKTOP] Creating Favorite manager...');
515+
favoriteManager = new FavoriteManager(storageProvider);
512516

513517
console.log('[Main Process] Core services initialized successfully.');
514518

@@ -598,6 +602,37 @@ function createDetailedErrorResponse(error) {
598602
return { success: false, error: detailedMessage };
599603
}
600604

605+
function formatFavoriteError(error) {
606+
if (!error || typeof error !== 'object') {
607+
return { message: String(error || 'Unknown error'), code: 'UNKNOWN_ERROR' };
608+
}
609+
610+
const formatted = {
611+
message: error.message || 'Unknown error',
612+
code: error.code || 'UNKNOWN_ERROR',
613+
name: error.name || 'Error'
614+
};
615+
616+
if (error.details) {
617+
formatted.details = error.details;
618+
}
619+
620+
if (error.cause) {
621+
formatted.cause = {
622+
message: error.cause.message || String(error.cause),
623+
code: error.cause.code,
624+
name: error.cause.name
625+
};
626+
}
627+
628+
return formatted;
629+
}
630+
631+
function createFavoriteErrorResponse(error) {
632+
console.error('[Favorite IPC Error]', error);
633+
return { success: false, error: formatFavoriteError(error) };
634+
}
635+
601636
// --- High-Level IPC Service Handlers ---
602637
function setupIPC() {
603638
console.log('[Main Process] Setting up high-level service IPC handlers...');
@@ -1485,6 +1520,227 @@ function setupIPC() {
14851520
}
14861521
});
14871522

1523+
// Favorite Manager handlers
1524+
ipcMain.handle('favorite-addFavorite', async (event, favorite) => {
1525+
try {
1526+
const safeFavorite = safeSerialize(favorite);
1527+
const result = await favoriteManager.addFavorite(safeFavorite);
1528+
return createSuccessResponse(result);
1529+
} catch (error) {
1530+
return createFavoriteErrorResponse(error);
1531+
}
1532+
});
1533+
1534+
ipcMain.handle('favorite-getFavorites', async (event, options) => {
1535+
try {
1536+
const safeOptions = safeSerialize(options);
1537+
const result = await favoriteManager.getFavorites(safeOptions || undefined);
1538+
return createSuccessResponse(result);
1539+
} catch (error) {
1540+
return createFavoriteErrorResponse(error);
1541+
}
1542+
});
1543+
1544+
ipcMain.handle('favorite-getFavorite', async (event, id) => {
1545+
try {
1546+
const result = await favoriteManager.getFavorite(id);
1547+
return createSuccessResponse(result);
1548+
} catch (error) {
1549+
return createFavoriteErrorResponse(error);
1550+
}
1551+
});
1552+
1553+
ipcMain.handle('favorite-updateFavorite', async (event, id, updates) => {
1554+
try {
1555+
const safeUpdates = safeSerialize(updates);
1556+
await favoriteManager.updateFavorite(id, safeUpdates);
1557+
return createSuccessResponse(null);
1558+
} catch (error) {
1559+
return createFavoriteErrorResponse(error);
1560+
}
1561+
});
1562+
1563+
ipcMain.handle('favorite-deleteFavorite', async (event, id) => {
1564+
try {
1565+
await favoriteManager.deleteFavorite(id);
1566+
return createSuccessResponse(null);
1567+
} catch (error) {
1568+
return createFavoriteErrorResponse(error);
1569+
}
1570+
});
1571+
1572+
ipcMain.handle('favorite-deleteFavorites', async (event, ids) => {
1573+
try {
1574+
const safeIds = safeSerialize(ids);
1575+
await favoriteManager.deleteFavorites(safeIds);
1576+
return createSuccessResponse(null);
1577+
} catch (error) {
1578+
return createFavoriteErrorResponse(error);
1579+
}
1580+
});
1581+
1582+
ipcMain.handle('favorite-incrementUseCount', async (event, id) => {
1583+
try {
1584+
await favoriteManager.incrementUseCount(id);
1585+
return createSuccessResponse(null);
1586+
} catch (error) {
1587+
return createFavoriteErrorResponse(error);
1588+
}
1589+
});
1590+
1591+
ipcMain.handle('favorite-getCategories', async () => {
1592+
try {
1593+
const result = await favoriteManager.getCategories();
1594+
return createSuccessResponse(result);
1595+
} catch (error) {
1596+
return createFavoriteErrorResponse(error);
1597+
}
1598+
});
1599+
1600+
ipcMain.handle('favorite-addCategory', async (event, category) => {
1601+
try {
1602+
const safeCategory = safeSerialize(category);
1603+
const result = await favoriteManager.addCategory(safeCategory);
1604+
return createSuccessResponse(result);
1605+
} catch (error) {
1606+
return createFavoriteErrorResponse(error);
1607+
}
1608+
});
1609+
1610+
ipcMain.handle('favorite-updateCategory', async (event, id, updates) => {
1611+
try {
1612+
const safeUpdates = safeSerialize(updates);
1613+
await favoriteManager.updateCategory(id, safeUpdates);
1614+
return createSuccessResponse(null);
1615+
} catch (error) {
1616+
return createFavoriteErrorResponse(error);
1617+
}
1618+
});
1619+
1620+
ipcMain.handle('favorite-deleteCategory', async (event, id) => {
1621+
try {
1622+
const result = await favoriteManager.deleteCategory(id);
1623+
return createSuccessResponse(result);
1624+
} catch (error) {
1625+
return createFavoriteErrorResponse(error);
1626+
}
1627+
});
1628+
1629+
ipcMain.handle('favorite-getStats', async () => {
1630+
try {
1631+
const result = await favoriteManager.getStats();
1632+
return createSuccessResponse(result);
1633+
} catch (error) {
1634+
return createFavoriteErrorResponse(error);
1635+
}
1636+
});
1637+
1638+
ipcMain.handle('favorite-searchFavorites', async (event, keyword, options) => {
1639+
try {
1640+
const safeOptions = safeSerialize(options);
1641+
const result = await favoriteManager.searchFavorites(keyword, safeOptions || undefined);
1642+
return createSuccessResponse(result);
1643+
} catch (error) {
1644+
return createFavoriteErrorResponse(error);
1645+
}
1646+
});
1647+
1648+
ipcMain.handle('favorite-exportFavorites', async (event, ids) => {
1649+
try {
1650+
const safeIds = safeSerialize(ids);
1651+
const result = await favoriteManager.exportFavorites(safeIds || undefined);
1652+
return createSuccessResponse(result);
1653+
} catch (error) {
1654+
return createFavoriteErrorResponse(error);
1655+
}
1656+
});
1657+
1658+
ipcMain.handle('favorite-importFavorites', async (event, data, options) => {
1659+
try {
1660+
const safeData = typeof data === 'string' ? data : safeSerialize(data);
1661+
const safeOptions = safeSerialize(options);
1662+
const result = await favoriteManager.importFavorites(safeData, safeOptions || undefined);
1663+
return createSuccessResponse(result);
1664+
} catch (error) {
1665+
return createFavoriteErrorResponse(error);
1666+
}
1667+
});
1668+
1669+
ipcMain.handle('favorite-getAllTags', async () => {
1670+
try {
1671+
const result = await favoriteManager.getAllTags();
1672+
return createSuccessResponse(result);
1673+
} catch (error) {
1674+
return createFavoriteErrorResponse(error);
1675+
}
1676+
});
1677+
1678+
ipcMain.handle('favorite-addTag', async (event, tag) => {
1679+
try {
1680+
await favoriteManager.addTag(tag);
1681+
return createSuccessResponse(null);
1682+
} catch (error) {
1683+
return createFavoriteErrorResponse(error);
1684+
}
1685+
});
1686+
1687+
ipcMain.handle('favorite-renameTag', async (event, oldTag, newTag) => {
1688+
try {
1689+
const result = await favoriteManager.renameTag(oldTag, newTag);
1690+
return createSuccessResponse(result);
1691+
} catch (error) {
1692+
return createFavoriteErrorResponse(error);
1693+
}
1694+
});
1695+
1696+
ipcMain.handle('favorite-mergeTags', async (event, sourceTags, targetTag) => {
1697+
try {
1698+
const safeSourceTags = safeSerialize(sourceTags);
1699+
const result = await favoriteManager.mergeTags(safeSourceTags, targetTag);
1700+
return createSuccessResponse(result);
1701+
} catch (error) {
1702+
return createFavoriteErrorResponse(error);
1703+
}
1704+
});
1705+
1706+
ipcMain.handle('favorite-deleteTag', async (event, tag) => {
1707+
try {
1708+
const result = await favoriteManager.deleteTag(tag);
1709+
return createSuccessResponse(result);
1710+
} catch (error) {
1711+
return createFavoriteErrorResponse(error);
1712+
}
1713+
});
1714+
1715+
ipcMain.handle('favorite-reorderCategories', async (event, categoryIds) => {
1716+
try {
1717+
const safeCategoryIds = safeSerialize(categoryIds);
1718+
await favoriteManager.reorderCategories(safeCategoryIds);
1719+
return createSuccessResponse(null);
1720+
} catch (error) {
1721+
return createFavoriteErrorResponse(error);
1722+
}
1723+
});
1724+
1725+
ipcMain.handle('favorite-getCategoryUsage', async (event, categoryId) => {
1726+
try {
1727+
const result = await favoriteManager.getCategoryUsage(categoryId);
1728+
return createSuccessResponse(result);
1729+
} catch (error) {
1730+
return createFavoriteErrorResponse(error);
1731+
}
1732+
});
1733+
1734+
ipcMain.handle('favorite-ensureDefaultCategories', async (event, defaultCategories) => {
1735+
try {
1736+
const safeCategories = safeSerialize(defaultCategories);
1737+
await favoriteManager.ensureDefaultCategories(safeCategories);
1738+
return createSuccessResponse(null);
1739+
} catch (error) {
1740+
return createFavoriteErrorResponse(error);
1741+
}
1742+
});
1743+
14881744
// Data Manager handlers
14891745
ipcMain.handle('data-exportAllData', async (event) => {
14901746
try {

0 commit comments

Comments
 (0)