1
0
forked from boos/BoosAPI

feat: add createdAt timestamps to check-in logs and improve i18n translations

- Added createdAt timestamps to various check-in log entries in checkinService.ts for better tracking.
- Updated i18n translations in Models.tsx to improve user experience with accurate Chinese translations.
- Reduced default page size in ProgramLogs.tsx and ProxyLogs.tsx from 80 and 100 to 50 for better performance.
- Enhanced ProxyLogs.tsx with memoization for filtered logs and summary statistics.
- Implemented support for parsing Unix timestamps and PostgreSQL-style timezone offsets in checkinLogTime.ts.
- Added comprehensive tests for database proxy query wrapper in index.proxy-wrap.test.ts to ensure functionality.
This commit is contained in:
cita
2026-03-05 21:18:49 +08:00
parent 0d5e56f478
commit fdf4f30987
19 changed files with 313 additions and 156 deletions
+57
View File
@@ -0,0 +1,57 @@
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { beforeAll, describe, expect, it, vi } from 'vitest';
type DbModule = typeof import('./index.js');
describe('db proxy query wrapper', () => {
let testUtils: DbModule['__dbProxyTestUtils'];
beforeAll(async () => {
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), 'metapi-db-proxy-wrap-'));
const dbModule = await import('./index.js');
testUtils = dbModule.__dbProxyTestUtils;
});
it('wraps thenable query builders and provides all/get shims', async () => {
const execute = vi.fn(async () => [{ id: 1, name: 'demo' }]);
const queryLike = {
execute,
where() {
return this;
},
then(onFulfilled: (v: unknown) => unknown, onRejected?: (e: unknown) => unknown) {
return Promise.resolve(execute()).then(onFulfilled, onRejected);
},
};
expect(testUtils.shouldWrapObject(queryLike)).toBe(true);
const wrapped = testUtils.wrapQueryLike(queryLike as any);
const rows = await wrapped.where().all();
const row = await wrapped.where().get();
expect(execute).toHaveBeenCalledTimes(2);
expect(rows).toEqual([{ id: 1, name: 'demo' }]);
expect(row).toEqual({ id: 1, name: 'demo' });
});
it('provides run shim for thenable query builders', async () => {
const execute = vi.fn(async () => [{ changes: 3, lastInsertRowid: 9 }]);
const queryLike = {
execute,
then(onFulfilled: (v: unknown) => unknown, onRejected?: (e: unknown) => unknown) {
return Promise.resolve(execute()).then(onFulfilled, onRejected);
},
};
const wrapped = testUtils.wrapQueryLike(queryLike as any);
const runResult = await wrapped.run();
expect(execute).toHaveBeenCalledTimes(1);
expect(runResult).toEqual({ changes: 3, lastInsertRowid: 9 });
expect(testUtils.shouldWrapObject(Promise.resolve())).toBe(false);
});
});
+16 -3
View File
@@ -411,8 +411,16 @@ function normalizeRunResult(result: unknown): { changes: number; lastInsertRowid
const wrappedObjects = new WeakMap<object, unknown>();
function shouldWrapObject(value: unknown): value is object {
if (!value || typeof value !== 'object') return false;
// Drizzle query builders are thenable objects (QueryPromise) but are not native Promises.
// They still need wrapping so we can provide sqlite-style `.all/.get/.run` shims.
if (value instanceof Promise) return false;
return true;
}
function wrapQueryLike<T>(value: T): T {
if (!value || typeof value !== 'object') return value;
if (!shouldWrapObject(value)) return value;
const target = value as unknown as object;
if (wrappedObjects.has(target)) {
return wrappedObjects.get(target) as T;
@@ -446,7 +454,7 @@ function wrapQueryLike<T>(value: T): T {
return (...args: unknown[]) => {
const result = original.apply(innerTarget, args);
if (result && typeof result === 'object' && typeof (result as Promise<unknown>).then !== 'function') {
if (shouldWrapObject(result)) {
return wrapQueryLike(result);
}
return result;
@@ -483,7 +491,7 @@ function wrapDbClient<T extends object>(
return (...args: unknown[]) => {
const result = original.apply(target, args);
if (result && typeof result === 'object' && typeof (result as Promise<unknown>).then !== 'function') {
if (shouldWrapObject(result)) {
return wrapQueryLike(result);
}
return result;
@@ -636,3 +644,8 @@ export async function switchRuntimeDatabase(nextDialect: RuntimeDbDialect, nextD
throw error;
}
}
export const __dbProxyTestUtils = {
wrapQueryLike,
shouldWrapObject,
};
+16
View File
@@ -23,6 +23,7 @@ import { startProxyLogRetentionService, stopProxyLogRetentionService } from './s
import { buildStartupSummaryLines } from './services/startupInfo.js';
import { existsSync } from 'fs';
import { normalize, resolve, sep } from 'path';
import { eq, isNull, or } from 'drizzle-orm';
import { db, runtimeDbDialect, schema, switchRuntimeDatabase, type RuntimeDbDialect } from './db/index.js';
function toSettingsMap(rows: Array<{ key: string; value: string }>) {
@@ -182,6 +183,21 @@ try {
const finalRows = await db.select().from(schema.settings).all();
const finalMap = toSettingsMap(finalRows);
applyRuntimeSettings(finalMap);
const repairedAt = new Date().toISOString();
await db.update(schema.events)
.set({ createdAt: repairedAt })
.where(or(isNull(schema.events.createdAt), eq(schema.events.createdAt, '')))
.run();
await db.update(schema.proxyLogs)
.set({ createdAt: repairedAt })
.where(or(isNull(schema.proxyLogs.createdAt), eq(schema.proxyLogs.createdAt, '')))
.run();
await db.update(schema.checkinLogs)
.set({ createdAt: repairedAt })
.where(or(isNull(schema.checkinLogs.createdAt), eq(schema.checkinLogs.createdAt, '')))
.run();
console.log('Loaded runtime settings overrides');
} catch { /* first run, table may not exist */ }
+1
View File
@@ -274,6 +274,7 @@ async function appendTokenSyncEvent(result: SyncExecutionResult) {
level,
relatedId: result.accountId,
relatedType: 'account',
createdAt: new Date().toISOString(),
}).run();
} catch {}
}
+2
View File
@@ -244,6 +244,7 @@ export async function sitesRoutes(app: FastifyInstance) {
level: 'warning',
relatedId: id,
relatedType: 'site',
createdAt: new Date().toISOString(),
}).run();
} catch {}
} else {
@@ -260,6 +261,7 @@ export async function sitesRoutes(app: FastifyInstance) {
level: 'info',
relatedId: id,
relatedType: 'site',
createdAt: new Date().toISOString(),
}).run();
} catch {}
}
+1
View File
@@ -739,6 +739,7 @@ async function logProxy(
estimatedCost,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt: new Date().toISOString(),
}).run();
} catch (error) {
console.warn('[proxy/chat] failed to write proxy log', error);
+14 -13
View File
@@ -247,20 +247,21 @@ async function logProxy(
errorMessage,
});
await db.insert(schema.proxyLogs).values({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens,
completionTokens,
totalTokens,
estimatedCost,
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens,
completionTokens,
totalTokens,
estimatedCost,
errorMessage: normalizedErrorMessage,
retryCount,
retryCount,
createdAt: new Date().toISOString(),
}).run();
} catch (error) {
console.warn('[proxy/completions] failed to write proxy log', error);
+14 -13
View File
@@ -162,20 +162,21 @@ async function logProxy(
errorMessage,
});
await db.insert(schema.proxyLogs).values({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens,
completionTokens,
totalTokens,
estimatedCost,
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens,
completionTokens,
totalTokens,
estimatedCost,
errorMessage: normalizedErrorMessage,
retryCount,
retryCount,
createdAt: new Date().toISOString(),
}).run();
} catch (error) {
console.warn('[proxy/embeddings] failed to write proxy log', error);
+14 -13
View File
@@ -132,20 +132,21 @@ async function logProxy(
errorMessage,
});
await db.insert(schema.proxyLogs).values({
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
estimatedCost,
routeId: selected.channel.routeId,
channelId: selected.channel.id,
accountId: selected.account.id,
modelRequested,
modelActual: selected.actualModel,
status,
httpStatus,
latencyMs,
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
estimatedCost,
errorMessage: normalizedErrorMessage,
retryCount,
retryCount,
createdAt: new Date().toISOString(),
}).run();
} catch (error) {
console.warn('[proxy/images] failed to write proxy log', error);
+1
View File
@@ -1597,6 +1597,7 @@ async function logProxy(
estimatedCost,
errorMessage: normalizedErrorMessage,
retryCount,
createdAt: new Date().toISOString(),
}).run();
} catch (error) {
console.warn('[proxy/responses] failed to write proxy log', error);
+2
View File
@@ -22,6 +22,7 @@ export async function reportTokenExpired(params: {
level: 'error',
relatedId: params.accountId,
relatedType: 'account',
createdAt: new Date().toISOString(),
}).run();
await db.update(schema.accounts).set({
@@ -49,6 +50,7 @@ export async function reportProxyAllFailed(params: { model: string; reason: stri
message: `模型=${params.model}, 原因=${params.reason}`,
level: 'error',
relatedType: 'route',
createdAt: new Date().toISOString(),
}).run();
await sendNotification(
+4
View File
@@ -137,6 +137,7 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
accountId: account.id,
status: 'skipped',
message: 'site disabled',
createdAt: new Date().toISOString(),
}).run();
if (!options?.skipEvent) {
@@ -147,6 +148,7 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
level: 'info',
relatedId: accountId,
relatedType: 'account',
createdAt: new Date().toISOString(),
}).run();
}
@@ -245,6 +247,7 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
status: normalizedStatus,
message: logMessage,
reward: logReward,
createdAt: new Date().toISOString(),
}).run();
if (!options?.skipEvent) {
@@ -257,6 +260,7 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
level: effectiveSuccess ? 'info' : 'error',
relatedId: accountId,
relatedType: 'account',
createdAt: new Date().toISOString(),
}).run();
}