diff --git a/api/scripts/sessions-patterns.ts b/api/scripts/sessions-patterns.ts index 4f36b59..34150f9 100644 --- a/api/scripts/sessions-patterns.ts +++ b/api/scripts/sessions-patterns.ts @@ -9,14 +9,14 @@ * 1. Resolver intervalo da semana (segunda 00:00 UTC → domingo 23:59 UTC) * 2. Correr detectPatterns (6 detectores heurísticos) * 3. Persistir com upsertPattern + consecutive_weeks - * 4. Se --publish: POST comentário HTML para Desk discussion #32, - * e para padrões com consecutive_weeks>=3 e severity∈(warning,action) - * abrir Ticket no Desk. + * 4. Se --publish: chamar gateway MCP (desk-crm) para publicar comentário HTML + * na discussão #32, e para padrões com consecutive_weeks>=3 e + * severity∈(warning,action) abrir Ticket no Desk. * 5. Output JSON-line final com contagens. * * Env obrigatório com --publish: - * DESK_API_TOKEN Token API Desk CRM - * DESK_BASE_URL Base URL (default https://desk.descomplicar.pt) + * MCP_GATEWAY_TOKEN Bearer token do gateway MCP + * MCP_GATEWAY_URL URL do MCP desk-crm (default https://gateway.descomplicar.pt/v1/desk-crm/mcp) */ import { openSessionsDb, type PatternRecord } from '../services/sessions/db.js' import { DEFAULT_DB_PATH } from '../services/sessions/indexer.js' @@ -24,7 +24,6 @@ import { detectPatterns, toPatternRecord, weekRange, - weekIso as computeWeekIso, type Pattern, } from '../services/sessions/patterns.js' @@ -34,6 +33,18 @@ interface Args { force: boolean } +interface MCPToolCallResult { + content?: Array<{ type: string; text: string }> + isError?: boolean +} + +/** Staff ID usado para publicar comentários automáticos (Observabilidade) */ +const OBSERVABILIDADE_STAFF_ID = 25 +/** Discussão Desk onde os resumos semanais são publicados */ +const OBSERVABILIDADE_DISCUSSION_ID = 32 +/** Departamento "Geral" no Desk CRM (id=1) */ +const OBSERVABILIDADE_DEPARTMENT_ID = 1 + function parseArgs(argv: string[]): Args { const a: Args = { publish: false, force: false } for (let i = 0; i < argv.length; i++) { @@ -64,7 +75,7 @@ function escapeHtml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } -function buildDiscussionHtml(weekIso: string, patterns: PatternRecord[], baseUrl: string): string { +function buildDiscussionHtml(weekIso: string, patterns: PatternRecord[]): string { const dateStr = new Date().toISOString().slice(0, 10) const lines: string[] = [] lines.push(`

Semana ${weekIso} — Padrões Detectados Automaticamente

