feat: add gateway management features and settings
- Implemented new API routes for handling logs and settings related to the gateway. - Added a new Gateway section in the General Settings panel to manage gateway status and auto-start options. - Introduced a state management hook for gateway settings, including status, logs, and auto-start functionality. - Updated configuration service to include gateway auto-start setting. - Enhanced internationalization support for new gateway-related messages. - Refactored existing settings store to accommodate new gateway settings. - Cleaned up code and improved logging functionality.
This commit is contained in:
@@ -11,7 +11,9 @@ import { handleCronRoutes } from './routes/cron';
|
||||
import { handleFileRoutes } from './routes/files';
|
||||
import { handleGatewayRoutes } from './routes/gateway';
|
||||
import { handleKnowledgeRoutes } from './routes/knowledge';
|
||||
import { handleLogRoutes } from './routes/logs';
|
||||
import { handleProviderRoutes } from './routes/providers';
|
||||
import { handleSettingsRoutes } from './routes/settings';
|
||||
import { handleSessionRoutes } from './routes/sessions';
|
||||
import { handleSkillRoutes } from './routes/skills';
|
||||
|
||||
@@ -27,8 +29,10 @@ const routeHandlers: RouteHandler[] = [
|
||||
handleCronRoutes,
|
||||
handleGatewayRoutes,
|
||||
handleKnowledgeRoutes,
|
||||
handleLogRoutes,
|
||||
handleFileRoutes,
|
||||
handleSessionRoutes,
|
||||
handleSettingsRoutes,
|
||||
handleSkillRoutes,
|
||||
];
|
||||
|
||||
|
||||
35
electron/api/routes/logs.ts
Normal file
35
electron/api/routes/logs.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import logManager from '@electron/service/logger';
|
||||
import type { HostApiResult } from '@src/types/runtime';
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||
import { fail, ok } from '../route-utils';
|
||||
|
||||
const DEFAULT_TAIL_LINES = 100;
|
||||
|
||||
function parseTailLines(rawValue: string | null): number {
|
||||
const parsed = Number(rawValue ?? DEFAULT_TAIL_LINES);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return DEFAULT_TAIL_LINES;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.floor(parsed));
|
||||
}
|
||||
|
||||
export async function handleLogRoutes(
|
||||
request: NormalizedHostApiRequest,
|
||||
_ctx: HostApiContext,
|
||||
): Promise<HostApiResult<unknown> | null> {
|
||||
const { pathname, method, url } = request;
|
||||
|
||||
if (pathname === '/api/logs' && method === 'GET') {
|
||||
try {
|
||||
return ok({
|
||||
content: await logManager.readRecentLogText(parseTailLines(url.searchParams.get('tailLines'))),
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
100
electron/api/routes/settings.ts
Normal file
100
electron/api/routes/settings.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { ConfigKeys, IConfig } from '@runtime/lib/types';
|
||||
import configManager from '@electron/service/config-service';
|
||||
import type { HostApiResult } from '@src/types/runtime';
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||
import { fail, ok, parseJsonBody } from '../route-utils';
|
||||
|
||||
function extractSettingKey(pathname: string): string | null {
|
||||
if (!pathname.startsWith('/api/settings/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawKey = pathname.slice('/api/settings/'.length).trim();
|
||||
if (!rawKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return decodeURIComponent(rawKey);
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function unwrapSettingValue(body: unknown): unknown {
|
||||
if (isPlainObject(body) && Object.prototype.hasOwnProperty.call(body, 'value')) {
|
||||
return body.value;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
export async function handleSettingsRoutes(
|
||||
request: NormalizedHostApiRequest,
|
||||
_ctx: HostApiContext,
|
||||
): Promise<HostApiResult<unknown> | null> {
|
||||
const { pathname, method } = request;
|
||||
|
||||
if (pathname === '/api/settings' && method === 'GET') {
|
||||
try {
|
||||
return ok(configManager.getConfig());
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/api/settings' && method === 'PUT') {
|
||||
try {
|
||||
const body = parseJsonBody<unknown>(request.body);
|
||||
if (!isPlainObject(body)) {
|
||||
return fail(400, 'settings payload must be an object');
|
||||
}
|
||||
|
||||
configManager.update(body as Partial<IConfig>);
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
settings: configManager.getConfig(),
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
const key = extractSettingKey(pathname);
|
||||
if (pathname.startsWith('/api/settings/') && !key) {
|
||||
return fail(400, 'setting key is required');
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (method === 'GET') {
|
||||
try {
|
||||
return ok({
|
||||
value: configManager.get(key as ConfigKeys),
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'PUT') {
|
||||
try {
|
||||
const value = unwrapSettingValue(parseJsonBody<unknown>(request.body));
|
||||
configManager.set(key as ConfigKeys, value);
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const api: WindowApi = {
|
||||
versions: process.versions,
|
||||
|
||||
external: {
|
||||
open: (url: string) => ipcRenderer.invoke('external-open', url)
|
||||
open: (url: string) => ipcRenderer.invoke('external-open', url),
|
||||
},
|
||||
|
||||
platform: process.platform,
|
||||
|
||||
@@ -6,7 +6,11 @@ import { debounce } from '@runtime/lib/utils'
|
||||
import logManager from '@electron/service/logger'
|
||||
import { getUserDataDir } from '@electron/utils/paths'
|
||||
|
||||
const DEFAULT_CONFIG: IConfig = {
|
||||
type AppConfig = IConfig & {
|
||||
[CONFIG_KEYS.GATEWAY_AUTO_START]: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: AppConfig = {
|
||||
[CONFIG_KEYS.THEME_MODE]: 'system',
|
||||
[CONFIG_KEYS.PRIMARY_COLOR]: '#BB5BE7',
|
||||
[CONFIG_KEYS.LANGUAGE]: 'zh',
|
||||
@@ -17,14 +21,15 @@ const DEFAULT_CONFIG: IConfig = {
|
||||
[CONFIG_KEYS.SELECTED_CHANNELS]: [],
|
||||
[CONFIG_KEYS.IMAGE_CACHE]: [],
|
||||
[CONFIG_KEYS.TASK_LIST]: [],
|
||||
[CONFIG_KEYS.GATEWAY_AUTO_START]: true,
|
||||
}
|
||||
|
||||
export class ConfigService {
|
||||
private static _instance: ConfigService;
|
||||
private _store: any;
|
||||
private _defaultConfig: IConfig = DEFAULT_CONFIG;
|
||||
private _defaultConfig: AppConfig = DEFAULT_CONFIG;
|
||||
|
||||
private _listeners: Array<(config: IConfig) => void> = [];
|
||||
private _listeners: Array<(config: AppConfig) => void> = [];
|
||||
|
||||
|
||||
private constructor() {
|
||||
@@ -34,7 +39,7 @@ export class ConfigService {
|
||||
public async init() {
|
||||
if (this._store) return;
|
||||
const { default: Store } = await import('electron-store');
|
||||
this._store = new Store<IConfig>({
|
||||
this._store = new Store<AppConfig>({
|
||||
name: 'config',
|
||||
cwd: getUserDataDir(),
|
||||
defaults: DEFAULT_CONFIG,
|
||||
@@ -74,7 +79,7 @@ export class ConfigService {
|
||||
this._listeners.forEach(listener => listener({ ...this._store.store }));
|
||||
}
|
||||
|
||||
public getConfig(): IConfig {
|
||||
public getConfig(): AppConfig {
|
||||
if (!this._store) {
|
||||
return { ...this._defaultConfig };
|
||||
}
|
||||
@@ -98,7 +103,7 @@ export class ConfigService {
|
||||
autoSave && this._notifyListeners();
|
||||
}
|
||||
|
||||
public update(updates: Partial<IConfig>, autoSave: boolean = true): void {
|
||||
public update(updates: Partial<AppConfig>, autoSave: boolean = true): void {
|
||||
this._ensureStore();
|
||||
(Object.keys(updates) as ConfigKeys[]).forEach((key) => {
|
||||
this._store.set(key, updates[key]);
|
||||
@@ -115,7 +120,7 @@ export class ConfigService {
|
||||
this._notifyListeners();
|
||||
}
|
||||
|
||||
public onConfigChange(listener: ((config: IConfig) => void)): () => void {
|
||||
public onConfigChange(listener: ((config: AppConfig) => void)): () => void {
|
||||
this._listeners.push(listener);
|
||||
|
||||
return () => this._listeners = this._listeners.filter(l => l !== listener);
|
||||
|
||||
@@ -13,6 +13,7 @@ const unlinkAsync = promisify(fs.unlink);
|
||||
|
||||
class LogService {
|
||||
private static _instance: LogService;
|
||||
private readonly logDirPath: string;
|
||||
|
||||
// 日志保留天数,默认7天
|
||||
private LOG_RETENTION_DAYS = 7;
|
||||
@@ -21,7 +22,8 @@ class LogService {
|
||||
private readonly CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
private constructor() {
|
||||
const logPath = path.join(getUserDataDir(), 'logs');
|
||||
this.logDirPath = path.join(getUserDataDir(), 'logs');
|
||||
const logPath = this.logDirPath;
|
||||
// c:users/{username}/AppData/Roaming/{appName}/logs
|
||||
|
||||
// 创建日志目录
|
||||
@@ -175,6 +177,54 @@ class LogService {
|
||||
this.info(`User Operation: ${operation} by ${userId}, Details: ${JSON.stringify(details)}`);
|
||||
}
|
||||
|
||||
public async readRecentLogText(tailLines: number = 200): Promise<string> {
|
||||
const safeTailLines = Number.isFinite(tailLines) ? Math.max(1, Math.floor(tailLines)) : 200;
|
||||
const filePath = this._getCurrentLogFilePath();
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const file = await fs.promises.open(filePath, 'r');
|
||||
|
||||
try {
|
||||
const fileStat = await file.stat();
|
||||
if (fileStat.size === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const chunkSize = 64 * 1024;
|
||||
let position = fileStat.size;
|
||||
let content = '';
|
||||
let lineCount = 0;
|
||||
|
||||
while (position > 0 && lineCount <= safeTailLines) {
|
||||
const bytesToRead = Math.min(chunkSize, position);
|
||||
position -= bytesToRead;
|
||||
|
||||
const buffer = Buffer.allocUnsafe(bytesToRead);
|
||||
await file.read(buffer, 0, bytesToRead, position);
|
||||
content = `${buffer.toString('utf-8')}${content}`;
|
||||
lineCount = content.split('\n').length - 1;
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
if (lines.length <= safeTailLines) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return lines.slice(-safeTailLines).join('\n');
|
||||
} finally {
|
||||
await file.close();
|
||||
}
|
||||
}
|
||||
|
||||
private _getCurrentLogFilePath(): string {
|
||||
const today = new Date();
|
||||
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
return path.join(this.logDirPath, `${formattedDate}.log`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const logManager = LogService.getInstance();
|
||||
|
||||
Reference in New Issue
Block a user