feat(observabilidade): tabela worklog_comments + parser HTML + importer MCP
- Schema worklog_comments (id, discussion, parent, datas, staff, campos parseados em JSON) - Parser HTML tolerante (h2/h3/h4) extrai title, task_ref, duration, work_items, files_modified, problems, patterns_text, actions - Módulo worklog-import com paginação MCP get_discussion_comments - Helper mcp-client.ts partilhado (gateway MCP JSON-RPC + SSE) - Dep runtime: node-html-parser Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Cliente HTTP mínimo para o gateway MCP (JSON-RPC 2.0 sobre HTTP).
|
||||
*
|
||||
* Suporta resposta em JSON puro ou SSE (text/event-stream). Partilhado entre
|
||||
* os scripts de Observabilidade (patterns + worklog import).
|
||||
*/
|
||||
|
||||
export interface MCPToolCallResult {
|
||||
content?: Array<{ type: string; text: string }>
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
export 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',
|
||||
Accept: 'application/json, text/event-stream',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const txt = await res.text().catch(() => '')
|
||||
throw new Error(`MCP gateway ${res.status}: ${txt.slice(0, 300)}`)
|
||||
}
|
||||
const raw = await res.text()
|
||||
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 ?? {}
|
||||
}
|
||||
|
||||
/** Extrai o primeiro bloco de texto JSON-encoded do resultado MCP. */
|
||||
export function extractMcpJsonPayload<T = unknown>(r: MCPToolCallResult): T {
|
||||
const text = r.content?.find((c) => c.type === 'text')?.text
|
||||
if (!text) throw new Error('MCP result sem content text')
|
||||
return JSON.parse(text) as T
|
||||
}
|
||||
Reference in New Issue
Block a user