@lobehub/chat 1.81.9 → 1.82.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.cursor/rules/desktop-local-tools-implement.mdc +80 -0
  2. package/.env.desktop +2 -1
  3. package/.github/scripts/pr-comment.js +4 -9
  4. package/CHANGELOG.md +50 -0
  5. package/changelog/v1.json +18 -0
  6. package/locales/ar/electron.json +38 -2
  7. package/locales/ar/plugin.json +33 -2
  8. package/locales/bg-BG/electron.json +38 -2
  9. package/locales/bg-BG/plugin.json +33 -2
  10. package/locales/de-DE/electron.json +38 -2
  11. package/locales/de-DE/plugin.json +28 -2
  12. package/locales/en-US/electron.json +38 -2
  13. package/locales/en-US/plugin.json +28 -2
  14. package/locales/es-ES/electron.json +38 -2
  15. package/locales/es-ES/plugin.json +33 -2
  16. package/locales/fa-IR/electron.json +38 -2
  17. package/locales/fa-IR/plugin.json +33 -2
  18. package/locales/fr-FR/electron.json +38 -2
  19. package/locales/fr-FR/plugin.json +33 -2
  20. package/locales/it-IT/electron.json +38 -2
  21. package/locales/it-IT/plugin.json +33 -2
  22. package/locales/ja-JP/electron.json +38 -2
  23. package/locales/ja-JP/plugin.json +33 -2
  24. package/locales/ko-KR/electron.json +38 -2
  25. package/locales/ko-KR/plugin.json +28 -2
  26. package/locales/nl-NL/electron.json +38 -2
  27. package/locales/nl-NL/plugin.json +33 -2
  28. package/locales/pl-PL/electron.json +38 -2
  29. package/locales/pl-PL/plugin.json +28 -2
  30. package/locales/pt-BR/electron.json +38 -2
  31. package/locales/pt-BR/plugin.json +33 -2
  32. package/locales/ru-RU/electron.json +38 -2
  33. package/locales/ru-RU/plugin.json +33 -2
  34. package/locales/tr-TR/electron.json +38 -2
  35. package/locales/tr-TR/plugin.json +33 -2
  36. package/locales/vi-VN/electron.json +38 -2
  37. package/locales/vi-VN/plugin.json +28 -2
  38. package/locales/zh-CN/electron.json +38 -2
  39. package/locales/zh-CN/plugin.json +38 -2
  40. package/locales/zh-TW/electron.json +38 -2
  41. package/locales/zh-TW/plugin.json +33 -2
  42. package/package.json +1 -1
  43. package/packages/electron-client-ipc/src/events/update.ts +3 -3
  44. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx +222 -0
  45. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Option.tsx +104 -0
  46. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx +42 -0
  47. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Waiting.tsx +203 -0
  48. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/index.tsx +57 -0
  49. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateModal.tsx +242 -0
  50. package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateNotification.tsx +193 -0
  51. package/src/app/[variants]/(main)/_layout/Desktop/{Titlebar.tsx → ElectronTitlebar/index.tsx} +15 -1
  52. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/BottomActions.tsx +3 -2
  53. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
  54. package/src/app/[variants]/layout.tsx +2 -1
  55. package/src/components/ManifestPreviewer/index.tsx +4 -1
  56. package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
  57. package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
  58. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
  59. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/LocalFile.tsx +65 -0
  60. package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +29 -0
  61. package/src/features/Conversation/components/MarkdownElements/LocalFile/index.ts +16 -0
  62. package/src/features/Conversation/components/MarkdownElements/index.ts +7 -1
  63. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +260 -0
  64. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +204 -0
  65. package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +133 -0
  66. package/src/features/Conversation/components/MarkdownElements/type.ts +5 -1
  67. package/src/features/PluginAvatar/index.tsx +2 -1
  68. package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +20 -0
  69. package/src/features/PluginDevModal/MCPManifestForm/MCPTypeSelect.tsx +176 -0
  70. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +226 -0
  71. package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
  72. package/src/features/PluginDevModal/index.tsx +43 -34
  73. package/src/features/PluginStore/AddPluginButton.tsx +3 -1
  74. package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
  75. package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
  76. package/src/features/PluginStore/PluginItem/index.tsx +4 -3
  77. package/src/features/PluginTag/index.tsx +8 -2
  78. package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap +0 -56
  79. package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
  80. package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
  81. package/src/libs/mcp/index.ts +2 -0
  82. package/src/libs/mcp/types.ts +27 -0
  83. package/src/locales/default/electron.ts +38 -2
  84. package/src/locales/default/plugin.ts +41 -3
  85. package/src/server/modules/ElectronIPCClient/index.ts +36 -0
  86. package/src/server/routers/lambda/session.ts +2 -6
  87. package/src/server/routers/tools/index.ts +2 -0
  88. package/src/server/routers/tools/mcp.ts +85 -0
  89. package/src/server/services/file/impls/index.ts +9 -1
  90. package/src/server/services/file/impls/local.test.ts +299 -0
  91. package/src/server/services/file/impls/local.ts +183 -0
  92. package/src/server/services/mcp/index.ts +176 -0
  93. package/src/services/aiModel/index.ts +5 -1
  94. package/src/services/aiProvider/index.ts +5 -1
  95. package/src/services/electron/autoUpdate.ts +4 -0
  96. package/src/services/file/index.ts +5 -1
  97. package/src/services/mcp.ts +36 -0
  98. package/src/services/message/index.ts +5 -1
  99. package/src/services/plugin/index.ts +5 -1
  100. package/src/services/session/index.ts +5 -1
  101. package/src/services/tableViewer/desktop.ts +15 -0
  102. package/src/services/tableViewer/index.ts +4 -1
  103. package/src/services/thread/index.ts +5 -1
  104. package/src/services/topic/index.ts +5 -1
  105. package/src/services/user/index.ts +5 -1
  106. package/src/store/chat/slices/plugin/action.ts +46 -2
  107. package/src/store/electron/actions/app.ts +59 -0
  108. package/src/store/electron/actions/sync.ts +5 -1
  109. package/src/store/electron/initialState.ts +3 -1
  110. package/src/store/electron/store.ts +6 -1
  111. package/src/store/tool/slices/customPlugin/action.ts +16 -4
  112. package/src/types/tool/plugin.ts +9 -0
  113. package/src/utils/client/GlobalAgentContextManager.ts +85 -0
  114. package/src/utils/promptTemplate.test.ts +78 -0
  115. package/src/utils/promptTemplate.ts +17 -0
