Skip to content

Commit fe2eb8c

Browse files
committed
Merge PR xiaoY233#85 from upstream
2 parents fe4721f + b48732c commit fe2eb8c

11 files changed

Lines changed: 530 additions & 56 deletions

File tree

src/main/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createTrayManager, TrayManager } from './tray/TrayManager'
55
import { registerIpcHandlers } from './ipc/handlers'
66
import { UpdaterManager } from './updater'
77
import { storeManager } from './store/store'
8+
import { logManager } from './logger/manager'
89

910
// Prevent uncaught exceptions from crashing the app
1011
process.on('uncaughtException', (error) => {
@@ -126,6 +127,7 @@ async function loadAppContent(mainWindow: BrowserWindow): Promise<void> {
126127

127128
function cleanup(): void {
128129
console.log('Application is exiting, performing cleanup...')
130+
logManager.flush()
129131
storeManager.flushPendingWrites()
130132
const updaterManager = UpdaterManager.getInstance()
131133
updaterManager.destroy()

src/main/ipc/handlers.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,30 @@ export async function registerIpcHandlers(mainWindow: BrowserWindow | null): Pro
193193
})
194194

195195
ipcMain.handle(IpcChannels.STORE_GET, async (_, key: string): Promise<unknown> => {
196+
if (key === 'logs' || key === 'requestLogs') {
197+
const logsStore = storeManager.getLogsStore()
198+
return logsStore?.get(key)
199+
}
196200
const store = storeManager.getStore()
197201
return store?.get(key)
198202
})
199203

200204
ipcMain.handle(IpcChannels.STORE_SET, async (_, key: string, value: unknown): Promise<void> => {
205+
if (key === 'logs' || key === 'requestLogs') {
206+
const logsStore = storeManager.getLogsStore()
207+
logsStore?.set(key as 'logs', value as never)
208+
return
209+
}
201210
const store = storeManager.getStore()
202211
store?.set(key as 'providers' | 'accounts' | 'config' | 'logs', value as never)
203212
})
204213

205214
ipcMain.handle(IpcChannels.STORE_DELETE, async (_, key: string): Promise<void> => {
215+
if (key === 'logs' || key === 'requestLogs') {
216+
const logsStore = storeManager.getLogsStore()
217+
logsStore?.delete(key as 'logs')
218+
return
219+
}
206220
const store = storeManager.getStore()
207221
store?.delete(key as 'providers' | 'accounts' | 'config' | 'logs')
208222
})

src/main/logger/manager.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,23 @@ interface LogTrend {
3131

3232
class LogManager {
3333
private logs: LogEntry[] = []
34+
private pendingLogs: LogEntry[] = []
3435
private logFile: string
3536
private maxLogs: number = 10000
3637
private retentionDays: number = 7
3738
private initialized: boolean = false
3839
private mainWindow: BrowserWindow | null = null
40+
private saveTimer: NodeJS.Timeout | null = null
41+
private readonly saveDelayMs = 2000
3942

4043
constructor() {
4144
const userDataPath = app.getPath('userData')
4245
const logDir = path.join(userDataPath, 'logs')
43-
46+
4447
if (!fs.existsSync(logDir)) {
4548
fs.mkdirSync(logDir, { recursive: true })
4649
}
47-
50+
4851
this.logFile = path.join(logDir, 'app.log')
4952
}
5053

@@ -116,18 +119,48 @@ class LogManager {
116119
}
117120

118121
this.logs.push(entry)
122+
this.pendingLogs.push(entry)
119123

120124
if (this.logs.length > this.maxLogs) {
121125
this.logs = this.logs.slice(-this.maxLogs)
122126
}
123127

124-
this.saveLogs().catch(console.error)
128+
this.scheduleSave()
125129

126130
this.mainWindow?.webContents.send(IpcChannels.LOGS_NEW_LOG, entry)
127131

128132
return entry
129133
}
130134

135+
private scheduleSave(): void {
136+
if (this.saveTimer) {
137+
clearTimeout(this.saveTimer)
138+
}
139+
140+
this.saveTimer = setTimeout(() => {
141+
this.saveTimer = null
142+
this.flushLogs()
143+
}, this.saveDelayMs)
144+
}
145+
146+
private flushLogs(): void {
147+
if (this.pendingLogs.length === 0) return
148+
149+
const toWrite = this.pendingLogs
150+
this.pendingLogs = []
151+
152+
const content = toWrite.map(log => JSON.stringify(log)).join('\n') + '\n'
153+
fs.appendFileSync(this.logFile, content, 'utf-8')
154+
}
155+
156+
async flush(): Promise<void> {
157+
if (this.saveTimer) {
158+
clearTimeout(this.saveTimer)
159+
this.saveTimer = null
160+
}
161+
this.flushLogs()
162+
}
163+
131164
info(message: string, data?: Parameters<LogManager['log']>[2]): LogEntry {
132165
return this.log('info', message, data)
133166
}
@@ -223,14 +256,20 @@ class LogManager {
223256

224257
async clearLogs(): Promise<void> {
225258
this.logs = []
259+
this.pendingLogs = []
260+
if (this.saveTimer) {
261+
clearTimeout(this.saveTimer)
262+
this.saveTimer = null
263+
}
226264
await this.saveLogs()
227265
}
228266

229267
async cleanOldLogs(): Promise<void> {
230268
const now = Date.now()
231269
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1000
232-
270+
233271
this.logs = this.logs.filter(log => now - log.timestamp < retentionMs)
272+
this.pendingLogs = this.pendingLogs.filter(log => now - log.timestamp < retentionMs)
234273
await this.saveLogs()
235274
}
236275

src/main/proxy/routes/completions.ts

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import Router from '@koa/router'
77
import type { Context } from 'koa'
8+
import { PassThrough } from 'stream'
89
import { loadBalancer } from '../loadbalancer'
910
import { requestForwarder } from '../forwarder'
1011
import { streamHandler } from '../stream'
@@ -33,6 +34,16 @@ function generateRequestId(): string {
3334
return `cmpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
3435
}
3536

37+
/**
38+
* Extract user input from prompt
39+
*/
40+
function extractUserInput(prompt: string | string[]): string | undefined {
41+
if (Array.isArray(prompt)) {
42+
return prompt.filter(p => p).join(' ')
43+
}
44+
return prompt || undefined
45+
}
46+
3647
/**
3748
* Convert prompt to messages format
3849
*/
@@ -159,6 +170,37 @@ router.post('/completions', async (ctx: Context) => {
159170
type: 'api_error',
160171
},
161172
}
173+
174+
storeManager.addLog('error', `Request failed: ${result.error}`, {
175+
requestId,
176+
providerId: provider.id,
177+
accountId: account.id,
178+
model: request.model,
179+
latency,
180+
})
181+
182+
storeManager.addRequestLog({
183+
timestamp: startTime,
184+
status: 'error',
185+
statusCode: result.status || 500,
186+
method: 'POST',
187+
url: '/v1/completions',
188+
model: request.model,
189+
actualModel,
190+
providerId: provider.id,
191+
providerName: provider.name,
192+
accountId: account.id,
193+
accountName: account.name,
194+
requestBody: JSON.stringify(request),
195+
userInput: extractUserInput(request.prompt),
196+
responseStatus: result.status || 500,
197+
latency,
198+
isStream: request.stream || false,
199+
errorMessage: result.error,
200+
})
201+
202+
storeManager.recordRequestInStats(false, latency, request.model, provider.id, account.id)
203+
162204
return
163205
}
164206

@@ -180,14 +222,82 @@ router.post('/completions', async (ctx: Context) => {
180222
isStream: request.stream,
181223
})
182224

225+
const userInput = extractUserInput(request.prompt)
226+
const responseBodyForLog = !request.stream && result.body
227+
? JSON.stringify(result.body)
228+
: undefined
229+
230+
let logEntryId: string | undefined
231+
232+
if (!request.stream) {
233+
const logEntry = storeManager.addRequestLog({
234+
timestamp: startTime,
235+
status: 'success',
236+
statusCode: 200,
237+
method: 'POST',
238+
url: '/v1/completions',
239+
model: request.model,
240+
actualModel,
241+
providerId: provider.id,
242+
providerName: provider.name,
243+
accountId: account.id,
244+
accountName: account.name,
245+
requestBody: JSON.stringify(request),
246+
userInput,
247+
responseStatus: 200,
248+
responseBody: responseBodyForLog,
249+
latency,
250+
isStream: false,
251+
})
252+
logEntryId = logEntry.id
253+
} else {
254+
const logEntry = storeManager.addRequestLog({
255+
timestamp: startTime,
256+
status: 'success',
257+
statusCode: 200,
258+
method: 'POST',
259+
url: '/v1/completions',
260+
model: request.model,
261+
actualModel,
262+
providerId: provider.id,
263+
providerName: provider.name,
264+
accountId: account.id,
265+
accountName: account.name,
266+
requestBody: JSON.stringify(request),
267+
userInput,
268+
responseStatus: 200,
269+
latency,
270+
isStream: true,
271+
})
272+
logEntryId = logEntry.id
273+
}
274+
275+
storeManager.recordRequestInStats(true, latency, request.model, provider.id, account.id)
276+
183277
if (request.stream && result.stream) {
184278
ctx.set('Content-Type', 'text/event-stream')
185279
ctx.set('Cache-Control', 'no-cache')
186280
ctx.set('Connection', 'keep-alive')
187281
ctx.set('X-Accel-Buffering', 'no')
188282

189283
const transformStream = streamHandler.createTransformStream(actualModel, requestId)
284+
285+
// Collect stream content for log update
286+
let collectedContent = ''
287+
transformStream.on('data', (chunk: Buffer) => {
288+
collectedContent += chunk.toString()
289+
})
290+
190291
result.stream.pipe(transformStream)
292+
293+
transformStream.once('end', () => {
294+
if (logEntryId) {
295+
storeManager.updateRequestLog(logEntryId, {
296+
responseBody: collectedContent || undefined,
297+
})
298+
}
299+
})
300+
191301
ctx.body = transformStream
192302
} else {
193303
ctx.set('Content-Type', 'application/json')
@@ -197,13 +307,46 @@ router.post('/completions', async (ctx: Context) => {
197307
const latency = Date.now() - startTime
198308
proxyStatusManager.recordRequestFailure(latency)
199309

310+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
311+
200312
ctx.status = 500
201313
ctx.body = {
202314
error: {
203-
message: error instanceof Error ? error.message : 'Unknown error',
315+
message: errorMessage,
204316
type: 'internal_error',
205317
},
206318
}
319+
320+
storeManager.addLog('error', `Request exception: ${errorMessage}`, {
321+
requestId,
322+
providerId: provider.id,
323+
accountId: account.id,
324+
model: request.model,
325+
latency,
326+
error: errorMessage,
327+
})
328+
329+
storeManager.addRequestLog({
330+
timestamp: startTime,
331+
status: 'error',
332+
statusCode: 500,
333+
method: 'POST',
334+
url: '/v1/completions',
335+
model: request.model,
336+
actualModel,
337+
providerId: provider.id,
338+
providerName: provider.name,
339+
accountId: account.id,
340+
accountName: account.name,
341+
requestBody: JSON.stringify(request),
342+
userInput: extractUserInput(request.prompt),
343+
responseStatus: 500,
344+
latency,
345+
isStream: request.stream || false,
346+
errorMessage,
347+
})
348+
349+
storeManager.recordRequestInStats(false, latency, request.model, provider.id, account.id)
207350
}
208351
})
209352

src/main/proxy/server.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { proxyStatusManager } from './status'
1313
import { storeManager } from '../store/store'
1414
import { sessionManager } from './sessionManager'
1515

16-
const SLOW_REQUEST_THRESHOLD_MS = 1500
16+
const SLOW_REQUEST_THRESHOLD_MS = 1000
1717

1818
/**
1919
* Proxy Server Class
@@ -131,12 +131,14 @@ export class ProxyServer {
131131
await next()
132132

133133
const latency = Date.now() - startTime
134-
const shouldRecordAccessLog =
135-
!ctx.path.startsWith('/v1/models') &&
136-
(ctx.status >= 400 || latency >= SLOW_REQUEST_THRESHOLD_MS)
137134

138-
if (shouldRecordAccessLog) {
139-
storeManager.addLog('warn', `${ctx.method} ${ctx.path} ${ctx.status} ${latency}ms`, {
135+
if (!ctx.path.startsWith('/v1/models')) {
136+
const level =
137+
ctx.status >= 400 ? 'error' :
138+
latency >= SLOW_REQUEST_THRESHOLD_MS ? 'warn' :
139+
'info'
140+
141+
storeManager.addLog(level, `${ctx.method} ${ctx.path} ${ctx.status} ${latency}ms`, {
140142
data: {
141143
method: ctx.method,
142144
path: ctx.path,

0 commit comments

Comments
 (0)