feat: add agent API and operational analytics
This commit is contained in:
1 parent
720ea0652a
commit
e8b730b592
65 files changed
+2650
-1214
No files matched your search
@@ -0,0 +1,177 @@
|
||||
import { z, type ZodType } from 'zod';
|
||||
import {
|
||||
AgentJobRequestSchema,
|
||||
AgentJobSchema,
|
||||
CapabilitiesResponseSchema,
|
||||
ControlRequestSchema,
|
||||
ControlResponseSchema,
|
||||
HealthResponseSchema,
|
||||
OverviewResponseSchema,
|
||||
ReadinessResponseSchema,
|
||||
} from './agent-api-contracts.js';
|
||||
|
||||
export type ApiAccess = 'read' | 'control' | 'admin';
|
||||
export type ApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
|
||||
export interface ApiOperation {
|
||||
operationId: string;
|
||||
method: ApiMethod;
|
||||
path: string;
|
||||
summary: string;
|
||||
access: ApiAccess;
|
||||
mutates: boolean;
|
||||
streaming?: boolean;
|
||||
requestSchema?: ZodType;
|
||||
responseSchema?: ZodType;
|
||||
successStatus?: number;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const operation = (
|
||||
operationId: string,
|
||||
method: ApiMethod,
|
||||
path: string,
|
||||
summary: string,
|
||||
tags: string[],
|
||||
options: Partial<Omit<ApiOperation, 'operationId' | 'method' | 'path' | 'summary' | 'tags'>> = {},
|
||||
): ApiOperation => ({
|
||||
operationId,
|
||||
method,
|
||||
path,
|
||||
summary,
|
||||
tags,
|
||||
access: options.access ?? (method === 'GET' ? 'read' : 'admin'),
|
||||
mutates: options.mutates ?? method !== 'GET',
|
||||
streaming: options.streaming ?? false,
|
||||
requestSchema: options.requestSchema,
|
||||
responseSchema: options.responseSchema,
|
||||
successStatus: options.successStatus,
|
||||
});
|
||||
|
||||
/**
|
||||
* Machine-readable catalogue for every HTTP operation exposed by the host.
|
||||
* Legacy dashboard routes remain here during the v1 migration so discovery
|
||||
* never depends on scraping Express source or stale prose documentation.
|
||||
*/
|
||||
export const API_OPERATIONS: readonly ApiOperation[] = [
|
||||
operation('health', 'GET', '/healthz', 'Process liveness', ['Operations'], { responseSchema: HealthResponseSchema }),
|
||||
operation('getOverview', 'GET', '/api/v1/overview', 'Consolidated operational snapshot', ['Agent'], { responseSchema: OverviewResponseSchema }),
|
||||
operation('getReadiness', 'GET', '/api/v1/readyz', 'Component readiness', ['Agent'], { responseSchema: ReadinessResponseSchema }),
|
||||
operation('getCapabilities', 'GET', '/api/v1/capabilities', 'Discover API operations and safety attributes', ['Agent'], { responseSchema: CapabilitiesResponseSchema }),
|
||||
operation('getOpenApi', 'GET', '/api/openapi.json', 'OpenAPI 3.1 description', ['Discovery']),
|
||||
operation('getControlStatus', 'GET', '/api/v1/control', 'Get bot running state', ['Control'], { responseSchema: ControlResponseSchema }),
|
||||
operation('setControl', 'POST', '/api/v1/control', 'Set bot running state', ['Control'], { access: 'control', requestSchema: ControlRequestSchema, responseSchema: ControlResponseSchema }),
|
||||
operation('startBotV1', 'POST', '/api/v1/control/start', 'Start bot processing', ['Control'], { access: 'control', responseSchema: ControlResponseSchema }),
|
||||
operation('stopBotV1', 'POST', '/api/v1/control/stop', 'Stop bot processing', ['Control'], { access: 'control', responseSchema: ControlResponseSchema }),
|
||||
operation('listJobs', 'GET', '/api/v1/jobs', 'List recent asynchronous jobs', ['Jobs'], { responseSchema: z.array(AgentJobSchema) }),
|
||||
operation('getJob', 'GET', '/api/v1/jobs/{id}', 'Get asynchronous job state', ['Jobs'], { responseSchema: AgentJobSchema }),
|
||||
operation('createJob', 'POST', '/api/v1/jobs', 'Start chat or media synchronisation', ['Jobs'], { access: 'control', requestSchema: AgentJobRequestSchema, responseSchema: AgentJobSchema, successStatus: 202 }),
|
||||
|
||||
operation('getStats', 'GET', '/api/stats', 'Get bot statistics', ['Dashboard']),
|
||||
operation('getActivity', 'GET', '/api/activity', 'Get recent activity', ['Dashboard']),
|
||||
operation('getPostsByAuthor', 'GET', '/api/posts/by-author/{username}', 'Get processed posts by author', ['Dashboard']),
|
||||
operation('getMessages', 'GET', '/api/messages', 'Get recent sent messages', ['Dashboard']),
|
||||
operation('getSuccessful', 'GET', '/api/successful', 'Get successful message examples', ['Dashboard']),
|
||||
operation('getChats', 'GET', '/api/chats', 'Get active chats', ['Chats']),
|
||||
operation('getChatSync', 'GET', '/api/chats/sync', 'Get chat sync state', ['Chats']),
|
||||
operation('triggerDeltaChatSync', 'POST', '/api/chats/sync/delta', 'Trigger incremental chat sync', ['Chats']),
|
||||
operation('triggerFullChatSync', 'POST', '/api/chats/sync/full', 'Trigger full chat sync', ['Chats']),
|
||||
operation('fetchAllChats', 'POST', '/api/chats/fetch-all', 'Fetch all chats synchronously', ['Chats']),
|
||||
operation('getConversations', 'GET', '/api/conversations', 'List conversations', ['Conversations']),
|
||||
operation('getConversation', 'GET', '/api/conversations/{id}', 'Get conversation detail', ['Conversations']),
|
||||
operation('suggestReply', 'POST', '/api/conversations/{id}/suggest-reply', 'Generate a reply suggestion', ['Conversations']),
|
||||
operation('suggestMediaAsk', 'POST', '/api/conversations/{id}/suggest-media-ask', 'Generate a media request suggestion', ['Conversations']),
|
||||
operation('sendReply', 'POST', '/api/conversations/{id}/send-reply', 'Send a conversation reply', ['Conversations']),
|
||||
operation('startConversation', 'POST', '/api/conversations/start', 'Start a new conversation', ['Conversations']),
|
||||
operation('getMedia', 'GET', '/api/media', 'List indexed private media', ['Media']),
|
||||
operation('getMediaSync', 'GET', '/api/media/sync', 'Get media sync state', ['Media']),
|
||||
operation('triggerMediaSync', 'POST', '/api/media/sync', 'Trigger media sync', ['Media']),
|
||||
operation('triggerFullMediaSync', 'POST', '/api/media/sync/full', 'Trigger full media sync', ['Media']),
|
||||
operation('startBot', 'POST', '/api/start', 'Start bot processing (legacy)', ['Control']),
|
||||
operation('stopBot', 'POST', '/api/stop', 'Stop bot processing (legacy)', ['Control']),
|
||||
operation('getTimers', 'GET', '/api/timers', 'Get rate-limit timers', ['Analytics']),
|
||||
operation('getSubredditAnalytics', 'GET', '/api/analytics/by-subreddit', 'Get performance by subreddit', ['Analytics']),
|
||||
operation('getStrategyAnalytics', 'GET', '/api/analytics/by-strategy', 'Get performance by strategy', ['Analytics']),
|
||||
operation('getFunnelAnalytics', 'GET', '/api/analytics/funnel', 'Get mature message outcome funnel', ['Analytics']),
|
||||
operation('getAnalyticsTrend', 'GET', '/api/analytics/trend', 'Get daily performance trend', ['Analytics']),
|
||||
operation('getConfig', 'GET', '/api/config', 'Get non-secret application configuration', ['Configuration']),
|
||||
operation('updateConfig', 'PUT', '/api/config', 'Update application configuration', ['Configuration']),
|
||||
operation('getLoginStatus', 'GET', '/api/login/status', 'Get Reddit session state', ['Accounts']),
|
||||
operation('importSession', 'POST', '/api/login/session-import', 'Import Reddit and Matrix credentials', ['Accounts']),
|
||||
operation('listAccounts', 'GET', '/api/accounts', 'List Reddit accounts', ['Accounts']),
|
||||
operation('activateAccount', 'POST', '/api/accounts/{accountId}/activate', 'Activate a Reddit account', ['Accounts']),
|
||||
operation('streamLogs', 'GET', '/api/logs/stream', 'Stream legacy log events', ['Events'], { streaming: true }),
|
||||
operation('streamEvents', 'GET', '/api/events/stream', 'Stream typed application events', ['Events'], { streaming: true }),
|
||||
operation('getLogs', 'GET', '/api/logs', 'Get recent structured logs', ['Events']),
|
||||
operation('getCookieDiagnostics', 'GET', '/api/debug/cookies', 'Get redacted cookie diagnostics when enabled', ['Debug'], { access: 'admin' }),
|
||||
operation('checkResponses', 'POST', '/api/debug/check-responses', 'Run response check when enabled', ['Debug']),
|
||||
];
|
||||
|
||||
function jsonSchema(schema: ZodType | undefined): Record<string, unknown> | undefined {
|
||||
return schema ? z.toJSONSchema(schema, { target: 'draft-7' }) as Record<string, unknown> : undefined;
|
||||
}
|
||||
|
||||
export function createOpenApiDocument(): Record<string, unknown> {
|
||||
const paths: Record<string, Record<string, unknown>> = {};
|
||||
for (const item of API_OPERATIONS) {
|
||||
const path = paths[item.path] ?? {};
|
||||
const request = jsonSchema(item.requestSchema);
|
||||
const response = jsonSchema(item.responseSchema) ?? {};
|
||||
path[item.method.toLowerCase()] = {
|
||||
operationId: item.operationId,
|
||||
summary: item.summary,
|
||||
tags: item.tags,
|
||||
security: item.path === '/healthz' ? [] : [{ bearerAuth: [] }, { cookieAuth: [] }],
|
||||
'x-access': item.access,
|
||||
'x-mutates': item.mutates,
|
||||
'x-streaming': item.streaming,
|
||||
...(request ? { requestBody: { required: true, content: { 'application/json': { schema: request } } } } : {}),
|
||||
responses: {
|
||||
[String(item.successStatus ?? 200)]: {
|
||||
description: 'Successful response',
|
||||
content: item.streaming
|
||||
? { 'text/event-stream': { schema: { type: 'string' } } }
|
||||
: { 'application/json': { schema: response } },
|
||||
},
|
||||
'401': { description: 'Authentication required' },
|
||||
'403': { description: 'Credential lacks the required access' },
|
||||
},
|
||||
};
|
||||
paths[item.path] = path;
|
||||
}
|
||||
return {
|
||||
openapi: '3.1.0',
|
||||
info: {
|
||||
title: 'RedditBot API',
|
||||
version: '1.0.0',
|
||||
description: 'Dashboard and agent API. Prefer /api/v1 operations for automation.',
|
||||
},
|
||||
servers: [{ url: '/' }],
|
||||
paths,
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: { type: 'http', scheme: 'bearer' },
|
||||
cookieAuth: { type: 'apiKey', in: 'cookie', name: 'reddit_bot_dashboard_session' },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function capabilities() {
|
||||
return {
|
||||
apiVersion: 'v1' as const,
|
||||
authentication: {
|
||||
schemes: ['Bearer token', 'HttpOnly dashboard session'],
|
||||
agentScopes: ['read', 'control', 'admin'] as const,
|
||||
},
|
||||
operations: API_OPERATIONS.map(({ operationId, method, path, summary, access, mutates, streaming }) => ({
|
||||
operationId,
|
||||
method,
|
||||
path,
|
||||
summary,
|
||||
access,
|
||||
mutates,
|
||||
streaming: Boolean(streaming),
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user