Initial commit
This commit is contained in:
357
mcp-server/tools/helpers/ACAI_ENDPOINTS.md
Normal file
357
mcp-server/tools/helpers/ACAI_ENDPOINTS.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# Acai CMS Endpoints Reference
|
||||
|
||||
Este documento mapea todos los endpoints de Acai CMS utilizados por las herramientas MCP.
|
||||
|
||||
## Endpoints Base
|
||||
|
||||
- **CMS Admin**: `https://[website]/admin.php` - Panel administrativo principal
|
||||
- **Viewer Functions**: `https://[website]/cms/lib/viewer_functions.php` - API de funciones Acai
|
||||
- **SAAS API**: `https://ws.cocosolution.com/api/schemas/` - API SaaS para esquemas
|
||||
- **File Upload**: `https://[website]/lib/menus/modals/plupload/multiupload/upload.php` - Subir archivos
|
||||
|
||||
## Categoría: Módulos (saveApartados)
|
||||
|
||||
### 1. Generar módulo desde HTML
|
||||
**Endpoint**: `https://acai.cms.cocosolution.com/admin.php?menu=apartados&action=edit&generateModuleFromString=1`
|
||||
**Método**: POST
|
||||
**Usado por**: `save_module`
|
||||
**Headers**: `Content-Type: application/json`, `X-Acai-Token`
|
||||
**Payload**: moduleData object con html, htmlParsed, vars, etc.
|
||||
|
||||
### 2. Obtener esquemas de módulos
|
||||
**Endpoint**: `/cms/lib/viewer_functions.php`
|
||||
**Método**: POST via getApiClient
|
||||
**Usado por**: `save_module`, `saveGeneralSection`, `check_module`, `list_modules`, `get_module`
|
||||
**Action**: `getModuleSchemas`
|
||||
**Payload via getCommonParams**:
|
||||
```javascript
|
||||
{
|
||||
action_ws: "getModuleSchemas",
|
||||
ids: [moduleId], // opcional, para un módulo específico
|
||||
full: 1 // opcional, para obtener contenido completo
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Verificar módulo
|
||||
**Endpoint**: `https://[website]/cms/lib/viewer_functions.php?action_ws=checkModuleCode`
|
||||
**Método**: POST
|
||||
**Usado por**: `check_module`
|
||||
**Payload**:
|
||||
```javascript
|
||||
{
|
||||
moduleName: string,
|
||||
vars: object // variables de prueba
|
||||
}
|
||||
```
|
||||
|
||||
## Categoría: Secciones Generales (saveLexicalData)
|
||||
|
||||
### 1. Guardar sección con contenido Twig/HTML
|
||||
**Endpoint**: `https://[website]/cms/lib/viewer_functions.php`
|
||||
**Método**: POST
|
||||
**Usado por**: `saveGeneralSection`
|
||||
**Action**: `saveLexicalData`
|
||||
**Payload**:
|
||||
```javascript
|
||||
{
|
||||
action_ws: 'saveLexicalData',
|
||||
token: credentials.token,
|
||||
tokenHash: credentials.tokenHash,
|
||||
content: string, // HTML parsed content
|
||||
rawDataSended: true,
|
||||
endPointFolder: string, // e.g., 'custom-productos'
|
||||
parserType: '2' | '0', // 2=Twig, 0=Acai
|
||||
aditionalFiles: [ // CSS, JS files
|
||||
{
|
||||
path: string,
|
||||
fileName: string,
|
||||
content: string
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Categoría: Registros (CRUD)
|
||||
|
||||
### 1. Crear/Actualizar registro
|
||||
**Endpoint**: `${CMS_URL}/admin.php`
|
||||
**Método**: POST
|
||||
**Usado por**: `create_or_update_record`
|
||||
**Content-Type**: `application/x-www-form-urlencoded`
|
||||
**Params**:
|
||||
```
|
||||
menu={tableName}
|
||||
_defaultAction=save
|
||||
num={recordId} // empty para crear
|
||||
type=
|
||||
preSaveTempId={timestamp}
|
||||
action=save
|
||||
{fieldname}={value} // campos del registro
|
||||
{fieldname}:year, :mon, etc // para campos date
|
||||
enlace={value}
|
||||
```
|
||||
|
||||
### 2. Listar registros
|
||||
**Endpoint**: `${CMS_URL}/admin.php?menu={tableName}&json=1&page={n}&keyword={q}`
|
||||
**Método**: GET
|
||||
**Usado por**: `list_table_records`
|
||||
**Headers**: `X-Acai-Token`, `X-Requested-With: XMLHttpRequest`
|
||||
|
||||
### 3. Eliminar registros
|
||||
**Endpoint**: `${CMS_URL}/admin.php`
|
||||
**Método**: POST
|
||||
**Usado por**: `delete_table_records`
|
||||
**Params**:
|
||||
```
|
||||
menu={tableName}
|
||||
_defaultAction=list
|
||||
page=1
|
||||
_advancedAction=eraseRecords
|
||||
_advancedActionSubmit=Ejecutar
|
||||
selectedRecords[]={id1}
|
||||
selectedRecords[]={id2}
|
||||
```
|
||||
|
||||
## Categoría: Archivos (saveFileBuilder, removeFileBuilder)
|
||||
|
||||
### 1. Escribir archivo
|
||||
**Endpoint**: `/cms/lib/viewer_functions.php`
|
||||
**Método**: POST via getApiClient
|
||||
**Usado por**: `write_file`
|
||||
**Action**: `saveFileBuilder`
|
||||
**Payload via getCommonParams**:
|
||||
```javascript
|
||||
{
|
||||
action_ws: "saveFileBuilder",
|
||||
path: string, // ej: '/modulos/mymodule/'
|
||||
fileName: string, // ej: 'style.css'
|
||||
content: string,
|
||||
rawDataSended: false,
|
||||
rootFolder: false
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Listar archivos (FTP)
|
||||
**Endpoint**: `/cms/lib/viewer_functions.php`
|
||||
**Método**: POST via getApiClient
|
||||
**Usado por**: `list_files`
|
||||
**Action**: `getFTPFiles`
|
||||
**Payload via getCommonParams**:
|
||||
```javascript
|
||||
{
|
||||
action_ws: "getFTPFiles",
|
||||
path: string // directorio a listar
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Eliminar archivo
|
||||
**Endpoint**: `/cms/lib/viewer_functions.php`
|
||||
**Método**: POST via getApiClient
|
||||
**Usado por**: `delete_file`
|
||||
**Action**: `removeFileBuilder`
|
||||
**Payload via getCommonParams**:
|
||||
```javascript
|
||||
{
|
||||
action_ws: "removeFileBuilder",
|
||||
path: string // ruta del archivo
|
||||
}
|
||||
```
|
||||
|
||||
## Categoría: Tablas (Database Schema)
|
||||
|
||||
### 1. Listar tablas (SaaS)
|
||||
**Endpoint**: `${SAAS_URL}`
|
||||
**Método**: POST
|
||||
**Usado por**: `list_tables`
|
||||
**Payload**:
|
||||
```javascript
|
||||
{
|
||||
action: 'getSchemaTables',
|
||||
type: 'acai'
|
||||
}
|
||||
```
|
||||
**Headers**: `Authorization: Bearer {token}`, `Content-Type: application/json`
|
||||
|
||||
### 2. Obtener esquema tabla (SaaS)
|
||||
**Endpoint**: `${SAAS_URL}`
|
||||
**Método**: POST
|
||||
**Usado por**: `get_table_schema`
|
||||
**Payload**:
|
||||
```javascript
|
||||
{
|
||||
action: 'getSchemaTables',
|
||||
type: 'acai'
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Actualizar esquema tabla (SaaS + CMS)
|
||||
**Endpoint SaaS**: `${SAAS_URL}` (PUT)
|
||||
**Método**: PUT
|
||||
**Usado por**: `update_table_schema`
|
||||
**Payload**:
|
||||
```javascript
|
||||
{
|
||||
action: "saveSchema",
|
||||
type: "acai",
|
||||
schema: object, // esquema completo o parcial
|
||||
dir: "",
|
||||
id: tableName
|
||||
}
|
||||
```
|
||||
|
||||
**Luego sincronizar en CMS**:
|
||||
**Endpoint CMS**: `/cms/lib/viewer_functions.php`
|
||||
**Método**: POST via getApiClient
|
||||
**Action**: `updateAllSchemas`
|
||||
**Payload via getCommonParams**:
|
||||
```javascript
|
||||
{
|
||||
action_ws: "updateAllSchemas",
|
||||
tokenHash: credentials.tokenHash
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Crear tabla
|
||||
**Endpoint**: `${CMS_URL}/admin.php`
|
||||
**Método**: POST
|
||||
**Usado por**: `create_table`
|
||||
**Params**:
|
||||
```
|
||||
menu=database
|
||||
_defaultAction=addTable_save
|
||||
type={multi|single|category|separador}
|
||||
preset=
|
||||
enlace={on|''}
|
||||
seo_metas={on|''}
|
||||
menuName={name}
|
||||
menuOrder={order}
|
||||
tableName={name}
|
||||
```
|
||||
|
||||
### 5. Eliminar tabla
|
||||
**Endpoint**: `${CMS_URL}/admin.php`
|
||||
**Método**: POST
|
||||
**Usado por**: `delete_table`
|
||||
**Params**:
|
||||
```
|
||||
menu=database
|
||||
action=editTable
|
||||
dropTable=1
|
||||
tableName={name}
|
||||
```
|
||||
|
||||
### 6. Editar campos tabla
|
||||
**Endpoint**: `${CMS_URL}/admin.php`
|
||||
**Método**: POST
|
||||
**Usado por**: `edit_table_field`
|
||||
**Params**:
|
||||
```
|
||||
menu=database
|
||||
_defaultAction=editTable
|
||||
editField=1
|
||||
tableName=cms_{tableName}
|
||||
save=1
|
||||
multipleFields={JSON.stringify(fieldArray)}
|
||||
```
|
||||
|
||||
### 7. Eliminar campo tabla
|
||||
**Endpoint**: `${CMS_URL}/admin.php`
|
||||
**Método**: POST
|
||||
**Usado por**: `delete_table_field`
|
||||
**Params**:
|
||||
```
|
||||
menu=database
|
||||
action=editTable
|
||||
editField=1
|
||||
tableName=cms_{tableName}
|
||||
fieldname={fieldname}
|
||||
deleteField=1
|
||||
```
|
||||
|
||||
### 8. Obtener templates tabla (general section)
|
||||
**Endpoint**: `/cms/lib/viewer_functions.php`
|
||||
**Método**: POST via getApiClient
|
||||
**Usado por**: `get_table_templates`
|
||||
**Action**: `getTableData`
|
||||
**Payload via getCommonParams**:
|
||||
```javascript
|
||||
{
|
||||
action_ws: "getTableData",
|
||||
menu: tableName
|
||||
}
|
||||
```
|
||||
|
||||
## Categoría: Media (Upload)
|
||||
|
||||
### 1. Subir imagen a campo
|
||||
**Endpoint**: `${CMS_URL}/lib/menus/modals/plupload/multiupload/upload.php?menu={table}&fieldName={field}&num={recordId}&preSaveTempId=`
|
||||
**Método**: POST (FormData)
|
||||
**Usado por**: `upload_record_image`
|
||||
**Form Fields**:
|
||||
```
|
||||
file={File buffer} // File object
|
||||
```
|
||||
|
||||
### 2. Listar uploads campo
|
||||
**Endpoint**: `${CMS_URL}/admin.php?menu={table}&action=uploadList&fieldName={field}&num={recordId}&preSaveTempId=&json=1`
|
||||
**Método**: GET
|
||||
**Usado por**: `list_record_uploads`
|
||||
**Headers**: `X-Acai-Token`
|
||||
|
||||
### 3. Reemplazar upload
|
||||
**Endpoint**: `${CMS_URL}/admin.php`
|
||||
**Método**: POST (FormData)
|
||||
**Usado por**: `replace_record_image`
|
||||
**Form Fields**:
|
||||
```
|
||||
_defaultAction=uploadModify
|
||||
menu={tableName}
|
||||
fieldName={fieldName}
|
||||
num={recordId}
|
||||
preSaveTempId=
|
||||
save=1
|
||||
uploadNums[]={uploadId}
|
||||
{uploadId}_file={File buffer}
|
||||
{uploadId}_name={originalFilePath}
|
||||
{uploadId}_alt={altText}
|
||||
action=uploadModify
|
||||
```
|
||||
|
||||
### 4. Eliminar upload
|
||||
**Endpoint**: `${CMS_URL}/admin.php?menu={table}&action=uploadErase&fieldName={field}&uploadNum={id}&num={recordId}&preSaveTempId=`
|
||||
**Método**: GET
|
||||
**Usado por**: `delete_record_upload`
|
||||
**Headers**: `X-Acai-Token`, `X-Requested-With: XMLHttpRequest`
|
||||
|
||||
## Patrones Comunes
|
||||
|
||||
### getApiClient Calls
|
||||
```javascript
|
||||
const client = getApiClient(extra.sessionId);
|
||||
const response = await client.post("/cms/lib/viewer_functions.php", getCommonParams(extra.sessionId, {
|
||||
action_ws: "actionName",
|
||||
// ... otros params
|
||||
}));
|
||||
```
|
||||
|
||||
### getCommonParams
|
||||
Agrega automáticamente:
|
||||
- token
|
||||
- tokenHash
|
||||
- website
|
||||
- session info
|
||||
|
||||
### Headers Recurrentes
|
||||
```javascript
|
||||
{
|
||||
"X-Acai-Token": credentials.token,
|
||||
"Content-Type": "application/json" | "application/x-www-form-urlencoded"
|
||||
}
|
||||
```
|
||||
|
||||
## Notas Importantes
|
||||
|
||||
1. **Construcción de URLs**: Algunos endpoints usan la URL base dinámicamente (`https://{website}/...`) mientras otros usan `CMS_URL` configurado.
|
||||
2. **Parámetros de formulario**: Algunos endpoints esperan URLSearchParams, otros JSON.
|
||||
3. **Token Auth**: Algunos usan `X-Acai-Token`, otros pasan token en payload.
|
||||
4. **Respuestas**: Varían entre `{success: true}`, `{result: true}`, o respuestas direc tas.
|
||||
270
mcp-server/tools/helpers/ERROR_HANDLING.md
Normal file
270
mcp-server/tools/helpers/ERROR_HANDLING.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Error Handling System for Tools
|
||||
|
||||
Centralizado error handling para todas las herramientas MCP del servidor.
|
||||
|
||||
## Características
|
||||
|
||||
✅ **Manejo consistente de errores** - Todas las herramientas retornan el mismo formato
|
||||
✅ **Logging automático** - Todos los errores se registran en consola
|
||||
✅ **Validación de parámetros** - Validación requerida y de tipos
|
||||
✅ **Detección de errores API** - Identifica patrones comunes de error en respuestas
|
||||
✅ **Información contextual** - Cada error incluye el contexto de dónde ocurrió
|
||||
|
||||
## Funciones Disponibles
|
||||
|
||||
### `handleToolError(error, context, additionalInfo)`
|
||||
|
||||
Maneja cualquier error y retorna una respuesta formateada.
|
||||
|
||||
```javascript
|
||||
import { handleToolError } from "../helpers/errorHandler.js";
|
||||
|
||||
try {
|
||||
// tu código
|
||||
} catch (error) {
|
||||
return handleToolError(error, 'my_tool', { userId: 123 });
|
||||
}
|
||||
```
|
||||
|
||||
**Retorna:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "ECONNREFUSED",
|
||||
"message": "connect ECONNREFUSED 127.0.0.1:3000",
|
||||
"context": "my_tool",
|
||||
"userId": 123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `handleApiResponse(data, context)`
|
||||
|
||||
Detecta errores en respuestas de API (busca patrones comunes).
|
||||
|
||||
```javascript
|
||||
const response = await axios.post(url, payload);
|
||||
|
||||
// Detecta automáticamente: error, Error, PHPSyntax, success: false, etc.
|
||||
const apiError = handleApiResponse(response.data, 'save_module');
|
||||
if (apiError) return apiError;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `validateRequired(params, requiredFields, context)`
|
||||
|
||||
Valida que los parámetros requeridos estén presentes.
|
||||
|
||||
```javascript
|
||||
const error = validateRequired(
|
||||
{ name: "Juan", email: "" },
|
||||
['name', 'email'],
|
||||
'create_user'
|
||||
);
|
||||
// error porque email está vacío
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `validateTypes(params, schema, context)`
|
||||
|
||||
Valida tipos de datos.
|
||||
|
||||
```javascript
|
||||
const error = validateTypes(
|
||||
{ age: "25", active: true },
|
||||
{ age: 'number', active: 'boolean' },
|
||||
'create_user'
|
||||
);
|
||||
// error porque age es string, no number
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `createValidator(requiredFields, typeSchema)`
|
||||
|
||||
Crea una función validadora reutilizable.
|
||||
|
||||
```javascript
|
||||
const validateUserInput = createValidator(
|
||||
['name', 'email'],
|
||||
{ age: 'number', active: 'boolean' }
|
||||
);
|
||||
|
||||
// Usar en múltiples lugares
|
||||
const error = validateUserInput(params, 'create_user');
|
||||
if (error) return error;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `withErrorHandling(handler, toolName)`
|
||||
|
||||
Envuelve un handler para manejar errores automáticamente.
|
||||
|
||||
```javascript
|
||||
const safeHandler = withErrorHandling(
|
||||
async (params, extra) => {
|
||||
// tu código
|
||||
},
|
||||
'my_tool'
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `safeJsonParse(jsonString, context)`
|
||||
|
||||
Parse JSON seguro con manejo de errores.
|
||||
|
||||
```javascript
|
||||
const result = safeJsonParse(jsonString, 'parse_config');
|
||||
if (!result.success) {
|
||||
// result.error contiene el error formateado
|
||||
return result.error;
|
||||
}
|
||||
const data = result.data;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patrón Recomendado para Tools
|
||||
|
||||
```javascript
|
||||
import { z } from "zod";
|
||||
import axios from "axios";
|
||||
import { withAuth, getSessionCredentials } from "../../auth/index.js";
|
||||
import {
|
||||
handleToolError,
|
||||
handleApiResponse,
|
||||
validateRequired
|
||||
} from "../helpers/errorHandler.js";
|
||||
|
||||
export function registerMyTool(server) {
|
||||
server.tool(
|
||||
"my_tool",
|
||||
"Descripción de la herramienta",
|
||||
{
|
||||
param1: z.string().describe("Parámetro 1"),
|
||||
param2: z.number().describe("Parámetro 2"),
|
||||
},
|
||||
withAuth(async ({ param1, param2 }, extra) => {
|
||||
try {
|
||||
// 1. Validar parámetros requeridos
|
||||
const validationError = validateRequired(
|
||||
{ param1, param2 },
|
||||
['param1', 'param2'],
|
||||
'my_tool'
|
||||
);
|
||||
if (validationError) return validationError;
|
||||
|
||||
// 2. Obtener credenciales
|
||||
const credentials = getSessionCredentials(extra.sessionId);
|
||||
|
||||
// 3. Hacer llamada API
|
||||
const response = await axios.post(url, payload, {
|
||||
headers: { /* ... */ }
|
||||
});
|
||||
|
||||
// 4. Verificar respuesta de API
|
||||
const apiError = handleApiResponse(response.data, 'my_tool');
|
||||
if (apiError) return apiError;
|
||||
|
||||
// 5. Retornar resultado
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(response.data, null, 2)
|
||||
}]
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
// Los errores se capturan y formatean automáticamente
|
||||
return handleToolError(error, 'my_tool', { param1, param2 });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Errores Detectados Automáticamente
|
||||
|
||||
`handleApiResponse()` detecta estos patrones en respuestas:
|
||||
|
||||
- ✅ `data.error` o `data.Error`
|
||||
- ✅ `data.PHPSyntax` - Errores de sintaxis PHP
|
||||
- ✅ `data.success === false` - Campo success explícito
|
||||
- ✅ Strings con palabras clave: "error", "fatal", "undefined", "syntax"
|
||||
- ✅ Respuestas vacías o null
|
||||
|
||||
---
|
||||
|
||||
## Formato de Error Consistente
|
||||
|
||||
Todos los errores retornan este formato:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Mensaje descriptivo del error",
|
||||
"context": "nombre_del_tool",
|
||||
"...": "información adicional"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migración de Tools Existentes
|
||||
|
||||
Para actualizar un tool existente:
|
||||
|
||||
1. Importar funciones de error handler
|
||||
2. Reemplazar `try-catch` genérico con `handleToolError()`
|
||||
3. Agregar validación con `validateRequired()`
|
||||
4. Agregar `handleApiResponse()` después de llamadas API
|
||||
5. Pasar información contextual útil a `handleToolError()`
|
||||
|
||||
**Ejemplo antes:**
|
||||
```javascript
|
||||
try {
|
||||
// código
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: " + error.message }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Ejemplo después:**
|
||||
```javascript
|
||||
try {
|
||||
// código
|
||||
} catch (error) {
|
||||
return handleToolError(error, 'my_tool', { extraInfo: value });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
Todos los errores se registran en stderr con contexto:
|
||||
|
||||
```
|
||||
[Tool Error - save_module] Cannot read property 'website' of undefined
|
||||
Stack: Error: Cannot read property 'website' of undefined
|
||||
at registerSaveModuleTool (/Users/...save.js:45:20)
|
||||
...
|
||||
```
|
||||
|
||||
Esto facilita debug y auditoría de errores en producción.
|
||||
587
mcp-server/tools/helpers/acaiHttpClient.js
Normal file
587
mcp-server/tools/helpers/acaiHttpClient.js
Normal file
@@ -0,0 +1,587 @@
|
||||
/**
|
||||
* Acai CMS HTTP Client
|
||||
*
|
||||
* Centralizado helper para todas las llamadas HTTP a Acai CMS.
|
||||
* Proporciona métodos consistentes para interactuar con:
|
||||
* - Admin panel (admin.php)
|
||||
* - Viewer functions API
|
||||
* - SaaS API
|
||||
* - File upload endpoints
|
||||
*
|
||||
* Ventajas:
|
||||
* - Consistencia en headers, manejo de errores, logging
|
||||
* - Reduce duplicación de código
|
||||
* - Facilita mantenimiento y debugging
|
||||
* - Centraliza URLs y configuración
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { getSessionCredentials } from '../../auth/index.js';
|
||||
import { CMS_URL } from '../../config/index.js';
|
||||
import { assertSafeCmsTarget } from '../../utils/cmsTargetSafety.js';
|
||||
|
||||
/**
|
||||
* AcaiHttpClient - Helper para solicitudes HTTP a Acai CMS
|
||||
*/
|
||||
export class AcaiHttpClient {
|
||||
static resolveCmsTarget(target) {
|
||||
const { publicUrl, apiUrl, forgeHost } = assertSafeCmsTarget(target, "AcaiHttpClient");
|
||||
const headers = {};
|
||||
if (forgeHost) {
|
||||
headers.Host = forgeHost;
|
||||
}
|
||||
|
||||
return {
|
||||
publicUrl,
|
||||
apiUrl,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
static buildViewerUrl(target, query = "") {
|
||||
const { apiUrl } = AcaiHttpClient.resolveCmsTarget(target);
|
||||
const baseUrl = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
||||
return `${baseUrl}/cms/lib/viewer_functions.php${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
static buildViewerHeaders(target, extraHeaders = {}) {
|
||||
const { headers } = AcaiHttpClient.resolveCmsTarget(target);
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
...extraHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
static async postViewerAction(target, actionWs, payload, token, tokenHash, extraHeaders = {}, timeout = 30000) {
|
||||
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, `action_ws=${actionWs}`);
|
||||
const body = {
|
||||
...payload,
|
||||
token,
|
||||
tokenHash,
|
||||
};
|
||||
return axios.post(viewerUrl, body, {
|
||||
headers: AcaiHttpClient.buildViewerHeaders(target, extraHeaders),
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a admin.php con URLSearchParams
|
||||
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
|
||||
* @param {URLSearchParams} params - Parámetros del formulario
|
||||
* @param {string} token - Token Acai
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async postAdminForm(website, params, token) {
|
||||
const cmsUrl = `${CMS_URL}/admin.php`;
|
||||
|
||||
try {
|
||||
console.error(`[AcaiHttpClient] postAdminForm - START: ${cmsUrl}`);
|
||||
const response = await axios.post(cmsUrl, params, {
|
||||
headers: {
|
||||
"X-Acai-Token": token,
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] postAdminForm - SUCCESS: ${cmsUrl} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] postAdminForm - ERROR: ${cmsUrl} - ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
|
||||
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
|
||||
} else if (error.code) {
|
||||
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a admin.php con FormData (para uploads)
|
||||
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
|
||||
* @param {FormData} formData - Datos del formulario
|
||||
* @param {string} token - Token Acai
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async postAdminFormData(website, formData, token) {
|
||||
const cmsUrl = `${CMS_URL}/admin.php`;
|
||||
|
||||
try {
|
||||
console.error(`[AcaiHttpClient] postAdminFormData - START: ${cmsUrl}`);
|
||||
const response = await axios.post(cmsUrl, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
"X-Acai-Token": token
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] postAdminFormData - SUCCESS: ${cmsUrl} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] postAdminFormData - ERROR: ${cmsUrl} - ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
|
||||
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
|
||||
} else if (error.code) {
|
||||
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET a admin.php con query parameters
|
||||
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
|
||||
* @param {URLSearchParams | string} params - Parámetros de query
|
||||
* @param {string} token - Token Acai
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async getAdminQuery(website, params, token) {
|
||||
const cmsUrl = `${CMS_URL}/admin.php`;
|
||||
const queryString = params instanceof URLSearchParams
|
||||
? params.toString()
|
||||
: params;
|
||||
|
||||
try {
|
||||
console.error(`[AcaiHttpClient] getAdminQuery - START: ${cmsUrl}?${queryString.substring(0, 100)}`);
|
||||
const response = await axios.get(
|
||||
`${cmsUrl}?${queryString}`,
|
||||
{
|
||||
headers: {
|
||||
"X-Acai-Token": token,
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
timeout: 30000
|
||||
}
|
||||
);
|
||||
console.error(`[AcaiHttpClient] getAdminQuery - SUCCESS: ${cmsUrl} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] getAdminQuery - ERROR: ${cmsUrl} - ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
|
||||
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
|
||||
} else if (error.code) {
|
||||
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a viewer_functions.php vía getApiClient
|
||||
* Requiere llamar desde dentro de withAuth para tener acceso a getApiClient
|
||||
* @param {Object} client - cliente de axios (getApiClient)
|
||||
* @param {Object} payload - Payload con action_ws y otros parámetros
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async postViewerFunctions(client, payload) {
|
||||
return client.post("/cms/lib/viewer_functions.php", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a viewer_functions.php para saveLexicalData (secciones, contenido)
|
||||
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {Object} credentials - {token, tokenHash}
|
||||
* @param {Object} data - Datos a guardar
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async saveLexicalData(target, credentials, data) {
|
||||
const viewerUrl = AcaiHttpClient.buildViewerUrl(target);
|
||||
|
||||
const payload = {
|
||||
action_ws: 'saveLexicalData',
|
||||
token: credentials.token,
|
||||
tokenHash: credentials.tokenHash,
|
||||
rawDataSended: true,
|
||||
...data
|
||||
};
|
||||
|
||||
return axios.post(viewerUrl, payload, {
|
||||
headers: AcaiHttpClient.buildViewerHeaders(target)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST para generar módulo desde HTML
|
||||
* @param {Object} moduleData - Datos del módulo
|
||||
* @param {string} token - Token Acai
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async generateModuleFromString(moduleData, token) {
|
||||
const cmsUrl = 'https://acai.cms.cocosolution.com/admin.php?menu=apartados&action=edit&generateModuleFromString=1';
|
||||
|
||||
return axios.post(cmsUrl, moduleData, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Acai-Token": token
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a viewer_functions para CMS API (insert, update, delete, get)
|
||||
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {string} action - 'insert', 'update', 'delete', 'get'
|
||||
* @param {Object} payload - Datos de la operación
|
||||
* @param {string} token - Token Acai
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async postCmsApi(target, action, payload, token, tokenHash) {
|
||||
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, `action_ws=cmsApi&subaction=${action}`);
|
||||
|
||||
try {
|
||||
console.error(`[AcaiHttpClient] postCmsApi - START: ${action} on ${viewerUrl}`);
|
||||
console.error(`[AcaiHttpClient] Payload:`, JSON.stringify(payload).substring(0, 500));
|
||||
console.error(`[AcaiHttpClient] Token: ${token ? '****' + token.slice(-4) : 'No token provided'}`);
|
||||
|
||||
payload["token"] = token;
|
||||
payload["tokenHash"] = tokenHash;
|
||||
|
||||
const response = await axios.post(viewerUrl, payload, {
|
||||
headers: AcaiHttpClient.buildViewerHeaders(target, {
|
||||
"X-Acai-Token": token
|
||||
}),
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] postCmsApi - SUCCESS: ${action} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] postCmsApi - ERROR: ${action} on ${viewerUrl} - ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
|
||||
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
|
||||
} else if (error.code) {
|
||||
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a viewer_functions para checkModuleCode
|
||||
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {string} token - Token Acai
|
||||
* @param {Object} data - {moduleName, vars}
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async checkModuleCode(target, token, data) {
|
||||
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, "action_ws=checkModuleCode");
|
||||
try {
|
||||
data["token"] = token;
|
||||
console.error(`[AcaiHttpClient] checkModuleCode - START: ${viewerUrl}`);
|
||||
const response = await axios.post(viewerUrl, data, {
|
||||
headers: AcaiHttpClient.buildViewerHeaders(target),
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] checkModuleCode - SUCCESS: ${viewerUrl} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] checkModuleCode - ERROR: ${viewerUrl}`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a viewer_functions para addModuleToRecord
|
||||
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {string} token - Token Acai
|
||||
* @param {Object} data - {moduleName, vars}
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async addModuleToRecord(target, token, tokenHash, data) {
|
||||
const viewerUrl = AcaiHttpClient.buildViewerUrl(target, "action_ws=addModuleToRecord");
|
||||
try {
|
||||
data["token"] = token;
|
||||
data["tokenHash"] = tokenHash;
|
||||
console.error(`[AcaiHttpClient] addModuleToRecord - START: ${viewerUrl}`);
|
||||
const response = await axios.post(viewerUrl, data, {
|
||||
headers: AcaiHttpClient.buildViewerHeaders(target),
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] addModuleToRecord - SUCCESS: ${viewerUrl} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] addModuleToRecord - ERROR: ${viewerUrl}`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a mcp_respond.php para setModuleConfigVars
|
||||
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {string} token - Token Acai
|
||||
* @param {string} tokenHash - Token hash Acai
|
||||
* @param {Object} data - {tableName, recordNum, sectionId, vars}
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async setModuleConfigVars(target, token, tokenHash, data) {
|
||||
const url = AcaiHttpClient.buildViewerUrl(target, "action_ws=setModuleConfigVars");
|
||||
try {
|
||||
data["token"] = token;
|
||||
data["tokenHash"] = tokenHash;
|
||||
console.error(`[AcaiHttpClient] setModuleConfigVars - START: ${url}`);
|
||||
const response = await axios.post(url, data, {
|
||||
headers: AcaiHttpClient.buildViewerHeaders(target),
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] setModuleConfigVars - SUCCESS: ${url} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] setModuleConfigVars - ERROR: ${url}`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a viewer_functions para getModuleConfigVars
|
||||
* @param {string} web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {string} token - Token Acai
|
||||
* @param {string} tokenHash - Token hash Acai
|
||||
* @param {Object} data - {tableName, recordNum, sectionId}
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async getModuleConfigVars(target, token, tokenHash, data) {
|
||||
const url = AcaiHttpClient.buildViewerUrl(target, "action_ws=getModuleConfigVars");
|
||||
try {
|
||||
data["token"] = token;
|
||||
data["tokenHash"] = tokenHash;
|
||||
console.error(`[AcaiHttpClient] getModuleConfigVars - START: ${url}`);
|
||||
const response = await axios.post(url, data, {
|
||||
headers: AcaiHttpClient.buildViewerHeaders(target),
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] getModuleConfigVars - SUCCESS: ${url} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] getModuleConfigVars - ERROR: ${url}`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST para subir imagen a campo de registro
|
||||
* @param {string} website - Website/domain (no usado, se usa CMS_URL del config)
|
||||
* @param {string} tableName - Nombre de la tabla
|
||||
* @param {string} recordId - ID del registro
|
||||
* @param {string} fieldName - Nombre del campo
|
||||
* @param {FormData} formData - Datos del archivo
|
||||
* @param {string} token - Token Acai
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async uploadRecordImage(website, tableName, recordId, fieldName, formData, token) {
|
||||
const uploadUrl = `${CMS_URL}/lib/menus/modals/plupload/multiupload/upload.php?menu=${tableName}&fieldName=${fieldName}&num=${recordId}&preSaveTempId=`;
|
||||
|
||||
return axios.post(uploadUrl, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
"X-Acai-Token": token
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST a SaaS API para guardar esquema
|
||||
* @param {Object} payload - {action, type, schema, dir, id, ...}
|
||||
* @param {string} token - Token autenticación
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async saasPostRequest(payload, token) {
|
||||
const SAAS_URL = 'https://ws.cocosolution.com/api/schemas/';
|
||||
|
||||
try {
|
||||
console.error(`[AcaiHttpClient] saasPostRequest - START: ${SAAS_URL} (action: ${payload.action})`);
|
||||
const response = await axios.post(SAAS_URL, payload, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] saasPostRequest - SUCCESS: ${SAAS_URL} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] saasPostRequest - ERROR: ${SAAS_URL} - ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
|
||||
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
|
||||
} else if (error.code) {
|
||||
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT a SaaS API para actualizar esquema
|
||||
* @param {Object} payload - {action, type, schema, dir, id}
|
||||
* @param {string} token - Token autenticación
|
||||
* @returns {Promise<Object>} Respuesta del servidor
|
||||
*/
|
||||
static async saasPutRequest(payload, token) {
|
||||
const SAAS_URL = 'https://ws.cocosolution.com/api/schemas/';
|
||||
|
||||
try {
|
||||
console.error(`[AcaiHttpClient] saasPutRequest - START: ${SAAS_URL} (action: ${payload.action})`);
|
||||
const response = await axios.put(SAAS_URL, payload, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
console.error(`[AcaiHttpClient] saasPutRequest - SUCCESS: ${SAAS_URL} (${response.status})`);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error(`[AcaiHttpClient] saasPutRequest - ERROR: ${SAAS_URL} - ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(`[AcaiHttpClient] Response status: ${error.response.status}`);
|
||||
console.error(`[AcaiHttpClient] Response data:`, error.response.data?.substring ? error.response.data.substring(0, 200) : error.response.data);
|
||||
} else if (error.code) {
|
||||
console.error(`[AcaiHttpClient] Error code: ${error.code}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para construir parámetros comunes de formulario
|
||||
*/
|
||||
export class FormParamsBuilder {
|
||||
static buildRecordSaveParams(tableName, recordId, fields, enlace) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('menu', tableName);
|
||||
params.append('_defaultAction', 'save');
|
||||
params.append('num', recordId ? String(recordId) : '');
|
||||
params.append('type', '');
|
||||
params.append('preSaveTempId', Date.now().toString());
|
||||
params.append('action=save', 'Guardar');
|
||||
|
||||
// Agregar todos los campos
|
||||
for (const [fieldName, value] of Object.entries(fields)) {
|
||||
if (fieldName === 'enlace') continue;
|
||||
|
||||
if (value !== null && value !== undefined) {
|
||||
const strValue = String(value);
|
||||
params.append(fieldName, strValue);
|
||||
|
||||
// Detectar y descomponer fechas
|
||||
const dateRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
|
||||
const match = strValue.match(dateRegex);
|
||||
if (match) {
|
||||
params.append(`${fieldName}:year`, match[1]);
|
||||
params.append(`${fieldName}:mon`, match[2]);
|
||||
params.append(`${fieldName}:day`, match[3]);
|
||||
params.append(`${fieldName}:hour24`, match[4]);
|
||||
params.append(`${fieldName}:min`, match[5]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.append('enlace', enlace);
|
||||
return params;
|
||||
}
|
||||
|
||||
static buildDeleteRecordsParams(tableName, recordIds) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('menu', tableName);
|
||||
params.append('_defaultAction', 'list');
|
||||
params.append('page', '1');
|
||||
params.append('_advancedAction', 'eraseRecords');
|
||||
params.append('_advancedActionSubmit', 'Ejecutar');
|
||||
|
||||
recordIds.forEach(id => {
|
||||
params.append('selectedRecords[]', String(id));
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
static buildTableCreateParams(menuName, tableName, type, enlace, seo_metas, menuOrder) {
|
||||
return new URLSearchParams({
|
||||
menu: "database",
|
||||
_defaultAction: "addTable_save",
|
||||
type: type,
|
||||
preset: "",
|
||||
enlace: enlace ? "on" : "",
|
||||
seo_metas: seo_metas ? "on" : "",
|
||||
menuName: menuName,
|
||||
menuOrder: menuOrder.toString(),
|
||||
tableName: tableName
|
||||
});
|
||||
}
|
||||
|
||||
static buildTableDeleteParams(tableName) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('menu', 'database');
|
||||
params.append('action', 'editTable');
|
||||
params.append('dropTable', '1');
|
||||
params.append('tableName', tableName);
|
||||
return params;
|
||||
}
|
||||
|
||||
static buildFieldEditParams(tableName, multipleFields) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('menu', 'database');
|
||||
params.append('_defaultAction', 'editTable');
|
||||
params.append('editField', '1');
|
||||
params.append('tableName', tableName);
|
||||
params.append('save', '1');
|
||||
params.append('multipleFields', JSON.stringify(multipleFields));
|
||||
return params;
|
||||
}
|
||||
|
||||
static buildFieldDeleteParams(tableName, fieldname) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('menu', 'database');
|
||||
params.append('action', 'editTable');
|
||||
params.append('editField', '1');
|
||||
params.append('tableName', tableName);
|
||||
params.append('fieldname', fieldname);
|
||||
params.append('deleteField', '1');
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para construir URLs de query
|
||||
*/
|
||||
export class QueryParamsBuilder {
|
||||
static buildListRecordsQuery(tableName, page, keyword) {
|
||||
const params = new URLSearchParams({
|
||||
menu: tableName,
|
||||
json: "1"
|
||||
});
|
||||
if (page) params.append("page", String(page));
|
||||
if (keyword) params.append("keyword", keyword);
|
||||
return params;
|
||||
}
|
||||
|
||||
static buildListUploadsQuery(tableName, recordId, fieldName) {
|
||||
return new URLSearchParams({
|
||||
menu: tableName,
|
||||
action: 'uploadList',
|
||||
fieldName: fieldName,
|
||||
num: recordId,
|
||||
preSaveTempId: '',
|
||||
json: '1'
|
||||
});
|
||||
}
|
||||
|
||||
static buildDeleteUploadQuery(tableName, recordId, fieldName, uploadId) {
|
||||
return new URLSearchParams({
|
||||
menu: tableName,
|
||||
action: 'uploadErase',
|
||||
fieldName: fieldName,
|
||||
uploadNum: uploadId,
|
||||
num: recordId,
|
||||
preSaveTempId: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AcaiHttpClient;
|
||||
6
mcp-server/tools/helpers/authSchema.js
Normal file
6
mcp-server/tools/helpers/authSchema.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Auth parameters helper.
|
||||
* In stdio mode, credentials come from environment variables — no inline params needed.
|
||||
* withAuthParams just passes through the schema unchanged.
|
||||
*/
|
||||
export const withAuthParams = (schema) => schema;
|
||||
215
mcp-server/tools/helpers/errorHandler.js
Normal file
215
mcp-server/tools/helpers/errorHandler.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Centralized error handling for tools
|
||||
* Provides consistent error responses and logging
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle and format tool errors
|
||||
* @param {Error|string} error - The error object or message
|
||||
* @param {string} context - Context where the error occurred (e.g., "save_module", "create_record")
|
||||
* @param {Object} additionalInfo - Additional information to include in response
|
||||
* @returns {Object} Formatted error response
|
||||
*/
|
||||
export function handleToolError(error, context = "unknown", additionalInfo = {}) {
|
||||
// Log error to console
|
||||
console.error(`[Tool Error - ${context}]`, error instanceof Error ? error.message : error);
|
||||
if (error instanceof Error && error.stack) {
|
||||
console.error(`Stack:`, error.stack);
|
||||
}
|
||||
|
||||
// Extract error message
|
||||
let errorMessage = error instanceof Error ? error.message : String(error);
|
||||
let errorCode = "UNKNOWN_ERROR";
|
||||
let statusCode = 500;
|
||||
|
||||
// Handle specific error types
|
||||
if (error.response) {
|
||||
// Axios error with response
|
||||
statusCode = error.response.status || 500;
|
||||
errorMessage = error.response.data?.message ||
|
||||
error.response.data?.error ||
|
||||
errorMessage;
|
||||
errorCode = `HTTP_${statusCode}`;
|
||||
} else if (error.code) {
|
||||
// Error with code (like ENOTFOUND, ECONNREFUSED, etc.)
|
||||
errorCode = error.code;
|
||||
errorMessage = `${error.code}: ${errorMessage}`;
|
||||
}
|
||||
|
||||
// Return formatted error response
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: {
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
context: context,
|
||||
...additionalInfo
|
||||
}
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API response errors (when response contains error indication)
|
||||
* @param {Object} data - Response data from API
|
||||
* @param {string} context - Context where error occurred
|
||||
* @returns {Object|null} Error response or null if no error
|
||||
*/
|
||||
export function handleApiResponse(data, context = "unknown") {
|
||||
// Check for common error patterns in Acai CMS responses
|
||||
/*if (!data) {
|
||||
return handleToolError("Empty response from API", context, { details: "API returned null or undefined" });
|
||||
}*/
|
||||
|
||||
// PHP/Acai error responses typically have error field or PHPSyntax errors
|
||||
if (data.error || data.Error) {
|
||||
return handleToolError(data.error || data.Error, context, { details: data });
|
||||
}
|
||||
|
||||
if (data.PHPSyntax) {
|
||||
return handleToolError(`PHP Syntax Error: ${data.PHPSyntax}`, context, { details: data });
|
||||
}
|
||||
|
||||
// If it's a string response with error indicators
|
||||
if (typeof data === 'string' && data.trim().length > 0) {
|
||||
// Check for common error patterns
|
||||
if (data.toLowerCase().includes('error') ||
|
||||
data.toLowerCase().includes('fatal') ||
|
||||
data.toLowerCase().includes('undefined') ||
|
||||
data.toLowerCase().includes('syntax')) {
|
||||
return handleToolError(data, context, { details: "API returned error string" });
|
||||
}
|
||||
}
|
||||
|
||||
// If success field exists and is false
|
||||
if (data.success === false) {
|
||||
return handleToolError(data.message || "API returned success: false", context, { details: data });
|
||||
}
|
||||
|
||||
// No error detected
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required parameters
|
||||
* @param {Object} params - Parameters object
|
||||
* @param {string[]} requiredFields - Array of required field names
|
||||
* @param {string} context - Context where validation occurs
|
||||
* @returns {Object|null} Error response or null if all valid
|
||||
*/
|
||||
export function validateRequired(params, requiredFields, context = "unknown") {
|
||||
const missingFields = [];
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
const value = params[field];
|
||||
if (value === null || value === undefined ||
|
||||
(typeof value === 'string' && value.trim() === '')) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return handleToolError(
|
||||
`Missing required parameters: ${missingFields.join(', ')}`,
|
||||
context,
|
||||
{ requiredFields, missingFields }
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameter types
|
||||
* @param {Object} params - Parameters object
|
||||
* @param {Object} schema - Schema of expected types {fieldName: 'string'|'number'|'boolean'|'array'|'object'}
|
||||
* @param {string} context - Context where validation occurs
|
||||
* @returns {Object|null} Error response or null if all valid
|
||||
*/
|
||||
export function validateTypes(params, schema, context = "unknown") {
|
||||
const typeErrors = [];
|
||||
|
||||
for (const [field, expectedType] of Object.entries(schema)) {
|
||||
const value = params[field];
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
continue; // Skip optional fields that are not provided
|
||||
}
|
||||
|
||||
let actualType = typeof value;
|
||||
if (Array.isArray(value)) actualType = 'array';
|
||||
if (value instanceof Date) actualType = 'date';
|
||||
|
||||
if (actualType !== expectedType) {
|
||||
typeErrors.push(`${field}: expected ${expectedType}, got ${actualType}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeErrors.length > 0) {
|
||||
return handleToolError(
|
||||
`Type validation failed: ${typeErrors.join('; ')}`,
|
||||
context,
|
||||
{ typeErrors }
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse JSON with error handling
|
||||
* @param {string} jsonString - JSON string to parse
|
||||
* @param {string} context - Context where parsing occurs
|
||||
* @returns {Object} Parsed object or error response object
|
||||
*/
|
||||
export function safeJsonParse(jsonString, context = "unknown") {
|
||||
try {
|
||||
return { success: true, data: JSON.parse(jsonString) };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: handleToolError(error, `${context} - JSON parsing`, { input: jsonString.substring(0, 100) })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validation middleware for tools
|
||||
* @param {string[]} requiredFields - Required parameter names
|
||||
* @param {Object} typeSchema - Type validation schema
|
||||
* @returns {Function} Middleware function
|
||||
*/
|
||||
export function createValidator(requiredFields = [], typeSchema = {}) {
|
||||
return function validateInput(params, context = "unknown") {
|
||||
// Check required fields
|
||||
const requiredError = validateRequired(params, requiredFields, context);
|
||||
if (requiredError) return requiredError;
|
||||
|
||||
// Check types
|
||||
const typeError = validateTypes(params, typeSchema, context);
|
||||
if (typeError) return typeError;
|
||||
|
||||
return null; // No errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a tool handler with automatic error handling
|
||||
* @param {Function} handler - The tool handler function
|
||||
* @param {string} toolName - Name of the tool for logging
|
||||
* @returns {Function} Wrapped handler
|
||||
*/
|
||||
export function withErrorHandling(handler, toolName = "unknown") {
|
||||
return async (params, extra) => {
|
||||
try {
|
||||
return await handler(params, extra);
|
||||
} catch (error) {
|
||||
return handleToolError(error, toolName);
|
||||
}
|
||||
};
|
||||
}
|
||||
102
mcp-server/tools/helpers/fileBuilder.js
Normal file
102
mcp-server/tools/helpers/fileBuilder.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* Helper to save files using saveFileBuilder action
|
||||
* Used by multiple tools (save.js, saveGeneralSection.js, write.js, etc.)
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {string} params.token - Session token
|
||||
* @param {string} params.tokenHash - Token hash
|
||||
* @param {string} params.path - Folder path (e.g., '/modulos/mymodule/')
|
||||
* @param {string} params.fileName - File name (e.g., 'script.js', 'style.css')
|
||||
* @param {string} params.content - File content
|
||||
* @returns {Promise<Object>} Response from the API
|
||||
*/
|
||||
export async function saveFileBuilder({
|
||||
web_url,
|
||||
token,
|
||||
tokenHash,
|
||||
path,
|
||||
fileName,
|
||||
content,
|
||||
rawDataSended = true
|
||||
}) {
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewerUrl = web_url + '/cms/lib/viewer_functions.php';
|
||||
|
||||
const payload = {
|
||||
action_ws: 'saveFileBuilder',
|
||||
token: token,
|
||||
tokenHash: tokenHash,
|
||||
fileName: fileName,
|
||||
content: content,
|
||||
rawDataSended: rawDataSended,
|
||||
rootFolder: false,
|
||||
path: path
|
||||
};
|
||||
|
||||
console.error(`[saveFileBuilder] URL: ${viewerUrl}`);
|
||||
console.error(`[saveFileBuilder] Path: ${path}`);
|
||||
console.error(`[saveFileBuilder] Content length: ${content.length} chars`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(viewerUrl, payload, {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
|
||||
console.error(`[saveFileBuilder] Response for ${fileName}:`, JSON.stringify(response.data, null, 2));
|
||||
|
||||
return {
|
||||
success: response.data.success || false,
|
||||
message: response.data.message || (response.data.success ? 'OK' : 'Error'),
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`[saveFileBuilder] Error saving ${fileName}:`, error.message);
|
||||
return {
|
||||
success: false,
|
||||
message: `Error saving ${fileName}: ${error.message}`,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to save multiple files at once
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.web_url - URL base del sitio (ej: http://localhost:PORT)
|
||||
* @param {string} params.token - Session token
|
||||
* @param {string} params.tokenHash - Token hash
|
||||
* @param {string} params.path - Folder path (e.g., '/modulos/mymodule/')
|
||||
* @param {Object} params.files - Object with fileName: content pairs
|
||||
* @returns {Promise<Object>} Results for each file
|
||||
*/
|
||||
export async function saveMultipleFiles({
|
||||
web_url,
|
||||
token,
|
||||
tokenHash,
|
||||
path,
|
||||
files
|
||||
}) {
|
||||
const results = {};
|
||||
|
||||
for (const [fileName, content] of Object.entries(files)) {
|
||||
if (content) {
|
||||
results[fileName] = await saveFileBuilder({
|
||||
web_url,
|
||||
token,
|
||||
tokenHash,
|
||||
path,
|
||||
fileName,
|
||||
content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user