feat(swads): add daily image report workflow

This commit is contained in:
Jeffrey Wu
2026-09-07 11:34:00 +08:00
parent f39f6ac881
commit 5c61371d7e
38 changed files with 2537 additions and 52 deletions
+14 -3
View File
@@ -13,6 +13,9 @@ import {
InMemorySessionRegistry,
PiAgentSessionFactory,
SkillCatalog,
SwadsGateway,
ReportStore,
REPORT_CSP,
safeErrorMessage,
type AgentSessionFactory,
} from "@agent-studio/harness";
@@ -25,7 +28,7 @@ const eventQuery = z.object({ afterSequence: z.coerce.number().int().nonnegative
function parse<T>(schema: z.ZodType<T>, value: unknown): T { return schema.parse(value); }
export interface AppServices { sessions: InMemorySessionRegistry; events: InMemoryEventStore; attachments: FileAttachmentStore; models: InMemoryModelConnectionStore; skills: SkillCatalog }
export interface AppServices { sessions: InMemorySessionRegistry; events: InMemoryEventStore; attachments: FileAttachmentStore; models: InMemoryModelConnectionStore; skills: SkillCatalog; reports: ReportStore }
export interface AppOptions { projectRoot?: string; runTimeoutMs?: number; approvalTimeoutMs?: number; heartbeatMs?: number; agentFactory?: AgentSessionFactory; onReady?: (services: AppServices) => void }
export async function buildApp(options: AppOptions = {}): Promise<FastifyInstance> {
@@ -35,6 +38,8 @@ export async function buildApp(options: AppOptions = {}): Promise<FastifyInstanc
await mkdir(sessionsRoot, { recursive: true, mode: 0o700 });
await mkdir(agentDir, { recursive: true, mode: 0o700 });
const gateway = new SwadsGateway();
const reports = new ReportStore(path.join(projectRoot, "workspace/reports"), path.join(projectRoot, ".agents/skills/swads-daily-report/assets/report.css"));
const events = new InMemoryEventStore(1_000);
const credentialVault = process.env.MODEL_CREDENTIAL_ENCRYPTION_KEY
? new EncryptedFileCredentialVault(path.join(projectRoot, "workspace", "model-credentials.enc.json"), process.env.MODEL_CREDENTIAL_ENCRYPTION_KEY)
@@ -46,8 +51,8 @@ export async function buildApp(options: AppOptions = {}): Promise<FastifyInstanc
const skills = new SkillCatalog(projectRoot, agentDir);
await skills.reload();
const sessions = new InMemorySessionRegistry(sessionsRoot, events, attachments, models, skills, undefined, options.approvalTimeoutMs ?? Number(process.env.APPROVAL_TIMEOUT_MS ?? 60_000), options.runTimeoutMs ?? Number(process.env.SESSION_RUN_TIMEOUT_MS ?? 120_000));
sessions.setFactory(options.agentFactory ?? new PiAgentSessionFactory(path.join(projectRoot, "fixtures", "ads-account.json"), skills, models, sessions.approvals));
options.onReady?.({ sessions, events, attachments, models, skills });
sessions.setFactory(options.agentFactory ?? new PiAgentSessionFactory(path.join(projectRoot, "fixtures", "ads-account.json"), skills, models, sessions.approvals, gateway, reports));
options.onReady?.({ sessions, events, attachments, models, skills, reports });
const app = Fastify({
genReqId: () => `req_${randomUUID()}`,
@@ -65,6 +70,12 @@ export async function buildApp(options: AppOptions = {}): Promise<FastifyInstanc
return sendError(reply, request, 500, "INTERNAL_ERROR", "Internal server error");
});
app.get("/api/reports/:accountKey/:reportDate/:reportId/:format", async (request, reply) => {
const params = request.params as Record<string, string>;
const result = await reports.read({ accountKey: params.accountKey, reportDate: params.reportDate, reportId: params.reportId }, params.format);
const mime = { png: "image/png", html: "text/html; charset=utf-8", json: "application/json; charset=utf-8" };
return reply.header("content-type", mime[result.format]).header("content-disposition", `${result.format === "png" ? "inline" : "attachment"}; filename="swads-daily-report.${result.format}"`).header("x-content-type-options", "nosniff").header("content-security-policy", REPORT_CSP).header("cache-control", "private, no-store").send(result.bytes);
});
app.get("/health", async (request) => ({ ok: true, requestId: request.id }));
app.get("/api/skills", async (request) => ({ skills: skills.list(), requestId: request.id }));
app.post("/api/sessions", async (request, reply) => { const session = await sessions.create(); return reply.code(201).send({ sessionId: session.sessionId, requestId: request.id }); });