@@ -0,0 +1,2 @@
1
+ export * from './client';
2
+ export * from './types';
@@ -0,0 +1,27 @@
1
+ interface InputSchema {
2
+ [k: string]: unknown;
3
+
4
+ properties?: unknown | null;
5
+ type: 'object';
6
+ }
7
+
8
+ export interface McpTool {
9
+ description: string;
10
+ inputSchema: InputSchema;
11
+ name: string;
12
+ }
13
+
14
+ interface HttpMCPClientParams {
15
+ name: string;
16
+ type: 'http';
17
+ url: string;
18
+ }
19
+
20
+ interface StdioMCPParams {
21
+ args: string[];
22
+ command: string;
23
+ name: string;
24
+ type: 'stdio';
25
+ }
26
+
27
+ export type MCPClientParams = HttpMCPClientParams | StdioMCPParams;
@@ -17,18 +17,54 @@ const electron = {
17
17
  statusDisconnected: '未连接',
18
18
  urlRequired: '请输入服务器地址',
19
19
  },
20
+ sync: {
21
+ continue: '继续',
22
+ inCloud: '当前使用云端同步',
23
+ inLocalStorage: '当前使用本地存储',
24
+ isIniting: '正在初始化...',
25
+ lobehubCloud: {
26
+ description: '官方提供的云版本',
27
+ title: 'LobeHub Cloud',
28
+ },
29
+ local: {
30
+ description: '使用本地数据库,完全离线可用',
31
+ title: '本地数据库',
32
+ },
33
+ mode: {
34
+ cloudSync: '云端同步',
35
+ localStorage: '本地存储',
36
+ title: '选择你的连接模式',
37
+ useSelfHosted: '使用自托管实例?',
38
+ },
39
+ selfHosted: {
40
+ description: '自行部署的社区版本',
41
+ title: '自托管实例',
42
+ },
43
+ },
20
44
  updater: {
45
+ checkingUpdate: '检查新版本',
46
+ checkingUpdateDesc: '正在获取版本信息...',
47
+ downloadNewVersion: '下载新版本',
21
48
  downloadingUpdate: '正在下载更新',
22
49
  downloadingUpdateDesc: '更新正在下载中,请稍候...',
50
+ installLater: '下次启动时更新',
51
+ isLatestVersion: '当前已是最新版本',
52
+ isLatestVersionDesc: '非常棒,使用的版本 {{version}} 已是最前沿的版本。',
23
53
  later: '稍后更新',
24
54
  newVersionAvailable: '新版本可用',
25
55
  newVersionAvailableDesc: '发现新版本 {{version}},是否立即下载?',
26
- restartAndInstall: '重启并安装',
56
+ restartAndInstall: '安装更新并重启',
27
57
  updateError: '更新错误',
28
58
  updateReady: '更新已就绪',
29
- updateReadyDesc: 'Lobe Chat {{version}} 已下载完成,重启应用后即可完成安装。',
59
+ updateReadyDesc: '新版本 {{version}} 已下载完成,重启应用后即可完成安装。',
30
60
  upgradeNow: '立即更新',
31
61
  },
