refactor(observabilidade): pattern detector usa gateway MCP em vez de API Desk directa
Substitui chamadas HTTP directas à API Desk (/api/v1/discussions, /api/v1/tickets) por JSON-RPC 2.0 ao gateway MCP (desk-crm). Helper callMcpTool lida com respostas JSON ou SSE. Substitui DESK_API_TOKEN/DESK_BASE_URL por MCP_GATEWAY_TOKEN/URL. - add_discussion_comment: discussion_id=32, staff_id=25 (Observabilidade) - create_ticket: subject/message/priority(1-4)/department=1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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, '>').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(`<h4>Semana ${weekIso} — Padrões Detectados Automaticamente</h4>`)
|
||||
@@ -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<unknown> {
|
||||
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<string, unknown>): Promise<MCPToolCallResult> {
|
||||
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<unknown> {
|
||||
async function postDeskDiscussionComment(html: string): Promise<unknown> {
|
||||
return callMcpTool('add_discussion_comment', {
|
||||
discussion_id: OBSERVABILIDADE_DISCUSSION_ID,
|
||||
content: html,
|
||||
staff_id: OBSERVABILIDADE_STAFF_ID,
|
||||
})
|
||||
}
|
||||
|
||||
async function createDeskTicket(p: PatternRecord): Promise<unknown> {
|
||||
const priority = p.severity === 'action' ? 4 : 3 // 4=alta, 3=média
|
||||
const body = [
|
||||
const message = [
|
||||
`<p><strong>Padrão recorrente detectado automaticamente — ${p.consecutive_weeks} semanas consecutivas.</strong></p>`,
|
||||
`<p>${escapeHtml(p.description)}</p>`,
|
||||
`<ul>`,
|
||||
@@ -125,28 +181,20 @@ async function createDeskTicket(baseUrl: string, token: string, p: PatternRecord
|
||||
`</ul>`,
|
||||
`<p><em>Sample sessions:</em> ${p.sample_session_ids.map((s) => `<code>${escapeHtml(s)}</code>`).join(', ')}</p>`,
|
||||
].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<void> {
|
||||
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<void> {
|
||||
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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user