diff --git a/src/app/(panel)/_components/instance-info-card.tsx b/src/app/(panel)/_components/instance-info-card.tsx index 5a4d4d1..3b4edfc 100644 --- a/src/app/(panel)/_components/instance-info-card.tsx +++ b/src/app/(panel)/_components/instance-info-card.tsx @@ -10,6 +10,8 @@ import { ChatGPTSharedInstanceGpt4UsageList } from "./chatgpt-shared/chatgpt-gpt import { type ServiceInstance } from "@/schema/serviceInstance.schema"; import { PoekmonAPIInstanceUsageStatistics } from "./poekmon-api/poekmon-api-statistics"; import { PoekmonAPIModelUsage } from "./poekmon-api/poekmon-api-model-usage"; +import { PoekmonSharedInstanceUsageStatistics } from "./poekmon-shared/poekmon-shared-statistics"; +import PoekmonSharedAccountUsage from "./poekmon-shared/poekmon-shared-account-usage"; interface Props extends React.HTMLAttributes { instance: ServiceInstance; @@ -26,8 +28,6 @@ export function SharedChatGPTCardContent({ instance }: { instance: ServiceInstan } export function PoekmonAPICardContent({ instance }: { instance: ServiceInstance }) { - - return (
@@ -36,6 +36,15 @@ export function PoekmonAPICardContent({ instance }: { instance: ServiceInstance ); } +export function PoekmonSharedCardContent({ instance }: { instance: ServiceInstance }) { + return ( +
+ + +
+ ); +} + export function InstanceInfoCard({ instance, className, children }: Props) { return ( @@ -48,6 +57,7 @@ export function InstanceInfoCard({ instance, className, children }: Props) { {instance.type === "CHATGPT_SHARED" && } {instance.type === "POEKMON_API" && } + {instance.type === "POEKMON_SHARED" && } {children} diff --git a/src/app/(panel)/_components/poekmon-shared/poekmon-shared-account-usage.tsx b/src/app/(panel)/_components/poekmon-shared/poekmon-shared-account-usage.tsx new file mode 100644 index 0000000..58a53ba --- /dev/null +++ b/src/app/(panel)/_components/poekmon-shared/poekmon-shared-account-usage.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Progress } from "@/components/ui/progress"; +import StatusLabel from "@/components/custom/status-label"; +import { type PoekmonSharedInstanceData } from "@/schema/service/poekmon-shared.schema"; +import { api } from "@/trpc/react"; + +export default function PoekmonSharedAccountUsage({ + instanceId, + className, +}: { + instanceId: string; + className?: string; +}) { + const accountInfoQuery = api.serviceInstance.getPoekmonSharedAccountInfo.useQuery({ id: instanceId }); + + const getBackgroundColor = (value: number) => { + let color; + if (value < 0.2) { + color = "bg-green-500"; + } else if (value < 0.6) { + color = "bg-yellow-500"; + } else if (value < 0.9) { + color = "bg-orange-500"; + } else { + color = "bg-red-500"; + } + return color; + }; + + let alloment = "n/a"; + let balance = "n/a"; + let percentage = 0; + let message_point_reset_time = "n/a"; + const accountInfo = accountInfoQuery.data; + if (accountInfo !== null && accountInfo !== undefined) { + alloment = accountInfo.message_point_alloment.toString(); + balance = accountInfo.message_point_balance.toString(); + if (accountInfo.message_point_alloment !== 0) { + percentage = 1 - accountInfo.message_point_balance / accountInfo.message_point_alloment; + } + message_point_reset_time = new Date(accountInfo.message_point_reset_time / 1000).toLocaleString(); + } + + return ( +
+ Poe 账号概况 +
+
+ 积分使用情况 +
+ + {balance} / {alloment} + + +
+
+
+ 积分刷新时间 +
+ {message_point_reset_time ?? "n/a"} +
+
+
+ 订阅状态 +
+ {accountInfo?.subscription_active ? "活跃" : "无"} +
+
+
+
+ ); +} diff --git a/src/app/(panel)/_components/poekmon-shared/poekmon-shared-statistics.tsx b/src/app/(panel)/_components/poekmon-shared/poekmon-shared-statistics.tsx new file mode 100644 index 0000000..c63f7c7 --- /dev/null +++ b/src/app/(panel)/_components/poekmon-shared/poekmon-shared-statistics.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { Icons } from "@/components/icons"; +import { cn } from "@/lib/utils"; +import { api } from "@/trpc/react"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; + +export function PoekmonSharedInstanceUsageStatistics({ + instanceId, + className, +}: { + instanceId: string; + className?: string; +}) { + const sumResult = api.resourceLog.sumPoekmonSharedResourceLogsInDurationWindowsByInstance.useQuery({ + durationWindows: ["10m", "1h", "8h", "24h"], + instanceId, + }); + + const allLogsAreEmpty = sumResult.data?.every((item) => item.stats.count === 0); + + return ( +
+ 最近使用情况 + {!allLogsAreEmpty && ( +
+ {sumResult.data?.map((item) => ( +
+
Last {item.durationWindow}
+ + + +
+ {/* {item.stats.userCount} + {item.stats.count} + {item.stats.sumUtf8Length ?? 0} */} +
+ {" "} + {item.stats.userCount} +
+
+ {" "} + {item.stats.count} +
+
+ {" "} + + {item.stats.sumPoints} +
+
+
+ +

+ 最近 {item.durationWindow}:{item.stats.userCount} 用户使用,请求 {item.stats.count} 次,消耗{" "} + {item.stats.sumPoints} 积分点数 +

+
+
+
+
+ ))} +
+ )} + {allLogsAreEmpty && ( +
+ No statistics available +
+ )} +
+ ); +} diff --git a/src/app/api/external/poekmon-shared/[instanceId]/[[...slug]]/hono-router.ts b/src/app/api/external/poekmon-shared/[instanceId]/[[...slug]]/hono-router.ts index 9a86528..d12991a 100644 --- a/src/app/api/external/poekmon-shared/[instanceId]/[[...slug]]/hono-router.ts +++ b/src/app/api/external/poekmon-shared/[instanceId]/[[...slug]]/hono-router.ts @@ -346,17 +346,33 @@ export const extendRouter = (router: Router) => { ...data, } as PoekmonSharedResourceUsageLogDetails; - await db - .insert(resourceUsageLogs) - .values({ + await db.transaction(async (tx) => { + await tx.insert(resourceUsageLogs).values({ id: createCUID(), userId: data.user_id, instanceId: c.var.instanceId, type: ServiceTypeSchema.Values.POEKMON_SHARED, details: logDetail, createdAt: new Date(), - }) - .returning(); + }); + + if (logDetail.status === "success") { + const instance = await tx.query.serviceInstances.findFirst({ + where: eq(serviceInstances.id, c.var.instanceId), + }); + const instanceData = instance!.data as PoekmonSharedInstanceData; + if (instanceData.poe_account.account_info !== null) { + instanceData.poe_account.account_info.message_point_balance -= logDetail.consume_point; + } + await tx + .update(serviceInstances) + .set({ + data: instanceData, + }) + .where(eq(serviceInstances.id, c.var.instanceId)); + } + }); + return c.json(ok()); }); diff --git a/src/schema/service/poekmon-shared.schema.ts b/src/schema/service/poekmon-shared.schema.ts index 8b92241..52480e4 100644 --- a/src/schema/service/poekmon-shared.schema.ts +++ b/src/schema/service/poekmon-shared.schema.ts @@ -3,26 +3,33 @@ import { z } from "zod"; import { createSelectSchema } from "drizzle-zod"; import { resourceUsageLogs } from "@/server/db/schema"; +export const PoekmonSharedAccountInfoSchema = z.object({ + account_email: z.string(), + subscription_active: z.boolean(), + subscription_plan_type: z.string().nullable(), + subscription_expires_time: z.number().int().nullable(), + message_point_balance: z.number().int(), + message_point_alloment: z.number().int(), + message_point_reset_time: z.number().int(), +}); + +export const PoekmonSharedAccountInfoUserReadableSchema = PoekmonSharedAccountInfoSchema.omit({ + account_email: true, + subscription_plan_type: true, + subscription_expires_time: true, +}); + export const PoekmonSharedAccountSchema = z.object({ status: z.enum(["active", "inactive", "initialized"]), - account_email: z.string().nullable(), - account_info: z - .object({ - subscription_active: z.boolean(), - subscription_plan_type: z.string(), - subscription_expires_time: z.number().int(), - message_point_balance: z.number().int(), - message_point_alloment: z.number().int(), - message_point_reset_time: z.number().int(), - }) - .nullable(), + account_info: PoekmonSharedAccountInfoSchema.nullable(), + account_info_dirty: z.boolean(), }); export type PoekmonSharedAccount = z.infer; export const defaultPoekmonSharedAccount = (): PoekmonSharedAccount => ({ status: "initialized", - account_email: null, account_info: null, + account_info_dirty: false, }); export const PoekmonSharedInstanceDataSchema = z.object({ @@ -62,18 +69,6 @@ export const PoekmonSharedResourceLogSumResultSchema = z.object({ }); export type PoekmonSharedResourceLogSumResult = z.infer; -export const PoekmonSharedLogGroupbyModelResultSchema = z.object({ - durationWindow: DurationWindowSchema, - groups: z.array( - z.object({ - model: z.string(), - count: z.number().int(), - sumTotalPoints: z.number().int(), - }), - ), -}); -export type PoekmonSharedLogGroupbyModelResult = z.infer; - export const PoekmonSharedResourceUsageLogSchema = createSelectSchema(resourceUsageLogs).merge( z.object({ details: PoekmonSharedResourceUsageLogDetailsSchema, diff --git a/src/server/api/routers/resourceLog.ts b/src/server/api/routers/resourceLog.ts index a7b1c56..2d92572 100644 --- a/src/server/api/routers/resourceLog.ts +++ b/src/server/api/routers/resourceLog.ts @@ -4,230 +4,14 @@ import { z } from "zod"; import { TRPCError } from "@trpc/server"; import { ResourceUsageLogWhereInputSchema, ResourceUsageLogSchema } from "@/schema/resourceLog.schema"; import { paginateQuery } from "../pagination"; -import { type SQL, and, count, eq, gte, lte, sql, countDistinct, desc, getTableColumns } from "drizzle-orm"; +import { type SQL, and, count, eq, gte, lte, desc, getTableColumns } from "drizzle-orm"; import { resourceUsageLogs, serviceInstances, users } from "@/server/db/schema"; import { UserRoles } from "@/schema/user.schema"; -import { DURATION_WINDOWS, type DurationWindow, DurationWindowSchema, ServiceTypeSchema } from "@/server/db/enum"; +import { DurationWindowSchema } from "@/server/db/enum"; import { alignTimeToGranularity } from "@/lib/utils"; -import { memoize } from "@/lib/memoize"; -import { - type ChatGPTSharedGPT4LogGroupbyAccountResult, - type ChatGPTSharedResourceLogSumResult, -} from "@/schema/service/chatgpt-shared.schema"; -import { - type PoekmonAPILogGroupbyModelResult, - type PoekmonAPIResourceLogSumResult, -} from "@/schema/service/poekmon-api.schema"; +import { groupGPT4LogsInDurationWindow, groupPoekmonAPILogsInDurationWindowByModel, sumChatGPTSharedLogsInDurationWindows, sumPoekmonAPILogsInDurationWindows, sumPoekmonSharedLogsInDurationWindows } from "./resourceLogHelper"; -const _sumChatGPTSharedLogsInDurationWindows = async ({ - ctx, - durationWindows, - timeEnd, - userId, - instanceId, -}: { - ctx: TRPCContext; - durationWindows: DurationWindow[]; - timeEnd: Date; - userId?: string; - instanceId?: string; -}): Promise => { - const results = [] as ChatGPTSharedResourceLogSumResult[]; - - for (const durationWindow of durationWindows) { - const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; - const aggResult = await ctx.db - .select({ - userCount: countDistinct(resourceUsageLogs.userId), - count: count(), - sumUtf8Length: sql`sum(${resourceUsageLogs.textBytes})`.mapWith(Number), - // sumTokensLength: sum(resourceUsageLogs.tokensLength).mapWith(Number), - sumTokensLength: sql`0`.mapWith(Number), // todo - }) - .from(resourceUsageLogs) - .where( - and( - eq(resourceUsageLogs.type, ServiceTypeSchema.Values.CHATGPT_SHARED), - gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), - userId ? eq(resourceUsageLogs.userId, userId) : sql`true`, - instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, - ), - ); - // .groupBy(resourceUsageLogs.userId); - - results.push({ - durationWindow, - stats: aggResult[0]!, - }); - } - - return results; -}; - -const sumChatGPTSharedLogsInDurationWindows = memoize(_sumChatGPTSharedLogsInDurationWindows, { - genKey: ({ durationWindows, timeEnd, userId, instanceId }) => { - return JSON.stringify({ durationWindows, timeEnd, userId, instanceId }); - }, - shouldUpdate: (args, lastArgs) => { - return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); - }, -}); - -const _groupGPT4LogsInDurationWindow = async ({ - ctx, - durationWindow, - instanceId, - timeEnd, -}: { - ctx: TRPCContext; - durationWindow: DurationWindow; - instanceId?: string; - timeEnd: Date; -}): Promise => { - const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; - const groupByResult = await ctx.db - .select({ - chatgptAccountId: sql`${resourceUsageLogs.details}->>'chatgptAccountId'`, - _count: count(), - }) - .from(resourceUsageLogs) - .where( - and( - eq(resourceUsageLogs.type, ServiceTypeSchema.Values.CHATGPT_SHARED), - // sql`${resourceUsageLogs.createdAt} >= ${new Date(new Date().getTime() - durationWindowSeconds * 1000)}`, - gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), - instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, - sql`${resourceUsageLogs.details}->>'model' LIKE 'gpt-4%'`, - ), - ) - .groupBy(sql`${resourceUsageLogs.details}->>'chatgptAccountId'`); - const result = { - durationWindow, - counts: groupByResult.map((item) => ({ - chatgptAccountId: item.chatgptAccountId, - count: item._count, - })), - }; - return result; -}; - -const groupGPT4LogsInDurationWindow = memoize(_groupGPT4LogsInDurationWindow, { - genKey: ({ durationWindow, instanceId }) => { - return JSON.stringify({ durationWindow, instanceId }); - }, - shouldUpdate: (args, lastArgs) => { - return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); - }, -}); - -const _sumPoekmonAPILogsInDurationWindows = async ({ - ctx, - durationWindows, - timeEnd, - userId, - instanceId, -}: { - ctx: TRPCContext; - durationWindows: DurationWindow[]; - timeEnd: Date; - userId?: string; - instanceId?: string; -}): Promise => { - const results = [] as PoekmonAPIResourceLogSumResult[]; - - for (const durationWindow of durationWindows) { - const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; - const aggResult = await ctx.db - .select({ - userCount: countDistinct(resourceUsageLogs.userId), - count: count(), - sumPromptTokens: - sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'prompt_tokens')::integer)`.mapWith(Number), - sumCompletionTokens: - sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'completion_tokens')::integer)`.mapWith(Number), - sumTotalTokens: sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'total_tokens')::integer)`.mapWith( - Number, - ), - }) - .from(resourceUsageLogs) - .where( - and( - eq(resourceUsageLogs.type, ServiceTypeSchema.Values.POEKMON_API), - gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), - userId ? eq(resourceUsageLogs.userId, userId) : sql`true`, - instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, - ), - ); - // .groupBy(resourceUsageLogs.userId); - - results.push({ - durationWindow, - stats: aggResult[0]!, - }); - } - - return results; -}; - -const sumPoekmonAPILogsInDurationWindows = memoize(_sumPoekmonAPILogsInDurationWindows, { - genKey: ({ durationWindows, timeEnd, userId, instanceId }) => { - return JSON.stringify({ durationWindows, timeEnd, userId, instanceId }); - }, - shouldUpdate: (args, lastArgs) => { - return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); - }, -}); - -const _groupPoekmonAPILogsInDurationWindowByModel = async ({ - ctx, - durationWindow, - instanceId, - timeEnd, -}: { - ctx: TRPCContext; - durationWindow: DurationWindow; - instanceId?: string; - timeEnd: Date; -}): Promise => { - const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; - const groupByResult = await ctx.db - .select({ - model: sql`${resourceUsageLogs.details}->>'model'`, - count: count(), - sumTotalTokens: sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'total_tokens')::integer)`.mapWith( - Number, - ), - }) - .from(resourceUsageLogs) - .where( - and( - eq(resourceUsageLogs.type, ServiceTypeSchema.Values.POEKMON_API), - gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), - instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, - ), - ) - .groupBy(sql`${resourceUsageLogs.details}->>'model'`); - const result = { - durationWindow, - groups: groupByResult - .filter((item) => item.model !== null) - .map((item) => ({ - model: item.model!, - count: item.count, - sumTotalTokens: item.sumTotalTokens, - })), - }; - return result; -}; -const groupPoekmonAPILogsInDurationWindowByModel = memoize(_groupPoekmonAPILogsInDurationWindowByModel, { - genKey: ({ durationWindow, instanceId }) => { - return JSON.stringify({ durationWindow, instanceId }); - }, - shouldUpdate: (args, lastArgs) => { - return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); - }, -}); const PaginationResourceLogsInputSchema = z.object({ where: ResourceUsageLogWhereInputSchema, @@ -398,4 +182,20 @@ export const resourceLogRouter = createTRPCRouter({ timeEnd: alignTimeToGranularity(60), }); }), + + sumPoekmonSharedResourceLogsInDurationWindowsByInstance: protectedProcedure + .input( + z.object({ + instanceId: z.string(), + durationWindows: DurationWindowSchema.array(), + }), + ) + .query(async ({ input, ctx }) => { + return sumPoekmonSharedLogsInDurationWindows({ + ctx, + durationWindows: input.durationWindows, + timeEnd: alignTimeToGranularity(60), + instanceId: input.instanceId, + }); + }), }); diff --git a/src/server/api/routers/resourceLogHelper.ts b/src/server/api/routers/resourceLogHelper.ts new file mode 100644 index 0000000..99c83c9 --- /dev/null +++ b/src/server/api/routers/resourceLogHelper.ts @@ -0,0 +1,278 @@ +import { type TRPCContext } from "@/server/trpc"; +import { and, count, eq, gte, sql, countDistinct } from "drizzle-orm"; +import { resourceUsageLogs } from "@/server/db/schema"; +import { DURATION_WINDOWS, type DurationWindow, ServiceTypeSchema } from "@/server/db/enum"; +import { memoize } from "@/lib/memoize"; +import { + type ChatGPTSharedGPT4LogGroupbyAccountResult, + type ChatGPTSharedResourceLogSumResult, +} from "@/schema/service/chatgpt-shared.schema"; +import { + type PoekmonAPILogGroupbyModelResult, + type PoekmonAPIResourceLogSumResult, +} from "@/schema/service/poekmon-api.schema"; +import { PoekmonSharedResourceLogSumResult } from "@/schema/service/poekmon-shared.schema"; + +const _sumChatGPTSharedLogsInDurationWindows = async ({ + ctx, + durationWindows, + timeEnd, + userId, + instanceId, +}: { + ctx: TRPCContext; + durationWindows: DurationWindow[]; + timeEnd: Date; + userId?: string; + instanceId?: string; +}): Promise => { + const results = [] as ChatGPTSharedResourceLogSumResult[]; + + for (const durationWindow of durationWindows) { + const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; + const aggResult = await ctx.db + .select({ + userCount: countDistinct(resourceUsageLogs.userId), + count: count(), + sumUtf8Length: sql`sum(${resourceUsageLogs.textBytes})`.mapWith(Number), + // sumTokensLength: sum(resourceUsageLogs.tokensLength).mapWith(Number), + sumTokensLength: sql`0`.mapWith(Number), // todo + }) + .from(resourceUsageLogs) + .where( + and( + eq(resourceUsageLogs.type, ServiceTypeSchema.Values.CHATGPT_SHARED), + gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), + userId ? eq(resourceUsageLogs.userId, userId) : sql`true`, + instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, + ), + ); + // .groupBy(resourceUsageLogs.userId); + + results.push({ + durationWindow, + stats: aggResult[0]!, + }); + } + + return results; +}; + +export const sumChatGPTSharedLogsInDurationWindows = memoize(_sumChatGPTSharedLogsInDurationWindows, { + genKey: ({ durationWindows, timeEnd, userId, instanceId }) => { + return JSON.stringify({ durationWindows, timeEnd, userId, instanceId }); + }, + shouldUpdate: (args, lastArgs) => { + return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); + }, +}); + +const _groupGPT4LogsInDurationWindow = async ({ + ctx, + durationWindow, + instanceId, + timeEnd, +}: { + ctx: TRPCContext; + durationWindow: DurationWindow; + instanceId?: string; + timeEnd: Date; +}): Promise => { + const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; + const groupByResult = await ctx.db + .select({ + chatgptAccountId: sql`${resourceUsageLogs.details}->>'chatgptAccountId'`, + _count: count(), + }) + .from(resourceUsageLogs) + .where( + and( + eq(resourceUsageLogs.type, ServiceTypeSchema.Values.CHATGPT_SHARED), + // sql`${resourceUsageLogs.createdAt} >= ${new Date(new Date().getTime() - durationWindowSeconds * 1000)}`, + gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), + instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, + sql`${resourceUsageLogs.details}->>'model' LIKE 'gpt-4%'`, + ), + ) + .groupBy(sql`${resourceUsageLogs.details}->>'chatgptAccountId'`); + const result = { + durationWindow, + counts: groupByResult.map((item) => ({ + chatgptAccountId: item.chatgptAccountId, + count: item._count, + })), + }; + return result; +}; + +export const groupGPT4LogsInDurationWindow = memoize(_groupGPT4LogsInDurationWindow, { + genKey: ({ durationWindow, instanceId }) => { + return JSON.stringify({ durationWindow, instanceId }); + }, + shouldUpdate: (args, lastArgs) => { + return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); + }, +}); + +const _sumPoekmonAPILogsInDurationWindows = async ({ + ctx, + durationWindows, + timeEnd, + userId, + instanceId, +}: { + ctx: TRPCContext; + durationWindows: DurationWindow[]; + timeEnd: Date; + userId?: string; + instanceId?: string; +}): Promise => { + const results = [] as PoekmonAPIResourceLogSumResult[]; + + for (const durationWindow of durationWindows) { + const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; + const aggResult = await ctx.db + .select({ + userCount: countDistinct(resourceUsageLogs.userId), + count: count(), + sumPromptTokens: + sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'prompt_tokens')::integer)`.mapWith(Number), + sumCompletionTokens: + sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'completion_tokens')::integer)`.mapWith(Number), + sumTotalTokens: sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'total_tokens')::integer)`.mapWith( + Number, + ), + }) + .from(resourceUsageLogs) + .where( + and( + eq(resourceUsageLogs.type, ServiceTypeSchema.Values.POEKMON_API), + gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), + userId ? eq(resourceUsageLogs.userId, userId) : sql`true`, + instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, + ), + ); + // .groupBy(resourceUsageLogs.userId); + + results.push({ + durationWindow, + stats: aggResult[0]!, + }); + } + + return results; +}; + +export const sumPoekmonAPILogsInDurationWindows = memoize(_sumPoekmonAPILogsInDurationWindows, { + genKey: ({ durationWindows, timeEnd, userId, instanceId }) => { + return JSON.stringify({ durationWindows, timeEnd, userId, instanceId }); + }, + shouldUpdate: (args, lastArgs) => { + return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); + }, +}); + +const _groupPoekmonAPILogsInDurationWindowByModel = async ({ + ctx, + durationWindow, + instanceId, + timeEnd, +}: { + ctx: TRPCContext; + durationWindow: DurationWindow; + instanceId?: string; + timeEnd: Date; +}): Promise => { + const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; + const groupByResult = await ctx.db + .select({ + model: sql`${resourceUsageLogs.details}->>'model'`, + count: count(), + sumTotalTokens: sql`SUM((${resourceUsageLogs.details} -> 'usage' ->> 'total_tokens')::integer)`.mapWith( + Number, + ), + }) + .from(resourceUsageLogs) + .where( + and( + eq(resourceUsageLogs.type, ServiceTypeSchema.Values.POEKMON_API), + gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), + instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, + ), + ) + .groupBy(sql`${resourceUsageLogs.details}->>'model'`); + const result = { + durationWindow, + groups: groupByResult + .filter((item) => item.model !== null) + .map((item) => ({ + model: item.model!, + count: item.count, + sumTotalTokens: item.sumTotalTokens, + })), + }; + return result; +}; + +export const groupPoekmonAPILogsInDurationWindowByModel = memoize(_groupPoekmonAPILogsInDurationWindowByModel, { + genKey: ({ durationWindow, instanceId }) => { + return JSON.stringify({ durationWindow, instanceId }); + }, + shouldUpdate: (args, lastArgs) => { + return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); + }, +}); + +const _sumPoekmonSharedLogsInDurationWindows = async ({ + ctx, + durationWindows, + timeEnd, + userId, + instanceId, + }: { + ctx: TRPCContext; + durationWindows: DurationWindow[]; + timeEnd: Date; + userId?: string; + instanceId?: string; + }): Promise => { + const results = [] as PoekmonSharedResourceLogSumResult[]; + + for (const durationWindow of durationWindows) { + const durationWindowSeconds = DURATION_WINDOWS[durationWindow]; + const aggResult = await ctx.db + .select({ + userCount: countDistinct(resourceUsageLogs.userId), + count: count(), + sumPoints: + sql`SUM((${resourceUsageLogs.details} -> 'consume_point')::integer)`.mapWith(Number), + }) + .from(resourceUsageLogs) + .where( + and( + eq(resourceUsageLogs.type, ServiceTypeSchema.Values.POEKMON_SHARED), + gte(resourceUsageLogs.createdAt, new Date(timeEnd.getTime() - durationWindowSeconds * 1000)), + userId ? eq(resourceUsageLogs.userId, userId) : sql`true`, + instanceId ? eq(resourceUsageLogs.instanceId, instanceId) : sql`true`, + ), + ); + // .groupBy(resourceUsageLogs.userId); + + results.push({ + durationWindow, + stats: aggResult[0]!, + }); + } + + return results; + }; + + export const sumPoekmonSharedLogsInDurationWindows = memoize(_sumPoekmonSharedLogsInDurationWindows, { + genKey: ({ durationWindows, timeEnd, userId, instanceId }) => { + return JSON.stringify({ durationWindows, timeEnd, userId, instanceId }); + }, + shouldUpdate: (args, lastArgs) => { + return args[0].timeEnd.getTime() !== lastArgs[0].timeEnd.getTime(); + }, + }); + \ No newline at end of file diff --git a/src/server/api/routers/serviceInstance.ts b/src/server/api/routers/serviceInstance.ts index a32cf4d..32fad67 100644 --- a/src/server/api/routers/serviceInstance.ts +++ b/src/server/api/routers/serviceInstance.ts @@ -1,11 +1,6 @@ import { TRPCError } from "@trpc/server"; -import { - createTRPCRouter, - adminProcedure, - protectedProcedure, - protectedWithUserProcedure, -} from "@/server/trpc"; +import { createTRPCRouter, adminProcedure, protectedProcedure, protectedWithUserProcedure } from "@/server/trpc"; import { z } from "zod"; import { ServiceInstanceCreateSchema, @@ -17,6 +12,10 @@ import { import { resourceUsageLogs, serviceInstances, userInstanceAbilities } from "@/server/db/schema"; import { createCUID } from "@/lib/cuid"; import { and, eq } from "drizzle-orm"; +import { + PoekmonSharedAccountInfoUserReadableSchema, + PoekmonSharedInstanceData, +} from "@/schema/service/poekmon-shared.schema"; export const serviceInstanceRouter = createTRPCRouter({ create: adminProcedure.input(ServiceInstanceCreateSchema).mutation(async ({ ctx, input }) => { @@ -91,6 +90,18 @@ export const serviceInstanceRouter = createTRPCRouter({ return ServiceInstanceWithToken.array().parse(instances); }), + getByIdAdmin: protectedProcedure + .input(ServiceInstanceUserReadSchema.pick({ id: true })) + .query(async ({ ctx, input }) => { + const result = await ctx.db.query.serviceInstances.findFirst({ + where: eq(serviceInstances.id, input.id), + }); + if (!result) { + throw new TRPCError({ code: "NOT_FOUND", message: "Service instance not found" }); + } + return ServiceInstanceAdminSchema.parse(result); + }), + getById: protectedProcedure.input(ServiceInstanceUserReadSchema.pick({ id: true })).query(async ({ ctx, input }) => { const result = await ctx.db.query.serviceInstances.findFirst({ where: eq(serviceInstances.id, input.id), @@ -101,6 +112,25 @@ export const serviceInstanceRouter = createTRPCRouter({ return ServiceInstanceUserReadSchema.parse(result); }), + getPoekmonSharedAccountInfo: protectedProcedure + .input(ServiceInstanceUserReadSchema.pick({ id: true })) + .query(async ({ ctx, input }) => { + const result = await ctx.db.query.serviceInstances.findFirst({ + where: eq(serviceInstances.id, input.id), + }); + if (!result) { + throw new TRPCError({ code: "NOT_FOUND", message: "Service instance not found" }); + } + if (result.type !== "POEKMON_SHARED") { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid instance type" }); + } + const data = result.data as PoekmonSharedInstanceData; + if (!data.poe_account.account_info) { + return null; + } + return PoekmonSharedAccountInfoUserReadableSchema.parse(data.poe_account.account_info); + }), + delete: adminProcedure .input(ServiceInstanceAdminSchema.pick({ id: true }).merge(z.object({ deleteLogs: z.boolean() }))) .mutation(async ({ ctx, input }) => {