feat(hub): add org sessions and usage admin APIs

List project agent sessions/runs and aggregate AgentRun cost and token
usage by project and optional folder for org admins.
This commit is contained in:
2026-07-10 00:55:43 +08:00
parent 683d5674d1
commit 008c8bcd09
4 changed files with 402 additions and 0 deletions
+5
View File
@@ -12,6 +12,7 @@ import {
import { registerAccessRoutes } from "./accessRoutes.js";
import { registerExplorerRoutes } from "./explorerRoutes.js";
import { registerMembersRoutes } from "./membersRoutes.js";
import { registerSessionsAndUsageRoutes } from "./sessionsRoutes.js";
import { registerTeamsRoutes } from "./teamsRoutes.js";
export interface OrgRouteConfig {
@@ -96,4 +97,8 @@ export async function registerOrgRoutes(app: FastifyInstance, config: OrgRouteCo
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerSessionsAndUsageRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
}
+80
View File
@@ -0,0 +1,80 @@
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { getSessionDetail, listProjectSessions } from "../../org/sessions.js";
import { getOrgUsage, getProjectUsage } from "../../org/usage.js";
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
import { handleRouteError } from "../errors.js";
export async function registerSessionsAndUsageRoutes(
app: FastifyInstance,
config: { readonly prisma: PrismaClient; readonly sessionSecret: string },
): Promise<void> {
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
app.get("/api/org/:orgSlug/projects/:projectId/sessions", async (request, reply) => {
try {
const { orgSlug, projectId } = request.params as { orgSlug: string; projectId: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
const q = request.query as { limit?: string };
const limit = q.limit !== undefined ? Number(q.limit) : undefined;
return {
sessions: await listProjectSessions(config.prisma, {
organizationId: auth.organization.id,
projectId,
...(limit !== undefined && Number.isFinite(limit) ? { limit } : {}),
}),
};
} catch (err) {
return handleRouteError(reply, err);
}
});
app.get("/api/org/:orgSlug/sessions/:sessionId", async (request, reply) => {
try {
const { orgSlug, sessionId } = request.params as { orgSlug: string; sessionId: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
return await getSessionDetail(config.prisma, {
organizationId: auth.organization.id,
sessionId,
});
} catch (err) {
return handleRouteError(reply, err);
}
});
app.get("/api/org/:orgSlug/usage", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
const q = request.query as { from?: string; to?: string; folderId?: string };
return await getOrgUsage(config.prisma, {
organizationId: auth.organization.id,
...(q.from !== undefined && q.from !== "" ? { from: new Date(q.from) } : {}),
...(q.to !== undefined && q.to !== "" ? { to: new Date(q.to) } : {}),
...(q.folderId !== undefined && q.folderId !== "" ? { folderId: q.folderId } : {}),
});
} catch (err) {
return handleRouteError(reply, err);
}
});
app.get("/api/org/:orgSlug/projects/:projectId/usage", async (request, reply) => {
try {
const { orgSlug, projectId } = request.params as { orgSlug: string; projectId: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
const q = request.query as { from?: string; to?: string };
return await getProjectUsage(config.prisma, {
organizationId: auth.organization.id,
projectId,
...(q.from !== undefined && q.from !== "" ? { from: new Date(q.from) } : {}),
...(q.to !== undefined && q.to !== "" ? { to: new Date(q.to) } : {}),
});
} catch (err) {
return handleRouteError(reply, err);
}
});
}