Files
Edu/apps/portal-shell/src/features/teacher/ai/services/usage-tracker.ts
SpecialX 04b7a40bdc feat(portal-shell): 学生域全页面迁移与规范合规修复
- 学生域 32 页全量迁移(含作答/自动保存/提交/诊断)

- 补齐 4 个 MSW mock 缺口,修 diagnostic case 名

- 修 4 处 Tailwind 任意值;新增共享组件与路由
2026-08-31 11:25:21 +08:00

133 lines
3.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.
/**
* portal-shell 适配trackEvent/EventName 在客户端微前端不存在,
* 服务端埋点由 BFF 负责;此处保留为 no-op 占位以保持类型契约。
*/
type EventName = string;
const trackEvent = async (_event: {
event: EventName;
userId: string;
targetType?: string;
properties?: Record<string, unknown>;
}): Promise<void> => {
/* no-op: portal-shell client-side stub */
};
/**
* portal-shell 适配data-access/recordAiEvent 在客户端微前端不存在,
* 由 BFF 维护事件存储;此处保留为 no-op 占位。
*/
const recordAiEvent = (_event: {
userId: string;
capability: string;
success: boolean;
durationMs: number;
timestamp: number;
errorMessage?: string;
}): void => {
/* no-op: portal-shell client-side stub */
};
export type AiUsageEvent = {
userId: string;
capability:
| "chat"
| "similar_question"
| "grading_assist"
| "lesson_content"
| "question_variant"
| "weakness_analysis"
| "child_summary"
| "study_path"
| "explain_error";
providerId?: string;
model?: string;
success: boolean;
durationMs: number;
tokenUsage?: number;
errorMessage?: string;
};
const AI_EVENT_MAP: Record<AiUsageEvent["capability"], EventName> = {
chat: "ai.chat",
similar_question: "ai.similar_question",
grading_assist: "ai.grading_assist",
lesson_content: "ai.lesson_content",
question_variant: "ai.question_variant",
weakness_analysis: "ai.weakness_analysis",
child_summary: "ai.child_summary",
study_path: "ai.study_path",
explain_error: "ai.explain_error",
};
/**
* AI 使用埋点
*
* 记录每次 AI 调用的元数据,用于监控、成本分析与异常排查。
* 同时写入 data-access 层的内存事件存储(供管理员仪表盘聚合查询)。
* 非阻塞,失败不影响主流程。
*/
export const trackAiUsage = (event: AiUsageEvent): void => {
const eventName = AI_EVENT_MAP[event.capability];
// 写入 data-access 层(供 getAiUsageStats 聚合)
recordAiEvent({
userId: event.userId,
capability: event.capability,
success: event.success,
durationMs: event.durationMs,
timestamp: Date.now(),
errorMessage: event.errorMessage,
});
// 写入全局 trackEvent供外部监控系统
void trackEvent({
event: eventName,
userId: event.userId,
targetType: event.capability,
properties: {
providerId: event.providerId,
model: event.model,
success: event.success,
durationMs: event.durationMs,
tokenUsage: event.tokenUsage,
errorMessage: event.errorMessage,
},
}).catch(() => {
// 静默失败:埋点不应影响业务流程
});
};
/**
* 测量 AI 调用耗时并自动埋点
*/
export const withAiTracking = async <T>(
userId: string,
capability: AiUsageEvent["capability"],
providerId: string | undefined,
fn: () => Promise<{ result: T; model?: string; tokenUsage?: number }>,
): Promise<T> => {
const start = Date.now();
try {
const { result, model, tokenUsage } = await fn();
trackAiUsage({
userId,
capability,
providerId,
model,
success: true,
durationMs: Date.now() - start,
tokenUsage,
});
return result;
} catch (error) {
trackAiUsage({
userId,
capability,
providerId,
success: false,
durationMs: Date.now() - start,
errorMessage: error instanceof Error ? error.message : String(error),
});
throw error;
}
};