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