From ca417f86d5bb81acce2566b5f12b81e9bb949c85 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Sun, 22 Jun 2025 14:55:26 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20perf:=20refactor=20Typogra?= =?UTF-8?q?phy=20to=20Text=20(#8250)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/debug-usage.mdc | 84 ++++++++ .cursor/rules/desktop-controller-tests.mdc | 188 ++++++++++++++++++ .../rules/desktop-feature-implementation.mdc | 2 +- src/config/modelProviders/index.ts | 1 - .../features/DatabaseRepair/Backup.tsx | 8 +- .../features/DatabaseRepair/Repair.tsx | 6 +- .../AssignKnowledgeBase/Item/index.tsx | 12 +- src/features/Portal/Artifacts/Header.tsx | 8 +- src/features/Portal/FilePreview/Header.tsx | 8 +- .../Portal/Home/Body/Files/FileList/Item.tsx | 6 +- .../Portal/Home/Body/Files/FileList/index.tsx | 5 +- src/features/Portal/Home/Body/Files/index.tsx | 6 +- .../Body/Plugins/ArtifactList/Item/index.tsx | 7 +- .../Home/Body/Plugins/ArtifactList/index.tsx | 6 +- .../Portal/Home/Body/Plugins/index.tsx | 6 +- src/features/Portal/Home/Title.tsx | 6 +- src/features/Portal/MessageDetail/Header.tsx | 6 +- src/features/Portal/Plugins/Header.tsx | 11 +- src/features/Portal/Thread/Header/Active.tsx | 7 +- src/features/Portal/Thread/Header/New.tsx | 8 +- .../SyncStatusInspector/EnableSync.tsx | 14 +- 21 files changed, 331 insertions(+), 74 deletions(-) create mode 100644 .cursor/rules/debug-usage.mdc create mode 100644 .cursor/rules/desktop-controller-tests.mdc diff --git a/.cursor/rules/debug-usage.mdc b/.cursor/rules/debug-usage.mdc new file mode 100644 index 0000000000..44947a58c5 --- /dev/null +++ b/.cursor/rules/debug-usage.mdc @@ -0,0 +1,84 @@ +--- +description: 包含添加 debug 日志请求时 +globs: +alwaysApply: false +--- +# Debug 包使用指南 + +本项目使用 [debug](mdc:https:/github.com/debug-js/debug) 包进行调试日志记录。使用此规则来确保团队成员统一调试日志格式。 + +## 基本用法 + +1. 导入 debug 包: + +```typescript +import debug from 'debug'; +``` + +2. 创建一个命名空间的日志记录器: + +```typescript +// 格式: lobe:[模块]:[子模块] +const log = debug('lobe-[模块名]:[子模块名]'); +``` + +3. 使用日志记录器: + +```typescript +log('简单消息'); +log('带变量的消息: %O', object); +log('格式化数字: %d', number); +``` + +## 命名空间约定 + +- 桌面应用相关: `lobe-desktop:[模块]` +- 服务端相关: `lobe-server:[模块]` +- 客户端相关: `lobe-client:[模块]` +- 路由相关: `lobe-[类型]-router:[模块]` + +## 格式说明符 + +- `%O` - 对象展开(推荐用于复杂对象) +- `%o` - 对象 +- `%s` - 字符串 +- `%d` - 数字 + +## 示例 + +查看 [market/index.ts](mdc:src/server/routers/edge/market/index.ts) 中的使用示例: + +```typescript +import debug from 'debug'; + +const log = debug('lobe-edge-router:market'); + +log('getAgent input: %O', input); +``` + +## 启用调试 + +要在开发时启用调试输出,需设置环境变量: + +### 在浏览器中 + +在控制台执行: +```javascript +localStorage.debug = 'lobe-*' +``` + +### 在 Node.js 环境中 + +```bash +DEBUG=lobe-* npm run dev +# 或者 +DEBUG=lobe-* pnpm dev +``` + +### 在 Electron 应用中 + +可以在主进程和渲染进程启动前设置环境变量: + +```typescript +process.env.DEBUG = 'lobe-*'; +``` diff --git a/.cursor/rules/desktop-controller-tests.mdc b/.cursor/rules/desktop-controller-tests.mdc new file mode 100644 index 0000000000..cafa486ba2 --- /dev/null +++ b/.cursor/rules/desktop-controller-tests.mdc @@ -0,0 +1,188 @@ +--- +description: 桌面端测试 +globs: +alwaysApply: false +--- +# 桌面端控制器单元测试指南 + +## 测试框架与目录结构 + +LobeChat 桌面端使用 Vitest 作为测试框架。控制器的单元测试应放置在对应控制器文件同级的 `__tests__` 目录下,并以原控制器文件名加 `.test.ts` 作为文件名。 + +``` +apps/desktop/src/main/controllers/ +├── __tests__/ +│ ├── index.test.ts +│ ├── MenuCtr.test.ts +│ └── ... +├── McpCtr.ts +├── MenuCtr.ts +└── ... +``` + +## 测试文件基本结构 + +```typescript +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { App } from '@/core/App'; + +import YourController from '../YourControllerName'; + +// 模拟依赖 +vi.mock('依赖模块', () => ({ + 依赖函数: vi.fn(), +})); + +// 模拟 App 实例 +const mockApp = { + // 按需模拟必要的 App 属性和方法 +} as unknown as App; + +describe('YourController', () => { + let controller: YourController; + + beforeEach(() => { + vi.clearAllMocks(); + controller = new YourController(mockApp); + }); + + describe('方法名', () => { + it('测试场景描述', async () => { + // 准备测试数据 + + // 执行被测方法 + const result = await controller.方法名(参数); + + // 验证结果 + expect(result).toMatchObject(预期结果); + }); + }); +}); +``` + +## 模拟外部依赖 + +### 模拟模块函数 + +```typescript +const mockFunction = vi.fn(); + +vi.mock('module-name', () => ({ + functionName: mockFunction, +})); +``` + +### 模拟 Node.js 核心模块 + +例如模拟 `child_process.exec` 和 `util.promisify`: + +```typescript +// 存储模拟的 exec 实现 +const mockExecImpl = vi.fn(); + +// 模拟 child_process.exec +vi.mock('child_process', () => ({ + exec: vi.fn((cmd, callback) => { + return mockExecImpl(cmd, callback); + }), +})); + +// 模拟 util.promisify +vi.mock('util', () => ({ + promisify: vi.fn((fn) => { + return async (cmd: string) => { + return new Promise((resolve, reject) => { + mockExecImpl(cmd, (error: Error | null, result: any) => { + if (error) reject(error); + else resolve(result); + }); + }); + }; + }), +})); +``` + +## 编写有效的测试用例 + +### 测试分类 + +将测试用例分为不同类别,每个类别测试一个特定场景: + +```typescript +// 成功场景 +it('应该成功完成操作', async () => {}); + +// 边界条件 +it('应该处理边界情况', async () => {}); + +// 错误处理 +it('应该优雅地处理错误', async () => {}); +``` + +### 设置测试数据 + +```typescript +// 模拟返回值 +mockExecImpl.mockImplementation((cmd: string, callback: any) => { + if (cmd === '命令') { + callback(null, { stdout: '成功输出' }); + } else { + callback(new Error('错误信息'), null); + } +}); +``` + +### 断言 + +使用 Vitest 的断言函数验证结果: + +```typescript +// 检查基本值 +expect(result.success).toBe(true); + +// 检查对象部分匹配 +expect(result.data).toMatchObject({ + key: 'value', +}); + +// 检查数组 +expect(result.items).toHaveLength(2); +expect(result.items[0].name).toBe('expectedName'); + +// 检查函数调用 +expect(mockFunction).toHaveBeenCalledWith(expectedArgs); +expect(mockFunction).toHaveBeenCalledTimes(1); +``` + +## 最佳实践 + +1. **隔离测试**:确保每个测试互不影响,使用 `beforeEach` 重置模拟和状态 +2. **全面覆盖**:测试正常流程、边界条件和错误处理 +3. **清晰命名**:测试名称应清晰描述测试内容和预期结果 +4. **避免测试实现细节**:测试应该关注行为而非实现细节,使代码重构不会破坏测试 +5. **模拟外部依赖**:使用 `vi.mock()` 模拟所有外部依赖,减少测试的不确定性 + +## 示例:测试 IPC 事件处理方法 + +```typescript +it('应该正确处理 IPC 事件', async () => { + // 模拟依赖 + mockSomething.mockReturnValue({ result: 'success' }); + + // 调用 IPC 方法 + const result = await controller.ipcMethodName({ + param1: 'value1', + param2: 'value2', + }); + + // 验证结果 + expect(result).toEqual({ + success: true, + data: { result: 'success' }, + }); + + // 验证依赖调用 + expect(mockSomething).toHaveBeenCalledWith('value1', 'value2'); +}); +``` diff --git a/.cursor/rules/desktop-feature-implementation.mdc b/.cursor/rules/desktop-feature-implementation.mdc index 7ab82d9114..834bd30b63 100644 --- a/.cursor/rules/desktop-feature-implementation.mdc +++ b/.cursor/rules/desktop-feature-implementation.mdc @@ -1,5 +1,5 @@ --- -description: +description: 当要做 electron 相关工作时 globs: alwaysApply: false --- diff --git a/src/config/modelProviders/index.ts b/src/config/modelProviders/index.ts index 3e3f837256..7c7831c4d9 100644 --- a/src/config/modelProviders/index.ts +++ b/src/config/modelProviders/index.ts @@ -121,7 +121,6 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [ GoogleProvider, VertexAIProvider, DeepSeekProvider, - PPIOProvider, HuggingFaceProvider, OpenRouterProvider, CloudflareProvider, diff --git a/src/features/InitClientDB/features/DatabaseRepair/Backup.tsx b/src/features/InitClientDB/features/DatabaseRepair/Backup.tsx index 4af36828cb..856ecec751 100644 --- a/src/features/InitClientDB/features/DatabaseRepair/Backup.tsx +++ b/src/features/InitClientDB/features/DatabaseRepair/Backup.tsx @@ -1,5 +1,5 @@ -import { Alert, Button } from '@lobehub/ui'; -import { App, Card, Typography } from 'antd'; +import { Alert, Button, Text } from '@lobehub/ui'; +import { App, Card } from 'antd'; import { createStyles } from 'antd-style'; import { AlertCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -7,8 +7,6 @@ import { Flexbox } from 'react-layout-kit'; import { resetClientDatabase } from '@/database/client/db'; -const { Text, Paragraph } = Typography; - const useStyles = createStyles(({ css, token }) => ({ card: css` border-radius: ${token.borderRadiusLG}px; @@ -27,7 +25,7 @@ const Backup = () => { extra={{t('clientDB.solve.backup.desc')}} title={t('clientDB.solve.backup.title')} > - {t('clientDB.solve.backup.exportDesc')} + {t('clientDB.solve.backup.exportDesc')} diff --git a/src/features/InitClientDB/features/DatabaseRepair/Repair.tsx b/src/features/InitClientDB/features/DatabaseRepair/Repair.tsx index 76a76e209d..41e012ecb1 100644 --- a/src/features/InitClientDB/features/DatabaseRepair/Repair.tsx +++ b/src/features/InitClientDB/features/DatabaseRepair/Repair.tsx @@ -1,7 +1,7 @@ 'use client'; -import { Alert, Button, CodeEditor, Icon } from '@lobehub/ui'; -import { Card, List, Typography } from 'antd'; +import { Alert, Button, CodeEditor, Icon, Text } from '@lobehub/ui'; +import { Card, List } from 'antd'; import { createStyles } from 'antd-style'; import dayjs from 'dayjs'; import isEqual from 'fast-deep-equal'; @@ -14,8 +14,6 @@ import { clientDB, updateMigrationRecord } from '@/database/client/db'; import { useGlobalStore } from '@/store/global'; import { clientDBSelectors } from '@/store/global/selectors'; -const { Text } = Typography; - // 使用 antd-style 创建样式 const useStyles = createStyles(({ css, token }) => ({ actionBar: css` diff --git a/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/index.tsx b/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/index.tsx index 82acde2563..d9c1c1f03a 100644 --- a/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/index.tsx +++ b/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/index.tsx @@ -1,4 +1,4 @@ -import { Typography } from 'antd'; +import { Text } from '@lobehub/ui'; import { createStyles } from 'antd-style'; import { memo } from 'react'; import { Flexbox } from 'react-layout-kit'; @@ -8,8 +8,6 @@ import { KnowledgeItem } from '@/types/knowledgeBase'; import Actions from './Action'; -const { Paragraph } = Typography; - const useStyles = createStyles(({ css, token }) => ({ desc: css` margin: 0 !important; @@ -51,14 +49,14 @@ const PluginItem = memo(({ id, fileType, name, type, description, - + {name} - + {description && ( - + {description} - + )} diff --git a/src/features/Portal/Artifacts/Header.tsx b/src/features/Portal/Artifacts/Header.tsx index f19cbfbd59..f254388210 100644 --- a/src/features/Portal/Artifacts/Header.tsx +++ b/src/features/Portal/Artifacts/Header.tsx @@ -1,5 +1,5 @@ -import { ActionIcon, Icon, Segmented } from '@lobehub/ui'; -import { ConfigProvider, Typography } from 'antd'; +import { ActionIcon, Icon, Segmented, Text } from '@lobehub/ui'; +import { ConfigProvider } from 'antd'; import { cx } from 'antd-style'; import { ArrowLeft, CodeIcon, EyeIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -34,9 +34,9 @@ const Header = () => { closeArtifact()} size={'small'} /> - + {artifactTitle} - + { {isLoading ? ( ) : ( - + {data?.name} - + )} ); diff --git a/src/features/Portal/Home/Body/Files/FileList/Item.tsx b/src/features/Portal/Home/Body/Files/FileList/Item.tsx index 664b4e2595..8c70d354c7 100644 --- a/src/features/Portal/Home/Body/Files/FileList/Item.tsx +++ b/src/features/Portal/Home/Body/Files/FileList/Item.tsx @@ -1,4 +1,4 @@ -import { Typography } from 'antd'; +import { Text } from '@lobehub/ui'; import { createStyles } from 'antd-style'; import { memo } from 'react'; import { Flexbox } from 'react-layout-kit'; @@ -43,8 +43,8 @@ const FileItem = memo(({ name, fileType, size, id }) => { > - {name} - {formatSize(size)} + {name} + {formatSize(size)} ); diff --git a/src/features/Portal/Home/Body/Files/FileList/index.tsx b/src/features/Portal/Home/Body/Files/FileList/index.tsx index 3a87b9145d..e4ea38af57 100644 --- a/src/features/Portal/Home/Body/Files/FileList/index.tsx +++ b/src/features/Portal/Home/Body/Files/FileList/index.tsx @@ -1,5 +1,4 @@ -import { Avatar, Icon } from '@lobehub/ui'; -import { Typography } from 'antd'; +import { Avatar, Icon, Text } from '@lobehub/ui'; import { useTheme } from 'antd-style'; import isEqual from 'fast-deep-equal'; import { InboxIcon } from 'lucide-react'; @@ -35,7 +34,7 @@ const FileList = () => { size={48} /> - {t('emptyKnowledgeList')} + {t('emptyKnowledgeList')} ) : ( diff --git a/src/features/Portal/Home/Body/Files/index.tsx b/src/features/Portal/Home/Body/Files/index.tsx index 233932874f..6c15fcaba2 100644 --- a/src/features/Portal/Home/Body/Files/index.tsx +++ b/src/features/Portal/Home/Body/Files/index.tsx @@ -1,4 +1,4 @@ -import { Typography } from 'antd'; +import { Text } from '@lobehub/ui'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; @@ -10,9 +10,9 @@ export const Files = memo(() => { return ( - + {t('files')} - + ); diff --git a/src/features/Portal/Home/Body/Plugins/ArtifactList/Item/index.tsx b/src/features/Portal/Home/Body/Plugins/ArtifactList/Item/index.tsx index afe0e86cda..e18a712ea1 100644 --- a/src/features/Portal/Home/Body/Plugins/ArtifactList/Item/index.tsx +++ b/src/features/Portal/Home/Body/Plugins/ArtifactList/Item/index.tsx @@ -1,5 +1,4 @@ -import { Icon, Tag } from '@lobehub/ui'; -import { Typography } from 'antd'; +import { Icon, Tag, Text } from '@lobehub/ui'; import isEqual from 'fast-deep-equal'; import { CircuitBoard } from 'lucide-react'; import { memo } from 'react'; @@ -53,9 +52,9 @@ const ArtifactItem = memo(({ payload, messageId, identifier = {payload?.apiName}
- + {args} - +
diff --git a/src/features/Portal/Home/Body/Plugins/ArtifactList/index.tsx b/src/features/Portal/Home/Body/Plugins/ArtifactList/index.tsx index b7a22c34a8..30a30e1c88 100644 --- a/src/features/Portal/Home/Body/Plugins/ArtifactList/index.tsx +++ b/src/features/Portal/Home/Body/Plugins/ArtifactList/index.tsx @@ -1,5 +1,5 @@ -import { Avatar, Icon } from '@lobehub/ui'; -import { Skeleton, Typography } from 'antd'; +import { Avatar, Icon, Text } from '@lobehub/ui'; +import { Skeleton } from 'antd'; import { useTheme } from 'antd-style'; import isEqual from 'fast-deep-equal'; import { Origami } from 'lucide-react'; @@ -41,7 +41,7 @@ const ArtifactList = () => { size={48} /> - {t('emptyArtifactList')} + {t('emptyArtifactList')} ) : ( diff --git a/src/features/Portal/Home/Body/Plugins/index.tsx b/src/features/Portal/Home/Body/Plugins/index.tsx index 77b773c876..d506dda8c3 100644 --- a/src/features/Portal/Home/Body/Plugins/index.tsx +++ b/src/features/Portal/Home/Body/Plugins/index.tsx @@ -1,4 +1,4 @@ -import { Typography } from 'antd'; +import { Text } from '@lobehub/ui'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; @@ -10,9 +10,9 @@ export const Artifacts = memo(() => { return ( - + {t('Plugins')} - + ); diff --git a/src/features/Portal/Home/Title.tsx b/src/features/Portal/Home/Title.tsx index 7a80da5ce4..7f423255a0 100644 --- a/src/features/Portal/Home/Title.tsx +++ b/src/features/Portal/Home/Title.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Typography } from 'antd'; +import { Text } from '@lobehub/ui'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,9 +8,9 @@ const Title = memo(() => { const { t } = useTranslation('portal'); return ( - + {t('title')} - + ); }); diff --git a/src/features/Portal/MessageDetail/Header.tsx b/src/features/Portal/MessageDetail/Header.tsx index 5f2f6c06c9..45509b20c7 100644 --- a/src/features/Portal/MessageDetail/Header.tsx +++ b/src/features/Portal/MessageDetail/Header.tsx @@ -1,4 +1,4 @@ -import { Typography } from 'antd'; +import { Text } from '@lobehub/ui'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; @@ -9,9 +9,9 @@ const Header = () => { return ( - + {t('messageDetail')} - + ); }; diff --git a/src/features/Portal/Plugins/Header.tsx b/src/features/Portal/Plugins/Header.tsx index f9a667e2f0..2a72fb07c9 100644 --- a/src/features/Portal/Plugins/Header.tsx +++ b/src/features/Portal/Plugins/Header.tsx @@ -1,5 +1,4 @@ -import { ActionIcon, Icon } from '@lobehub/ui'; -import { Typography } from 'antd'; +import { ActionIcon, Icon, Text } from '@lobehub/ui'; import isEqual from 'fast-deep-equal'; import { ArrowLeft, Globe } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -27,9 +26,9 @@ const Header = () => { closeToolUI()} size={'small'} /> - + {t('search.title')} - + ); } @@ -37,9 +36,9 @@ const Header = () => { closeToolUI()} size={'small'} /> - + {pluginTitle} - + ); }; diff --git a/src/features/Portal/Thread/Header/Active.tsx b/src/features/Portal/Thread/Header/Active.tsx index 5022a39181..081b8d88bb 100644 --- a/src/features/Portal/Thread/Header/Active.tsx +++ b/src/features/Portal/Thread/Header/Active.tsx @@ -1,5 +1,4 @@ -import { Icon } from '@lobehub/ui'; -import { Typography } from 'antd'; +import { Icon, Text } from '@lobehub/ui'; import { useTheme } from 'antd-style'; import isEqual from 'fast-deep-equal'; import { ListTree } from 'lucide-react'; @@ -20,7 +19,7 @@ const Active = memo(() => { currentThread && ( - { ) : ( currentThread?.title )} - + ) ); diff --git a/src/features/Portal/Thread/Header/New.tsx b/src/features/Portal/Thread/Header/New.tsx index 8299e3e1f4..3eaedfb773 100644 --- a/src/features/Portal/Thread/Header/New.tsx +++ b/src/features/Portal/Thread/Header/New.tsx @@ -1,5 +1,5 @@ -import { Icon } from '@lobehub/ui'; -import { Switch, Typography } from 'antd'; +import { Icon, Text } from '@lobehub/ui'; +import { Switch } from 'antd'; import { GitBranch } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; @@ -17,9 +17,9 @@ const NewThreadHeader = () => { return ( - + {t('newPortalThread.title')} - + ({ text: css` max-width: 100%; @@ -67,9 +65,7 @@ const EnableSync = memo(({ hiddenActions, placement = 'bottomLe style={{ paddingInlineEnd: 12 }} > {t('sync.channel')} - - {channelName} - + {channelName} @@ -99,9 +95,9 @@ const EnableSync = memo(({ hiddenActions, placement = 'bottomLe )} - + {user.os} · {user.browser} - + ))}