mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-15 12:10:16 +00:00
♻️ refactor: clean desktop relative code
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { pino } from '@/libs/logger';
|
||||
import { createLambdaContext } from '@/libs/trpc/lambda/context';
|
||||
import { prepareRequestForTRPC } from '@/libs/trpc/utils/request-adapter';
|
||||
import { desktopRouter } from '@/server/routers/desktop';
|
||||
|
||||
const handler = (req: NextRequest) => {
|
||||
// Clone the request to avoid "Response body object should not be disturbed or locked" error
|
||||
// in Next.js 16 when the body stream has been consumed by Next.js internal mechanisms
|
||||
const preparedReq = prepareRequestForTRPC(req);
|
||||
|
||||
return fetchRequestHandler({
|
||||
/**
|
||||
* @link https://trpc.io/docs/v11/context
|
||||
*/
|
||||
createContext: () => createLambdaContext(req),
|
||||
|
||||
endpoint: '/trpc/desktop',
|
||||
|
||||
onError: ({ error, path, type }) => {
|
||||
pino.info(`Error in tRPC handler (desktop) on path: ${path}, type: ${type}`);
|
||||
console.error(error);
|
||||
},
|
||||
|
||||
req: preparedReq,
|
||||
responseMeta({ ctx }) {
|
||||
const headers = ctx?.resHeaders;
|
||||
|
||||
return { headers };
|
||||
},
|
||||
router: desktopRouter,
|
||||
});
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -3,8 +3,12 @@ import { join } from 'path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Desktop TRPC Route', () => {
|
||||
it('should have desktop directory', () => {
|
||||
const desktopPath = join(__dirname, 'desktop');
|
||||
expect(existsSync(desktopPath)).toBe(true);
|
||||
it('should have expected trpc route directories', () => {
|
||||
const routeDirs = ['async', 'lambda', 'mobile', 'tools'];
|
||||
|
||||
for (const dir of routeDirs) {
|
||||
const routePath = join(__dirname, dir);
|
||||
expect(existsSync(routePath)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,14 +5,21 @@ import { describe, expect, it } from 'vitest';
|
||||
describe('Desktop Routes', () => {
|
||||
const appRootDir = resolve(__dirname, '..');
|
||||
|
||||
const desktopRoutes = [
|
||||
'(backend)/trpc/desktop/[trpc]/route.ts',
|
||||
'desktop/devtools/page.tsx',
|
||||
'desktop/layout.tsx',
|
||||
/**
|
||||
* Desktop-specific "routes" used by the desktop app are implemented via:
|
||||
* - backend desktop OIDC callback route
|
||||
* - desktop router configs/components under `[variants]/router`
|
||||
*
|
||||
* This test is intentionally a "smoke check" to prevent accidental deletion.
|
||||
*/
|
||||
const desktopRelatedEntries = [
|
||||
'(backend)/oidc/callback/desktop/route.ts',
|
||||
'[variants]/router/DesktopClientRouter.tsx',
|
||||
'[variants]/router/desktopRouter.config.tsx',
|
||||
];
|
||||
|
||||
it.each(desktopRoutes)('should have file: %s', (route) => {
|
||||
const filePath = resolve(appRootDir, route);
|
||||
it.each(desktopRelatedEntries)('should have file: %s', (entry) => {
|
||||
const filePath = resolve(appRootDir, entry);
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,14 @@ describe('MCPClient', () => {
|
||||
// --- Updated Stdio Transport tests ---
|
||||
describe('Stdio Transport', () => {
|
||||
let mcpClient: MCPClient;
|
||||
const TIMEOUT = 120_000;
|
||||
const stdioConnection = {
|
||||
id: 'mcp-hello-world',
|
||||
name: 'Stdio SDK Test Connection',
|
||||
type: 'stdio' as const,
|
||||
command: 'npx', // Use node to run the compiled mock server
|
||||
args: ['mcp-hello-world@1.1.2'], // Use the path to the compiled JS file
|
||||
// Ensure non-interactive execution to avoid hanging on install confirmation in CI.
|
||||
args: ['--yes', 'mcp-hello-world@1.1.2'],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -21,48 +23,64 @@ describe('MCPClient', () => {
|
||||
await mcpClient.initialize();
|
||||
// Add a small delay to allow the server process to fully start (optional, but can help)
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}, 30000);
|
||||
}, TIMEOUT);
|
||||
|
||||
afterEach(async () => {
|
||||
// Assume SDK client/transport handles process termination gracefully
|
||||
// If processes leak, more explicit cleanup might be needed here
|
||||
}, 30000);
|
||||
}, TIMEOUT);
|
||||
|
||||
it('should create and initialize an instance with stdio transport', () => {
|
||||
expect(mcpClient).toBeInstanceOf(MCPClient);
|
||||
}, 30000);
|
||||
it(
|
||||
'should create and initialize an instance with stdio transport',
|
||||
() => {
|
||||
expect(mcpClient).toBeInstanceOf(MCPClient);
|
||||
},
|
||||
TIMEOUT,
|
||||
);
|
||||
|
||||
it('should list tools via stdio', async () => {
|
||||
const result = await mcpClient.listTools();
|
||||
it(
|
||||
'should list tools via stdio',
|
||||
async () => {
|
||||
const result = await mcpClient.listTools();
|
||||
|
||||
// Check exact length if no other tools are expected
|
||||
expect(result).toHaveLength(3);
|
||||
// Check exact length if no other tools are expected
|
||||
expect(result).toHaveLength(3);
|
||||
|
||||
// Expect the tools defined in mock-sdk-server.ts
|
||||
expect(result).toMatchSnapshot();
|
||||
}, 30000);
|
||||
// Expect the tools defined in mock-sdk-server.ts
|
||||
expect(result).toMatchSnapshot();
|
||||
},
|
||||
TIMEOUT,
|
||||
);
|
||||
|
||||
it('should call the "echo" tool via stdio', async () => {
|
||||
const toolName = 'echo';
|
||||
const toolArgs = { message: 'hello stdio' };
|
||||
// Expect the result format defined in mock-sdk-server.ts
|
||||
const expectedResult = {
|
||||
content: [{ type: 'text', text: 'You said: hello stdio' }],
|
||||
};
|
||||
it(
|
||||
'should call the "echo" tool via stdio',
|
||||
async () => {
|
||||
const toolName = 'echo';
|
||||
const toolArgs = { message: 'hello stdio' };
|
||||
// Expect the result format defined in mock-sdk-server.ts
|
||||
const expectedResult = {
|
||||
content: [{ type: 'text', text: 'You said: hello stdio' }],
|
||||
};
|
||||
|
||||
const result = await mcpClient.callTool(toolName, toolArgs);
|
||||
expect(result).toEqual(expectedResult);
|
||||
}, 30000);
|
||||
const result = await mcpClient.callTool(toolName, toolArgs);
|
||||
expect(result).toEqual(expectedResult);
|
||||
},
|
||||
TIMEOUT,
|
||||
);
|
||||
|
||||
it('should call the "add" tool via stdio', async () => {
|
||||
const toolName = 'add';
|
||||
const toolArgs = { a: 5, b: 7 };
|
||||
it(
|
||||
'should call the "add" tool via stdio',
|
||||
async () => {
|
||||
const toolName = 'add';
|
||||
const toolArgs = { a: 5, b: 7 };
|
||||
|
||||
const result = await mcpClient.callTool(toolName, toolArgs);
|
||||
expect(result).toEqual({
|
||||
content: [{ type: 'text', text: 'The sum is: 12' }],
|
||||
});
|
||||
}, 30000);
|
||||
const result = await mcpClient.callTool(toolName, toolArgs);
|
||||
expect(result).toEqual({
|
||||
content: [{ type: 'text', text: 'The sum is: 12' }],
|
||||
});
|
||||
},
|
||||
TIMEOUT,
|
||||
);
|
||||
});
|
||||
|
||||
// Error Handling tests remain the same...
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { withElectronProtocolIfElectron } from '@/const/protocol';
|
||||
import { AsyncRouter } from '@/server/routers/async';
|
||||
import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
|
||||
|
||||
export const asyncClient = createTRPCClient<AsyncRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
fetch: isDesktop
|
||||
? // eslint-disable-next-line no-undef
|
||||
(input, init) => fetchWithDesktopRemoteRPC(input as string, init as RequestInit)
|
||||
: undefined,
|
||||
maxURLLength: 2083,
|
||||
transformer: superjson,
|
||||
url: '/trpc/async',
|
||||
url: withElectronProtocolIfElectron('/trpc/async'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createTRPCClient, httpLink } from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import type { DesktopRouter } from '@/server/routers/desktop';
|
||||
|
||||
export const desktopClient = createTRPCClient<DesktopRouter>({
|
||||
links: [
|
||||
httpLink({
|
||||
transformer: superjson,
|
||||
url: '/trpc/desktop',
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
export { asyncClient } from './async';
|
||||
export * from './desktop';
|
||||
export * from './lambda';
|
||||
export * from './tools';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { TRPCLink, createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||
import { TRPCLink, createTRPCClient, httpBatchLink, httpLink, splitLink } from '@trpc/client';
|
||||
import { createTRPCReact } from '@trpc/react-query';
|
||||
import { observable } from '@trpc/server/observable';
|
||||
import debug from 'debug';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { withElectronProtocolIfElectron } from '@/const/protocol';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import type { LambdaRouter } from '@/server/routers/lambda';
|
||||
|
||||
@@ -33,15 +34,19 @@ const errorHandlingLink: TRPCLink<LambdaRouter> = () => {
|
||||
|
||||
// Don't show notifications for abort errors
|
||||
if (showError && !isAbortError) {
|
||||
const { loginRequired } = await import('@/components/Error/loginRequiredNotification');
|
||||
|
||||
switch (status) {
|
||||
case 401: {
|
||||
// Debounce: only show login notification once every 5 seconds
|
||||
const now = Date.now();
|
||||
if (now - last401Time > MIN_401_INTERVAL) {
|
||||
last401Time = now;
|
||||
loginRequired.redirect();
|
||||
// Desktop app doesn't have the web auth routes like `/signin`,
|
||||
// so skip the login redirect/notification there.
|
||||
if (!isDesktop) {
|
||||
const { loginRequired } =
|
||||
await import('@/components/Error/loginRequiredNotification');
|
||||
loginRequired.redirect();
|
||||
}
|
||||
}
|
||||
// Mark error as non-retryable to prevent SWR infinite retry loop
|
||||
err.meta = { ...err.meta, shouldRetry: false };
|
||||
@@ -61,14 +66,13 @@ const errorHandlingLink: TRPCLink<LambdaRouter> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 2. httpBatchLink
|
||||
const customHttpBatchLink = httpBatchLink({
|
||||
fetch: async (input, init) => {
|
||||
// 2. Shared link options
|
||||
const linkOptions = {
|
||||
// eslint-disable-next-line no-undef
|
||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
if (isDesktop) {
|
||||
const { desktopRemoteRPCFetch } = await import('@/utils/electron/desktopRemoteRPCFetch');
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const res = await desktopRemoteRPCFetch(input as string, init as RequestInit);
|
||||
const res = await fetch(input as string, init as RequestInit);
|
||||
|
||||
if (res) return res;
|
||||
}
|
||||
@@ -85,9 +89,8 @@ const customHttpBatchLink = httpBatchLink({
|
||||
log('Getting provider from store for image page: %s', location.pathname);
|
||||
if (location.pathname === '/image') {
|
||||
const { getImageStoreState } = await import('@/store/image');
|
||||
const { imageGenerationConfigSelectors } = await import(
|
||||
'@/store/image/slices/generationConfig/selectors'
|
||||
);
|
||||
const { imageGenerationConfigSelectors } =
|
||||
await import('@/store/image/slices/generationConfig/selectors');
|
||||
provider = imageGenerationConfigSelectors.provider(getImageStoreState()) as ModelProvider;
|
||||
log('Getting provider from store for image page: %s', provider);
|
||||
}
|
||||
@@ -98,13 +101,24 @@ const customHttpBatchLink = httpBatchLink({
|
||||
log('Headers: %O', headers);
|
||||
return headers;
|
||||
},
|
||||
maxURLLength: 2083,
|
||||
transformer: superjson,
|
||||
url: '/trpc/lambda',
|
||||
url: withElectronProtocolIfElectron('/trpc/lambda'),
|
||||
};
|
||||
|
||||
// Procedures that should skip batching for faster initial load
|
||||
const initialLoadProcedures = new Set(['user.getUserState', 'config.getGlobalConfig']);
|
||||
const slowProcedures = new Set(['market.getAssistantList']);
|
||||
const SKIP_BATCH_PROCEDURES = new Set([...initialLoadProcedures, ...slowProcedures]);
|
||||
|
||||
// 3. splitLink to conditionally disable batching
|
||||
const customSplitLink = splitLink({
|
||||
condition: (op) => SKIP_BATCH_PROCEDURES.has(op.path),
|
||||
false: httpBatchLink({ ...linkOptions, maxURLLength: 2083 }),
|
||||
true: httpLink(linkOptions),
|
||||
});
|
||||
|
||||
// 3. assembly links
|
||||
const links = [errorHandlingLink, customHttpBatchLink];
|
||||
// 4. assembly links
|
||||
const links = [errorHandlingLink, customSplitLink];
|
||||
|
||||
export const lambdaClient = createTRPCClient<LambdaRouter>({
|
||||
links,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { withElectronProtocolIfElectron } from '@/const/protocol';
|
||||
import type { ToolsRouter } from '@/server/routers/tools';
|
||||
import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
|
||||
|
||||
export const toolsClient = createTRPCClient<ToolsRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
fetch: isDesktop
|
||||
? // eslint-disable-next-line no-undef
|
||||
(input, init) => fetchWithDesktopRemoteRPC(input as string, init as RequestInit)
|
||||
: undefined,
|
||||
headers: async () => {
|
||||
// dynamic import to avoid circular dependency
|
||||
const { createHeaderWithAuth } = await import('@/services/_auth');
|
||||
@@ -20,7 +15,7 @@ export const toolsClient = createTRPCClient<ToolsRouter>({
|
||||
},
|
||||
maxURLLength: 2083,
|
||||
transformer: superjson,
|
||||
url: '/trpc/tools',
|
||||
url: withElectronProtocolIfElectron('/trpc/tools'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { router } from '@/libs/trpc/lambda';
|
||||
import { mcpRouter } from '@/server/routers/desktop/mcp';
|
||||
|
||||
import { pgTableRouter } from './pgTable';
|
||||
|
||||
export const desktopRouter = router({
|
||||
mcp: mcpRouter,
|
||||
pgTable: pgTableRouter,
|
||||
});
|
||||
|
||||
export type DesktopRouter = typeof desktopRouter;
|
||||
@@ -1,121 +0,0 @@
|
||||
import { GetStreamableMcpServerManifestInputSchema } from '@lobechat/types';
|
||||
import debug from 'debug';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ToolCallContent } from '@/libs/mcp';
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
import { FileService } from '@/server/services/file';
|
||||
import { mcpService } from '@/server/services/mcp';
|
||||
import { processContentBlocks } from '@/server/services/mcp/contentProcessor';
|
||||
|
||||
const log = debug('lobe-mcp:router');
|
||||
|
||||
const stdioParamsSchema = z.object({
|
||||
args: z.array(z.string()).optional().default([]),
|
||||
command: z.string().min(1),
|
||||
env: z.any().optional(),
|
||||
metadata: z
|
||||
.object({
|
||||
avatar: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
name: z.string().min(1),
|
||||
type: z.literal('stdio').default('stdio'),
|
||||
});
|
||||
|
||||
const mcpProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next }) => {
|
||||
return next({
|
||||
ctx: {
|
||||
fileService: new FileService(ctx.serverDB, ctx.userId),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const mcpRouter = router({
|
||||
getStdioMcpServerManifest: mcpProcedure.input(stdioParamsSchema).query(async ({ input }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { env: _, ...rest } = input;
|
||||
log('getStdioMcpServerManifest input: %O', rest);
|
||||
|
||||
return await mcpService.getStdioMcpServerManifest(input, input.metadata);
|
||||
}),
|
||||
|
||||
getStreamableMcpServerManifest: mcpProcedure
|
||||
.input(GetStreamableMcpServerManifestInputSchema)
|
||||
.query(async ({ input }) => {
|
||||
log('getStreamableMcpServerManifest input: %O', {
|
||||
identifier: input.identifier,
|
||||
url: input.url,
|
||||
});
|
||||
return await mcpService.getStreamableMcpServerManifest(
|
||||
input.identifier,
|
||||
input.url,
|
||||
input.metadata,
|
||||
input.auth,
|
||||
input.headers,
|
||||
);
|
||||
}),
|
||||
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
// --- MCP Interaction ---
|
||||
// listTools now accepts MCPClientParams directly
|
||||
listTools: mcpProcedure
|
||||
.input(stdioParamsSchema) // Use the unified schema
|
||||
.query(async ({ input }) => {
|
||||
// Pass the validated MCPClientParams to the service
|
||||
return await mcpService.listTools(input);
|
||||
}),
|
||||
|
||||
// listResources now accepts MCPClientParams directly
|
||||
listResources: mcpProcedure
|
||||
.input(stdioParamsSchema) // Use the unified schema
|
||||
.query(async ({ input }) => {
|
||||
// Pass the validated MCPClientParams to the service
|
||||
return await mcpService.listResources(input);
|
||||
}),
|
||||
|
||||
// listPrompts now accepts MCPClientParams directly
|
||||
listPrompts: mcpProcedure
|
||||
.input(stdioParamsSchema) // Use the unified schema
|
||||
.query(async ({ input }) => {
|
||||
// Pass the validated MCPClientParams to the service
|
||||
return await mcpService.listPrompts(input);
|
||||
}),
|
||||
|
||||
// callTool now accepts MCPClientParams, toolName, and args
|
||||
callTool: mcpProcedure
|
||||
.input(
|
||||
z.object({
|
||||
params: stdioParamsSchema, // Use the unified schema for client params
|
||||
args: z.any(), // Arguments for the tool call
|
||||
env: z.any(), // Arguments for the tool call
|
||||
toolName: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Create a closure that binds fileService and userId to processContentBlocks
|
||||
const boundProcessContentBlocks = async (blocks: ToolCallContent[]) =>
|
||||
processContentBlocks(blocks, ctx.fileService);
|
||||
|
||||
// Pass the validated params, toolName, args, and bound processContentBlocks to the service
|
||||
return await mcpService.callTool({
|
||||
clientParams: { ...input.params, env: input.env },
|
||||
toolName: input.toolName,
|
||||
argsStr: input.args,
|
||||
processContentBlocks: boundProcessContentBlocks,
|
||||
});
|
||||
}),
|
||||
|
||||
validMcpServerInstallable: mcpProcedure
|
||||
.input(
|
||||
z.object({
|
||||
deploymentOptions: z.array(z.object({}).passthrough()),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await mcpService.checkMcpInstall(input as any);
|
||||
}),
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DESKTOP_USER_ID } from '@/const/desktop';
|
||||
import { TableViewerRepo } from '@/database/repositories/tableViewer';
|
||||
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
||||
|
||||
const pgTableProcedure = publicProcedure.use(serverDatabase).use(async ({ ctx, next }) => {
|
||||
return next({
|
||||
ctx: {
|
||||
tableViewerRepo: new TableViewerRepo(ctx.serverDB, DESKTOP_USER_ID),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const pgTableRouter = router({
|
||||
getAllTables: pgTableProcedure.query(async ({ ctx }) => {
|
||||
return ctx.tableViewerRepo.getAllTables();
|
||||
}),
|
||||
getTableData: pgTableProcedure
|
||||
.input(
|
||||
z.object({
|
||||
page: z.number(),
|
||||
pageSize: z.number(),
|
||||
tableName: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return ctx.tableViewerRepo.getTableData(input.tableName, {
|
||||
page: input.page,
|
||||
pageSize: input.pageSize,
|
||||
});
|
||||
}),
|
||||
getTableDetails: pgTableProcedure
|
||||
.input(
|
||||
z.object({
|
||||
tableName: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return ctx.tableViewerRepo.getTableDetails(input.tableName);
|
||||
}),
|
||||
});
|
||||
@@ -50,7 +50,7 @@ class LocalFileService {
|
||||
}
|
||||
|
||||
async openLocalFolder(params: OpenLocalFolderParams) {
|
||||
return ensureElectronIpc().localSystem.handleOpenLocalFile(params);
|
||||
return ensureElectronIpc().localSystem.handleOpenLocalFolder(params);
|
||||
}
|
||||
|
||||
async moveLocalFiles(params: MoveLocalFilesParams): Promise<LocalMoveFilesResultItem[]> {
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ElectronStore } from '../store';
|
||||
* 设置操作
|
||||
*/
|
||||
export interface ElectronRemoteServerAction {
|
||||
clearRemoteServerSyncError: () => void;
|
||||
connectRemoteServer: (params: DataSyncConfig) => Promise<void>;
|
||||
disconnectRemoteServer: () => Promise<void>;
|
||||
refreshServerConfig: () => Promise<void>;
|
||||
@@ -27,10 +28,15 @@ export const remoteSyncSlice: StateCreator<
|
||||
[],
|
||||
ElectronRemoteServerAction
|
||||
> = (set, get) => ({
|
||||
clearRemoteServerSyncError: () => {
|
||||
set({ remoteServerSyncError: undefined }, false, 'clearRemoteServerSyncError');
|
||||
},
|
||||
|
||||
connectRemoteServer: async (values) => {
|
||||
if (values.storageMode === 'selfHost' && !values.remoteServerUrl) return;
|
||||
|
||||
set({ isConnectingServer: true });
|
||||
get().clearRemoteServerSyncError();
|
||||
try {
|
||||
// 获取当前配置
|
||||
const config = await remoteServerService.getRemoteServerConfig();
|
||||
@@ -64,8 +70,9 @@ export const remoteSyncSlice: StateCreator<
|
||||
|
||||
disconnectRemoteServer: async () => {
|
||||
set({ isConnectingServer: false });
|
||||
get().clearRemoteServerSyncError();
|
||||
try {
|
||||
await remoteServerService.setRemoteServerConfig({ active: false, storageMode: 'local' });
|
||||
await remoteServerService.setRemoteServerConfig({ active: false, storageMode: 'cloud' });
|
||||
// 更新表单URL为空
|
||||
set({ dataSyncConfig: initialState.dataSyncConfig });
|
||||
// 刷新状态
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface ElectronState {
|
||||
|
||||
export const initialState: ElectronState = {
|
||||
appState: {},
|
||||
dataSyncConfig: { storageMode: 'local' },
|
||||
dataSyncConfig: { storageMode: 'cloud' },
|
||||
desktopHotkeys: {},
|
||||
isAppStateInit: false,
|
||||
isConnectingServer: false,
|
||||
|
||||
Vendored
+8
@@ -12,3 +12,11 @@ declare module 'antd-style' {
|
||||
declare module 'styled-components' {
|
||||
export interface DefaultTheme extends AntdToken, LobeCustomToken {}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
lobeEnv?: {
|
||||
darwinMajorVersion?: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user