Files
zn-ai/electron/main.ts
DEV_DSW 78d3235ab6 feat: enhance token usage tracking and history management
- Updated HTML assets for improved loading.
- Integrated token usage tracking in chat processing, appending usage details to transcripts.
- Enhanced OpenAIProvider to include usage data in chat completion responses.
- Implemented asynchronous retrieval of recent token usage history.
- Added utility functions for managing transcript files and parsing usage data.
- Updated UI components to reflect changes in usage status handling.
- Ensured consistent usage status definitions across the application.
2026-04-15 11:45:33 +08:00

190 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { app, BrowserWindow, ipcMain } from 'electron'
import { CONFIG_KEYS } from '@lib/constants'
import { setupMainWindow } from './wins';
import started from 'electron-squirrel-startup'
import configManager from '@electron/service/config-service'
import { runTaskOperationService } from '@electron/process/runTaskOperationService'
import { initScriptStoreService } from '@electron/service/script-store-service'
import log from 'electron-log';
import 'bytenode'; // Ensure bytenode is bundled/externalized correctly
import { appUpdater } from '@electron/service/updater';
import axios from 'axios';
import { providerApiService, onProviderChange } from '@electron/service/provider-api-service';
import { gatewayManager } from '@electron/gateway/manager';
// 初始化 updater确保在 app ready 之前或者之中注册好 IPC
appUpdater.init();
// 注册 hostapi:fetch IPC 代理
// 模型管理相关接口在本地处理(对齐 ClawX其余接口代理到远端后端
const HOST_API_BASE_URL = process.env.VITE_SERVICE_URL || 'http://8.138.234.141/ingress';
async function handleLocalProviderApi(path: string, method: string, body: any) {
const parsedBody = typeof body === 'string' && body ? JSON.parse(body) : body;
if (path === '/api/provider-vendors' && method === 'GET') {
return { success: true, ok: true, json: providerApiService.getVendors(), data: providerApiService.getVendors() };
}
if (path === '/api/provider-accounts' && method === 'GET') {
return { success: true, ok: true, json: providerApiService.getAccounts(), data: providerApiService.getAccounts() };
}
if (path === '/api/providers' && method === 'GET') {
return { success: true, ok: true, json: providerApiService.getProviders(), data: providerApiService.getProviders() };
}
if (path === '/api/provider-accounts/default' && method === 'GET') {
return { success: true, ok: true, json: providerApiService.getDefault(), data: providerApiService.getDefault() };
}
if (path === '/api/provider-accounts' && method === 'POST') {
const result = providerApiService.createAccount(parsedBody || {});
return { success: true, ok: true, json: result, data: result };
}
if (path === '/api/provider-accounts/default' && method === 'PUT') {
const result = providerApiService.setDefault(parsedBody || {});
return { success: result.success, ok: result.success, json: result, data: result };
}
if (path.startsWith('/api/provider-accounts/') && method === 'PUT') {
const id = decodeURIComponent(path.replace('/api/provider-accounts/', ''));
const result = providerApiService.updateAccount(id, parsedBody || {});
return { success: result.success, ok: result.success, json: result, data: result };
}
if (path.startsWith('/api/provider-accounts/') && method === 'DELETE') {
const id = decodeURIComponent(path.replace('/api/provider-accounts/', ''));
const result = providerApiService.deleteAccount(id);
return { success: result.success, ok: result.success, json: result, data: result };
}
if (path === '/api/providers/default' && method === 'PUT') {
const result = providerApiService.setDefault({ accountId: parsedBody?.providerId });
return { success: result.success, ok: result.success, json: result, data: result };
}
if (path.startsWith('/api/providers/') && path.endsWith('/api-key') && method === 'GET') {
const id = decodeURIComponent(path.replace('/api/providers/', '').replace('/api-key', ''));
const result = providerApiService.getApiKey(id);
return { success: true, ok: true, json: result, data: result };
}
if (path.startsWith('/api/providers/') && method === 'PUT') {
// Provider updates are mapped to account updates for local storage
const id = decodeURIComponent(path.replace('/api/providers/', ''));
const result = providerApiService.updateAccount(id, parsedBody || {});
return { success: result.success, ok: result.success, json: result, data: result };
}
if (path.startsWith('/api/providers/') && method === 'DELETE') {
const [rawId, query] = path.replace('/api/providers/', '').split('?');
const id = decodeURIComponent(rawId);
if (query && query.includes('apiKeyOnly=1')) {
const result = providerApiService.deleteApiKey(id);
return { success: result.success, ok: result.success, json: result, data: result };
}
const result = providerApiService.deleteAccount(id);
return { success: result.success, ok: result.success, json: result, data: result };
}
if (path === '/api/providers/validate' && method === 'POST') {
const result = await providerApiService.validateApiKey(parsedBody || {});
return { success: true, ok: true, json: result, data: result };
}
if (path === '/api/usage/recent-token-history' && method === 'GET') {
const usageHistory = await providerApiService.getUsageHistory();
return { success: true, ok: true, json: usageHistory, data: usageHistory };
}
return null;
}
ipcMain.handle('hostapi:fetch', async (_event, { path, method, headers, body }) => {
// 1. 优先本地处理模型管理接口
const localResult = await handleLocalProviderApi(path, method || 'GET', body);
if (localResult) return localResult;
// 2. 其余接口代理到远端后端
const url = `${HOST_API_BASE_URL}${path}`;
try {
const response = await axios({
url,
method: method || 'GET',
headers: {
'Content-Type': 'application/json',
...headers,
},
data: body ?? undefined,
timeout: 30000,
});
return {
success: true,
ok: true,
json: response.data,
data: response.data,
};
} catch (error: any) {
if (error.response) {
return {
success: false,
ok: false,
status: error.response.status,
error: error.response.data?.message || error.message,
text: error.response.statusText,
data: error.response.data,
};
}
return {
success: false,
ok: false,
error: error.message || 'Unknown error',
};
}
});
// Gateway RPC IPC handler
ipcMain.handle('gateway:rpc', async (_event, method: string, params: any) => {
return gatewayManager.rpc(method, params);
});
// import logManager from '@electron/service/logger'
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (started) {
app.quit();
}
// process.on('uncaughtException', (err) => {
// logManager.error('uncaughtException', err);
// });
// process.on('unhandledRejection', (reason, promise) => {
// logManager.error('unhandledRejection', reason, promise);
// });
app.whenReady().then(() => {
gatewayManager.init();
onProviderChange(() => {
gatewayManager.reloadProviders();
});
setupMainWindow();
// 初始化脚本存储服务
initScriptStoreService()
// 开启任务操作子进程
runTaskOperationService()
// 开启subagent子进程
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin' && !configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY)) {
log.info('app closing due to all windows being closed');
app.quit();
}
});
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
setupMainWindow();
}
});