`) @@ -95,25 +106,70 @@ function buildDiscussionHtml(weekIso: string, patterns: PatternRecord[], baseUrl return lines.join('\n') } -async function postDeskDiscussionComment(baseUrl: string, token: string, html: string): Promise { - const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/v1/discussions/32/comments`, { +/** + * Chama uma ferramenta do gateway MCP (JSON-RPC 2.0 sobre HTTP). + * O gateway pode responder em SSE (text/event-stream) ou JSON — tratamos ambos. + */ +async function callMcpTool(tool: string, args: Record): Promise { + const url = process.env.MCP_GATEWAY_URL ?? 'https://gateway.descomplicar.pt/v1/desk-crm/mcp' + const token = process.env.MCP_GATEWAY_TOKEN + if (!token) throw new Error('MCP_GATEWAY_TOKEN não definido') + const body = { + jsonrpc: '2.0', + id: Date.now(), + method: 'tools/call', + params: { name: tool, arguments: args }, + } + const res = await fetch(url, { method: 'POST', headers: { + Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', - authtoken: token, + Accept: 'application/json, text/event-stream', }, - body: JSON.stringify({ content: html }), + body: JSON.stringify(body), }) if (!res.ok) { - const body = await res.text().catch(() => '') - throw new Error(`Desk discussion POST falhou: ${res.status} ${body.slice(0, 200)}`) + const txt = await res.text().catch(() => '') + throw new Error(`MCP gateway ${res.status}: ${txt.slice(0, 300)}`) } - return res.json().catch(() => ({})) + const raw = await res.text() + // Responde JSON puro ou SSE (linhas "data: {...}") + let payload: string | null = null + for (const line of raw.split(/\r?\n/)) { + const trimmed = line.trim() + if (!trimmed) continue + if (trimmed.startsWith('data: ')) { + payload = trimmed.slice(6) + break + } + if (trimmed.startsWith('{')) { + payload = trimmed + break + } + } + if (!payload) throw new Error(`MCP resposta sem payload JSON: ${raw.slice(0, 200)}`) + const parsed = JSON.parse(payload) as { error?: unknown; result?: MCPToolCallResult } + if (parsed.error) throw new Error(`MCP error: ${JSON.stringify(parsed.error)}`) + const result = parsed.result as MCPToolCallResult | undefined + if (result?.isError) { + const txt = result.content?.map((c) => c.text).join('\n') ?? '' + throw new Error(`MCP tool ${tool} devolveu isError: ${txt.slice(0, 300)}`) + } + return result ?? {} } -async function createDeskTicket(baseUrl: string, token: string, p: PatternRecord): Promise { +async function postDeskDiscussionComment(html: string): Promise { + return callMcpTool('add_discussion_comment', { + discussion_id: OBSERVABILIDADE_DISCUSSION_ID, + content: html, + staff_id: OBSERVABILIDADE_STAFF_ID, + }) +} + +async function createDeskTicket(p: PatternRecord): Promise { const priority = p.severity === 'action' ? 4 : 3 // 4=alta, 3=média - const body = [ + const message = [ `

Padrão recorrente detectado automaticamente — ${p.consecutive_weeks} semanas consecutivas.

`, `

${escapeHtml(p.description)}

`, `
    `, @@ -125,28 +181,20 @@ async function createDeskTicket(baseUrl: string, token: string, p: PatternRecord `
`, `

Sample sessions: ${p.sample_session_ids.map((s) => `${escapeHtml(s)}`).join(', ')}

`, ].join('\n') - const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/v1/tickets`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - authtoken: token, - }, - body: JSON.stringify({ - subject: `Padrão recorrente: ${p.title}`, - body, - priority, - department: 1, - }), + return callMcpTool('create_ticket', { + subject: `Padrão recorrente: ${p.title}`, + message, + priority, + department: OBSERVABILIDADE_DEPARTMENT_ID, }) - if (!res.ok) { - const t = await res.text().catch(() => '') - throw new Error(`Desk ticket POST falhou: ${res.status} ${t.slice(0, 200)}`) - } - return res.json().catch(() => ({})) } async function main(): Promise { const args = parseArgs(process.argv.slice(2)) + if (args.publish && !process.env.MCP_GATEWAY_TOKEN) { + console.error('[patterns] --publish requer MCP_GATEWAY_TOKEN. Aborta.') + process.exit(1) + } const dbPath = process.env.OBSERVABILIDADE_DB ?? DEFAULT_DB_PATH const db = openSessionsDb(dbPath) @@ -177,35 +225,28 @@ async function main(): Promise { let publishError: string | null = null if (args.publish) { - const baseUrl = process.env.DESK_BASE_URL ?? 'https://desk.descomplicar.pt' - const token = process.env.DESK_API_TOKEN - if (!token) { - publishError = 'DESK_API_TOKEN ausente' - } else { - try { - const html = buildDiscussionHtml(weekIso, records, baseUrl) - await postDeskDiscussionComment(baseUrl, token, html) - commentPosted = true - for (const rec of records) { - if (rec.consecutive_weeks >= 3 && (rec.severity === 'warning' || rec.severity === 'action')) { - try { - await createDeskTicket(baseUrl, token, rec) - ticketsCreated++ - } catch (e) { - console.error(`[patterns] falha ao criar ticket para ${rec.pattern_key}:`, (e as Error).message) - } + try { + const html = buildDiscussionHtml(weekIso, records) + await postDeskDiscussionComment(html) + commentPosted = true + for (const rec of records) { + if (rec.consecutive_weeks >= 3 && (rec.severity === 'warning' || rec.severity === 'action')) { + try { + await createDeskTicket(rec) + ticketsCreated++ + } catch (e) { + console.error(`[patterns] falha ao criar ticket para ${rec.pattern_key}:`, (e as Error).message) } } - } catch (e) { - publishError = (e as Error).message } + } catch (e) { + publishError = (e as Error).message } } // Dry-run: render HTML para stderr para verificação manual if (!args.publish) { - const baseUrl = process.env.DESK_BASE_URL ?? 'https://desk.descomplicar.pt' - const html = buildDiscussionHtml(weekIso, records, baseUrl) + const html = buildDiscussionHtml(weekIso, records) console.error('\n--- HTML (dry-run) ---') console.error(html) console.error('--- /HTML ---\n') diff --git a/systemd/observabilidade-patterns.env.example b/systemd/observabilidade-patterns.env.example index 32ed8a9..613075d 100644 --- a/systemd/observabilidade-patterns.env.example +++ b/systemd/observabilidade-patterns.env.example @@ -1,2 +1,2 @@ -DESK_API_TOKEN=coloca-token-aqui -DESK_BASE_URL=https://desk.descomplicar.pt +MCP_GATEWAY_TOKEN=coloca-token-aqui +MCP_GATEWAY_URL=https://gateway.descomplicar.pt/v1/desk-crm/mcp