ca9498ce83
Task 8.6: CalendarController exposes schedule/request(+fromCallback/fromClaim)/ list/get/consent/transcript/summarize/action-items/providers(+sync) (session-auth). kernel-client RestClient gains scheduleMeeting/requestMeeting/listMeetings/ getMeeting/setConsent/generateTranscript/summarizeMeeting/listActionItems/ connect+syncCalendarProvider + Meeting/MeetingParticipant/MeetingActionItem types. New @insignia/iios-meeting-web (MeetingProvider + useMeetings/useMeeting/useSchedule/ useConsent/useSummarize/useActionItems). Boundary: meeting-web -> contracts + kernel-client (fails on meeting-web->service). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
217 lines
10 KiB
TypeScript
217 lines
10 KiB
TypeScript
import type { IngestInteractionRequest } from '@insignia/iios-contracts';
|
|
import type { Message, InboxItem, InboxState, Ticket, TicketState, CallbackRequest, RouteBinding, RouteDecision, AiArtifact, AiJobResult, Meeting, MeetingActionItem } from './types';
|
|
|
|
export interface RestConfig {
|
|
serviceUrl: string;
|
|
token?: string;
|
|
}
|
|
|
|
/** REST/polling client for kernel reads and the native-send fallback. */
|
|
export class RestClient {
|
|
constructor(private readonly config: RestConfig) {}
|
|
|
|
private url(path: string): string {
|
|
return `${this.config.serviceUrl.replace(/\/$/, '')}${path}`;
|
|
}
|
|
|
|
private headers(extra: Record<string, string> = {}): Record<string, string> {
|
|
const h: Record<string, string> = { 'content-type': 'application/json', ...extra };
|
|
if (this.config.token) h.authorization = `Bearer ${this.config.token}`;
|
|
return h;
|
|
}
|
|
|
|
async getThreadMessages(threadId: string): Promise<{ threadId: string; messages: unknown[] }> {
|
|
const r = await fetch(this.url(`/v1/threads/${threadId}/messages`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`getThreadMessages ${r.status}`);
|
|
return (await r.json()) as { threadId: string; messages: unknown[] };
|
|
}
|
|
|
|
async sendMessage(
|
|
threadId: string,
|
|
content: string,
|
|
opts?: { contentRef?: string; idempotencyKey?: string },
|
|
): Promise<Message> {
|
|
const r = await fetch(this.url(`/v1/threads/${threadId}/messages`), {
|
|
method: 'POST',
|
|
headers: this.headers(opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {}),
|
|
body: JSON.stringify({ content, contentRef: opts?.contentRef }),
|
|
});
|
|
if (!r.ok) throw new Error(`sendMessage ${r.status}`);
|
|
return (await r.json()) as Message;
|
|
}
|
|
|
|
async listInboxItems(state?: InboxState): Promise<InboxItem[]> {
|
|
const q = state ? `?state=${encodeURIComponent(state)}` : '';
|
|
const r = await fetch(this.url(`/v1/inbox/items${q}`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`listInboxItems ${r.status}`);
|
|
return (await r.json()) as InboxItem[];
|
|
}
|
|
|
|
async patchInboxItem(id: string, body: { state: InboxState; reason?: string }): Promise<InboxItem> {
|
|
const r = await fetch(this.url(`/v1/inbox/items/${id}`), {
|
|
method: 'PATCH',
|
|
headers: this.headers(),
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!r.ok) throw new Error(`patchInboxItem ${r.status}`);
|
|
return (await r.json()) as InboxItem;
|
|
}
|
|
|
|
// ─── support ──────────────────────────────────────────────────
|
|
async createTicket(body: { subject: string; priority?: string; threadId?: string }): Promise<Ticket> {
|
|
return this.post<Ticket>('/v1/support/tickets', body);
|
|
}
|
|
async escalate(threadId: string, subject?: string): Promise<Ticket> {
|
|
return this.post<Ticket>('/v1/support/escalate', { threadId, subject });
|
|
}
|
|
async listTickets(scope: 'mine' | 'assigned' = 'mine'): Promise<Ticket[]> {
|
|
const r = await fetch(this.url(`/v1/support/tickets?scope=${scope}`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`listTickets ${r.status}`);
|
|
return (await r.json()) as Ticket[];
|
|
}
|
|
async patchTicket(id: string, state: TicketState): Promise<Ticket> {
|
|
const r = await fetch(this.url(`/v1/support/tickets/${id}`), {
|
|
method: 'PATCH',
|
|
headers: this.headers(),
|
|
body: JSON.stringify({ state }),
|
|
});
|
|
if (!r.ok) throw new Error(`patchTicket ${r.status}`);
|
|
return (await r.json()) as Ticket;
|
|
}
|
|
async requestCallback(body: { preferChannel: string; preferredTime?: string; notes?: string; ticketId?: string }): Promise<CallbackRequest> {
|
|
return this.post<CallbackRequest>('/v1/support/callbacks', body);
|
|
}
|
|
async createQueue(name: string): Promise<{ id: string }> {
|
|
return this.post<{ id: string }>('/v1/support/queues', { name });
|
|
}
|
|
async joinQueue(queueId: string): Promise<unknown> {
|
|
return this.post('/v1/support/queues/' + queueId + '/members', {});
|
|
}
|
|
async joinDefaultQueue(): Promise<unknown> {
|
|
return this.post('/v1/support/agents/me/join', {});
|
|
}
|
|
async setAvailability(state: string): Promise<unknown> {
|
|
const r = await fetch(this.url('/v1/support/members/me/availability'), {
|
|
method: 'PATCH',
|
|
headers: this.headers(),
|
|
body: JSON.stringify({ state }),
|
|
});
|
|
if (!r.ok) throw new Error(`setAvailability ${r.status}`);
|
|
return r.json();
|
|
}
|
|
|
|
// ─── routing (P6) ─────────────────────────────────────────────
|
|
async createBinding(input: Partial<RouteBinding>): Promise<RouteBinding> {
|
|
return this.post<RouteBinding>('/v1/routes/bindings', input);
|
|
}
|
|
async listBindings(): Promise<RouteBinding[]> {
|
|
const r = await fetch(this.url('/v1/routes/bindings'), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`listBindings ${r.status}`);
|
|
return (await r.json()) as RouteBinding[];
|
|
}
|
|
async simulateRoute(input: { interactionId: string; originChannelType: string; originRef?: string }): Promise<{ simulationId: string; decisions: RouteDecision[] }> {
|
|
return this.post('/v1/routes/simulate', input);
|
|
}
|
|
async listRouteDecisions(state?: string): Promise<RouteDecision[]> {
|
|
const r = await fetch(this.url(`/v1/routes/decisions${state ? `?state=${state}` : ''}`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`listRouteDecisions ${r.status}`);
|
|
return (await r.json()) as RouteDecision[];
|
|
}
|
|
async approveDecision(id: string): Promise<RouteDecision> {
|
|
return this.post<RouteDecision>(`/v1/routes/decisions/${id}/approve`, {});
|
|
}
|
|
async denyDecision(id: string): Promise<RouteDecision> {
|
|
return this.post<RouteDecision>(`/v1/routes/decisions/${id}/deny`, {});
|
|
}
|
|
|
|
// ─── AI proposal layer (P7) ───────────────────────────────────
|
|
async runAiJob(input: { interactionId: string; jobType: 'CLASSIFY' | 'SUMMARIZE' | 'EXTRACT' }): Promise<AiJobResult> {
|
|
return this.post<AiJobResult>('/v1/ai/jobs', input);
|
|
}
|
|
async listArtifacts(opts?: { interactionId?: string; status?: string }): Promise<AiArtifact[]> {
|
|
const q = new URLSearchParams();
|
|
if (opts?.interactionId) q.set('interactionId', opts.interactionId);
|
|
if (opts?.status) q.set('status', opts.status);
|
|
const qs = q.toString();
|
|
const r = await fetch(this.url(`/v1/ai/artifacts${qs ? `?${qs}` : ''}`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`listArtifacts ${r.status}`);
|
|
return (await r.json()) as AiArtifact[];
|
|
}
|
|
async getArtifact(id: string): Promise<AiArtifact> {
|
|
const r = await fetch(this.url(`/v1/ai/artifacts/${id}`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`getArtifact ${r.status}`);
|
|
return (await r.json()) as AiArtifact;
|
|
}
|
|
async acceptArtifact(id: string): Promise<AiArtifact> {
|
|
return this.post<AiArtifact>(`/v1/ai/artifacts/${id}/accept`, {});
|
|
}
|
|
async rejectArtifact(id: string): Promise<AiArtifact> {
|
|
return this.post<AiArtifact>(`/v1/ai/artifacts/${id}/reject`, {});
|
|
}
|
|
|
|
// ─── Calendar & Meeting (P8) ──────────────────────────────────
|
|
async scheduleMeeting(input: {
|
|
meetingType: string;
|
|
title: string;
|
|
startAt: string;
|
|
endAt?: string;
|
|
timezone?: string;
|
|
attendees?: { userId: string; displayName?: string; role?: string; visibility?: string }[];
|
|
}): Promise<Meeting> {
|
|
return this.post<Meeting>('/v1/calendar/meetings', input);
|
|
}
|
|
async requestMeeting(input: { requestedWindow: Record<string, unknown>; meetingType?: string }, opts?: { fromCallback?: string; fromClaim?: string }): Promise<unknown> {
|
|
const q = new URLSearchParams();
|
|
if (opts?.fromCallback) q.set('fromCallback', opts.fromCallback);
|
|
if (opts?.fromClaim) q.set('fromClaim', opts.fromClaim);
|
|
const qs = q.toString();
|
|
return this.post(`/v1/calendar/requests${qs ? `?${qs}` : ''}`, input);
|
|
}
|
|
async listMeetings(): Promise<Meeting[]> {
|
|
const r = await fetch(this.url('/v1/calendar/meetings'), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`listMeetings ${r.status}`);
|
|
return (await r.json()) as Meeting[];
|
|
}
|
|
async getMeeting(id: string): Promise<Meeting> {
|
|
const r = await fetch(this.url(`/v1/calendar/meetings/${id}`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`getMeeting ${r.status}`);
|
|
return (await r.json()) as Meeting;
|
|
}
|
|
async setConsent(meetingId: string, actorRefId: string, status: string): Promise<unknown> {
|
|
return this.post(`/v1/calendar/meetings/${meetingId}/consent`, { actorRefId, status });
|
|
}
|
|
async generateTranscript(meetingId: string): Promise<unknown> {
|
|
return this.post(`/v1/calendar/meetings/${meetingId}/transcript`, {});
|
|
}
|
|
async summarizeMeeting(meetingId: string): Promise<Meeting> {
|
|
return this.post<Meeting>(`/v1/calendar/meetings/${meetingId}/summarize`, {});
|
|
}
|
|
async listActionItems(meetingId: string): Promise<MeetingActionItem[]> {
|
|
const r = await fetch(this.url(`/v1/calendar/meetings/${meetingId}/action-items`), { headers: this.headers() });
|
|
if (!r.ok) throw new Error(`listActionItems ${r.status}`);
|
|
return (await r.json()) as MeetingActionItem[];
|
|
}
|
|
async connectCalendarProvider(): Promise<{ id: string }> {
|
|
return this.post<{ id: string }>('/v1/calendar/providers', {});
|
|
}
|
|
async syncCalendarProvider(id: string): Promise<{ pulled: number; cursor: string }> {
|
|
return this.post(`/v1/calendar/providers/${id}/sync`, {});
|
|
}
|
|
|
|
private async post<T>(path: string, body: unknown): Promise<T> {
|
|
const r = await fetch(this.url(path), { method: 'POST', headers: this.headers(), body: JSON.stringify(body) });
|
|
if (!r.ok) throw new Error(`POST ${path} ${r.status}`);
|
|
return (await r.json()) as T;
|
|
}
|
|
|
|
async ingest(body: IngestInteractionRequest, idempotencyKey: string): Promise<unknown> {
|
|
const r = await fetch(this.url('/v1/interactions/ingest'), {
|
|
method: 'POST',
|
|
headers: this.headers({ 'idempotency-key': idempotencyKey }),
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!r.ok) throw new Error(`ingest ${r.status}`);
|
|
return r.json();
|
|
}
|
|
}
|