- gti-scheduling-mcp: MCP server com ZProAdapter e KlingoAdapter (agendar() completo pros dois) - Workflow n8n 'Agendamento - GChat - Z-PRO': webhook -> AI Agent -> MCP -> Z-PRO, testado com sucesso real - Documentacao: specs, plano, runbook de problemas reais e export do workflow Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
/**
|
|
* scripts/diagnose-zpro-list.ts
|
|
*
|
|
* Script CLI temporário e seguro para descobrir o contrato real do
|
|
* endpoint:
|
|
*
|
|
* GET /v2/api/external/{ApiID}/appointment/list
|
|
*
|
|
* Faz apenas UMA requisição GET de leitura (page=1, limit=5), com timeout
|
|
* de 15s, sem retry e sem modificar nenhum dado. Imprime a resposta
|
|
* sanitizada e um resumo estrutural.
|
|
*
|
|
* O script nunca imprime token, header Authorization, stack trace ou
|
|
* dados pessoais completos dos pacientes.
|
|
*/
|
|
|
|
import { loadSchedulingConfig } from "../src/config/env.config.js";
|
|
import {
|
|
buildAppointmentListUrl,
|
|
fetchAppointmentList,
|
|
NetworkRequestError,
|
|
RequestTimeoutError,
|
|
} from "../src/diagnostics/client.js";
|
|
import { buildResponseReport } from "../src/diagnostics/response.js";
|
|
import { sanitizeText } from "../src/diagnostics/sanitize.js";
|
|
import type { StructureSummary } from "../src/diagnostics/analyze.js";
|
|
|
|
function list(values: string[]): string {
|
|
return values.length > 0 ? values.join(", ") : "(nenhum)";
|
|
}
|
|
|
|
function classifyHttpStatus(status: number): string {
|
|
if (status === 401 || status === 403) {
|
|
return `Erro de autenticação (status ${status}). Verifique ZPRO_API_TOKEN.`;
|
|
}
|
|
if (status === 404) {
|
|
return `ApiID inválido ou recurso não encontrado (status ${status}). Verifique ZPRO_API_ID.`;
|
|
}
|
|
return `Status HTTP ${status} (esperado 2xx).`;
|
|
}
|
|
|
|
function printStructureSummary(summary: StructureSummary): void {
|
|
console.log(`Tipo da raiz: ${summary.rootType}`);
|
|
|
|
if (summary.rootType === "array") {
|
|
console.log(`Quantidade de itens no array: ${summary.arrayItemCount}`);
|
|
console.log(`Chaves do primeiro item: ${list(summary.firstItemKeys)}`);
|
|
} else if (summary.rootType === "object") {
|
|
console.log(`Chaves na raiz: ${list(summary.rootKeys)}`);
|
|
}
|
|
|
|
console.log("Tipos dos campos:");
|
|
for (const [key, type] of Object.entries(summary.fieldTypes)) {
|
|
console.log(` ${key}: ${type}`);
|
|
}
|
|
|
|
console.log(
|
|
`Possíveis campos de paginação: ${list(summary.possiblePaginationFields)}`,
|
|
);
|
|
console.log(
|
|
`Possíveis campos de identificação: ${list(summary.possibleIdFields)}`,
|
|
);
|
|
console.log(`Possíveis campos de data: ${list(summary.possibleDateFields)}`);
|
|
console.log(
|
|
`Possíveis campos de status: ${list(summary.possibleStatusFields)}`,
|
|
);
|
|
}
|
|
|
|
async function main(): Promise<number> {
|
|
let config;
|
|
try {
|
|
config = loadSchedulingConfig();
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error
|
|
? error.message
|
|
: "Falha ao carregar a configuração.";
|
|
console.error(message);
|
|
return 1;
|
|
}
|
|
|
|
const secrets = [config.apiToken];
|
|
|
|
let response;
|
|
try {
|
|
response = await fetchAppointmentList(config);
|
|
} catch (error) {
|
|
if (error instanceof RequestTimeoutError) {
|
|
console.error("Timeout ao consultar a API do Z-PRO (15s).");
|
|
} else if (error instanceof NetworkRequestError) {
|
|
console.error("Falha de rede ao consultar a API do Z-PRO.");
|
|
} else {
|
|
console.error("Erro inesperado ao consultar a API do Z-PRO.");
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
console.log("=== Informações da requisição ===");
|
|
console.log("Método: GET");
|
|
console.log(`URL: ${buildAppointmentListUrl(config)}`);
|
|
console.log(`Status: ${response.status}`);
|
|
console.log(`Content-Type: ${response.contentType ?? "(ausente)"}`);
|
|
console.log(`Duração: ${response.durationMs}ms`);
|
|
|
|
if (response.status < 200 || response.status >= 300) {
|
|
console.log("");
|
|
console.log("=== Erro HTTP ===");
|
|
console.log(classifyHttpStatus(response.status));
|
|
if (response.bodyText) {
|
|
console.log("");
|
|
console.log("Corpo da resposta (primeiros 500 caracteres, sanitizado):");
|
|
console.log(sanitizeText(response.bodyText, secrets));
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
const report = buildResponseReport(response.bodyText, secrets);
|
|
|
|
if (!report.isJson) {
|
|
console.log("");
|
|
console.log("=== A resposta não é JSON ===");
|
|
console.log("Conteúdo (primeiros 500 caracteres, sanitizado):");
|
|
console.log(report.sanitizedText);
|
|
return 1;
|
|
}
|
|
|
|
console.log("");
|
|
console.log("=== Estrutura da resposta ===");
|
|
console.log(report.sanitizedText);
|
|
|
|
if (report.structure) {
|
|
console.log("");
|
|
console.log("=== Resumo estrutural ===");
|
|
printStructureSummary(report.structure);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
main()
|
|
.then((code) => {
|
|
process.exit(code);
|
|
})
|
|
.catch(() => {
|
|
process.exit(1);
|
|
});
|