62
+ waitingOAuth: {
63
+ cancel: '取消',
64
+ description: '浏览器已打开授权页面,请在浏览器中完成授权',
65
+ helpText: '如果浏览器没有自动打开,请点击取消后重新尝试',
66
+ title: '等待授权连接',
67
+ },
32
68
  };
33
69
 
34
70
  export default electron;
@@ -35,9 +35,9 @@ export default {
35
35
  label: '标识符',
36
36
  },
37
37
  mode: {
38
- 'local': '可视化配置',
39
- 'local-tooltip': '暂时不支持可视化配置',
40
- 'url': '在线链接',
38
+ mcp: 'MCP 插件',
39
+ mcpExp: '实验性',
40
+ url: '在线链接',
41
41
  },
42
42
  name: {
43
43
  desc: '插件标题',
@@ -45,6 +45,44 @@ export default {
45
45
  placeholder: '搜索引擎',
46
46
  },
47
47
  },
48
+ mcp: {
49
+ args: {
50
+ desc: '传递给 STDIO 命令的参数列表,一般在这里输入 MCP 服务器名称',
51
+ label: '命令参数',
52
+ placeholder: '例如:mcp-hello-world',
53
+ },
54
+ command: {
55
+ desc: '用于启动 MCP STDIO Server 的可执行文件或脚本',
56
+ label: '命令',
57
+ placeholder: '例如:npx / uv / docker 等',
58
+ },
59
+ endpoint: {
60
+ desc: '输入你的 MCP Streamable HTTP Server 的地址',
61
+ label: 'MCP Endpoint URL',
62
+ },
63
+ identifier: {
64
+ desc: '为你的 MCP 插件指定一个名称,需要使用英文字符',
65
+ invalid: '只能输入英文字符、数字 、- 和_ 这两个符号',
66
+ label: 'MCP 插件名称',
67
+ placeholder: '例如:my-mcp-plugin',
68
+ },
69
+ type: {
70
+ desc: '选择 MCP 插件的通信方式,网页版只支持 Streamable HTTP',
71
+ httpFeature1: '兼容网页版与桌面端',
72
+ httpFeature2: '连接远程 MCP 服务端, 无需额外安装配置',
73
+ httpShortDesc: '基于流式 HTTP 的通信协议',
74
+ label: 'MCP 插件类型',
75
+ stdioFeature1: '更低的通信延迟, 适合本地执行',
76
+ stdioFeature2: '通过系统标准输入输出通信,需在本地安装运行 MCP 服务端',
77
+ stdioNotAvailable: 'STDIO 模式仅在桌面版可用',
78
+ stdioShortDesc: '基于标准输入输出的通信协议',
79
+ title: 'MCP 插件类型',
80
+ },
81
+ url: {
82
+ desc: '输入你的 MCP Server Streamable HTTP 地址,不会以 /sse 结尾',
83
+ label: 'Streamable HTTP Endpoint URL',
84
+ },
85
+ },
48
86
  meta: {
49
87
  author: {
50
88
  desc: '插件的作者',
@@ -0,0 +1,36 @@
1
+ import { ElectronIpcClient } from '@lobechat/electron-server-ipc';
2
+
3
+ class LobeHubElectronIpcClient extends ElectronIpcClient {
4
+ // 获取数据库路径
5
+ getDatabasePath = async (): Promise<string> => {
6
+ return this.sendRequest<string>('getDatabasePath');
7
+ };
8
+
9
+ // 获取用户数据路径
10
+ getUserDataPath = async (): Promise<string> => {
11
+ return this.sendRequest<string>('getUserDataPath');
12
+ };
13
+
14
+ getDatabaseSchemaHash = async () => {
15
+ return this.sendRequest<string>('setDatabaseSchemaHash');
16
+ };
17
+
18
+ setDatabaseSchemaHash = async (hash: string | undefined) => {
19
+ if (!hash) return;
20
+
21
+ return this.sendRequest('setDatabaseSchemaHash', hash);
22
+ };
23
+
24
+ getFilePathById = async (id: string) => {
25
+ return this.sendRequest<string>('getStaticFilePath', id);
26
+ };
27
+
28
+ deleteFiles = async (paths: string[]) => {
29
+ return this.sendRequest<{ errors?: { message: string; path: string }[]; success: boolean }>(
30
+ 'deleteFiles',
31
+ paths,
32
+ );
33
+ };
34
+ }
35
+
36
+ export const electronIpcClient = new LobeHubElectronIpcClient();
@@ -97,14 +97,10 @@ export const sessionRouter = router({
97
97
  }),
98
98
 
99
99
  getGroupedSessions: publicProcedure.query(async ({ ctx }): Promise<ChatSessionList> => {
100
- if (!ctx.userId)
101
- return {
102
- sessionGroups: [],
103
- sessions: [],
104
- };
100
+ if (!ctx.userId) return { sessionGroups: [], sessions: [] };
105
101
 
106
102
  const serverDB = await getServerDB();
107
- const sessionModel = new SessionModel(serverDB, ctx.userId);
103
+ const sessionModel = new SessionModel(serverDB, ctx.userId!);
108
104
 
109
105
  return sessionModel.queryWithGroups();
110
106
  }),
@@ -1,9 +1,11 @@
1
1
  import { publicProcedure, router } from '@/libs/trpc/lambda';
2
2
 
3
+ import { mcpRouter } from './mcp';
3
4
  import { searchRouter } from './search';
4
5
 
5
6
  export const toolsRouter = router({
6
7
  healthcheck: publicProcedure.query(() => "i'm live!"),
8
+ mcp: mcpRouter,
7
9
  search: searchRouter,
8
10
  });
9
11
 
@@ -0,0 +1,85 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { z } from 'zod';
3
+
4
+ import { isDesktop, isServerMode } from '@/const/version';
5
+ import { passwordProcedure } from '@/libs/trpc/edge';
6
+ import { authedProcedure, router } from '@/libs/trpc/lambda';
7
+ import { mcpService } from '@/server/services/mcp';
8
+
9
+ // Define Zod schemas for MCP Client parameters
10
+ const httpParamsSchema = z.object({
11
+ name: z.string().min(1),
12
+ type: z.literal('http'),
13
+ url: z.string().url(),
14
+ });
15
+
16
+ const stdioParamsSchema = z.object({
17
+ args: z.array(z.string()).optional().default([]),
18
+ command: z.string().min(1),
19
+ name: z.string().min(1),
20
+ type: z.literal('stdio'),
21
+ });
22
+
23
+ // Union schema for MCPClientParams
24
+ const mcpClientParamsSchema = z.union([httpParamsSchema, stdioParamsSchema]);
25
+
26
+ const checkStdioEnvironment = (params: z.infer<typeof mcpClientParamsSchema>) => {
27
+ if (params.type === 'stdio' && !isDesktop) {
28
+ throw new TRPCError({
29
+ code: 'BAD_REQUEST',
30
+ message: 'Stdio MCP type is not supported in web environment.',
31
+ });
32
+ }
33
+ };
34
+
35
+ const mcpProcedure = isServerMode ? authedProcedure : passwordProcedure;
36
+
37
+ export const mcpRouter = router({
38
+ getStdioMcpServerManifest: mcpProcedure.input(stdioParamsSchema).query(async ({ input }) => {
39
+ // Stdio check can be done here or rely on the service/client layer
40
+ checkStdioEnvironment(input);
41
+
42
+ return await mcpService.getStdioMcpServerManifest(input.name, input.command, input.args);
43
+ }),
44
+ getStreamableMcpServerManifest: mcpProcedure
45
+ .input(
46
+ z.object({
47
+ identifier: z.string(),
48
+ url: z.string().url(),
49
+ }),
50
+ )
51
+ .query(async ({ input }) => {
52
+ return await mcpService.getStreamableMcpServerManifest(input.identifier, input.url);
53
+ }),
54
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
55
+ // --- MCP Interaction ---
56
+ // listTools now accepts MCPClientParams directly
57
+ listTools: mcpProcedure
58
+ .input(mcpClientParamsSchema) // Use the unified schema
59
+ .query(async ({ input }) => {
60
+ // Stdio check can be done here or rely on the service/client layer
61
+ checkStdioEnvironment(input);
62
+
63
+ // Pass the validated MCPClientParams to the service
64
+ return await mcpService.listTools(input);
65
+ }),
66
+
67
+ // callTool now accepts MCPClientParams, toolName, and args
68
+ callTool: mcpProcedure
69
+ .input(
70
+ z.object({
71
+ params: mcpClientParamsSchema, // Use the unified schema for client params
72
+ args: z.any(), // Arguments for the tool call
73
+ toolName: z.string(),
74
+ }),
75
+ )
76
+ .mutation(async ({ input }) => {
77
+ // Stdio check can be done here or rely on the service/client layer
78
+ checkStdioEnvironment(input.params);
79
+
80
+ // Pass the validated params, toolName, and args to the service
81
+ const data = await mcpService.callTool(input.params, input.toolName, input.args);
82
+
83
+ return JSON.stringify(data);
84
+ }),
85
+ });
@@ -1,11 +1,19 @@
1
+ import { isDesktop } from '@/const/version';
2
+
3
+ import { DesktopLocalFileImpl } from './local';
1
4
  import { S3StaticFileImpl } from './s3';
2
5
  import { FileServiceImpl } from './type';
3
6
 
4
7
  /**
5
8
  * 创建文件服务模块
9
+ * 根据环境自动选择使用S3或桌面本地文件实现
6
10
  */
7
11
  export const createFileServiceModule = (): FileServiceImpl => {
8
- // 默认使用 S3 实现
12
+ // 如果在桌面应用环境,使用本地文件实现
13
+ if (isDesktop) {
14
+ return new DesktopLocalFileImpl();
15
+ }
16
+
9
17
  return new S3StaticFileImpl();
10
18
  };
11
19
 
@@ -0,0 +1,299 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
6
+
7
+ import { DesktopLocalFileImpl } from './local';
8
+
9
+ // 模拟依赖项
10
+ vi.mock('node:fs', async (importOriginal) => ({
11
+ ...((await importOriginal()) as any),
12
+ existsSync: vi.fn(),
13
+ readFileSync: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('@/server/modules/ElectronIPCClient', () => ({
17
+ electronIpcClient: {
18
+ getFilePathById: vi.fn(),
19
+ deleteFiles: vi.fn(),
20
+ },
21
+ }));
22
+
23
+ describe('DesktopLocalFileImpl', () => {
24
+ let service: DesktopLocalFileImpl;
25
+ const testFilePath = '/path/to/file.txt';
26
+ const testFileKey = 'desktop://file.txt';
27
+ const testFileContent = 'test file content';
28
+ const testFileBuffer = Buffer.from(testFileContent);
29
+
30
+ beforeEach(() => {
31
+ service = new DesktopLocalFileImpl();
32
+
33
+ // 重置所有模拟
34
+ vi.resetAllMocks();
35
+
36
+ // 设置默认模拟行为
37
+ vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue(testFilePath);
38
+ vi.mocked(existsSync).mockReturnValue(true);
39
+ vi.mocked(readFileSync).mockReturnValueOnce(testFileBuffer);
40
+ });
41
+
42
+ describe('getLocalFileUrl', () => {
43
+ it.skip('应该正确获取本地文件URL并转换为data URL', async () => {
44
+ // 准备: readFileSync在第一次被调用时返回文件内容
45
+ vi.mocked(readFileSync).mockReturnValueOnce(testFileBuffer);
46
+
47
+ // 使用私有方法进行测试,通过原型访问
48
+ const result = await (service as any).getLocalFileUrl(testFileKey);
49
+
50
+ // 验证
51
+ expect(electronIpcClient.getFilePathById).toHaveBeenCalledWith(testFileKey);
52
+ expect(existsSync).toHaveBeenCalledWith(testFilePath);
53
+ expect(readFileSync).toHaveBeenCalledWith(testFilePath);
54
+
55
+ // 验证返回的data URL格式正确
56
+ expect(result).toContain('data:text/plain;base64,');
57
+ expect(result).toContain(testFileBuffer.toString('base64'));
58
+ });
59
+
60
+ it('当文件不存在时应返回原始键', async () => {
61
+ // 准备: 文件不存在
62
+ vi.mocked(existsSync).mockReturnValueOnce(false);
63
+
64
+ // 使用私有方法进行测试
65
+ const result = await (service as any).getLocalFileUrl(testFileKey);
66
+
67
+ // 验证
68
+ expect(result).toBe(testFileKey);
69
+ });
70
+
71
+ it('当发生错误时应返回空字符串', async () => {
72
+ // 准备: 模拟错误
73
+ vi.mocked(electronIpcClient.getFilePathById).mockRejectedValueOnce(new Error('测试错误'));
74
+
75
+ // 使用私有方法进行测试
76
+ const result = await (service as any).getLocalFileUrl(testFileKey);
77
+
78
+ // 验证
79
+ expect(result).toBe('');
80
+ });
81
+ });
82
+
83
+ describe('getMimeTypeFromPath', () => {
84
+ it('应该返回正确的MIME类型', () => {
85
+ // 使用私有方法进行测试
86
+ const jpgResult = (service as any).getMimeTypeFromPath('test.jpg');
87
+ const pngResult = (service as any).getMimeTypeFromPath('test.png');
88
+ const unknownResult = (service as any).getMimeTypeFromPath('test.unknown');
89
+
90
+ // 验证
91
+ expect(jpgResult).toBe('image/jpeg');
92
+ expect(pngResult).toBe('image/png');
93
+ expect(unknownResult).toBe('application/octet-stream');
94
+ });
95
+ });
96
+
97
+ describe('createPreSignedUrl', () => {
98
+ it('应该返回原始键', async () => {
99
+ const result = await service.createPreSignedUrl(testFileKey);
100
+
101
+ expect(result).toBe(testFileKey);
102
+ });
103
+ });
104
+
105
+ describe('createPreSignedUrlForPreview', () => {
106
+ it('应该调用getLocalFileUrl获取预览URL', async () => {
107
+ // 准备
108
+ const getLocalFileUrlSpy = vi.spyOn(service as any, 'getLocalFileUrl');
109
+ getLocalFileUrlSpy.mockResolvedValueOnce('data:text/plain;base64,dGVzdA==');
110
+
111
+ // 执行
112
+ const result = await service.createPreSignedUrlForPreview(testFileKey);
113
+
114
+ // 验证
115
+ expect(getLocalFileUrlSpy).toHaveBeenCalledWith(testFileKey);
116
+ expect(result).toBe('data:text/plain;base64,dGVzdA==');
117
+ });
118
+ });
119
+
120
+ describe('deleteFile', () => {
121
+ it('应该调用deleteFiles方法删除单个文件', async () => {
122
+ // 准备
123
+ vi.mocked(electronIpcClient.deleteFiles).mockResolvedValueOnce({ success: true });
124
+ const deleteFilesSpy = vi.spyOn(service, 'deleteFiles');
125
+
126
+ // 执行
127
+ await service.deleteFile(testFileKey);
128
+
129
+ // 验证
130
+ expect(deleteFilesSpy).toHaveBeenCalledWith([testFileKey]);
131
+ });
132
+ });
133
+
134
+ describe('deleteFiles', () => {
135
+ it('应该成功删除有效的文件', async () => {
136
+ // 准备
137
+ const keys = ['desktop://file1.txt', 'desktop://file2.png'];
138
+ vi.mocked(electronIpcClient.deleteFiles).mockResolvedValueOnce({ success: true });
139
+
140
+ // 执行
141
+ const result = await service.deleteFiles(keys);
142
+
143
+ // 验证
144
+ expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith(keys);
145
+ expect(result).toEqual({ success: true });
146
+ });
147
+
148
+ it('当提供无效键时应返回错误', async () => {
149
+ // 准备: 包含无效的文件路径
150
+ const keys = ['invalid://file1.txt', 'desktop://file2.png'];
151
+
152
+ // 执行
153
+ const result = await service.deleteFiles(keys);
154
+
155
+ // 验证
156
+ expect(electronIpcClient.deleteFiles).not.toHaveBeenCalled();
157
+ expect(result.success).toBe(false);
158
+ expect(result.errors).toBeDefined();
159
+ expect(result.errors!.length).toBe(1);
160
+ expect(result.errors![0].path).toBe('invalid://file1.txt');
161
+ });
162
+
163
+ it('当未提供键时应返回成功', async () => {
164
+ // 执行
165
+ const result = await service.deleteFiles([]);
166
+
167
+ // 验证
168
+ expect(electronIpcClient.deleteFiles).not.toHaveBeenCalled();
169
+ expect(result).toEqual({ success: true });
170
+ });
171
+
172
+ it('当删除过程中出现错误时应正确处理', async () => {
173
+ // 准备
174
+ const keys = ['desktop://file1.txt'];
175
+ vi.mocked(electronIpcClient.deleteFiles).mockRejectedValueOnce(new Error('删除错误'));
176
+
177
+ // 执行
178
+ const result = await service.deleteFiles(keys);
179
+
180
+ // 验证
181
+ expect(result.success).toBe(false);
182
+ expect(result.errors).toBeDefined();
183
+ expect(result.errors![0].message).toContain('删除错误');
184
+ });
185
+ });
186
+
187
+ describe.skip('getFileByteArray', () => {
188
+ it('应该返回文件的字节数组', async () => {
189
+ // 准备
190
+ vi.mocked(readFileSync).mockReturnValueOnce(Buffer.from('测试内容'));
191
+
192
+ // 执行
193
+ const result = await service.getFileByteArray(testFileKey);
194
+
195
+ // 验证
196
+ expect(electronIpcClient.getFilePathById).toHaveBeenCalledWith(testFileKey);
197
+ expect(existsSync).toHaveBeenCalledWith(testFilePath);
198
+ expect(readFileSync).toHaveBeenCalledWith(testFilePath);
199
+ expect(result).toBeInstanceOf(Uint8Array);
200
+ expect(Buffer.from(result).toString()).toBe('测试内容');
201
+ });
202
+
203
+ it('当文件不存在时应返回空数组', async () => {
204
+ // 准备
205
+ vi.mocked(existsSync).mockReturnValueOnce(false);
206
+
207
+ // 执行
208
+ const result = await service.getFileByteArray(testFileKey);
209
+
210
+ // 验证
211
+ expect(result).toBeInstanceOf(Uint8Array);
212
+ expect(result.length).toBe(0);
213
+ });
214
+
215
+ it('当发生错误时应返回空数组', async () => {
216
+ // 准备
217
+ vi.mocked(electronIpcClient.getFilePathById).mockRejectedValueOnce(new Error('测试错误'));
218
+
219
+ // 执行
220
+ const result = await service.getFileByteArray(testFileKey);
221
+
222
+ // 验证
223
+ expect(result).toBeInstanceOf(Uint8Array);
224
+ expect(result.length).toBe(0);
225
+ });
226
+ });
227
+
228
+ describe.skip('getFileContent', () => {
229
+ it('应该返回文件内容', async () => {
230
+ // 准备
231
+ vi.mocked(readFileSync).mockReturnValueOnce('文件内容');
232
+
233
+ // 执行
234
+ const result = await service.getFileContent(testFileKey);
235
+
236
+ // 验证
237
+ expect(electronIpcClient.getFilePathById).toHaveBeenCalledWith(testFileKey);
238
+ expect(existsSync).toHaveBeenCalledWith(testFilePath);
239
+ expect(readFileSync).toHaveBeenCalledWith(testFilePath, 'utf8');
240
+ expect(result).toBe('文件内容');
241
+ });
242
+
243
+ it('当文件不存在时应返回空字符串', async () => {
244
+ // 准备
245
+ vi.mocked(existsSync).mockReturnValueOnce(false);
246
+
247
+ // 执行
248
+ const result = await service.getFileContent(testFileKey);
249
+
250
+ // 验证
251
+ expect(result).toBe('');
252
+ });
253
+
254
+ it('当发生错误时应返回空字符串', async () => {
255
+ // 准备
256
+ vi.mocked(electronIpcClient.getFilePathById).mockRejectedValueOnce(new Error('测试错误'));
257
+
258
+ // 执行
259
+ const result = await service.getFileContent(testFileKey);
260
+
261
+ // 验证
262
+ expect(result).toBe('');
263
+ });
264
+ });
265
+
266
+ describe('getFullFileUrl', () => {
267
+ it('应该调用getLocalFileUrl获取完整URL', async () => {
268
+ // 准备
269
+ const getLocalFileUrlSpy = vi.spyOn(service as any, 'getLocalFileUrl');
270
+ getLocalFileUrlSpy.mockResolvedValueOnce('data:image/png;base64,test');
271
+
272
+ // 执行
273
+ const result = await service.getFullFileUrl(testFileKey);
274
+
275
+ // 验证
276
+ expect(getLocalFileUrlSpy).toHaveBeenCalledWith(testFileKey);
277
+ expect(result).toBe('data:image/png;base64,test');
278
+ });
279
+
280
+ it('当url为空时应返回空字符串', async () => {
281
+ // 执行
282
+ const result = await service.getFullFileUrl(null);
283
+
284
+ // 验证
285
+ expect(result).toBe('');
286
+ });
287
+ });
288
+
289
+ describe('uploadContent', () => {
290
+ it('应该正确处理上传内容的请求', async () => {
291
+ // 目前这个方法未实现,仅验证调用不会导致错误
292
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
293
+
294
+ await service.uploadContent('path/to/file', 'content');
295
+
296
+ expect(consoleSpy).toHaveBeenCalled();
297
+ });
298
+ });
299
+ });