diff --git a/packages/shared/data/preference/preferenceSchemas.ts b/packages/shared/data/preference/preferenceSchemas.ts index 0e09764733..c29e842cbc 100644 --- a/packages/shared/data/preference/preferenceSchemas.ts +++ b/packages/shared/data/preference/preferenceSchemas.ts @@ -1,6 +1,6 @@ /** * Auto-generated preferences configuration - * Generated at: 2025-11-29T03:45:07.207Z + * Generated at: 2025-12-02T03:48:58.274Z * * This file is automatically generated from classification.json * To update this file, modify classification.json and run: @@ -303,7 +303,7 @@ export interface PreferenceSchemas { // redux/settings/yuqueUrl 'data.integration.yuque.url': string // redux/settings/apiServer.apiKey - 'feature.csaas.api_key': string + 'feature.csaas.api_key': string | null // redux/settings/apiServer.enabled 'feature.csaas.enabled': boolean // redux/settings/apiServer.host @@ -581,7 +581,7 @@ export const DefaultPreferences: PreferenceSchemas = { 'data.integration.yuque.repo_id': '', 'data.integration.yuque.token': '', 'data.integration.yuque.url': '', - 'feature.csaas.api_key': '`cs-sk-${uuid()}`', + 'feature.csaas.api_key': null, 'feature.csaas.enabled': false, 'feature.csaas.host': 'localhost', 'feature.csaas.port': 23333, diff --git a/src/main/apiServer/config.ts b/src/main/apiServer/config.ts deleted file mode 100644 index 60b1986be9..0000000000 --- a/src/main/apiServer/config.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { ApiServerConfig } from '@types' -import { v4 as uuidv4 } from 'uuid' - -import { loggerService } from '../services/LoggerService' -import { reduxService } from '../services/ReduxService' - -const logger = loggerService.withContext('ApiServerConfig') - -const defaultHost = 'localhost' -const defaultPort = 23333 - -class ConfigManager { - private _config: ApiServerConfig | null = null - - private generateApiKey(): string { - return `cs-sk-${uuidv4()}` - } - - async load(): Promise { - try { - const settings = await reduxService.select('state.settings') - const serverSettings = settings?.apiServer - let apiKey = serverSettings?.apiKey - if (!apiKey || apiKey.trim() === '') { - apiKey = this.generateApiKey() - await reduxService.dispatch({ - type: 'settings/setApiServerApiKey', - payload: apiKey - }) - } - this._config = { - enabled: serverSettings?.enabled ?? false, - port: serverSettings?.port ?? defaultPort, - host: defaultHost, - apiKey: apiKey - } - return this._config - } catch (error: any) { - logger.warn('Failed to load config from Redux, using defaults', { error }) - this._config = { - enabled: false, - port: defaultPort, - host: defaultHost, - apiKey: this.generateApiKey() - } - return this._config - } - } - - async get(): Promise { - if (!this._config) { - await this.load() - } - if (!this._config) { - throw new Error('Failed to load API server configuration') - } - return this._config - } - - async reload(): Promise { - return await this.load() - } -} - -export const config = new ConfigManager() diff --git a/src/main/apiServer/index.ts b/src/main/apiServer/index.ts index 765ca05fba..9cde8fbe42 100644 --- a/src/main/apiServer/index.ts +++ b/src/main/apiServer/index.ts @@ -1,2 +1 @@ -export { config } from './config' export { apiServer } from './server' diff --git a/src/main/apiServer/middleware/__tests__/auth.test.ts b/src/main/apiServer/middleware/__tests__/auth.test.ts index 859050fb2b..db2cb1f8ad 100644 --- a/src/main/apiServer/middleware/__tests__/auth.test.ts +++ b/src/main/apiServer/middleware/__tests__/auth.test.ts @@ -1,12 +1,12 @@ +import { preferenceService } from '@data/PreferenceService' import type { NextFunction, Request, Response } from 'express' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { config } from '../../config' import { authMiddleware } from '../auth' -// Mock the config module -vi.mock('../../config', () => ({ - config: { +// Mock the preferenceService module +vi.mock('@data/PreferenceService', () => ({ + preferenceService: { get: vi.fn() } })) @@ -20,7 +20,7 @@ vi.mock('@logger', () => ({ } })) -const mockConfig = config as any +const mockPreferenceService = preferenceService as any describe('authMiddleware', () => { let req: Partial @@ -45,24 +45,24 @@ describe('authMiddleware', () => { }) describe('Missing credentials', () => { - it('should return 401 when both auth headers are missing', async () => { + it('should return 401 when both auth headers are missing', () => { ;(req.header as any).mockReturnValue('') - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(401) expect(jsonMock).toHaveBeenCalledWith({ error: 'Unauthorized: missing credentials' }) expect(next).not.toHaveBeenCalled() }) - it('should return 401 when both auth headers are empty strings', async () => { + it('should return 401 when both auth headers are empty strings', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return '' if (header === 'x-api-key') return '' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(401) expect(jsonMock).toHaveBeenCalledWith({ error: 'Unauthorized: missing credentials' }) @@ -71,30 +71,30 @@ describe('authMiddleware', () => { }) describe('Server configuration', () => { - it('should return 403 when API key is not configured', async () => { + it('should return 403 when API key is not configured', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return 'some-key' return '' }) - mockConfig.get.mockResolvedValue({ apiKey: '' }) + mockPreferenceService.get.mockReturnValue('') - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) expect(next).not.toHaveBeenCalled() }) - it('should return 403 when API key is null', async () => { + it('should return 403 when API key is null', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return 'some-key' return '' }) - mockConfig.get.mockResolvedValue({ apiKey: null }) + mockPreferenceService.get.mockReturnValue(null) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) @@ -106,80 +106,80 @@ describe('authMiddleware', () => { const validApiKey = 'valid-api-key-123' beforeEach(() => { - mockConfig.get.mockResolvedValue({ apiKey: validApiKey }) + mockPreferenceService.get.mockReturnValue(validApiKey) }) - it('should authenticate successfully with valid API key', async () => { + it('should authenticate successfully with valid API key', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return validApiKey return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(next).toHaveBeenCalled() expect(statusMock).not.toHaveBeenCalled() }) - it('should return 403 with invalid API key', async () => { + it('should return 403 with invalid API key', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return 'invalid-key' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) expect(next).not.toHaveBeenCalled() }) - it('should return 401 with empty API key', async () => { + it('should return 401 with empty API key', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return ' ' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(401) expect(jsonMock).toHaveBeenCalledWith({ error: 'Unauthorized: empty x-api-key' }) expect(next).not.toHaveBeenCalled() }) - it('should handle API key with whitespace', async () => { + it('should handle API key with whitespace', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return ` ${validApiKey} ` return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(next).toHaveBeenCalled() expect(statusMock).not.toHaveBeenCalled() }) - it('should prioritize API key over Bearer token when both are present', async () => { + it('should prioritize API key over Bearer token when both are present', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return validApiKey if (header === 'authorization') return 'Bearer invalid-token' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(next).toHaveBeenCalled() expect(statusMock).not.toHaveBeenCalled() }) - it('should return 403 when API key is invalid even if Bearer token is valid', async () => { + it('should return 403 when API key is invalid even if Bearer token is valid', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return 'invalid-key' if (header === 'authorization') return `Bearer ${validApiKey}` return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) @@ -191,92 +191,92 @@ describe('authMiddleware', () => { const validApiKey = 'valid-api-key-123' beforeEach(() => { - mockConfig.get.mockResolvedValue({ apiKey: validApiKey }) + mockPreferenceService.get.mockReturnValue(validApiKey) }) - it('should authenticate successfully with valid Bearer token when no API key', async () => { + it('should authenticate successfully with valid Bearer token when no API key', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return `Bearer ${validApiKey}` return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(next).toHaveBeenCalled() expect(statusMock).not.toHaveBeenCalled() }) - it('should return 403 with invalid Bearer token', async () => { + it('should return 403 with invalid Bearer token', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return 'Bearer invalid-token' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) expect(next).not.toHaveBeenCalled() }) - it('should return 401 with malformed authorization header', async () => { + it('should return 401 with malformed authorization header', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return 'Basic sometoken' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(401) expect(jsonMock).toHaveBeenCalledWith({ error: 'Unauthorized: invalid authorization format' }) expect(next).not.toHaveBeenCalled() }) - it('should return 401 with Bearer without space', async () => { + it('should return 401 with Bearer without space', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return 'Bearer' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(401) expect(jsonMock).toHaveBeenCalledWith({ error: 'Unauthorized: invalid authorization format' }) expect(next).not.toHaveBeenCalled() }) - it('should handle Bearer token with only trailing spaces (edge case)', async () => { + it('should handle Bearer token with only trailing spaces (edge case)', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return 'Bearer ' // This will be trimmed to "Bearer" and fail format check return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(401) expect(jsonMock).toHaveBeenCalledWith({ error: 'Unauthorized: invalid authorization format' }) expect(next).not.toHaveBeenCalled() }) - it('should handle Bearer token with case insensitive prefix', async () => { + it('should handle Bearer token with case insensitive prefix', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return `bearer ${validApiKey}` return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(next).toHaveBeenCalled() expect(statusMock).not.toHaveBeenCalled() }) - it('should handle Bearer token with whitespace', async () => { + it('should handle Bearer token with whitespace', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return ` Bearer ${validApiKey} ` return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(next).toHaveBeenCalled() expect(statusMock).not.toHaveBeenCalled() @@ -287,40 +287,29 @@ describe('authMiddleware', () => { const validApiKey = 'valid-api-key-123' beforeEach(() => { - mockConfig.get.mockResolvedValue({ apiKey: validApiKey }) + mockPreferenceService.get.mockReturnValue(validApiKey) }) - it('should handle config.get() rejection', async () => { - ;(req.header as any).mockImplementation((header: string) => { - if (header === 'x-api-key') return validApiKey - return '' - }) - - mockConfig.get.mockRejectedValue(new Error('Config error')) - - await expect(authMiddleware(req as Request, res as Response, next)).rejects.toThrow('Config error') - }) - - it('should use timing-safe comparison for different length tokens', async () => { + it('should use timing-safe comparison for different length tokens', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'x-api-key') return 'short' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) expect(next).not.toHaveBeenCalled() }) - it('should return 401 when neither credential format is valid', async () => { + it('should return 401 when neither credential format is valid', () => { ;(req.header as any).mockImplementation((header: string) => { if (header === 'authorization') return 'Invalid format' return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(401) expect(jsonMock).toHaveBeenCalledWith({ error: 'Unauthorized: invalid authorization format' }) @@ -332,10 +321,10 @@ describe('authMiddleware', () => { const validApiKey = 'valid-api-key-123' beforeEach(() => { - mockConfig.get.mockResolvedValue({ apiKey: validApiKey }) + mockPreferenceService.get.mockReturnValue(validApiKey) }) - it('should handle similar length but different API keys securely', async () => { + it('should handle similar length but different API keys securely', () => { const similarKey = 'valid-api-key-124' // Same length, different last char ;(req.header as any).mockImplementation((header: string) => { @@ -343,14 +332,14 @@ describe('authMiddleware', () => { return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) expect(next).not.toHaveBeenCalled() }) - it('should handle similar length but different Bearer tokens securely', async () => { + it('should handle similar length but different Bearer tokens securely', () => { const similarKey = 'valid-api-key-124' // Same length, different last char ;(req.header as any).mockImplementation((header: string) => { @@ -358,7 +347,7 @@ describe('authMiddleware', () => { return '' }) - await authMiddleware(req as Request, res as Response, next) + authMiddleware(req as Request, res as Response, next) expect(statusMock).toHaveBeenCalledWith(403) expect(jsonMock).toHaveBeenCalledWith({ error: 'Forbidden' }) diff --git a/src/main/apiServer/middleware/auth.ts b/src/main/apiServer/middleware/auth.ts index bf44e4eb37..38309ee641 100644 --- a/src/main/apiServer/middleware/auth.ts +++ b/src/main/apiServer/middleware/auth.ts @@ -1,8 +1,7 @@ +import { preferenceService } from '@data/PreferenceService' import crypto from 'crypto' import type { NextFunction, Request, Response } from 'express' -import { config } from '../config' - const isValidToken = (token: string, apiKey: string): boolean => { if (token.length !== apiKey.length) { return false @@ -12,7 +11,7 @@ const isValidToken = (token: string, apiKey: string): boolean => { return crypto.timingSafeEqual(tokenBuf, keyBuf) } -export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { +export const authMiddleware = (req: Request, res: Response, next: NextFunction) => { const auth = req.header('authorization') || '' const xApiKey = req.header('x-api-key') || '' @@ -21,7 +20,7 @@ export const authMiddleware = async (req: Request, res: Response, next: NextFunc return res.status(401).json({ error: 'Unauthorized: missing credentials' }) } - const { apiKey } = await config.get() + const apiKey = preferenceService.get('feature.csaas.api_key') if (!apiKey) { return res.status(403).json({ error: 'Forbidden' }) diff --git a/src/main/apiServer/server.ts b/src/main/apiServer/server.ts index e59e6bd504..07a156cce8 100644 --- a/src/main/apiServer/server.ts +++ b/src/main/apiServer/server.ts @@ -1,11 +1,11 @@ import { createServer } from 'node:http' +import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' import { IpcChannel } from '@shared/IpcChannel' import { windowService } from '../services/WindowService' import { app } from './app' -import { config } from './config' const logger = loggerService.withContext('ApiServer') @@ -28,8 +28,9 @@ export class ApiServer { this.server = null } - // Load config - const { port, host } = await config.load() + // Load config from preference service + const port = preferenceService.get('feature.csaas.port') + const host = preferenceService.get('feature.csaas.host') // Create server with Express app this.server = createServer(app) @@ -78,7 +79,6 @@ export class ApiServer { async restart(): Promise { await this.stop() - await config.reload() await this.start() } diff --git a/src/main/services/ApiServerService.ts b/src/main/services/ApiServerService.ts index 40452b6d19..f70aab162e 100644 --- a/src/main/services/ApiServerService.ts +++ b/src/main/services/ApiServerService.ts @@ -1,3 +1,4 @@ +import { preferenceService } from '@data/PreferenceService' import { IpcChannel } from '@shared/IpcChannel' import type { ApiServerConfig, @@ -7,12 +8,14 @@ import type { StopApiServerStatusResult } from '@types' import { ipcMain } from 'electron' +import { v4 as uuidv4 } from 'uuid' import { apiServer } from '../apiServer' -import { config } from '../apiServer/config' import { loggerService } from './LoggerService' const logger = loggerService.withContext('ApiServerService') +//FIXME v2 refactor: ApiServer的启动/停止,是否运行的逻辑,现在比较乱。特别是在v2的新数据架构下,需要进一步优化,现在仅仅是做了简单的替换 +// 例如: 这样的warning:[mainWindow::PreferenceService] Attempted to confirm mismatched request for feature.csaas.enabled: expected req_1764650398717_0zb6wc350_feature.csaas.enabled, got req_1764650398704_41o5b5l1b_feature.csaas.enabled export class ApiServerService { constructor() { // Use the new clean implementation @@ -20,6 +23,8 @@ export class ApiServerService { async start(): Promise { try { + // Ensure valid API key before starting + await this.ensureValidApiKey() await apiServer.start() logger.info('API Server started successfully') } catch (error: any) { @@ -52,8 +57,36 @@ export class ApiServerService { return apiServer.isRunning() } - async getCurrentConfig(): Promise { - return config.get() + /** + * Get current API server configuration from preference service + */ + getCurrentConfig(): ApiServerConfig { + const config = preferenceService.getMultiple([ + 'feature.csaas.enabled', + 'feature.csaas.host', + 'feature.csaas.port', + 'feature.csaas.api_key' + ]) + + return { + enabled: config['feature.csaas.enabled'] ?? false, + host: config['feature.csaas.host'] ?? 'localhost', + port: config['feature.csaas.port'] ?? 23333, + apiKey: config['feature.csaas.api_key'] ?? '' + } + } + + /** + * Ensure a valid API key exists, generate one if null + */ + async ensureValidApiKey(): Promise { + let apiKey = preferenceService.get('feature.csaas.api_key') + if (apiKey === null) { + apiKey = `cs-sk-${uuidv4()}` + await preferenceService.set('feature.csaas.api_key', apiKey) + logger.info('Generated new API key') + } + return apiKey } registerIpcHandlers(): void { @@ -85,14 +118,15 @@ export class ApiServerService { } }) - ipcMain.handle(IpcChannel.ApiServer_GetStatus, async (): Promise => { + ipcMain.handle(IpcChannel.ApiServer_GetStatus, (): GetApiServerStatusResult => { try { - const config = await this.getCurrentConfig() + const config = this.getCurrentConfig() return { running: this.isRunning(), config } } catch (error: any) { + logger.error('IpcChannel.ApiServer_GetStatus', error as Error) return { running: this.isRunning(), config: null @@ -100,7 +134,7 @@ export class ApiServerService { } }) - ipcMain.handle(IpcChannel.ApiServer_GetConfig, async () => { + ipcMain.handle(IpcChannel.ApiServer_GetConfig, () => { try { return this.getCurrentConfig() } catch (error: any) { diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index e5cefadd68..40e972f8a1 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -12,8 +12,8 @@ import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk' import { query } from '@anthropic-ai/claude-agent-sdk' +import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' -import { config as apiConfigService } from '@main/apiServer/config' import { validateModelId } from '@main/apiServer/utils' import getLoginShellEnvironment from '@main/utils/shell-env' import { app } from 'electron' @@ -101,7 +101,11 @@ class ClaudeCodeService implements AgentServiceInterface { return aiStream } - const apiConfig = await apiConfigService.get() + const apiConfig = preferenceService.getMultiple([ + 'feature.csaas.host', + 'feature.csaas.port', + 'feature.csaas.api_key' + ]) const loginShellEnv = await getLoginShellEnvironment() const loginShellEnvWithoutProxies = Object.fromEntries( Object.entries(loginShellEnv).filter(([key]) => !key.toLowerCase().endsWith('_proxy')) @@ -110,9 +114,9 @@ class ClaudeCodeService implements AgentServiceInterface { const env = { ...loginShellEnvWithoutProxies, // TODO: fix the proxy api server - // ANTHROPIC_API_KEY: apiConfig.apiKey, - // ANTHROPIC_AUTH_TOKEN: apiConfig.apiKey, - // ANTHROPIC_BASE_URL: `http://${apiConfig.host}:${apiConfig.port}/${modelInfo.provider.id}`, + // ANTHROPIC_API_KEY: apiConfig['feature.csaas.api_key'], + // ANTHROPIC_AUTH_TOKEN: apiConfig['feature.csaas.api_key'], + // ANTHROPIC_BASE_URL: `http://${apiConfig['feature.csaas.host']}:${apiConfig['feature.csaas.port']}/${modelInfo.provider.id}`, ANTHROPIC_API_KEY: modelInfo.provider.apiKey, ANTHROPIC_AUTH_TOKEN: modelInfo.provider.apiKey, ANTHROPIC_BASE_URL: modelInfo.provider.anthropicApiHost?.trim() || modelInfo.provider.apiHost, @@ -269,9 +273,9 @@ class ClaudeCodeService implements AgentServiceInterface { for (const mcpId of session.mcps) { mcpList[mcpId] = { type: 'http', - url: `http://${apiConfig.host}:${apiConfig.port}/v1/mcps/${mcpId}/mcp`, + url: `http://${apiConfig['feature.csaas.host']}:${apiConfig['feature.csaas.port']}/v1/mcps/${mcpId}/mcp`, headers: { - Authorization: `Bearer ${apiConfig.apiKey}` + Authorization: `Bearer ${apiConfig['feature.csaas.api_key']}` } } } diff --git a/src/renderer/src/hooks/useApiServer.ts b/src/renderer/src/hooks/useApiServer.ts index bdf9f9d8de..353c5b9310 100644 --- a/src/renderer/src/hooks/useApiServer.ts +++ b/src/renderer/src/hooks/useApiServer.ts @@ -1,6 +1,5 @@ +import { useMultiplePreferences } from '@data/hooks/usePreference' import { loggerService } from '@logger' -import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setApiServerEnabled as setApiServerEnabledAction } from '@renderer/store/settings' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -28,11 +27,14 @@ const cleanupIpcIfEmpty = () => { export const useApiServer = () => { const { t } = useTranslation() - // FIXME: We currently store two copies of the config data in both the renderer and the main processes, - // which carries the risk of data inconsistency. This should be modified so that the main process stores - // the data, and the renderer retrieves it. - const apiServerConfig = useAppSelector((state) => state.settings.apiServer) - const dispatch = useAppDispatch() + + // Use new preference system for API server configuration + const [apiServerConfig, setApiServerConfig] = useMultiplePreferences({ + enabled: 'feature.csaas.enabled', + host: 'feature.csaas.host', + port: 'feature.csaas.port', + apiKey: 'feature.csaas.api_key' + }) // Initial state - no longer optimistic, wait for actual status const [apiServerRunning, setApiServerRunning] = useState(false) @@ -40,9 +42,9 @@ export const useApiServer = () => { const setApiServerEnabled = useCallback( (enabled: boolean) => { - dispatch(setApiServerEnabledAction(enabled)) + setApiServerConfig({ enabled }) }, - [dispatch] + [setApiServerConfig] ) // API Server functions @@ -51,15 +53,15 @@ export const useApiServer = () => { try { const status = await window.api.apiServer.getStatus() setApiServerRunning(status.running) - if (status.running && !apiServerConfig.enabled) { - setApiServerEnabled(true) - } + // if (status.running && !apiServerConfig.enabled) { + // setApiServerEnabled(true) + // } } catch (error: any) { logger.error('Failed to check API server status:', error) } finally { setApiServerLoading(false) } - }, [apiServerConfig.enabled, setApiServerEnabled]) + }, []) const startApiServer = useCallback(async () => { if (apiServerLoading) return @@ -153,6 +155,7 @@ export const useApiServer = () => { stopApiServer, restartApiServer, checkApiServerStatus, - setApiServerEnabled + setApiServerEnabled, + setApiServerConfig } } diff --git a/src/renderer/src/pages/home/Tabs/components/Topics.tsx b/src/renderer/src/pages/home/Tabs/components/Topics.tsx index 5420788ff4..d1a51fccde 100644 --- a/src/renderer/src/pages/home/Tabs/components/Topics.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Topics.tsx @@ -1,5 +1,5 @@ import { useCache } from '@data/hooks/useCache' -import { usePreference } from '@data/hooks/usePreference' +import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference' import { DraggableVirtualList } from '@renderer/components/DraggableList' import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' @@ -196,7 +196,19 @@ export const Topics: React.FC = ({ assistant: _assistant, activeTopic, se [setActiveTopic] ) - const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) + const [exportMenuOptions] = useMultiplePreferences({ + docx: 'data.export.menus.docx', + image: 'data.export.menus.image', + joplin: 'data.export.menus.joplin', + markdown: 'data.export.menus.markdown', + markdown_reason: 'data.export.menus.markdown_reason', + notes: 'data.export.menus.notes', + notion: 'data.export.menus.notion', + obsidian: 'data.export.menus.obsidian', + plain_text: 'data.export.menus.plain_text', + siyuan: 'data.export.menus.siyuan', + yuque: 'data.export.menus.yuque' + }) const [_targetTopic, setTargetTopic] = useState(null) const targetTopic = useDeferredValue(_targetTopic) diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index 8663d9625d..12fd1e6fc9 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -1,3 +1,4 @@ +import { useMultiplePreferences } from '@data/hooks/usePreference' import { loggerService } from '@logger' import HighlightText from '@renderer/components/HighlightText' import { DeleteIcon } from '@renderer/components/Icons' @@ -10,7 +11,6 @@ import NotesSidebarHeader from '@renderer/pages/notes/NotesSidebarHeader' import { fetchNoteSummary } from '@renderer/services/ApiService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import type { SearchMatch, SearchResult } from '@renderer/services/NotesSearchService' -import type { RootState } from '@renderer/store' import { useAppSelector } from '@renderer/store' import { selectSortType } from '@renderer/store/note' import type { NotesSortType, NotesTreeNode } from '@renderer/types/note' @@ -38,7 +38,6 @@ import { import type { FC, Ref } from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import styled from 'styled-components' import { useFullTextSearch } from './hooks/useFullTextSearch' @@ -348,7 +347,16 @@ const NotesSidebar: FC = ({ const { bases } = useKnowledgeBases() const { activeNode } = useActiveNode(notesTree) const sortType = useAppSelector(selectSortType) - const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) + const [exportMenuOptions] = useMultiplePreferences({ + docx: 'data.export.menus.docx', + image: 'data.export.menus.image', + joplin: 'data.export.menus.joplin', + markdown: 'data.export.menus.markdown', + notion: 'data.export.menus.notion', + obsidian: 'data.export.menus.obsidian', + siyuan: 'data.export.menus.siyuan', + yuque: 'data.export.menus.yuque' + }) const [editingNodeId, setEditingNodeId] = useState(null) const [renamingNodeIds, setRenamingNodeIds] = useState>(new Set()) const [newlyRenamedNodeIds, setNewlyRenamedNodeIds] = useState>(new Set()) diff --git a/src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/ApiServerSettings.tsx b/src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/ApiServerSettings.tsx index 0205ec676a..1d71c768b5 100644 --- a/src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/ApiServerSettings.tsx +++ b/src/renderer/src/pages/settings/ToolSettings/ApiServerSettings/ApiServerSettings.tsx @@ -1,14 +1,10 @@ import { useTheme } from '@renderer/context/ThemeProvider' import { useApiServer } from '@renderer/hooks/useApiServer' -import type { RootState } from '@renderer/store' -import { useAppDispatch } from '@renderer/store' -import { setApiServerApiKey, setApiServerPort } from '@renderer/store/settings' import { formatErrorMessage } from '@renderer/utils/error' import { Alert, Button, Input, InputNumber, Tooltip, Typography } from 'antd' import { Copy, ExternalLink, Play, RotateCcw, Square } from 'lucide-react' import type { FC } from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import styled from 'styled-components' import { v4 as uuidv4 } from 'uuid' @@ -18,13 +14,19 @@ const { Text, Title } = Typography const ApiServerSettings: FC = () => { const { theme } = useTheme() - const dispatch = useAppDispatch() const { t } = useTranslation() - // API Server state with proper defaults - const apiServerConfig = useSelector((state: RootState) => state.settings.apiServer) - const { apiServerRunning, apiServerLoading, startApiServer, stopApiServer, restartApiServer, setApiServerEnabled } = - useApiServer() + // API Server state from useApiServer hook + const { + apiServerConfig, + apiServerRunning, + apiServerLoading, + startApiServer, + stopApiServer, + restartApiServer, + setApiServerEnabled, + setApiServerConfig + } = useApiServer() const handleApiServerToggle = async (enabled: boolean) => { try { @@ -45,20 +47,26 @@ const ApiServerSettings: FC = () => { } const copyApiKey = () => { - navigator.clipboard.writeText(apiServerConfig.apiKey) + if (apiServerConfig.apiKey) { + navigator.clipboard.writeText(apiServerConfig.apiKey) + } window.toast.success(t('apiServer.messages.apiKeyCopied')) } + const generateApiKey = () => { + return `cs-sk-${uuidv4()}` + } + + const regenerateApiKey = () => { - const newApiKey = `cs-sk-${uuidv4()}` - dispatch(setApiServerApiKey(newApiKey)) + setApiServerConfig({ apiKey: generateApiKey() }) window.toast.success(t('apiServer.messages.apiKeyRegenerated')) } const handlePortChange = (value: string) => { const port = parseInt(value) || 23333 if (port >= 1000 && port <= 65535) { - dispatch(setApiServerPort(port)) + setApiServerConfig({ port }) } } @@ -152,7 +160,7 @@ const ApiServerSettings: FC = () => { {t('apiServer.fields.apiKey.description')}