@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
336 lines (283 loc) • 11.4 kB
text/typescript
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
import { safeStorage } from 'electron';
import querystring from 'node:querystring';
import { URL } from 'node:url';
import { OFFICIAL_CLOUD_SERVER } from '@/const/env';
import { createLogger } from '@/utils/logger';
import { ControllerModule, ipcClientEvent } from './index';
// Create logger
const logger = createLogger('controllers:RemoteServerConfigCtr');
/**
* Remote Server Configuration Controller
* Used to manage custom remote LobeChat server configuration
*/
export default class RemoteServerConfigCtr extends ControllerModule {
/**
* Key used to store encrypted tokens in electron-store.
*/
private readonly encryptedTokensKey = 'encryptedTokens';
/**
* Get remote server configuration
*/
async getRemoteServerConfig() {
logger.debug('Getting remote server configuration');
const { storeManager } = this.app;
const config: DataSyncConfig = storeManager.get('dataSyncConfig');
logger.debug(
`Remote server config: active=${config.active}, storageMode=${config.storageMode}, url=${config.remoteServerUrl}`,
);
return config;
}
/**
* Set remote server configuration
*/
async setRemoteServerConfig(config: Partial<DataSyncConfig>) {
logger.info(
`Setting remote server storageMode: active=${config.active}, storageMode=${config.storageMode}, url=${config.remoteServerUrl}`,
);
const { storeManager } = this.app;
const prev: DataSyncConfig = storeManager.get('dataSyncConfig');
// Save configuration
storeManager.set('dataSyncConfig', { ...prev, ...config });
return true;
}
/**
* Clear remote server configuration
*/
async clearRemoteServerConfig() {
logger.info('Clearing remote server configuration');
const { storeManager } = this.app;
// Clear instance configuration
storeManager.set('dataSyncConfig', { storageMode: 'local' });
// Clear tokens (if any)
await this.clearTokens();
return true;
}
/**
* Encrypted tokens
* Stored in memory for quick access, loaded from persistent storage on init.
*/
private encryptedAccessToken?: string;
private encryptedRefreshToken?: string;
/**
* Promise representing the ongoing token refresh operation.
* Used to prevent concurrent refreshes and allow callers to wait.
*/
private refreshPromise: Promise<{ error?: string; success: boolean }> | null = null;
/**
* Encrypt and store tokens
* @param accessToken Access token
* @param refreshToken Refresh token
*/
async saveTokens(accessToken: string, refreshToken: string) {
logger.info('Saving encrypted tokens');
// If platform doesn't support secure storage, store raw tokens
if (!safeStorage.isEncryptionAvailable()) {
logger.warn('Safe storage not available, storing tokens unencrypted');
this.encryptedAccessToken = accessToken;
this.encryptedRefreshToken = refreshToken;
// Persist unencrypted tokens (consider security implications)
this.app.storeManager.set(this.encryptedTokensKey, {
accessToken: this.encryptedAccessToken,
refreshToken: this.encryptedRefreshToken,
});
return;
}
// Encrypt tokens
logger.debug('Encrypting tokens using safe storage');
this.encryptedAccessToken = Buffer.from(safeStorage.encryptString(accessToken)).toString(
'base64',
);
this.encryptedRefreshToken = Buffer.from(safeStorage.encryptString(refreshToken)).toString(
'base64',
);
// Persist encrypted tokens
logger.debug(`Persisting encrypted tokens to store key: ${this.encryptedTokensKey}`);
this.app.storeManager.set(this.encryptedTokensKey, {
accessToken: this.encryptedAccessToken,
refreshToken: this.encryptedRefreshToken,
});
}
/**
* Get decrypted access token
*/
async getAccessToken(): Promise<string | null> {
// Try loading from memory first
if (!this.encryptedAccessToken) {
logger.debug('Access token not in memory, trying to load from store...');
this.loadTokensFromStore(); // Attempt to load from persistent storage
}
if (!this.encryptedAccessToken) {
logger.debug('No access token found in memory or store.');
return null;
}
// If platform doesn't support secure storage, return stored token
if (!safeStorage.isEncryptionAvailable()) {
logger.debug(
'Safe storage not available, returning potentially unencrypted token from memory/store',
);
return this.encryptedAccessToken;
}
try {
// Decrypt token
logger.debug('Decrypting access token');
const encryptedData = Buffer.from(this.encryptedAccessToken, 'base64');
return safeStorage.decryptString(encryptedData);
} catch (error) {
logger.error('Failed to decrypt access token:', error);
return null;
}
}
/**
* Get decrypted refresh token
*/
async getRefreshToken(): Promise<string | null> {
// Try loading from memory first
if (!this.encryptedRefreshToken) {
logger.debug('Refresh token not in memory, trying to load from store...');
this.loadTokensFromStore(); // Attempt to load from persistent storage
}
if (!this.encryptedRefreshToken) {
logger.debug('No refresh token found in memory or store.');
return null;
}
// If platform doesn't support secure storage, return stored token
if (!safeStorage.isEncryptionAvailable()) {
logger.debug(
'Safe storage not available, returning potentially unencrypted token from memory/store',
);
return this.encryptedRefreshToken;
}
try {
// Decrypt token
logger.debug('Decrypting refresh token');
const encryptedData = Buffer.from(this.encryptedRefreshToken, 'base64');
return safeStorage.decryptString(encryptedData);
} catch (error) {
logger.error('Failed to decrypt refresh token:', error);
return null;
}
}
/**
* Clear tokens
*/
async clearTokens() {
logger.info('Clearing access and refresh tokens');
this.encryptedAccessToken = undefined;
this.encryptedRefreshToken = undefined;
// Also clear from persistent storage
logger.debug(`Deleting tokens from store key: ${this.encryptedTokensKey}`);
this.app.storeManager.delete(this.encryptedTokensKey);
}
/**
* 刷新访问令牌
* 使用存储的刷新令牌获取新的访问令牌
* Handles concurrent requests by returning the existing refresh promise if one is in progress.
*/
async refreshAccessToken(): Promise<{ error?: string; success: boolean }> {
// If a refresh is already in progress, return the existing promise
if (this.refreshPromise) {
logger.debug('Token refresh already in progress, returning existing promise.');
return this.refreshPromise;
}
// Start a new refresh operation
logger.info('Initiating new token refresh operation.');
this.refreshPromise = this.performTokenRefresh();
// Return the promise so callers can wait
return this.refreshPromise;
}
/**
* Performs the actual token refresh logic.
* This method is called by refreshAccessToken and wrapped in a promise.
*/
private async performTokenRefresh(): Promise<{ error?: string; success: boolean }> {
try {
// 获取配置信息
const config = await this.getRemoteServerConfig();
if (!config.remoteServerUrl || !config.active) {
logger.warn('Remote server not active or configured, skipping refresh.');
return { error: '远程服务器未激活或未配置', success: false };
}
// 获取刷新令牌
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
logger.error('No refresh token available for refresh operation.');
return { error: '没有可用的刷新令牌', success: false };
}
// 构造刷新请求
const remoteUrl = await this.getRemoteServerUrl(config);
const tokenUrl = new URL('/oidc/token', remoteUrl);
// 构造请求体
const body = querystring.stringify({
client_id: 'lobehub-desktop',
grant_type: 'refresh_token',
refresh_token: refreshToken,
});
logger.debug(`Sending token refresh request to ${tokenUrl.toString()}`);
// 发送请求
const response = await fetch(tokenUrl.toString(), {
body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
if (!response.ok) {
// 尝试解析错误响应
const errorData = await response.json().catch(() => ({}));
const errorMessage = `刷新令牌失败: ${response.status} ${response.statusText} ${
errorData.error_description || errorData.error || ''
}`.trim();
logger.error(errorMessage, errorData);
return { error: errorMessage, success: false };
}
// 解析响应
const data = await response.json();
// 检查响应中是否包含必要令牌
if (!data.access_token || !data.refresh_token) {
logger.error('Refresh response missing access_token or refresh_token', data);
return { error: '刷新响应中缺少令牌', success: false };
}
// 保存新令牌
logger.info('Token refresh successful, saving new tokens.');
await this.saveTokens(data.access_token, data.refresh_token);
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Exception during token refresh operation:', errorMessage, error);
return { error: `刷新令牌时发生异常: ${errorMessage}`, success: false };
} finally {
// Ensure the promise reference is cleared once the operation completes
logger.debug('Clearing the refresh promise reference.');
this.refreshPromise = null;
}
}
/**
* Load encrypted tokens from persistent storage (electron-store) into memory.
* This should be called during initialization or if memory tokens are missing.
*/
private loadTokensFromStore() {
logger.debug(`Attempting to load tokens from store key: ${this.encryptedTokensKey}`);
const storedTokens = this.app.storeManager.get(this.encryptedTokensKey);
if (storedTokens && storedTokens.accessToken && storedTokens.refreshToken) {
logger.info('Successfully loaded tokens from store into memory.');
this.encryptedAccessToken = storedTokens.accessToken;
this.encryptedRefreshToken = storedTokens.refreshToken;
} else {
logger.debug('No valid tokens found in store.');
}
}
// Initialize by loading tokens from store when the controller is ready
// We might need a dedicated lifecycle method if constructor is too early for storeManager
afterAppReady() {
this.loadTokensFromStore();
}
async getRemoteServerUrl(config?: DataSyncConfig) {
const dataConfig = config ? config : await this.getRemoteServerConfig();
return dataConfig.storageMode === 'cloud' ? OFFICIAL_CLOUD_SERVER : dataConfig.remoteServerUrl;
}
}