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:
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 */ }
|
||||
|
||||
|
||||
@@ -274,6 +274,7 @@ async function appendTokenSyncEvent(result: SyncExecutionResult) {
|
||||
level,
|
||||
relatedId: result.accountId,
|
||||
relatedType: 'account',
|
||||
createdAt: new Date().toISOString(),
|
||||
}).run();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user