feat(api-ai): persist usage logs

This commit is contained in:
2026-04-06 12:42:56 +08:00
parent 2ca790abf9
commit 45177e9fad
6 changed files with 205 additions and 2 deletions
+60 -1
View File
@@ -15,7 +15,12 @@ import { PrismaService } from "../prisma/prisma.service";
import { AiProviderRegistryService } from "./ai-provider-registry.service";
import { AiChatDto } from "./dto/ai-chat.dto";
import { UpsertAiProviderBindingDto } from "./dto/upsert-ai-provider-binding.dto";
import { AiResolvedRouteCandidate, AiRouteAttempt, AiRouteFailureError } from "./ai.types";
import {
AiResolvedRouteCandidate,
AiRouteAttempt,
AiRouteFailureError,
AiUsageMetrics
} from "./ai.types";
type AiBindingSummary = {
id: string;
@@ -198,6 +203,7 @@ export class AiService {
}
const executor = this.aiProviderRegistryService.getExecutor(entry.candidate.channel);
const startedAt = Date.now();
try {
const result = await executor.execute(entry.candidate, {
@@ -205,6 +211,7 @@ export class AiService {
message: dto.message,
sessionId: dto.sessionId ?? null
});
const latencyMs = Date.now() - startedAt;
attempts.push({
channel: result.channel,
@@ -214,6 +221,16 @@ export class AiService {
reasonCode: null,
reasonMessage: null
});
await this.recordUsageLog({
userId,
channel: result.channel,
providerName: result.providerName,
model: result.model,
usage: result.usage,
latencyMs,
success: true,
errorCode: null
});
return {
channel: result.channel,
@@ -224,8 +241,19 @@ export class AiService {
attempts
};
} catch (error) {
const latencyMs = Date.now() - startedAt;
const failureAttempt = this.toFailureAttempt(entry.candidate, error);
attempts.push(failureAttempt);
await this.recordUsageLog({
userId,
channel: failureAttempt.channel,
providerName: failureAttempt.providerName,
model: failureAttempt.model,
usage: null,
latencyMs,
success: false,
errorCode: failureAttempt.reasonCode
});
this.logger.warn(
`AI 通道降级:channel=${failureAttempt.channel} provider=${failureAttempt.providerName ?? "unknown"} code=${failureAttempt.reasonCode ?? "UNKNOWN"} message=${failureAttempt.reasonMessage ?? "unknown"}`
);
@@ -464,4 +492,35 @@ export class AiService {
return `${secret.slice(0, 4)}***${secret.slice(-2)}`;
}
private async recordUsageLog(input: {
userId: string;
channel: AiChannel;
providerName: string | null;
model: string | null;
usage: AiUsageMetrics | null;
latencyMs: number;
success: boolean;
errorCode: string | null;
}): Promise<void> {
try {
await this.prismaService.aiUsageLog.create({
data: {
userId: input.userId,
channel: input.channel,
providerName: input.providerName,
model: input.model,
promptTokens: input.usage?.promptTokens ?? 0,
completionTokens: input.usage?.completionTokens ?? 0,
totalTokens: input.usage?.totalTokens ?? 0,
latencyMs: input.latencyMs,
success: input.success,
errorCode: input.errorCode
}
});
} catch (error) {
const message = error instanceof Error ? error.message : "未知错误";
this.logger.warn(`写入 AI 使用日志失败:${message}`);
}
}
}