@tiflux/mcp
Version:
TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients
1,446 lines (1,286 loc) • 61.1 kB
JavaScript
/**
* Handlers para operações relacionadas a tickets
*/
const fs = require('fs');
const TiFluxAPI = require('../api/tiflux-api');
class TicketHandlers {
constructor() {
this.api = new TiFluxAPI();
}
/**
* Handler para buscar um ticket específico
*/
async handleGetTicket(args) {
const { ticket_number, show_entities, include_filled_entity } = args;
if (!ticket_number) {
throw new Error('ticket_number é obrigatório');
}
try {
const options = {};
if (show_entities) options.show_entities = true;
if (include_filled_entity) options.include_filled_entity = true;
const response = await this.api.fetchTicket(ticket_number, options);
if (response.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao buscar ticket #${ticket_number}**\n\n` +
`**Código:** ${response.status}\n` +
`**Mensagem:** ${response.error}\n\n` +
`*Verifique se o ticket existe e se você tem permissão para acessá-lo.*`
}
]
};
}
const ticket = response.data;
// Formatar campos personalizados se existirem
let entitiesText = '';
if (ticket.entities || ticket.entity_fields) {
const entities = ticket.entities || [];
const entityFields = ticket.entity_fields || [];
if (entities.length > 0) {
entitiesText = '\n\n**Campos Personalizados (entities):**\n';
entities.forEach(entity => {
entitiesText += `\n**${entity.name || 'Menu'}** (ID: ${entity.id})\n`;
if (entity.entity_fields && entity.entity_fields.length > 0) {
entity.entity_fields.forEach(field => {
const value = field.value !== null && field.value !== undefined ? field.value : '(vazio)';
entitiesText += ` • ${field.name} (${field.field_type}): ${value}\n`;
entitiesText += ` - entity_field_id: ${field.entity_field_id}\n`;
});
}
});
} else if (entityFields.length > 0) {
entitiesText = '\n\n**Campos Personalizados (entity_fields):**\n';
entityFields.forEach(field => {
const value = field.value !== null && field.value !== undefined ? field.value : '(vazio)';
entitiesText += ` • ${field.name} (${field.field_type}): ${value}\n`;
entitiesText += ` - entity_field_id: ${field.entity_field_id}\n`;
});
}
}
// Formatar informações expandidas
let statusInfo = '';
if (ticket.status) {
statusInfo = `**Status:** ${ticket.status.name || 'N/A'} (ID: ${ticket.status.id || 'N/A'})\n`;
statusInfo += ` • Aberto: ${ticket.status.default_open ? 'Sim' : 'Não'}\n`;
statusInfo += ` • Fechado: ${ticket.is_closed ? 'Sim' : 'Não'}\n`;
}
let priorityInfo = '';
if (ticket.priority) {
priorityInfo = `**Prioridade:** ${ticket.priority.name || 'N/A'} (ID: ${ticket.priority.id || 'N/A'})\n`;
} else {
priorityInfo = `**Prioridade:** Não definida\n`;
}
let deskInfo = '';
if (ticket.desk) {
deskInfo = `**Mesa:** ${ticket.desk.display_name || ticket.desk.name || 'N/A'} (ID: ${ticket.desk.id || 'N/A'})\n`;
deskInfo += ` • Nome interno: ${ticket.desk.name || 'N/A'}\n`;
deskInfo += ` • Ativa: ${ticket.desk.active ? 'Sim' : 'Não'}\n`;
}
let stageInfo = '';
if (ticket.stage) {
// Emoji indicator baseado no tipo de estágio
let stageEmoji = '📊';
if (ticket.stage.first_stage) stageEmoji = '🟢';
else if (ticket.stage.last_stage) stageEmoji = '🏁';
else if (ticket.stage.name && ticket.stage.name.toLowerCase().includes('review')) stageEmoji = '🟡';
stageInfo = `**Estágio:** ${ticket.stage.name || 'N/A'} ${stageEmoji} (ID: ${ticket.stage.id || 'N/A'})\n`;
stageInfo += ` • Primeiro estágio: ${ticket.stage.first_stage ? 'Sim' : 'Não'}\n`;
stageInfo += ` • Último estágio: ${ticket.stage.last_stage ? 'Sim' : 'Não'}\n`;
if (ticket.stage.max_time) {
stageInfo += ` • Tempo máximo: ${ticket.stage.max_time}\n`;
}
}
let catalogInfo = '';
if (ticket.services_catalog) {
catalogInfo = `\n**Catálogo de Serviços:**\n`;
catalogInfo += ` • Item: ${ticket.services_catalog.item_name || 'N/A'} (ID: ${ticket.services_catalog.id || 'N/A'})\n`;
catalogInfo += ` • Área: ${ticket.services_catalog.area_name || 'N/A'}`;
if (ticket.services_catalog.area_id) {
catalogInfo += ` (ID: ${ticket.services_catalog.area_id})`;
}
catalogInfo += `\n`;
catalogInfo += ` • Catálogo: ${ticket.services_catalog.catalog_name || 'N/A'}`;
if (ticket.services_catalog.catalog_id) {
catalogInfo += ` (ID: ${ticket.services_catalog.catalog_id})`;
}
catalogInfo += `\n`;
}
let responsibleInfo = '';
if (ticket.responsible) {
responsibleInfo = `**Responsável:** ${ticket.responsible.name || 'N/A'} (ID: ${ticket.responsible.id || 'N/A'})\n`;
responsibleInfo += ` • Email: ${ticket.responsible.email || 'N/A'}\n`;
responsibleInfo += ` • Tipo: ${ticket.responsible._type || 'N/A'}\n`;
responsibleInfo += ` • Ativo: ${ticket.responsible.active ? 'Sim' : 'Não'}\n`;
if (ticket.responsible.technical_group_id) {
responsibleInfo += ` • Grupo técnico ID: ${ticket.responsible.technical_group_id}\n`;
}
} else {
responsibleInfo = `**Responsável:** Não atribuído\n`;
}
let clientInfo = '';
if (ticket.client) {
clientInfo = `**Cliente:** ${ticket.client.name || 'N/A'} (ID: ${ticket.client.id || 'N/A'})\n`;
if (ticket.client.social) {
clientInfo += ` • Razão social: ${ticket.client.social}\n`;
}
clientInfo += ` • Ativo: ${ticket.client.status ? 'Sim' : 'Não'}\n`;
}
let createdByInfo = '';
if (ticket.created_by_id) {
createdByInfo = `**Criado por:** `;
if (ticket.created_by && ticket.created_by.name) {
createdByInfo += `${ticket.created_by.name} (ID: ${ticket.created_by_id})`;
} else {
createdByInfo += `ID ${ticket.created_by_id}`;
}
if (ticket.created_by_way_of) {
createdByInfo += ` (via ${ticket.created_by_way_of})`;
}
createdByInfo += `\n`;
}
let updatedByInfo = '';
if (ticket.updated_by_id) {
updatedByInfo = `**Atualizado por:** `;
if (ticket.updated_by && ticket.updated_by.name) {
updatedByInfo += `${ticket.updated_by.name} (ID: ${ticket.updated_by_id})`;
} else {
updatedByInfo += `ID ${ticket.updated_by_id}`;
}
updatedByInfo += `\n`;
}
let slaInfo = '';
if (ticket.sla_info) {
slaInfo = `\n**SLA:**\n`;
slaInfo += ` • Parado: ${ticket.sla_info.stopped ? 'Sim' : 'Não'}\n`;
if (ticket.sla_info.stage_expiration) {
slaInfo += ` • Expiração do estágio: ${ticket.sla_info.stage_expiration}\n`;
}
if (ticket.sla_info.attend_sla) {
slaInfo += ` • SLA de atendimento: ${ticket.sla_info.attend_sla}\n`;
}
if (ticket.sla_info.attend_expiration) {
slaInfo += ` • Expiração atendimento: ${ticket.sla_info.attend_expiration}\n`;
}
if (ticket.sla_info.solve_expiration) {
slaInfo += ` • Expiração resolução: ${ticket.sla_info.solve_expiration}\n`;
}
if (ticket.sla_info.solved_in_time !== null) {
slaInfo += ` • Resolvido no prazo: ${ticket.sla_info.solved_in_time ? 'Sim' : 'Não'}\n`;
}
}
let additionalInfo = '';
if (ticket.followers) {
additionalInfo += `**Seguidores:** ${ticket.followers}\n`;
}
if (ticket.tags && Array.isArray(ticket.tags) && ticket.tags.length > 0) {
additionalInfo += `**Tags:** ${ticket.tags.join(', ')}\n`;
} else if (ticket.tags && typeof ticket.tags === 'string' && ticket.tags.trim()) {
additionalInfo += `**Tags:** ${ticket.tags}\n`;
}
if (ticket.worked_hours) {
additionalInfo += `**Horas trabalhadas:** ${ticket.worked_hours}\n`;
}
if (ticket.closed_at) {
additionalInfo += `**Fechado em:** ${ticket.closed_at}\n`;
}
if (ticket.reopen_count > 0) {
additionalInfo += `**Reaberturas:** ${ticket.reopen_count}\n`;
}
if (ticket.last_reopen_date) {
additionalInfo += `**Última reabertura:** ${ticket.last_reopen_date}\n`;
}
if (ticket.is_grouped) {
additionalInfo += `**Agrupado:** Sim\n`;
}
if (ticket.is_revised) {
additionalInfo += `**Revisado:** Sim\n`;
}
let urlInfo = '';
if (ticket.url_internal_path || ticket.url_external_path) {
urlInfo = `\n**URLs:**\n`;
if (ticket.url_internal_path) {
urlInfo += ` • Interna: ${ticket.url_internal_path}\n`;
}
if (ticket.url_external_path) {
urlInfo += ` • Externa: ${ticket.url_external_path}\n`;
}
}
return {
content: [
{
type: 'text',
text: `**Ticket #${ticket_number}**\n\n` +
`**Título:** ${ticket.title || 'N/A'}\n\n` +
`${statusInfo}` +
`${priorityInfo}\n` +
`${deskInfo}\n` +
`${stageInfo}\n` +
`${catalogInfo}\n` +
`${responsibleInfo}\n` +
`${clientInfo}\n` +
`${createdByInfo}` +
`**Criado em:** ${ticket.created_at || 'N/A'}\n` +
`${updatedByInfo}` +
`**Atualizado em:** ${ticket.updated_at || 'N/A'}\n` +
`${additionalInfo}` +
`${slaInfo}` +
`${urlInfo}\n` +
`**Descrição:**\n${ticket.description || 'Sem descrição'}${entitiesText}\n\n` +
`*✅ Dados obtidos da API TiFlux em tempo real*`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro interno ao buscar ticket #${ticket_number}**\n\n` +
`**Erro:** ${error.message}\n\n` +
`*Verifique sua conexão e configurações da API.*`
}
]
};
}
}
/**
* Handler para criar um novo ticket
*/
async handleCreateTicket(args) {
const {
title,
description,
client_id,
client_name,
desk_id,
desk_name,
priority_id,
services_catalogs_item_id,
catalog_item_name,
status_id,
requestor_name,
requestor_email,
requestor_telephone,
responsible_id,
responsible_name,
followers
} = args;
if (!title || !description) {
throw new Error('title e description são obrigatórios');
}
try {
let finalClientId = client_id;
// Se client_name foi fornecido, buscar o ID do cliente
if (client_name && !client_id) {
const clientSearchResponse = await this.api.searchClients(client_name);
if (clientSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao buscar cliente "${client_name}"**\n\n` +
`**Erro:** ${clientSearchResponse.error}\n\n` +
`*Verifique se o nome do cliente está correto ou use client_id diretamente.*`
}
]
};
}
const clients = clientSearchResponse.data || [];
if (clients.length === 0) {
return {
content: [
{
type: 'text',
text: `**❌ Cliente "${client_name}" não encontrado**\n\n` +
`*Verifique se o nome está correto ou use client_id diretamente.*`
}
]
};
}
if (clients.length > 1) {
let clientsList = '**Clientes encontrados:**\n';
clients.forEach((client, index) => {
clientsList += `${index + 1}. **ID:** ${client.id} | **Nome:** ${client.name}\n`;
});
return {
content: [
{
type: 'text',
text: `**⚠️ Múltiplos clientes encontrados para "${client_name}"**\n\n` +
`${clientsList}\n` +
`*Use client_id específico ou seja mais específico no client_name.*`
}
]
};
}
finalClientId = clients[0].id;
}
let finalDeskId = desk_id;
// Se desk_name foi fornecido, buscar o ID da mesa
if (desk_name && !desk_id) {
const deskSearchResponse = await this.api.searchDesks(desk_name);
if (deskSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao buscar mesa "${desk_name}"**\n\n` +
`**Erro:** ${deskSearchResponse.error}\n\n` +
`*Verifique se o nome da mesa está correto ou use desk_id diretamente.*`
}
]
};
}
const desks = deskSearchResponse.data || [];
if (desks.length === 0) {
return {
content: [
{
type: 'text',
text: `**❌ Mesa "${desk_name}" não encontrada**\n\n` +
`*Verifique se o nome está correto ou use desk_id diretamente.*`
}
]
};
}
if (desks.length > 1) {
let desksList = '**Mesas encontradas:**\n';
desks.forEach((desk, index) => {
desksList += `${index + 1}. **ID:** ${desk.id} | **Nome:** ${desk.name} | **Display:** ${desk.display_name}\n`;
});
return {
content: [
{
type: 'text',
text: `**⚠️ Múltiplas mesas encontradas para "${desk_name}"**\n\n` +
`${desksList}\n` +
`*Use desk_id específico ou seja mais específico no desk_name.*`
}
]
};
}
finalDeskId = desks[0].id;
}
let finalResponsibleId = responsible_id;
// Se responsible_name foi fornecido, buscar o ID do usuário
if (responsible_name && !responsible_id) {
const userSearchResponse = await this.api.searchUsers({
name: responsible_name,
active: true,
type: 'attendant', // Apenas atendentes podem ser responsáveis
limit: 10
});
if (userSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**Erro ao buscar usuario "${responsible_name}"**\n\n` +
`**Erro:** ${userSearchResponse.error}\n\n` +
`*Verifique se o nome do usuario esta correto ou use responsible_id diretamente.*`
}
]
};
}
const users = userSearchResponse.data || [];
if (users.length === 0) {
return {
content: [
{
type: 'text',
text: `**Usuario "${responsible_name}" nao encontrado**\n\n` +
`*Verifique se o nome esta correto ou use responsible_id diretamente.*`
}
]
};
}
if (users.length > 1) {
let usersList = '**Usuarios encontrados:**\n';
users.forEach((user, index) => {
usersList += `${index + 1}. **ID:** ${user.id} | **Nome:** ${user.name} | **Email:** ${user.email}\n`;
});
return {
content: [
{
type: 'text',
text: `**Multiplos usuarios encontrados para "${responsible_name}"**\n\n` +
`${usersList}\n` +
`*Use responsible_id especifico ou seja mais especifico no responsible_name.*`
}
]
};
}
finalResponsibleId = users[0].id;
}
// Usar valores padrão das variáveis de ambiente se não informados
finalClientId = finalClientId || process.env.TIFLUX_DEFAULT_CLIENT_ID;
finalDeskId = finalDeskId || process.env.TIFLUX_DEFAULT_DESK_ID;
const finalPriorityId = priority_id || process.env.TIFLUX_DEFAULT_PRIORITY_ID;
let finalCatalogItemId = services_catalogs_item_id || process.env.TIFLUX_DEFAULT_CATALOG_ITEM_ID;
// Se catalog_item_name foi fornecido, buscar o ID do item de catálogo
if (catalog_item_name && !services_catalogs_item_id && finalDeskId) {
const catalogSearchResponse = await this.api.searchCatalogItems(finalDeskId, { limit: 200 });
if (catalogSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**Erro ao buscar item de catalogo "${catalog_item_name}"**\n\n` +
`**Erro:** ${catalogSearchResponse.error}\n\n` +
`*Verifique se o nome do item esta correto ou use services_catalogs_item_id diretamente.*`
}
]
};
}
const catalogItems = catalogSearchResponse.data || [];
if (catalogItems.length === 0) {
return {
content: [
{
type: 'text',
text: `**Nenhum item de catalogo encontrado na mesa ${finalDeskId}**\n\n` +
`*Verifique se a mesa possui itens de catalogo configurados.*`
}
]
};
}
// Filtrar por nome (busca parcial case-insensitive)
const searchTerm = catalog_item_name.toLowerCase();
const matchingItems = catalogItems.filter(item =>
item.name.toLowerCase().includes(searchTerm)
);
if (matchingItems.length === 0) {
return {
content: [
{
type: 'text',
text: `**Item de catalogo "${catalog_item_name}" nao encontrado**\n\n` +
`*Verifique se o nome esta correto ou use services_catalogs_item_id diretamente.*`
}
]
};
}
if (matchingItems.length > 1) {
let itemsList = '**Itens de catalogo encontrados:**\n';
matchingItems.forEach((item, index) => {
itemsList += `${index + 1}. **ID:** ${item.id} | **Nome:** ${item.name} | **Area:** ${item.area.name} | **Catalogo:** ${item.catalog.name}\n`;
});
return {
content: [
{
type: 'text',
text: `**Multiplos itens de catalogo encontrados para "${catalog_item_name}"**\n\n` +
`${itemsList}\n` +
`*Use services_catalogs_item_id especifico ou seja mais especifico no catalog_item_name.*`
}
]
};
}
finalCatalogItemId = matchingItems[0].id;
}
if (!finalClientId || !finalDeskId) {
throw new Error('client_id e desk_id são obrigatórios (configure TIFLUX_DEFAULT_CLIENT_ID e TIFLUX_DEFAULT_DESK_ID ou informe nos parâmetros)');
}
// Criar ticket via API
const response = await this.api.createTicket({
title,
description,
client_id: parseInt(finalClientId),
desk_id: parseInt(finalDeskId),
priority_id: finalPriorityId ? parseInt(finalPriorityId) : undefined,
services_catalogs_item_id: finalCatalogItemId ? parseInt(finalCatalogItemId) : undefined,
status_id: status_id ? parseInt(status_id) : undefined,
requestor_name,
requestor_email,
requestor_telephone,
responsible_id: finalResponsibleId ? parseInt(finalResponsibleId) : undefined,
followers
});
if (response.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao criar ticket**\n\n` +
`**Código:** ${response.status}\n` +
`**Mensagem:** ${response.error}\n\n` +
`*Verifique os parâmetros e configurações.*`
}
]
};
}
const ticket = response.data.ticket;
return {
content: [
{
type: 'text',
text: `**✅ Ticket criado com sucesso!**\n\n` +
`**Número:** #${ticket.ticket_number}\n` +
`**Título:** ${ticket.title}\n` +
`**Cliente:** ${ticket.client.name}\n` +
`**Mesa:** ${ticket.desk.display_name}\n` +
`**Status:** ${ticket.status.name}\n` +
`**Prioridade:** ${ticket.priority?.name || 'N/A'}\n` +
`**Criado em:** ${ticket.created_at}\n\n` +
`**URL Externa:** ${ticket.url_external_path}\n` +
`**URL Interna:** ${ticket.url_internal_path}\n\n` +
`*✅ Ticket criado via API TiFlux*`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro interno ao criar ticket**\n\n` +
`**Erro:** ${error.message}\n\n` +
`*Verifique sua conexão e configurações da API.*`
}
]
};
}
}
/**
* Handler para atualizar um ticket existente
*/
async handleUpdateTicket(args) {
const {
ticket_number,
title,
description,
client_id,
desk_id,
desk_name,
stage_id,
stage_name,
responsible_id,
responsible_name,
followers,
services_catalogs_item_id,
catalog_item_name
} = args;
if (!ticket_number) {
throw new Error('ticket_number é obrigatório');
}
try {
let finalDeskId = desk_id;
let finalStageId = stage_id;
let finalResponsibleId = responsible_id;
// Se desk_name foi fornecido, buscar o ID da mesa
if (desk_name && !desk_id) {
const deskSearchResponse = await this.api.searchDesks(desk_name);
if (deskSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**Erro ao buscar mesa "${desk_name}"**\n\n` +
`**Erro:** ${deskSearchResponse.error}\n\n` +
`*Verifique se o nome da mesa esta correto ou use desk_id diretamente.*`
}
]
};
}
const desks = deskSearchResponse.data || [];
if (desks.length === 0) {
return {
content: [
{
type: 'text',
text: `**Mesa "${desk_name}" nao encontrada**\n\n` +
`*Verifique se o nome esta correto ou use desk_id diretamente.*`
}
]
};
}
if (desks.length > 1) {
let desksList = '**Mesas encontradas:**\n';
desks.forEach((desk, index) => {
desksList += `${index + 1}. **ID:** ${desk.id} | **Nome:** ${desk.name} | **Display:** ${desk.display_name}\n`;
});
return {
content: [
{
type: 'text',
text: `**Multiplas mesas encontradas para "${desk_name}"**\n\n` +
`${desksList}\n` +
`*Use desk_id especifico ou seja mais especifico no desk_name.*`
}
]
};
}
finalDeskId = desks[0].id;
}
// Se stage_name foi fornecido, buscar o ID do estágio
// Precisa de desk_id ou desk_name para buscar estágios
if (stage_name && !stage_id) {
const deskIdForStage = finalDeskId || desk_id;
if (!deskIdForStage) {
return {
content: [
{
type: 'text',
text: `**Erro: desk_id ou desk_name obrigatorio para buscar estagio por nome**\n\n` +
`*Para usar stage_name, informe tambem desk_id ou desk_name.*`
}
]
};
}
const stageSearchResponse = await this.api.searchStages(deskIdForStage);
if (stageSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**Erro ao buscar estagios da mesa ID ${deskIdForStage}**\n\n` +
`**Erro:** ${stageSearchResponse.error}\n\n` +
`*Verifique se a mesa existe e possui estagios.*`
}
]
};
}
const stages = stageSearchResponse.data || [];
const matchingStages = stages.filter(s =>
s.name.toLowerCase().includes(stage_name.toLowerCase())
);
if (matchingStages.length === 0) {
return {
content: [
{
type: 'text',
text: `**Estagio "${stage_name}" nao encontrado na mesa ID ${deskIdForStage}**\n\n` +
`*Verifique se o nome esta correto ou use stage_id diretamente.*`
}
]
};
}
if (matchingStages.length > 1) {
let stagesList = '**Estagios encontrados:**\n';
matchingStages.forEach((stage, index) => {
stagesList += `${index + 1}. **ID:** ${stage.id} | **Nome:** ${stage.name} | **Ordem:** ${stage.index}\n`;
});
return {
content: [
{
type: 'text',
text: `**Multiplos estagios encontrados para "${stage_name}"**\n\n` +
`${stagesList}\n` +
`*Use stage_id especifico ou seja mais especifico no stage_name.*`
}
]
};
}
finalStageId = matchingStages[0].id;
}
// Se responsible_name foi fornecido, buscar o ID do usuário
if (responsible_name && !responsible_id) {
const userSearchResponse = await this.api.searchUsers({
name: responsible_name,
active: true,
type: 'attendant',
limit: 10
});
if (userSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**Erro ao buscar usuario "${responsible_name}"**\n\n` +
`**Erro:** ${userSearchResponse.error}\n\n` +
`*Verifique se o nome do usuario esta correto ou use responsible_id diretamente.*`
}
]
};
}
const users = userSearchResponse.data || [];
if (users.length === 0) {
return {
content: [
{
type: 'text',
text: `**Usuario "${responsible_name}" nao encontrado**\n\n` +
`*Verifique se o nome esta correto ou use responsible_id diretamente.*`
}
]
};
}
if (users.length > 1) {
let usersList = '**Usuarios encontrados:**\n';
users.forEach((user, index) => {
usersList += `${index + 1}. **ID:** ${user.id} | **Nome:** ${user.name} | **Email:** ${user.email}\n`;
});
return {
content: [
{
type: 'text',
text: `**Multiplos usuarios encontrados para "${responsible_name}"**\n\n` +
`${usersList}\n` +
`*Use responsible_id especifico ou seja mais especifico no responsible_name.*`
}
]
};
}
finalResponsibleId = users[0].id;
}
let finalCatalogItemId = services_catalogs_item_id;
// Se catalog_item_name foi fornecido, buscar o ID do item de catálogo
if (catalog_item_name && !services_catalogs_item_id) {
const deskIdForCatalog = finalDeskId || desk_id;
if (!deskIdForCatalog) {
return {
content: [
{
type: 'text',
text: `**Erro: desk_id ou desk_name obrigatorio para buscar item de catalogo por nome**\n\n` +
`*Para usar catalog_item_name, informe tambem desk_id ou desk_name.*`
}
]
};
}
const catalogSearchResponse = await this.api.searchCatalogItems(deskIdForCatalog, { limit: 200 });
if (catalogSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**Erro ao buscar item de catalogo "${catalog_item_name}"**\n\n` +
`**Erro:** ${catalogSearchResponse.error}\n\n` +
`*Verifique se o nome do item esta correto ou use services_catalogs_item_id diretamente.*`
}
]
};
}
const catalogItems = catalogSearchResponse.data || [];
if (catalogItems.length === 0) {
return {
content: [
{
type: 'text',
text: `**Nenhum item de catalogo encontrado na mesa ${deskIdForCatalog}**\n\n` +
`*Verifique se a mesa possui itens de catalogo configurados.*`
}
]
};
}
// Filtrar por nome (busca parcial case-insensitive)
const searchTerm = catalog_item_name.toLowerCase();
const matchingItems = catalogItems.filter(item =>
item.name.toLowerCase().includes(searchTerm)
);
if (matchingItems.length === 0) {
return {
content: [
{
type: 'text',
text: `**Item de catalogo "${catalog_item_name}" nao encontrado**\n\n` +
`*Verifique se o nome esta correto ou use services_catalogs_item_id diretamente.*`
}
]
};
}
if (matchingItems.length > 1) {
let itemsList = '**Itens de catalogo encontrados:**\n';
matchingItems.forEach((item, index) => {
itemsList += `${index + 1}. **ID:** ${item.id} | **Nome:** ${item.name} | **Area:** ${item.area.name} | **Catalogo:** ${item.catalog.name}\n`;
});
return {
content: [
{
type: 'text',
text: `**Multiplos itens de catalogo encontrados para "${catalog_item_name}"**\n\n` +
`${itemsList}\n` +
`*Use services_catalogs_item_id especifico ou seja mais especifico no catalog_item_name.*`
}
]
};
}
finalCatalogItemId = matchingItems[0].id;
}
// Preparar dados de atualização (apenas campos fornecidos)
const updateData = {};
if (title !== undefined) updateData.title = title;
if (description !== undefined) updateData.description = description;
if (client_id !== undefined) updateData.client_id = parseInt(client_id);
if (finalDeskId !== undefined) updateData.desk_id = parseInt(finalDeskId);
if (finalStageId !== undefined) updateData.stage_id = parseInt(finalStageId);
if (followers !== undefined) updateData.followers = followers;
if (finalCatalogItemId !== undefined) updateData.services_catalogs_item_id = parseInt(finalCatalogItemId);
// Tratamento especial para responsible_id (pode ser null)
if (finalResponsibleId !== undefined) {
updateData.responsible_id = finalResponsibleId ? parseInt(finalResponsibleId) : null;
}
// Verificar se há campos para atualizar
if (Object.keys(updateData).length === 0) {
return {
content: [
{
type: 'text',
text: `**⚠️ Nenhum campo informado para atualização**\n\n` +
`**Ticket ID:** #${ticket_number}\n\n` +
`*Informe pelo menos um campo para atualizar: title, description, client_id, desk_id, stage_id, responsible_id, followers*`
}
]
};
}
// Atualizar ticket via API
const response = await this.api.updateTicket(ticket_number, updateData);
if (response.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao atualizar ticket #${ticket_number}**\n\n` +
`**Código:** ${response.status}\n` +
`**Mensagem:** ${response.error}\n\n` +
`*Verifique se o ticket existe e se você tem permissão para editá-lo.*`
}
]
};
}
const ticket = response.data;
// Preparar resumo das alterações
let changesText = '**Alterações realizadas:**\n';
if (title !== undefined) changesText += `• Título: ${title}\n`;
if (description !== undefined) changesText += `• Descrição: ${description.substring(0, 50)}...\n`;
if (client_id !== undefined) changesText += `• Cliente ID: ${client_id}\n`;
if (desk_id !== undefined) changesText += `• Mesa transferida: ID ${desk_id}\n`;
if (stage_id !== undefined) changesText += `• Estágio ID: ${stage_id}\n`;
if (responsible_id !== undefined) {
changesText += `• Responsável: ${responsible_id ? `ID ${responsible_id}` : 'Removido (não atribuído)'}\n`;
}
if (followers !== undefined) changesText += `• Seguidores: ${followers}\n`;
if (finalCatalogItemId !== undefined) changesText += `• Item de Catálogo ID: ${finalCatalogItemId}\n`;
return {
content: [
{
type: 'text',
text: `**✅ Ticket #${ticket_number} atualizado com sucesso!**\n\n` +
`${changesText}\n` +
`**Atualizado em:** ${new Date().toISOString()}\n\n` +
`*✅ Ticket atualizado via API TiFlux*`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro interno ao atualizar ticket #${ticket_number}**\n\n` +
`**Erro:** ${error.message}\n\n` +
`*Verifique sua conexão e configurações da API.*`
}
]
};
}
}
/**
* Handler para cancelar um ticket específico
*/
async handleCancelTicket(args) {
const { ticket_number } = args;
if (!ticket_number) {
throw new Error('ticket_number é obrigatório');
}
try {
const response = await this.api.cancelTicket(ticket_number);
if (response.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao cancelar ticket #${ticket_number}**\n\n` +
`**Código:** ${response.status}\n` +
`**Mensagem:** ${response.error}\n\n` +
`*Verifique se o ticket existe e se você tem permissão para cancelá-lo.*`
}
]
};
}
return {
content: [
{
type: 'text',
text: `**✅ Ticket #${ticket_number} cancelado com sucesso!**\n\n` +
`**Mensagem:** ${response.data?.message || response.message || 'Ticket cancelado'}\n\n` +
`*O ticket foi cancelado e não pode mais receber atualizações.*`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro interno ao cancelar ticket #${ticket_number}**\n\n` +
`**Erro:** ${error.message}\n\n` +
`*Verifique sua conexão e tente novamente.*`
}
]
};
}
}
/**
* Handler para fechar um ticket específico
*/
async handleCloseTicket(args) {
const { ticket_number } = args;
if (!ticket_number) {
throw new Error('ticket_number é obrigatório');
}
try {
const response = await this.api.closeTicket(ticket_number);
if (response.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao fechar ticket #${ticket_number}**\n\n` +
`**Código:** ${response.status}\n` +
`**Mensagem:** ${response.error}\n\n` +
`*Verifique se o ticket existe e se você tem permissão para fechá-lo.*`
}
]
};
}
return {
content: [
{
type: 'text',
text: `**✅ Ticket #${ticket_number} fechado com sucesso!**\n\n` +
`**Mensagem:** ${response.data?.message || response.message || 'Ticket fechado'}\n\n` +
`*O ticket foi fechado e marcado como resolvido.*`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro interno ao fechar ticket #${ticket_number}**\n\n` +
`**Erro:** ${error.message}\n\n` +
`*Verifique sua conexão e tente novamente.*`
}
]
};
}
}
/**
* Handler para criar uma resposta (comunicação com cliente) em um ticket
*/
async handleCreateTicketAnswer(args) {
const { ticket_number, text, with_signature, files = [], files_base64 = [] } = args;
if (!ticket_number) {
throw new Error('ticket_number é obrigatório');
}
if (!text) {
throw new Error('text é obrigatório');
}
try {
// Combinar arquivos locais e base64
const allFiles = [...files, ...files_base64];
// Validar número total de arquivos
if (allFiles.length > 10) {
return {
content: [
{
type: 'text',
text: `**⚠️ Muitos arquivos**\n\n` +
`**Ticket:** #${ticket_number}\n` +
`**Arquivos fornecidos:** ${allFiles.length} (${files.length} locais + ${files_base64.length} base64)\n` +
`**Limite:** 10 arquivos por resposta\n\n` +
`*Remova alguns arquivos e tente novamente.*`
}
]
};
}
// Validar estrutura dos arquivos base64
if (files_base64.length > 0) {
for (let i = 0; i < files_base64.length; i++) {
const file = files_base64[i];
if (!file || typeof file !== 'object') {
return {
content: [
{
type: 'text',
text: `**❌ Erro de validação no arquivo base64 #${i + 1}**\n\n` +
`O arquivo deve ser um objeto com as propriedades "content" e "filename".\n\n` +
`**Exemplo correto:**\n` +
`\`\`\`json\n` +
`{\n` +
` "content": "base64string...",\n` +
` "filename": "documento.pdf"\n` +
`}\n` +
`\`\`\`\n\n` +
`*Verifique a estrutura do arquivo e tente novamente.*`
}
]
};
}
if (!file.content || typeof file.content !== 'string') {
return {
content: [
{
type: 'text',
text: `**❌ Erro de validação no arquivo base64 #${i + 1}**\n\n` +
`A propriedade "content" é obrigatória e deve ser uma string em base64.\n\n` +
`*Verifique o conteúdo do arquivo e tente novamente.*`
}
]
};
}
if (!file.filename || typeof file.filename !== 'string') {
return {
content: [
{
type: 'text',
text: `**❌ Erro de validação no arquivo base64 #${i + 1}**\n\n` +
`A propriedade "filename" é obrigatória e deve ser uma string.\n\n` +
`*Exemplo: "documento.pdf", "relatorio.xlsx", "imagem.png"*`
}
]
};
}
// Validar tamanho do base64 antes de enviar (aproximado)
const estimatedSize = Math.ceil((file.content.length * 3) / 4);
const maxSize = 41943040; // 40MB
if (estimatedSize > maxSize) {
return {
content: [
{
type: 'text',
text: `**❌ Arquivo base64 muito grande**\n\n` +
`**Arquivo:** ${file.filename}\n` +
`**Tamanho estimado:** ${Math.round(estimatedSize / 1024 / 1024)}MB\n` +
`**Limite:** 40MB\n\n` +
`*Reduza o tamanho do arquivo ou envie em múltiplas respostas.*`
}
]
};
}
}
}
// Preparar dados da resposta
const answerData = {
name: text, // O campo 'name' na API corresponde ao texto da resposta
with_signature: with_signature || false
};
// Adicionar arquivos se fornecidos
if (allFiles.length > 0) {
answerData.files = allFiles;
}
// Criar resposta via API
const response = await this.api.createTicketAnswer(ticket_number, answerData);
if (response.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao criar resposta no ticket #${ticket_number}**\n\n` +
`**Código:** ${response.status}\n` +
`**Mensagem:** ${response.error}\n\n` +
`*Verifique se o ticket existe e se você tem permissão para responder.*`
}
]
};
}
const answer = response.data;
const filesInfo = files && files.length > 0
? `\n**Arquivos anexados:** ${files.length} arquivo(s)`
: '';
return {
content: [
{
type: 'text',
text: `**✅ Resposta criada com sucesso no ticket #${ticket_number}!**\n\n` +
`**ID da resposta:** ${answer.id}\n` +
`**Autor:** ${answer.author}\n` +
`**Data/Hora:** ${answer.answer_time}\n` +
`**Origem:** ${answer.answer_origin}\n` +
`**Com assinatura:** ${answer.signature ? 'Sim' : 'Não'}${filesInfo}\n\n` +
`**Conteúdo enviado:**\n${text}\n\n` +
`*✅ Resposta enviada via API TiFlux*`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro interno ao criar resposta no ticket #${ticket_number}**\n\n` +
`**Erro:** ${error.message}\n\n` +
`*Verifique sua conexão e configurações da API.*`
}
]
};
}
}
/**
* Handler para listar tickets com filtros
*/
async handleListTickets(args) {
const {
desk_ids,
desk_name,
client_ids,
stage_ids,
stage_name,
responsible_ids,
offset,
limit,
is_closed
} = args;
// Validar se pelo menos um dos filtros obrigatórios foi informado
if (!desk_ids && !desk_name && !client_ids && !stage_ids && !stage_name && !responsible_ids) {
return {
content: [
{
type: 'text',
text: `**⚠️ Filtro obrigatório não informado**\n\n` +
`Você deve informar pelo menos um dos seguintes filtros:\n` +
`• **desk_ids** - IDs das mesas (ex: "1,2,3")\n` +
`• **desk_name** - Nome da mesa (ex: "cansados")\n` +
`• **client_ids** - IDs dos clientes (ex: "1,2,3")\n` +
`• **stage_ids** - IDs dos estágios (ex: "1,2,3")\n` +
`• **stage_name** - Nome do estágio (deve usar junto com desk_name, ex: "to do")\n` +
`• **responsible_ids** - IDs dos responsáveis (ex: "1,2,3")\n\n` +
`*Esta validação evita retornar uma quantidade excessiva de tickets.*`
}
]
};
}
try {
let finalDeskIds = desk_ids;
let finalStageIds = stage_ids;
// Resolver nome da mesa em ID se fornecido
if (desk_name && !desk_ids) {
const deskSearchResponse = await this.api.searchDesks(desk_name);
if (deskSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao buscar mesa "${desk_name}"**\n\n` +
`**Erro:** ${deskSearchResponse.error}\n\n` +
`*Verifique se o nome da mesa está correto ou use desk_ids diretamente.*`
}
]
};
}
const desks = deskSearchResponse.data || [];
if (desks.length === 0) {
return {
content: [
{
type: 'text',
text: `**❌ Mesa "${desk_name}" não encontrada**\n\n` +
`*Verifique se o nome está correto ou use desk_ids diretamente.*`
}
]
};
}
if (desks.length > 1) {
let desksList = '**Mesas encontradas:**\n';
desks.forEach((desk, index) => {
desksList += `${index + 1}. **ID:** ${desk.id} | **Nome:** ${desk.name} | **Display:** ${desk.display_name}\n`;
});
return {
content: [
{
type: 'text',
text: `**⚠️ Múltiplas mesas encontradas para "${desk_name}"**\n\n` +
`${desksList}\n` +
`*Use desk_ids específico ou seja mais específico no desk_name.*`
}
]
};
}
const foundDesk = desks[0];
finalDeskIds = foundDesk.id.toString();
// Se stage_name foi fornecido junto com desk_name, buscar o estágio
if (stage_name && !stage_ids) {
const stageSearchResponse = await this.api.searchStages(foundDesk.id);
if (stageSearchResponse.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao buscar estágios da mesa "${desk_name}"**\n\n` +
`**Erro:** ${stageSearchResponse.error}\n\n` +
`*Verifique se a mesa existe e tem estágios configurados.*`
}
]
};
}
const stages = stageSearchResponse.data || [];
const matchingStages = stages.filter(stage =>
stage.name.toLowerCase().includes(stage_name.toLowerCase())
);
if (matchingStages.length === 0) {
let stagesList = stages.map(stage => `• ${stage.name}`).join('\n');
return {
content: [
{
type: 'text',
text: `**❌ Estágio "${stage_name}" não encontrado na mesa "${desk_name}"**\n\n` +
`**Estágios disponíveis:**\n${stagesList}\n\n` +
`*Use stage_ids diretamente ou ajuste o stage_name.*`
}
]
};
}
if (matchingStages.length > 1) {
let stagesList = '**Estágios encontrados:**\n';
matchingStages.forEach((stage, index) => {
stagesList += `${index + 1}. **ID:** ${stage.id} | **Nome:** ${stage.name}\n`;
});
return {
content: [
{
type: 'text',
text: `**⚠️ Múltiplos estágios encontrados para "${stage_name}" na mesa "${desk_name}"**\n\n` +
`${stagesList}\n` +
`*Use stage_ids específico ou seja mais específico no stage_name.*`
}
]
};
}
finalStageIds = matchingStages[0].id.toString();
}
}
// Preparar filtros para a API
const filters = {};
if (finalDeskIds) filters.desk_ids = finalDeskIds;
if (client_ids) filters.client_ids = client_ids;
if (finalStageIds) filters.stage_ids = finalStageIds;
if (responsible_ids) filters.responsible_ids = responsible_ids;
if (offset) filters.offset = parseInt(offset);
if (limit) filters.limit = parseInt(limit);
if (is_closed !== undefined) filters.is_closed = is_closed;
// Chamar API para listar tickets
const response = await this.api.listTickets(filters);
if (response.error) {
return {
content: [
{
type: 'text',
text: `**❌ Erro ao listar tickets**\n\n` +
`**Código:** ${response.status}\n` +
`**Mensagem:** ${response.error}\n\n` +
`*Verifique os filtros informados e suas permissões.*`
}
]
};
}
const tickets = response.data || [];
if (tickets.length === 0) {
return {
content: [
{
typ