Add multi-session auth and changelog tracking

这个提交包含在:
cryptocommuniums-afk
2026-03-15 17:30:19 +08:00
父节点 c4ec397ed3
当前提交 a9ea94fb78
修改 27 个文件,包含 1280 行新增89 行删除

查看文件

@@ -21,7 +21,8 @@ const isNonEmptyString = (value: unknown): value is string =>
export type SessionPayload = {
openId: string;
appId: string;
name: string;
name?: string;
sid?: string;
};
const EXCHANGE_TOKEN_PATH = `/webdev.v1.WebDevAuthPublicService/ExchangeToken`;
@@ -173,6 +174,7 @@ class SDKServer {
openId,
appId: ENV.appId,
name: options.name || "",
sid: crypto.randomUUID(),
},
options
);
@@ -190,7 +192,8 @@ class SDKServer {
return new SignJWT({
openId: payload.openId,
appId: payload.appId,
name: payload.name,
name: payload.name || "",
sid: payload.sid || crypto.randomUUID(),
})
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setExpirationTime(expirationSeconds)
@@ -199,7 +202,7 @@ class SDKServer {
async verifySession(
cookieValue: string | undefined | null
): Promise<{ openId: string; appId: string; name: string } | null> {
): Promise<{ openId: string; appId: string; name?: string; sid?: string } | null> {
if (!cookieValue) {
console.warn("[Auth] Missing session cookie");
return null;
@@ -210,12 +213,11 @@ class SDKServer {
const { payload } = await jwtVerify(cookieValue, secretKey, {
algorithms: ["HS256"],
});
const { openId, appId, name } = payload as Record<string, unknown>;
const { openId, appId, name, sid } = payload as Record<string, unknown>;
if (
!isNonEmptyString(openId) ||
!isNonEmptyString(appId) ||
!isNonEmptyString(name)
!isNonEmptyString(appId)
) {
console.warn("[Auth] Session payload missing required fields");
return null;
@@ -224,7 +226,8 @@ class SDKServer {
return {
openId,
appId,
name,
name: typeof name === "string" ? name : undefined,
sid: typeof sid === "string" ? sid : undefined,
};
} catch (error) {
console.warn("[Auth] Session verification failed", String(error));

查看文件

@@ -6,6 +6,16 @@ export type RemoteMediaSession = {
userId: string;
title: string;
archiveStatus: "idle" | "queued" | "processing" | "completed" | "failed";
previewStatus?: "idle" | "processing" | "ready" | "failed";
previewSegments?: number;
markers?: Array<{
id: string;
type: string;
label: string;
timestampMs: number;
confidence?: number;
createdAt: string;
}>;
playback: {
webmUrl?: string;
mp4Url?: string;

查看文件

@@ -664,6 +664,11 @@ export const appRouter = router({
exerciseType: z.string().optional(),
sessionMode: z.enum(["practice", "pk"]).default("practice"),
durationMinutes: z.number().min(1).max(720).optional(),
actionCount: z.number().min(0).max(100000).optional(),
actionSummary: z.record(z.string(), z.number()).optional(),
dominantAction: z.string().optional(),
validityStatus: z.enum(["pending", "valid", "valid_manual", "invalid_auto", "invalid_manual"]).optional(),
invalidReason: z.string().max(512).optional(),
}))
.mutation(async ({ ctx, input }) => {
return enqueueTask({

查看文件

@@ -34,8 +34,13 @@ type StructuredParams<T> = {
};
};
parse: (content: unknown) => T;
timeoutMs?: number;
retryCount?: number;
};
const TRAINING_PLAN_LLM_TIMEOUT_MS = Math.max(ENV.llmTimeoutMs, 120_000);
const TRAINING_PLAN_LLM_RETRY_COUNT = Math.max(ENV.llmRetryCount, 2);
async function invokeStructured<T>(params: StructuredParams<T>) {
let lastError: unknown;
@@ -56,6 +61,8 @@ async function invokeStructured<T>(params: StructuredParams<T>) {
model: params.model,
messages: [...params.baseMessages, ...retryHint],
response_format: params.responseFormat,
timeoutMs: params.timeoutMs,
retryCount: params.retryCount,
});
try {
@@ -136,6 +143,17 @@ async function runTrainingPlanGenerateTask(task: NonNullable<TaskRow>) {
durationDays: number;
focusAreas?: string[];
};
const user = await db.getUserById(task.userId);
if (!user) {
throw new Error("User not found");
}
const latestSnapshot = await db.getLatestNtrpSnapshot(task.userId);
const trainingProfileStatus = db.getTrainingProfileStatus(user, latestSnapshot);
if (!trainingProfileStatus.isComplete) {
const missingLabels = trainingProfileStatus.missingFields.map((field) => db.TRAINING_PROFILE_FIELD_LABELS[field]).join("、");
throw new Error(`训练计划生成前请先完善训练档案:${missingLabels}`);
}
const analyses = await db.getUserAnalyses(task.userId);
const recentScores = analyses.slice(0, 5).map((analysis) => ({
score: analysis.overallScore ?? null,
@@ -154,6 +172,9 @@ async function runTrainingPlanGenerateTask(task: NonNullable<TaskRow>) {
content: buildTrainingPlanPrompt({
...payload,
recentScores,
effectiveNtrpRating: trainingProfileStatus.effectiveNtrp,
ntrpSource: trainingProfileStatus.ntrpSource,
assessmentSnapshot: trainingProfileStatus.assessmentSnapshot,
}),
},
],
@@ -194,6 +215,8 @@ async function runTrainingPlanGenerateTask(task: NonNullable<TaskRow>) {
content,
fallbackTitle: `${payload.durationDays}天训练计划`,
}),
timeoutMs: TRAINING_PLAN_LLM_TIMEOUT_MS,
retryCount: TRAINING_PLAN_LLM_RETRY_COUNT,
});
const planId = await db.createTrainingPlan({
@@ -280,6 +303,8 @@ async function runTrainingPlanAdjustTask(task: NonNullable<TaskRow>) {
content,
fallbackTitle: currentPlan.title,
}),
timeoutMs: TRAINING_PLAN_LLM_TIMEOUT_MS,
retryCount: TRAINING_PLAN_LLM_RETRY_COUNT,
});
await db.updateTrainingPlan(payload.planId, {
@@ -418,6 +443,11 @@ async function runMediaFinalizeTask(task: NonNullable<TaskRow>) {
exerciseType?: string;
sessionMode?: "practice" | "pk";
durationMinutes?: number;
actionCount?: number;
actionSummary?: Record<string, number>;
dominantAction?: string;
validityStatus?: string;
invalidReason?: string;
};
const session = await getRemoteMediaSession(payload.sessionId);
@@ -495,6 +525,11 @@ async function runMediaFinalizeTask(task: NonNullable<TaskRow>) {
title: payload.title || session.title,
sessionMode: payload.sessionMode || "practice",
durationMinutes: payload.durationMinutes ?? 5,
actionCount: payload.actionCount ?? 0,
actionSummary: payload.actionSummary ?? {},
dominantAction: payload.dominantAction ?? null,
validityStatus: payload.validityStatus ?? "pending",
invalidReason: payload.invalidReason ?? null,
});
return {

查看文件

@@ -199,21 +199,28 @@ export async function syncRecordingTrainingData(input: {
title: string;
sessionMode?: "practice" | "pk";
durationMinutes?: number | null;
actionCount?: number | null;
actionSummary?: Record<string, number> | null;
dominantAction?: string | null;
validityStatus?: string | null;
invalidReason?: string | null;
}) {
const trainingDate = db.getDateKey();
const planMatch = await db.matchActivePlanForExercise(input.userId, input.exerciseType);
const exerciseLabel = ACTION_LABELS[input.exerciseType || "unknown"] || input.exerciseType || input.title;
const resolvedExerciseType = input.exerciseType || input.dominantAction || "recording";
const planMatch = await db.matchActivePlanForExercise(input.userId, resolvedExerciseType);
const exerciseLabel = ACTION_LABELS[resolvedExerciseType || "unknown"] || resolvedExerciseType || input.title;
const totalActions = Math.max(0, input.actionCount ?? 0);
const recordResult = await db.upsertTrainingRecordBySource({
userId: input.userId,
planId: planMatch?.planId ?? null,
linkedPlanId: planMatch?.planId ?? null,
matchConfidence: planMatch?.confidence ?? null,
exerciseName: exerciseLabel,
exerciseType: input.exerciseType || "unknown",
exerciseType: resolvedExerciseType,
sourceType: "recording",
sourceId: `recording:${input.videoId}`,
videoId: input.videoId,
actionCount: 0,
actionCount: totalActions,
durationMinutes: Math.max(1, input.durationMinutes ?? 5),
completed: 1,
poseScore: null,
@@ -222,8 +229,15 @@ export async function syncRecordingTrainingData(input: {
source: "recording",
sessionMode: input.sessionMode || "practice",
title: input.title,
actionCount: totalActions,
actionSummary: input.actionSummary ?? {},
dominantAction: input.dominantAction ?? null,
validityStatus: input.validityStatus ?? "pending",
invalidReason: input.invalidReason ?? null,
},
notes: "自动写入:录制归档",
notes: input.validityStatus?.startsWith("invalid")
? `自动写入:录制归档(无效录制)${input.invalidReason ? ` · ${input.invalidReason}` : ""}`
: "自动写入:录制归档",
});
if (recordResult.isNew) {
@@ -234,7 +248,12 @@ export async function syncRecordingTrainingData(input: {
deltaSessions: 1,
deltaRecordingCount: 1,
deltaPkCount: input.sessionMode === "pk" ? 1 : 0,
metadata: { latestRecordingExerciseType: input.exerciseType || "unknown" },
deltaTotalActions: totalActions,
deltaEffectiveActions: input.validityStatus?.startsWith("invalid") ? 0 : totalActions,
metadata: {
latestRecordingExerciseType: resolvedExerciseType,
latestRecordingValidity: input.validityStatus ?? "pending",
},
});
}