@lobehub/chat 1.81.8 → 1.82.0
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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/plugin.json +33 -2
- package/locales/bg-BG/plugin.json +33 -2
- package/locales/de-DE/plugin.json +33 -2
- package/locales/en-US/plugin.json +33 -2
- package/locales/es-ES/plugin.json +33 -2
- package/locales/fa-IR/plugin.json +33 -2
- package/locales/fr-FR/plugin.json +33 -2
- package/locales/it-IT/plugin.json +33 -2
- package/locales/ja-JP/plugin.json +33 -2
- package/locales/ko-KR/plugin.json +33 -2
- package/locales/nl-NL/plugin.json +33 -2
- package/locales/pl-PL/plugin.json +33 -2
- package/locales/pt-BR/plugin.json +33 -2
- package/locales/ru-RU/plugin.json +33 -2
- package/locales/tr-TR/plugin.json +33 -2
- package/locales/vi-VN/plugin.json +33 -2
- package/locales/zh-CN/plugin.json +33 -2
- package/locales/zh-TW/plugin.json +33 -2
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/localFile.ts +8 -2
- package/packages/electron-client-ipc/src/events/system.ts +3 -0
- package/packages/electron-client-ipc/src/types/index.ts +1 -0
- package/packages/electron-client-ipc/src/types/localFile.ts +46 -0
- package/packages/electron-client-ipc/src/types/system.ts +24 -0
- package/packages/file-loaders/src/blackList.ts +9 -0
- package/packages/file-loaders/src/index.ts +1 -0
- package/packages/file-loaders/src/loaders/pdf/index.test.ts +1 -0
- package/packages/file-loaders/src/loaders/pdf/index.ts +1 -7
- package/src/components/FileIcon/index.tsx +7 -3
- package/src/components/ManifestPreviewer/index.tsx +4 -1
- package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
- package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
- package/src/features/PluginAvatar/index.tsx +2 -1
- package/src/features/PluginDevModal/MCPManifestForm.tsx +164 -0
- package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
- package/src/features/PluginDevModal/index.tsx +43 -34
- package/src/features/PluginStore/AddPluginButton.tsx +3 -1
- package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
- package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
- package/src/features/PluginStore/PluginItem/index.tsx +4 -3
- package/src/features/PluginTag/index.tsx +8 -2
- package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
- package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
- package/src/libs/mcp/index.ts +2 -0
- package/src/libs/mcp/types.ts +27 -0
- package/src/locales/default/plugin.ts +34 -3
- package/src/server/routers/tools/index.ts +2 -0
- package/src/server/routers/tools/mcp.ts +79 -0
- package/src/server/services/mcp/index.ts +157 -0
- package/src/services/electron/localFileService.ts +19 -0
- package/src/services/electron/system.ts +21 -0
- package/src/services/mcp.ts +25 -0
- package/src/store/chat/slices/builtinTool/actions/search.ts +0 -3
- package/src/store/chat/slices/plugin/action.ts +46 -2
- package/src/tools/local-files/Render/ListFiles/index.tsx +24 -17
- package/src/tools/local-files/Render/ReadLocalFile/ReadFileView.tsx +28 -28
- package/src/tools/local-files/components/FileItem.tsx +9 -11
- package/src/tools/local-files/index.ts +60 -2
- package/src/tools/local-files/systemRole.ts +53 -13
- package/src/tools/local-files/type.ts +19 -1
- package/src/tools/web-browsing/systemRole.ts +40 -38
- package/src/types/tool/plugin.ts +9 -0
- /package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap +0 -0
@@ -0,0 +1,79 @@
|
|
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
|
+
getStreamableMcpServerManifest: mcpProcedure
|
39
|
+
.input(
|
40
|
+
z.object({
|
41
|
+
identifier: z.string(),
|
42
|
+
url: z.string().url(),
|
43
|
+
}),
|
44
|
+
)
|
45
|
+
.query(async ({ input }) => {
|
46
|
+
return await mcpService.getStreamableMcpServerManifest(input.identifier, input.url);
|
47
|
+
}),
|
48
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
49
|
+
// --- MCP Interaction ---
|
50
|
+
// listTools now accepts MCPClientParams directly
|
51
|
+
listTools: mcpProcedure
|
52
|
+
.input(mcpClientParamsSchema) // Use the unified schema
|
53
|
+
.query(async ({ input }) => {
|
54
|
+
// Stdio check can be done here or rely on the service/client layer
|
55
|
+
checkStdioEnvironment(input);
|
56
|
+
|
57
|
+
// Pass the validated MCPClientParams to the service
|
58
|
+
return await mcpService.listTools(input);
|
59
|
+
}),
|
60
|
+
|
61
|
+
// callTool now accepts MCPClientParams, toolName, and args
|
62
|
+
callTool: mcpProcedure
|
63
|
+
.input(
|
64
|
+
z.object({
|
65
|
+
params: mcpClientParamsSchema, // Use the unified schema for client params
|
66
|
+
args: z.any(), // Arguments for the tool call
|
67
|
+
toolName: z.string(),
|
68
|
+
}),
|
69
|
+
)
|
70
|
+
.mutation(async ({ input }) => {
|
71
|
+
// Stdio check can be done here or rely on the service/client layer
|
72
|
+
checkStdioEnvironment(input.params);
|
73
|
+
|
74
|
+
// Pass the validated params, toolName, and args to the service
|
75
|
+
const data = await mcpService.callTool(input.params, input.toolName, input.args);
|
76
|
+
|
77
|
+
return JSON.stringify(data);
|
78
|
+
}),
|
79
|
+
});
|
@@ -0,0 +1,157 @@
|
|
1
|
+
import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
|
2
|
+
import { TRPCError } from '@trpc/server';
|
3
|
+
import debug from 'debug';
|
4
|
+
|
5
|
+
import { MCPClient, MCPClientParams } from '@/libs/mcp';
|
6
|
+
import { safeParseJSON } from '@/utils/safeParseJSON';
|
7
|
+
|
8
|
+
const log = debug('lobe-mcp:service');
|
9
|
+
|
10
|
+
// Removed MCPConnection interface as it's no longer needed
|
11
|
+
|
12
|
+
class MCPService {
|
13
|
+
// Store instances of the custom MCPClient, keyed by serialized MCPClientParams
|
14
|
+
private clients: Map<string, MCPClient> = new Map();
|
15
|
+
|
16
|
+
constructor() {
|
17
|
+
log('MCPService initialized');
|
18
|
+
}
|
19
|
+
|
20
|
+
// --- MCP Interaction ---
|
21
|
+
|
22
|
+
// listTools now accepts MCPClientParams
|
23
|
+
async listTools(params: MCPClientParams): Promise<LobeChatPluginApi[]> {
|
24
|
+
const client = await this.getClient(params); // Get client using params
|
25
|
+
log(`Listing tools using client for params: %O`, params);
|
26
|
+
|
27
|
+
try {
|
28
|
+
const result = await client.listTools();
|
29
|
+
log(`Tools listed successfully for params: %O, result count: %d`, params, result.length);
|
30
|
+
return result.map<LobeChatPluginApi>((item) => ({
|
31
|
+
// Assuming identifier is the unique name/id
|
32
|
+
description: item.description,
|
33
|
+
name: item.name,
|
34
|
+
parameters: item.inputSchema as PluginSchema,
|
35
|
+
}));
|
36
|
+
} catch (error) {
|
37
|
+
console.error(`Error listing tools for params %O:`, params, error);
|
38
|
+
// Propagate a TRPCError for better handling upstream
|
39
|
+
throw new TRPCError({
|
40
|
+
cause: error,
|
41
|
+
code: 'INTERNAL_SERVER_ERROR',
|
42
|
+
message: `Error listing tools from MCP server: ${(error as Error).message}`,
|
43
|
+
});
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
// callTool now accepts MCPClientParams, toolName, and args
|
48
|
+
async callTool(params: MCPClientParams, toolName: string, argsStr: any): Promise<any> {
|
49
|
+
const client = await this.getClient(params); // Get client using params
|
50
|
+
|
51
|
+
const args = safeParseJSON(argsStr);
|
52
|
+
|
53
|
+
log(`Calling tool "${toolName}" using client for params: %O with args: %O`, params, args);
|
54
|
+
|
55
|
+
try {
|
56
|
+
// Delegate the call to the MCPClient instance
|
57
|
+
const result = await client.callTool(toolName, args); // Pass args directly
|
58
|
+
log(`Tool "${toolName}" called successfully for params: %O, result: %O`, params, result);
|
59
|
+
const { content, isError } = result;
|
60
|
+
if (!isError) return content;
|
61
|
+
|
62
|
+
return result;
|
63
|
+
} catch (error) {
|
64
|
+
console.error(`Error calling tool "${toolName}" for params %O:`, params, error);
|
65
|
+
// Propagate a TRPCError
|
66
|
+
throw new TRPCError({
|
67
|
+
cause: error,
|
68
|
+
code: 'INTERNAL_SERVER_ERROR',
|
69
|
+
message: `Error calling tool "${toolName}" on MCP server: ${(error as Error).message}`,
|
70
|
+
});
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
// TODO: Consider adding methods for managing the client lifecycle if needed,
|
75
|
+
// e.g., explicitly closing clients on shutdown or after inactivity,
|
76
|
+
// although for serverless, on-demand creation/retrieval might be sufficient.
|
77
|
+
|
78
|
+
// TODO: Implement methods like listResources, getResource, listPrompts, getPrompt if needed,
|
79
|
+
// following the pattern of accepting MCPClientParams.
|
80
|
+
|
81
|
+
// --- Client Management (Replaces Connection Management) ---
|
82
|
+
|
83
|
+
// Private method to get or initialize a client based on parameters
|
84
|
+
private async getClient(params: MCPClientParams): Promise<MCPClient> {
|
85
|
+
const key = this.serializeParams(params); // Use custom serialization
|
86
|
+
log(`Attempting to get client for key: ${key} (params: %O)`, params);
|
87
|
+
|
88
|
+
if (this.clients.has(key)) {
|
89
|
+
log(`Returning cached client for key: ${key}`);
|
90
|
+
return this.clients.get(key)!;
|
91
|
+
}
|
92
|
+
|
93
|
+
log(`No cached client found for key: ${key}. Initializing new client.`);
|
94
|
+
try {
|
95
|
+
// Ensure stdio is only attempted in desktop/server environments within the client itself
|
96
|
+
// or add a check here if MCPClient doesn't handle it.
|
97
|
+
// Example check (adjust based on where environment check is best handled):
|
98
|
+
// if (params.type === 'stdio' && typeof window !== 'undefined') {
|
99
|
+
// throw new Error('Stdio MCP type is not supported in browser environment.');
|
100
|
+
// }
|
101
|
+
|
102
|
+
const client = new MCPClient(params);
|
103
|
+
await client.initialize(); // Initialization logic should be within MCPClient
|
104
|
+
this.clients.set(key, client);
|
105
|
+
log(`New client initialized and cached for key: ${key}`);
|
106
|
+
return client;
|
107
|
+
} catch (error) {
|
108
|
+
console.error(`Failed to initialize MCP client for key ${key}:`, error);
|
109
|
+
// Do not cache failed initializations
|
110
|
+
throw new TRPCError({
|
111
|
+
cause: error,
|
112
|
+
code: 'INTERNAL_SERVER_ERROR',
|
113
|
+
message: `Failed to initialize MCP client: ${(error as Error).message}`,
|
114
|
+
});
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
// Custom serialization function to ensure consistent keys
|
119
|
+
private serializeParams(params: MCPClientParams): string {
|
120
|
+
const sortedKeys = Object.keys(params).sort();
|
121
|
+
const sortedParams: Record<string, any> = {};
|
122
|
+
|
123
|
+
for (const key of sortedKeys) {
|
124
|
+
const value = (params as any)[key];
|
125
|
+
// Sort the 'args' array if it exists
|
126
|
+
if (key === 'args' && Array.isArray(value)) {
|
127
|
+
sortedParams[key] = JSON.stringify(key);
|
128
|
+
} else {
|
129
|
+
sortedParams[key] = value;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
return JSON.stringify(sortedParams);
|
134
|
+
}
|
135
|
+
|
136
|
+
async getStreamableMcpServerManifest(
|
137
|
+
identifier: string,
|
138
|
+
url: string,
|
139
|
+
): Promise<LobeChatPluginManifest> {
|
140
|
+
const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
|
141
|
+
|
142
|
+
return {
|
143
|
+
api: tools,
|
144
|
+
identifier,
|
145
|
+
meta: {
|
146
|
+
avatar: 'MCP_AVATAR',
|
147
|
+
description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
|
148
|
+
title: identifier,
|
149
|
+
},
|
150
|
+
// TODO: temporary
|
151
|
+
type: 'mcp' as any,
|
152
|
+
};
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
// Export a singleton instance
|
157
|
+
export const mcpService = new MCPService();
|
@@ -1,12 +1,15 @@
|
|
1
1
|
import {
|
2
2
|
ListLocalFileParams,
|
3
3
|
LocalFileItem,
|
4
|
+
LocalMoveFilesResultItem,
|
4
5
|
LocalReadFileParams,
|
5
6
|
LocalReadFileResult,
|
6
7
|
LocalReadFilesParams,
|
7
8
|
LocalSearchFilesParams,
|
9
|
+
MoveLocalFilesParams,
|
8
10
|
OpenLocalFileParams,
|
9
11
|
OpenLocalFolderParams,
|
12
|
+
RenameLocalFileParams,
|
10
13
|
dispatch,
|
11
14
|
} from '@lobechat/electron-client-ipc';
|
12
15
|
|
@@ -34,6 +37,22 @@ class LocalFileService {
|
|
34
37
|
async openLocalFolder(params: OpenLocalFolderParams) {
|
35
38
|
return dispatch('openLocalFolder', params);
|
36
39
|
}
|
40
|
+
|
41
|
+
async moveLocalFiles(params: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
|
42
|
+
return dispatch('moveLocalFiles', params);
|
43
|
+
}
|
44
|
+
|
45
|
+
async renameLocalFile(params: RenameLocalFileParams) {
|
46
|
+
return dispatch('renameLocalFile', params);
|
47
|
+
}
|
48
|
+
|
49
|
+
async openLocalFileOrFolder(path: string, isDirectory: boolean) {
|
50
|
+
if (isDirectory) {
|
51
|
+
return this.openLocalFolder({ isDirectory, path });
|
52
|
+
} else {
|
53
|
+
return this.openLocalFile({ path });
|
54
|
+
}
|
55
|
+
}
|
37
56
|
}
|
38
57
|
|
39
58
|
export const localFileService = new LocalFileService();
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { ElectronAppState, dispatch } from '@lobechat/electron-client-ipc';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Service class for interacting with Electron's system-level information and actions.
|
5
|
+
*/
|
6
|
+
class ElectronSystemService {
|
7
|
+
/**
|
8
|
+
* Fetches the application state from the Electron main process.
|
9
|
+
* This includes system information (platform, arch) and user-specific paths.
|
10
|
+
* @returns {Promise<DesktopAppState>} A promise that resolves with the desktop app state.
|
11
|
+
*/
|
12
|
+
async getAppState(): Promise<ElectronAppState> {
|
13
|
+
// Calls the underlying IPC function to get data from the main process
|
14
|
+
return dispatch('getDesktopAppState');
|
15
|
+
}
|
16
|
+
|
17
|
+
// Add other system-related service methods here if needed in the future
|
18
|
+
}
|
19
|
+
|
20
|
+
// Export a singleton instance of the service
|
21
|
+
export const electronSystemService = new ElectronSystemService();
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { toolsClient } from '@/libs/trpc/client';
|
2
|
+
import { getToolStoreState } from '@/store/tool';
|
3
|
+
import { pluginSelectors } from '@/store/tool/slices/plugin/selectors';
|
4
|
+
import { ChatToolPayload } from '@/types/message';
|
5
|
+
|
6
|
+
class MCPService {
|
7
|
+
async invokeMcpToolCall(payload: ChatToolPayload, { signal }: { signal?: AbortSignal }) {
|
8
|
+
const s = getToolStoreState();
|
9
|
+
const { identifier, arguments: args, apiName } = payload;
|
10
|
+
const plugin = pluginSelectors.getCustomPluginById(identifier)(s);
|
11
|
+
|
12
|
+
if (!plugin) return;
|
13
|
+
|
14
|
+
return toolsClient.mcp.callTool.mutate(
|
15
|
+
{ args, params: { ...plugin.customParams?.mcp, name: identifier } as any, toolName: apiName },
|
16
|
+
{ signal },
|
17
|
+
);
|
18
|
+
}
|
19
|
+
|
20
|
+
async getStreamableMcpServerManifest(identifier: string, url: string) {
|
21
|
+
return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, url });
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
export const mcpService = new MCPService();
|
@@ -190,9 +190,6 @@ export const searchSlice: StateCreator<
|
|
190
190
|
|
191
191
|
await get().internal_updateMessageContent(id, JSON.stringify(searchContent));
|
192
192
|
|
193
|
-
// 如果没搜索到结果,那么不触发 ai 总结
|
194
|
-
if (searchContent.length === 0) return;
|
195
|
-
|
196
193
|
// 如果 aiSummary 为 true,则会自动触发总结
|
197
194
|
return aiSummary;
|
198
195
|
},
|
@@ -8,6 +8,7 @@ import { StateCreator } from 'zustand/vanilla';
|
|
8
8
|
import { LOADING_FLAT } from '@/const/message';
|
9
9
|
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
10
10
|
import { chatService } from '@/services/chat';
|
11
|
+
import { mcpService } from '@/services/mcp';
|
11
12
|
import { messageService } from '@/services/message';
|
12
13
|
import { ChatStore } from '@/store/chat/store';
|
13
14
|
import { useToolStore } from '@/store/tool';
|
@@ -41,6 +42,7 @@ export interface ChatPluginAction {
|
|
41
42
|
invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>;
|
42
43
|
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
|
43
44
|
invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
45
|
+
invokeMCPTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
44
46
|
|
45
47
|
invokeStandaloneTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
46
48
|
|
@@ -271,7 +273,7 @@ export const chatPlugin: StateCreator<
|
|
271
273
|
// trigger the plugin call
|
272
274
|
const data = await get().internal_invokeDifferentTypePlugin(id, payload);
|
273
275
|
|
274
|
-
if (
|
276
|
+
if (data && !['markdown', 'standalone'].includes(payload.type)) {
|
275
277
|
shouldCreateMessage = true;
|
276
278
|
latestToolId = id;
|
277
279
|
}
|
@@ -328,7 +330,9 @@ export const chatPlugin: StateCreator<
|
|
328
330
|
|
329
331
|
const updateAssistantMessage = async () => {
|
330
332
|
if (!assistantMessage) return;
|
331
|
-
await messageService.updateMessage(assistantMessage!.id, {
|
333
|
+
await messageService.updateMessage(assistantMessage!.id, {
|
334
|
+
tools: assistantMessage?.tools,
|
335
|
+
});
|
332
336
|
};
|
333
337
|
|
334
338
|
await Promise.all([
|
@@ -438,11 +442,51 @@ export const chatPlugin: StateCreator<
|
|
438
442
|
return await get().invokeBuiltinTool(id, payload);
|
439
443
|
}
|
440
444
|
|
445
|
+
// @ts-ignore
|
446
|
+
case 'mcp': {
|
447
|
+
return await get().invokeMCPTypePlugin(id, payload);
|
448
|
+
}
|
449
|
+
|
441
450
|
default: {
|
442
451
|
return await get().invokeDefaultTypePlugin(id, payload);
|
443
452
|
}
|
444
453
|
}
|
445
454
|
},
|
455
|
+
invokeMCPTypePlugin: async (id, payload) => {
|
456
|
+
const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
|
457
|
+
get();
|
458
|
+
let data: string = '';
|
459
|
+
|
460
|
+
try {
|
461
|
+
const abortController = internal_togglePluginApiCalling(
|
462
|
+
true,
|
463
|
+
id,
|
464
|
+
n('fetchPlugin/start') as string,
|
465
|
+
);
|
466
|
+
|
467
|
+
const result = await mcpService.invokeMcpToolCall(payload, {
|
468
|
+
signal: abortController?.signal,
|
469
|
+
});
|
470
|
+
if (!!result) data = result;
|
471
|
+
} catch (error) {
|
472
|
+
console.log(error);
|
473
|
+
const err = error as Error;
|
474
|
+
|
475
|
+
// ignore the aborted request error
|
476
|
+
if (!err.message.includes('The user aborted a request.')) {
|
477
|
+
await messageService.updateMessageError(id, error as any);
|
478
|
+
await refreshMessages();
|
479
|
+
}
|
480
|
+
}
|
481
|
+
|
482
|
+
internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string);
|
483
|
+
// 如果报错则结束了
|
484
|
+
if (!data) return;
|
485
|
+
|
486
|
+
await internal_updateMessageContent(id, data);
|
487
|
+
|
488
|
+
return data;
|
489
|
+
},
|
446
490
|
|
447
491
|
internal_togglePluginApiCalling: (loading, id, action) => {
|
448
492
|
return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action);
|
@@ -1,12 +1,10 @@
|
|
1
1
|
import { ListLocalFileParams } from '@lobechat/electron-client-ipc';
|
2
|
-
import { ActionIcon } from '@lobehub/ui';
|
3
2
|
import { Typography } from 'antd';
|
4
3
|
import { createStyles } from 'antd-style';
|
5
|
-
import { FolderOpen } from 'lucide-react';
|
6
4
|
import React, { memo } from 'react';
|
7
|
-
import { useTranslation } from 'react-i18next';
|
8
5
|
import { Flexbox } from 'react-layout-kit';
|
9
6
|
|
7
|
+
import FileIcon from '@/components/FileIcon';
|
10
8
|
import { localFileService } from '@/services/electron/localFileService';
|
11
9
|
import { LocalFileListState } from '@/tools/local-files/type';
|
12
10
|
import { ChatMessagePluginError } from '@/types/message';
|
@@ -20,8 +18,21 @@ const useStyles = createStyles(({ css, token, cx }) => ({
|
|
20
18
|
opacity: 1;
|
21
19
|
transition: opacity 0.2s ${token.motionEaseInOut};
|
22
20
|
`),
|
21
|
+
container: css`
|
22
|
+
cursor: pointer;
|
23
|
+
|
24
|
+
padding-block: 2px;
|
25
|
+
padding-inline: 4px;
|
26
|
+
border-radius: 4px;
|
27
|
+
|
28
|
+
color: ${token.colorTextSecondary};
|
29
|
+
|
30
|
+
:hover {
|
31
|
+
color: ${token.colorText};
|
32
|
+
background: ${token.colorFillTertiary};
|
33
|
+
}
|
34
|
+
`,
|
23
35
|
path: css`
|
24
|
-
padding-inline-start: 8px;
|
25
36
|
color: ${token.colorTextSecondary};
|
26
37
|
`,
|
27
38
|
}));
|
@@ -34,25 +45,21 @@ interface ListFilesProps {
|
|
34
45
|
}
|
35
46
|
|
36
47
|
const ListFiles = memo<ListFilesProps>(({ messageId, pluginError, args, pluginState }) => {
|
37
|
-
const { t } = useTranslation('tool');
|
38
|
-
|
39
48
|
const { styles } = useStyles();
|
40
49
|
return (
|
41
50
|
<>
|
42
|
-
<Flexbox
|
51
|
+
<Flexbox
|
52
|
+
className={styles.container}
|
53
|
+
gap={8}
|
54
|
+
horizontal
|
55
|
+
onClick={() => {
|
56
|
+
localFileService.openLocalFolder({ isDirectory: true, path: args.path });
|
57
|
+
}}
|
58
|
+
>
|
59
|
+
<FileIcon fileName={args.path} isDirectory size={22} variant={'pure'} />
|
43
60
|
<Typography.Text className={styles.path} ellipsis>
|
44
61
|
{args.path}
|
45
62
|
</Typography.Text>
|
46
|
-
<Flexbox className={styles.actions} gap={8} horizontal style={{ marginLeft: 8 }}>
|
47
|
-
<ActionIcon
|
48
|
-
icon={FolderOpen}
|
49
|
-
onClick={() => {
|
50
|
-
localFileService.openLocalFolder({ isDirectory: true, path: args.path });
|
51
|
-
}}
|
52
|
-
size="small"
|
53
|
-
title={t('localFiles.openFolder')}
|
54
|
-
/>
|
55
|
-
</Flexbox>
|
56
63
|
</Flexbox>
|
57
64
|
<SearchResult
|
58
65
|
listResults={pluginState?.listResults}
|
@@ -46,6 +46,9 @@ const useStyles = createStyles(({ css, token, cx }) => ({
|
|
46
46
|
header: css`
|
47
47
|
cursor: pointer;
|
48
48
|
`,
|
49
|
+
lineCount: css`
|
50
|
+
color: ${token.colorTextQuaternary};
|
51
|
+
`,
|
49
52
|
meta: css`
|
50
53
|
font-size: 12px;
|
51
54
|
color: ${token.colorTextTertiary};
|
@@ -70,7 +73,6 @@ const useStyles = createStyles(({ css, token, cx }) => ({
|
|
70
73
|
background: ${token.colorFillQuaternary};
|
71
74
|
`,
|
72
75
|
previewText: css`
|
73
|
-
font-family: ${token.fontFamilyCode};
|
74
76
|
font-size: 12px;
|
75
77
|
line-height: 1.6;
|
76
78
|
word-break: break-all;
|
@@ -84,14 +86,7 @@ interface ReadFileViewProps extends LocalReadFileResult {
|
|
84
86
|
}
|
85
87
|
|
86
88
|
const ReadFileView = memo<ReadFileViewProps>(
|
87
|
-
({
|
88
|
-
filename,
|
89
|
-
path,
|
90
|
-
fileType,
|
91
|
-
charCount,
|
92
|
-
lineCount, // Assuming the 250 is total lines?
|
93
|
-
content, // The actual content preview
|
94
|
-
}) => {
|
89
|
+
({ filename, path, fileType, charCount, content, totalLineCount, totalCharCount, loc }) => {
|
95
90
|
const { t } = useTranslation('tool');
|
96
91
|
const { styles } = useStyles();
|
97
92
|
const [isExpanded, setIsExpanded] = useState(false);
|
@@ -115,6 +110,7 @@ const ReadFileView = memo<ReadFileViewProps>(
|
|
115
110
|
<Flexbox
|
116
111
|
align={'center'}
|
117
112
|
className={styles.header}
|
113
|
+
gap={12}
|
118
114
|
horizontal
|
119
115
|
justify={'space-between'}
|
120
116
|
onClick={handleToggleExpand}
|
@@ -126,7 +122,7 @@ const ReadFileView = memo<ReadFileViewProps>(
|
|
126
122
|
{filename}
|
127
123
|
</Typography.Text>
|
128
124
|
{/* Actions on Hover */}
|
129
|
-
<Flexbox className={styles.actions} gap={
|
125
|
+
<Flexbox className={styles.actions} gap={2} horizontal style={{ marginLeft: 8 }}>
|
130
126
|
<ActionIcon
|
131
127
|
icon={ExternalLink}
|
132
128
|
onClick={handleOpenFile}
|
@@ -143,16 +139,26 @@ const ReadFileView = memo<ReadFileViewProps>(
|
|
143
139
|
</Flexbox>
|
144
140
|
</Flexbox>
|
145
141
|
<Flexbox align={'center'} className={styles.meta} gap={8} horizontal>
|
146
|
-
|
147
|
-
<
|
148
|
-
|
149
|
-
|
142
|
+
{isExpanded && (
|
143
|
+
<Flexbox align={'center'} gap={4} horizontal>
|
144
|
+
<Icon icon={Asterisk} size={'small'} />
|
145
|
+
<span>
|
146
|
+
{charCount} / <span className={styles.lineCount}>{totalCharCount}</span>
|
147
|
+
</span>
|
148
|
+
</Flexbox>
|
149
|
+
)}
|
150
150
|
<Flexbox align={'center'} gap={4} horizontal>
|
151
151
|
<Icon icon={AlignLeft} size={'small'} />
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
152
|
+
{isExpanded ? (
|
153
|
+
<span>
|
154
|
+
L{loc?.[0]}-{loc?.[1]} /{' '}
|
155
|
+
<span className={styles.lineCount}>{totalLineCount}</span>
|
156
|
+
</span>
|
157
|
+
) : (
|
158
|
+
<span>
|
159
|
+
L{loc?.[0]}-{loc?.[1]}
|
160
|
+
</span>
|
161
|
+
)}
|
156
162
|
</Flexbox>
|
157
163
|
<ActionIcon
|
158
164
|
active={isExpanded}
|
@@ -160,7 +166,6 @@ const ReadFileView = memo<ReadFileViewProps>(
|
|
160
166
|
onClick={handleToggleExpand}
|
161
167
|
size="small"
|
162
168
|
style={{
|
163
|
-
marginLeft: 8,
|
164
169
|
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
165
170
|
transition: 'transform 0.2s',
|
166
171
|
}}
|
@@ -173,19 +178,14 @@ const ReadFileView = memo<ReadFileViewProps>(
|
|
173
178
|
{path}
|
174
179
|
</Typography.Text>
|
175
180
|
|
176
|
-
{/* Content Preview (Collapsible) */}
|
177
181
|
{isExpanded && (
|
178
|
-
<Flexbox className={styles.previewBox}>
|
182
|
+
<Flexbox className={styles.previewBox} style={{ maxHeight: 240, overflow: 'auto' }}>
|
179
183
|
{fileType === 'md' ? (
|
180
|
-
<Markdown
|
184
|
+
<Markdown>{content}</Markdown>
|
181
185
|
) : (
|
182
|
-
<
|
183
|
-
className={styles.previewText}
|
184
|
-
ellipsis={{ expandable: true, rows: 10, symbol: t('localFiles.read.more') }}
|
185
|
-
style={{ maxHeight: 240, overflow: 'auto' }}
|
186
|
-
>
|
186
|
+
<div className={styles.previewText} style={{ width: '100%' }}>
|
187
187
|
{content}
|
188
|
-
</
|
188
|
+
</div>
|
189
189
|
)}
|
190
190
|
</Flexbox>
|
191
191
|
)}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { LocalFileItem } from '@lobechat/electron-client-ipc';
|
2
|
-
import { ActionIcon
|
2
|
+
import { ActionIcon } from '@lobehub/ui';
|
3
3
|
import { createStyles } from 'antd-style';
|
4
4
|
import dayjs from 'dayjs';
|
5
5
|
import { FolderOpen } from 'lucide-react';
|
@@ -64,22 +64,20 @@ const FileItem = memo<FileItemProps>(
|
|
64
64
|
gap={12}
|
65
65
|
horizontal
|
66
66
|
onClick={() => {
|
67
|
-
|
68
|
-
localFileService.openLocalFolder({ isDirectory, path });
|
69
|
-
} else {
|
70
|
-
localFileService.openLocalFile({ path });
|
71
|
-
}
|
67
|
+
localFileService.openLocalFileOrFolder(path, isDirectory);
|
72
68
|
}}
|
73
69
|
onMouseEnter={() => setIsHovering(true)}
|
74
70
|
onMouseLeave={() => setIsHovering(false)}
|
75
71
|
padding={'2px 8px'}
|
76
72
|
style={{ cursor: 'pointer', fontSize: 12, width: '100%' }}
|
77
73
|
>
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
74
|
+
<FileIcon
|
75
|
+
fileName={name}
|
76
|
+
fileType={type}
|
77
|
+
isDirectory={isDirectory}
|
78
|
+
size={16}
|
79
|
+
variant={'pure'}
|
80
|
+
/>
|
83
81
|
<Flexbox
|
84
82
|
align={'baseline'}
|
85
83
|
gap={4}
|