Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
This commit is contained in:
69
hermes/docker-compose.dev.yml
Normal file
69
hermes/docker-compose.dev.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL (reemplaza a Supabase)
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
# Redis
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
# WACRM (Next.js)
|
||||
wacrm:
|
||||
build:
|
||||
context: ./skeen-wacrm
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
- NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||
- NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3NTc0MDAsImV4cCI6MTkxNzUxMzgwMH0.skeen_dev_key
|
||||
- SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTc1NzQwMCwiZXhwIjoxOTE3NTEzODAwfQ.skeen_dev_service
|
||||
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
volumes:
|
||||
- ./skeen-wacrm:/app
|
||||
- /app/node_modules
|
||||
|
||||
# Hermes (Python)
|
||||
hermes:
|
||||
build:
|
||||
context: ./hermes
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- HERMES_MODEL_PROVIDER=openai
|
||||
- HERMES_MODEL=gpt-4o
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-test}
|
||||
- HERMES_WEBHOOK_PORT=8080
|
||||
- HERMES_API_SERVER_ENABLED=true
|
||||
- HERMES_CRON_ENABLED=true
|
||||
- WACRM_URL=http://wacrm:3000
|
||||
- WACRM_API_KEY=wacrm_dev_key
|
||||
volumes:
|
||||
- ./hermes/.hermes:/root/.hermes
|
||||
- ./hermes/skills:/skills:ro
|
||||
depends_on:
|
||||
- wacrm
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
759
hermes/docs/ROADMAP_SKEEN_HERMES.md
Normal file
759
hermes/docs/ROADMAP_SKEEN_HERMES.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# ROADMAP SKEEN Derma Experts: WACRM + Hermes + Odoo
|
||||
|
||||
## Arquitectura Tecnica Actualizada | Reemplazo Twilio → WACRM | FastAPI → Hermes
|
||||
|
||||
**Consultoria Alcaraz Salazar, S.A.S.**
|
||||
**5 de julio de 2026**
|
||||
|
||||
---
|
||||
|
||||
## Tabla de Contenidos
|
||||
|
||||
1. Comparativa: Arquitectura Original vs Nueva
|
||||
2. Arquitectura Detallada con WACRM + Hermes
|
||||
3. Stack Tecnologico Actualizado
|
||||
4. Estructura de Proyecto Actualizada
|
||||
5. Skills de Hermes para SKEEN
|
||||
6. Integraciones Detalladas
|
||||
7. Configuracion de WACRM
|
||||
8. Configuracion de Hermes
|
||||
9. Roadmap Actualizado por Fase
|
||||
10. Ventajas y Desventajas de esta Arquitectura
|
||||
11. Requisitos del Cliente (Actualizados)
|
||||
12. Presupuesto de Infraestructura Actualizado
|
||||
|
||||
---
|
||||
|
||||
## 1. Comparativa: Arquitectura Original vs Nueva
|
||||
|
||||
### 1.1 Tabla Comparativa General
|
||||
|
||||
| Componente | Arquitectura ORIGINAL | Arquitectura NUEVA (WACRM + Hermes) | Justificacion |
|
||||
|------------|----------------------|-------------------------------------|---------------|
|
||||
| WhatsApp Gateway | Twilio API ($0.005/msg) + webhook custom | WACRM + Meta WhatsApp Cloud API directo | Sin intermediario, inbox nativo, costo solo Meta fees (~$0.003-0.008/msg) |
|
||||
| Inbox / CRM | Inbox custom en React (a construir) | WACRM shared inbox (listo) | Inbox multi-agente con asignacion, notas internas, Kanban pipelines, broadcast — sin desarrollo |
|
||||
| Backend IA | FastAPI custom + GPT-4o directo | Hermes gateway + skills (SKILL.md) | Agente autonomo con 25+ tools, memoria persistente SQLite, cron nativo, multi-model, subagentes |
|
||||
| Agente "Sofia" | API endpoint REST stateless | Agente autonomo Hermes con contexto | Memoria persistente, proactive (cron), skills modulares, handoff a humano, context compression |
|
||||
| Pipelines de ventas | A construir en Odoo/Odoo + custom | WACRM Kanban pipelines nativos | Drag-drop, deals vinculados a conversaciones, win-rate analytics |
|
||||
| Base de datos CRM | PostgreSQL custom | Supabase (PostgreSQL + RLS + Auth) | Auth built-in, Row-Level Security, Realtime subscriptions, Storage |
|
||||
| Automatizaciones | Codigo custom en FastAPI | WACRM no-code automations + Hermes skills | Visual builder (Figma-like) + agente IA con razonamiento |
|
||||
| Broadcasts | Twilio API custom | WACRM broadcasts con templates Meta | Templates aprobados por Meta, delivery tracking, segmentacion |
|
||||
| Contactos / Leads | Tabla custom en PostgreSQL | WACRM contacts + tags + custom fields | Deduplicacion automatica, CSV import, tags, campos custom |
|
||||
| AI Assistant | GPT-4o directo via API | WACRM AI Assistant (BYOK) + Hermes skills | Draft replies, auto-reply bot, knowledge base, handoff a humano |
|
||||
| Costo mensual infra | ~$150-250 USD/mes | ~$80-150 USD/mes | WACRM es MIT (gratis), Hermes es MIT (gratis), solo hosting + API fees |
|
||||
| Tiempo de desarrollo inbox | ~2 semanas | ~0 dias (listo) | WACRM inbox funcional dia 1 post-configuracion |
|
||||
| Tiempo de desarrollo agente IA | ~3 semanas | ~1 semana (skills + config) | Hermes ya tiene tools, memoria, gateway, cron — solo configurar skills |
|
||||
|
||||
### 1.2 Diferencias Clave por Dimension
|
||||
|
||||
| Dimension | Original | Nueva | Impacto |
|
||||
|-----------|----------|-------|---------|
|
||||
| Complejidad de codigo | Alto (inbox + agente + CRM desde cero) | Medio (solo skills + integraciones) | ~60% menos codigo custom |
|
||||
| Mantenimiento | Equipo propio mantiene todo | Comunidad open-source mantiene WACRM/Hermes | Menos deuda tecnica |
|
||||
| Escalabilidad | Manual | WACRM escala con Supabase; Hermes con gateway | Mejor arquitectura cloud-native |
|
||||
| Vendor lock-in | Twilio (API propietaria) | Meta Cloud API (estandar) + MIT code | Codigo propio, migrable |
|
||||
| Onboarding de agentes | Desarrollo custom | WACRM invita por link, roles nativos | Minutos vs dias |
|
||||
| Analytics | A construir | WACRM dashboard real-time nativo | Disponible dia 1 |
|
||||
| Subagentes | No disponible | Sí — delegacion aislada con terminal propio | Tareas paralelas sin costo de contexto |
|
||||
| Fallback providers | No disponible | Sí — cadena de failover automatico | Mayor resiliencia |
|
||||
| Context compression | No disponible | Sí nativo — hasta 50% de ventana | Manejo de conversaciones largas |
|
||||
| Credential pools | No disponible | Sí — rotacion de API keys | Mayor tolerancia a rate limits |
|
||||
|
||||
### 1.3 Mapa de Reemplazo de Componentes
|
||||
|
||||
```
|
||||
Twilio API ──────────────→ Meta WhatsApp Cloud API (via WACRM)
|
||||
Twilio webhooks ─────────→ WACRM webhooks (/api/v1/webhooks)
|
||||
Inbox React custom ──────→ WACRM shared inbox (Next.js 16 App Router)
|
||||
FastAPI backend ─────────→ Hermes gateway (Python daemon)
|
||||
GPT-4o directo ──────────→ Hermes agent runtime (multi-model + fallback)
|
||||
PostgreSQL CRM ──────────→ Supabase PostgreSQL + RLS
|
||||
Automatizaciones code ───→ WACRM no-code automations + Hermes skills + cron
|
||||
Dashboard analytics ─────→ WACRM dashboard real-time
|
||||
Contact management ──────→ WACRM contacts + tags + pipelines
|
||||
OpenClaw gateway ────────→ Hermes gateway (Python, mas plataformas nativas)
|
||||
OpenClaw heartbeat ───────→ Hermes cron nativo (mas flexible)
|
||||
OpenClaw skills ─────────→ Hermes skills (compatible agentskills.io)
|
||||
OpenClaw memory MD ──────→ Hermes SQLite + MEMORY.md + plugins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Arquitectura Detallada con WACRM + Hermes
|
||||
|
||||
### 2.1 Diagrama de Componentes Completo
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ LAYER: PACIENTE │
|
||||
│ WhatsApp Business (telefono SKEEN) ─── App movil del paciente │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼ Meta Cloud API (HTTPS webhook)
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ LAYER: WACRM (CRM) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ WACRM — Next.js 16 + Supabase PostgreSQL │ │
|
||||
│ │ Puerto: 3000 (interno) / HTTPS via Nginx │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
|
||||
│ │ │ Shared Inbox │ │ Contacts │ │ Sales Pipelines (Kanban)│ │ │
|
||||
│ │ │ Multi-agente │ │ + Tags + │ │ Deals → Conversaciones │ │ │
|
||||
│ │ │ Asignacion │ │ Custom Fields│ │ Win-rate analytics │ │ │
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
|
||||
│ │ │ Broadcasts │ │ Automations │ │ AI Assistant (BYOK) │ │ │
|
||||
│ │ │ Templates │ │ No-code │ │ OpenAI/Anthropic key │ │ │
|
||||
│ │ │ Meta │ │ Triggers │ │ Auto-reply + Handoff │ │ │
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
|
||||
│ │ │ Dashboard │ │ Flows │ │ Templates Manager │ │ │
|
||||
│ │ │ Real-time │ │ Chatbots │ │ Meta approval sync │ │ │
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ API REST PUBLICA: /api/v1 │ │
|
||||
│ │ ├── POST /messages (enviar WhatsApp) │ │
|
||||
│ │ ├── GET /contacts (listar contactos) │ │
|
||||
│ │ ├── POST /contacts (crear/find-or-create) │ │
|
||||
│ │ ├── GET /conversations (listar conversaciones) │ │
|
||||
│ │ ├── GET /conversations/:id/messages (thread) │ │
|
||||
│ │ ├── POST /broadcasts (campaña broadcast) │ │
|
||||
│ │ ├── POST /webhooks (registrar webhook outbound) │ │
|
||||
│ │ └── GET /me (info cuenta + scopes) │ │
|
||||
│ │ │ │
|
||||
│ │ Webhooks outbound (a Hermes): │ │
|
||||
│ │ ├── message.received (nuevo mensaje) │ │
|
||||
│ │ ├── message.status_updated (delivery/read) │ │
|
||||
│ │ └── conversation.created (nueva conversacion) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────────────┐ ┌─────────────────┐ ┌──────────────────────────┐
|
||||
│ LAYER: Hermes │ │ LAYER: Odoo 17 │ │ LAYER: Frontend React │
|
||||
│ (Agente IA "Sofia") │ │ (Backend ERP) │ │ (app.skeen.mx) │
|
||||
│ │ │ │ │ │
|
||||
│ Gateway Python │ │ Modulos: │ │ Dashboard clinicos │
|
||||
│ Puerto: 8080 (HTTP) │ │ ├── Pacientes │ │ Agenda citas │
|
||||
│ + WS API (cron) │ │ ├── Citas │ │ Monedero digital │
|
||||
│ Daemon (systemd) │ │ ├── Historial │ │ Pagos (Stripe/MP) │
|
||||
│ │ │ ├── Inventario │ │ Reportes │
|
||||
│ ┌──────────────────┐ │ │ ├── POS │ │ │
|
||||
│ │ Agent Runtime │ │ │ ├── Monedero │ │ Auth: Odoo JWT │
|
||||
│ │ ├─ SOUL.md │ │ │ └── Website │ │ API: Odoo REST + │
|
||||
│ │ ├─ AGENTS.md │ │ │ │ │ WACRM REST │
|
||||
│ │ ├─ SKILL.md (5) │ │ │ API REST XML/ │ │ │
|
||||
│ │ ├─ MEMORY.md │ │ │ JSON (custom) │ │ Puerto: 5173 (dev) │
|
||||
│ │ └─ SQLite state │ │ │ │ │ 80/443 (prod) │
|
||||
│ └──────────────────┘ │ └─────────────────┘ └──────────────────────────┘
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Cron (nativo) │ │
|
||||
│ │ ├─ Recordatorios│ │
|
||||
│ │ ├─ Follow-ups │ │
|
||||
│ │ └─ Re-engagement│ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Tools Built-in │ │
|
||||
│ │ ├─ web_search │ │
|
||||
│ │ ├─ web_fetch │ │
|
||||
│ │ ├─ exec (python)│ │
|
||||
│ │ ├─ shell │ │
|
||||
│ │ ├─ browser │ │
|
||||
│ │ └─ file R/W │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ Custom Plugins │ │
|
||||
│ │ ├─ odoo_api_call│ │
|
||||
│ │ ├─ wacrm_send │ │
|
||||
│ │ └─ calendar_check│ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ API Server │ │
|
||||
│ │ /v1/chat/completions│ │
|
||||
│ │ /api/jobs (cron)│ │
|
||||
│ └──────────────────┘ │
|
||||
└───────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Flujo de Datos Paso a Paso
|
||||
|
||||
#### FLUJO 1: Primera vez que un paciente escribe por WhatsApp
|
||||
|
||||
```
|
||||
[1] Paciente envia "Hola, quiero agendar una cita" por WhatsApp
|
||||
│
|
||||
▼
|
||||
[2] Meta WhatsApp Cloud API recibe el mensaje
|
||||
│
|
||||
▼
|
||||
[3] Meta envia webhook HTTPS a WACRM: POST /api/whatsapp/webhook
|
||||
│
|
||||
▼
|
||||
[4] WACRM procesa:
|
||||
- Crea/actualiza contacto (telefono, nombre si disponible)
|
||||
- Crea conversacion en inbox
|
||||
- Almacena mensaje en Supabase
|
||||
- Dispara trigger de automatizacion ("Nuevo mensaje")
|
||||
│
|
||||
├──► WACRM AI Assistant (opcional, si auto-reply activo)
|
||||
│
|
||||
└──► Webhook outbound a Hermes: POST http://hermes:8080/webhook/wacrm
|
||||
│
|
||||
▼
|
||||
[5] Hermes Gateway recibe webhook via HTTP endpoint custom
|
||||
- Parsea payload: { event: "message.received", data: { from, text, conversation_id } }
|
||||
- Inicia/continua sesion para ese contacto (SQLite session store)
|
||||
- Inyecta skills relevantes al contexto
|
||||
│
|
||||
▼
|
||||
[6] Hermes Agent Runtime procesa con LLM (GPT-4o):
|
||||
- Lee skills: skeen-rag.md (catalogo) + skeen-agendar.md (citas)
|
||||
- Detecta intencion: "agendar_cita"
|
||||
- Consulta disponibilidad via plugin odoo_api_call
|
||||
│
|
||||
▼
|
||||
[7] Hermes → Odoo API (XML-RPC/JSON-RPC):
|
||||
- Consulta slots disponibles del dermatologo
|
||||
- Recibe: ["2026-07-05 10:00", "2026-07-05 14:00", "2026-07-06 09:00"]
|
||||
│
|
||||
▼
|
||||
[8] Hermes formatea respuesta y envia a WACRM:
|
||||
POST /api/v1/messages
|
||||
{ "to": "+526641234567", "type": "text",
|
||||
"text": "Hola! Soy Sofia de SKEEN. Puedo agendarte para:
|
||||
1. Sab 5 jul 10:00 AM
|
||||
2. Sab 5 jul 2:00 PM
|
||||
3. Dom 6 jul 9:00 AM
|
||||
Cual prefieres?" }
|
||||
│
|
||||
▼
|
||||
[9] WACRM envia via Meta Cloud API → WhatsApp → Paciente
|
||||
│
|
||||
▼
|
||||
[10] Respuesta del paciente: "El sabado a las 10"
|
||||
│
|
||||
└──► Repite [2]→[6], Hermes confirma cita
|
||||
└──► Odoo: crea cita + WACRM: envia confirmacion
|
||||
```
|
||||
|
||||
#### FLUJO 2: Confirmacion de cita + Recordatorio automatico
|
||||
|
||||
```
|
||||
[1] Hermes crea la cita en Odoo via odoo_api_call
|
||||
- Modelo: salon.booking (o calendar.event)
|
||||
- Datos: paciente_id, servicio_id, fecha_hora, estado='confirmada'
|
||||
│
|
||||
▼
|
||||
[2] Hermes programa recordatorio via cron nativo:
|
||||
- hermes cron schedule --name "recordatorio-cita-[ID]" \
|
||||
--at "2026-07-04 18:00" \
|
||||
--command "skill skeen-recordatorios --phone +526641234567 --cita 2026-07-05T10:00"
|
||||
│
|
||||
▼
|
||||
[3] Cron de Hermes ejecuta a la hora programada:
|
||||
- Skill skeen-recordatorios se activa
|
||||
- Consulta Odoo: citas para manana (5 jul)
|
||||
- Para cada cita sin recordatorio enviado:
|
||||
└──► WACRM API: POST /api/v1/messages
|
||||
{ "to": "+526641234567", "type": "template",
|
||||
"template": { "name": "recordatorio_cita_skeen",
|
||||
"language": "es_MX",
|
||||
"params": ["Sofia", "5 de julio", "10:00 AM"] } }
|
||||
│
|
||||
▼
|
||||
[4] WACRM envia template aprobado por Meta → WhatsApp → Paciente
|
||||
"Hola Sofia! Te recordamos tu cita manana 5 de julio a las 10:00 AM
|
||||
en SKEEN Derma Experts. Ubicacion: Blvd. Popotla 123, Rosarito."
|
||||
│
|
||||
▼
|
||||
[5] Odoo marca recordatorio como enviado
|
||||
```
|
||||
|
||||
#### FLUJO 3: Pago de cita + Monedero digital
|
||||
|
||||
```
|
||||
[1] Paciente despues de cita: "Quiero pagar mi tratamiento"
|
||||
│
|
||||
▼
|
||||
[2] WACRM → Hermes (webhook message.received)
|
||||
│
|
||||
▼
|
||||
[3] Hermes skill skeen-pagos.md:
|
||||
- Consulta Odoo: citas del paciente + servicios pendientes de pago
|
||||
- Calcula total
|
||||
- Genera link de pago (Stripe Checkout / MercadoPago preference)
|
||||
│
|
||||
▼
|
||||
[4] Hermes responde via WACRM:
|
||||
"Tu tratamiento de Acne Control tiene un saldo de $1,200 MXN.
|
||||
Puedes pagar aqui: [Link Stripe] o [Link MercadoPago]
|
||||
Tambien acumulas 120 puntos en tu monedero SKEEN!"
|
||||
│
|
||||
▼
|
||||
[5] Paciente paga via Stripe/MP
|
||||
│
|
||||
▼
|
||||
[6] Webhook Stripe/MP → Odoo (confirmacion de pago)
|
||||
- Odoo actualiza estado de factura
|
||||
- Odoo acumula puntos en monedero del paciente
|
||||
│
|
||||
▼
|
||||
[7] Hermes cron detecta pago confirmado:
|
||||
- WACRM: envia confirmacion + saldo de monedero
|
||||
"Pago confirmado! Gracias Sofia. Tu monedero SKEEN: 450 puntos
|
||||
(equivalente a $450 MXN). Los puedes usar en tu proxima visita."
|
||||
```
|
||||
|
||||
### 2.3 Diagrama de Secuencia: Agendar Cita Completa
|
||||
|
||||
```
|
||||
Paciente WhatsApp Meta API WACRM Hermes Odoo
|
||||
│ │ │ │ │ │
|
||||
│ "Hola, quiero │ │ │ │ │
|
||||
│ agendar" │ │ │ │ │
|
||||
│─────────────────>│ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Webhook msg │ │ │ │
|
||||
│ │<───────────────│ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ POST /webhook │ │
|
||||
│ │ │─────────────>│ │ │
|
||||
│ │ │ │ message.received │
|
||||
│ │ │ │ webhook outbound │
|
||||
│ │ │ │──────────────>│ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ odoo_api_call│
|
||||
│ │ │ │ │ (slots libres)│
|
||||
│ │ │ │ │─────────────>│
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │<─────────────│
|
||||
│ │ │ │ │ ["10:00", │
|
||||
│ │ │ │ │ "14:00"] │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ POST /api/v1/messages │ │
|
||||
│ │ │<─────────────│ │ │
|
||||
│ │ │ (WACRM envia opciones) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Template msg │ │ │ │
|
||||
│ │<───────────────│ │ │ │
|
||||
│ "Hola! Opciones:│ │ │ │ │
|
||||
│ 1. 10:00..." │ │ │ │ │
|
||||
│<─────────────────│ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ "Opcion 1" │ │ │ │ │
|
||||
│─────────────────>│ │ │ │ │
|
||||
│ │ ... (mismo flujo) ... │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ │ odoo_api_call│
|
||||
│ │ │ │ │ (crear cita) │
|
||||
│ │ │ │ │─────────────>│
|
||||
│ │ │ │ │ │
|
||||
│ │ │ POST /api/v1/messages │ │
|
||||
│ │ │<─────────────│ │ │
|
||||
│ │ │ │ │ │
|
||||
│ "Cita confirmada│ │ │ │ │
|
||||
│ para 5 jul 10AM│ │ │ │ │
|
||||
│ con Dra. Ana" │ │ │ │ │
|
||||
│<─────────────────│ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
```
|
||||
|
||||
### 2.4 Comunicacion entre Componentes
|
||||
|
||||
| De | A | Protocolo | Auth | Payload |
|
||||
|----|---|-----------|------|---------|
|
||||
| Meta API | WACRM | HTTPS webhook | Verify Token HMAC | JSON mensaje |
|
||||
| WACRM | Meta API | HTTPS REST | Bearer Access Token | JSON mensaje/template |
|
||||
| WACRM | Hermes | HTTPS webhook outbound | HMAC-SHA256 signature | JSON evento |
|
||||
| Hermes | WACRM | HTTPS REST | Bearer API Key (wacrm_live_*) | JSON mensaje |
|
||||
| Hermes | Odoo | XML-RPC / JSON-RPC / HTTP | API Key / OAuth | XML/JSON |
|
||||
| Odoo | Frontend | HTTPS REST (CORS) | JWT Token | JSON |
|
||||
| Frontend | WACRM | HTTPS REST | Bearer API Key | JSON (dashboard unificado) |
|
||||
| Hermes | LLM Provider | HTTPS SSE/REST | Bearer API Key | JSON (OpenAI/Anthropic format) |
|
||||
| Hermes | Hermes API | HTTPS REST | Bearer Token | JSON (OpenAI-compatible) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Stack Tecnologico Actualizado
|
||||
|
||||
### 3.1 Tabla Completa de Tecnologias
|
||||
|
||||
#### WACRM (CRM WhatsApp)
|
||||
|
||||
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|
||||
|------|-----------|---------|-----------|---------------|
|
||||
| Framework | Next.js | 16 (App Router) | SSR + API routes + ISR | MIT / Gratis |
|
||||
| UI | React | 19 | Componentes interactivos | MIT / Gratis |
|
||||
| Lenguaje | TypeScript | 5.x | Tipado estatico | OSS / Gratis |
|
||||
| Estilos | Tailwind CSS | v4 | Utility-first CSS | MIT / Gratis |
|
||||
| UI Kit | shadcn/ui + base-ui | latest | Componentes accesibles | MIT / Gratis |
|
||||
| Database | Supabase (PostgreSQL) | 15+ | Datos + Auth + RLS + Realtime | OSS Cloud / ~$25/mes |
|
||||
| Auth | Supabase Auth | built-in | Email/password + JWT | Incluido |
|
||||
| Storage | Supabase Storage | built-in | Avatares + media | Incluido |
|
||||
| WhatsApp | Meta Cloud API | v18.0+ | Mensajeria oficial | Meta fees |
|
||||
| Encriptacion | node:crypto | built-in | AES-256-GCM tokens | built-in |
|
||||
|
||||
#### Hermes (Agente IA)
|
||||
|
||||
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|
||||
|------|-----------|---------|-----------|---------------|
|
||||
| Runtime | Python | 3.11+ | Gateway daemon | OSS / Gratis |
|
||||
| Package | uv / pip | latest | Gestor de paquetes | OSS / Gratis |
|
||||
| Gateway | hermes-agent | 0.14.0+ | Gateway HTTP + WS + API | MIT / Gratis |
|
||||
| Comunicacion | HTTP / WebSocket | RFC 6455 | Gateway ↔ clients | built-in |
|
||||
| Skills | SKILL.md (Markdown) | agentskills.io | Capacidades del agente | MIT / Gratis |
|
||||
| Memory | SQLite + MEMORY.md | - | Memoria persistente + FTS5 search | File system |
|
||||
| Cron | hermes cron | built-in | Tareas programadas | built-in |
|
||||
| LLM Provider | OpenAI GPT-4o | latest | Modelo principal | ~$0.005-0.015/token |
|
||||
| Fallback LLM | Claude 3.5 Sonnet | latest | Fallback / tareas complejas | ~$0.003-0.015/token |
|
||||
| Fallback chain | Hermes nativo | 0.6.0+ | Failover automatico | built-in |
|
||||
| Local LLM | Ollama / LM Studio | 0.4+ / 0.12.0+ | Desarrollo offline | OSS / Gratis |
|
||||
| Context compression | Hermes nativo | built-in | Compresion de contexto | built-in |
|
||||
| Credential pools | Hermes nativo | 0.7.0+ | Rotacion de API keys | built-in |
|
||||
| Subagentes | Hermes delegate | built-in | Tareas aisladas | built-in |
|
||||
| API Server | OpenAI-compatible | 0.4.0+ | /v1/chat/completions | built-in |
|
||||
| TUI | Ink (React) | 0.11.0+ | Interfaz terminal interactiva | built-in |
|
||||
| Plugins | Python files | 0.3.0+ | Custom tools | built-in |
|
||||
| MCP | Model Context Protocol | 0.4.0+ | Integracion MCP | built-in |
|
||||
|
||||
#### Odoo 17 (ERP)
|
||||
|
||||
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|
||||
|------|-----------|---------|-----------|---------------|
|
||||
| ERP | Odoo Community | 17.0 | Backend negocio completo | LGPL / Gratis |
|
||||
| Lenguaje | Python | 3.10+ | Logica de servidor | OSS |
|
||||
| Database | PostgreSQL | 15+ | Datos ERP (compartido con Supabase o separado) | OSS |
|
||||
| Cache | Redis | 7.x | Cache + sessions + queue | BSD / Gratis |
|
||||
| Queue | Celery + Redis | 5.x+ | Tareas asincronas | OSS |
|
||||
|
||||
#### Frontend SKEEN
|
||||
|
||||
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|
||||
|------|-----------|---------|-----------|---------------|
|
||||
| Framework | React | 18 | UI components | MIT / Gratis |
|
||||
| Lenguaje | TypeScript | 5.x | Tipado estatico | OSS |
|
||||
| Build | Vite | 5.x+ | Bundler + dev server | MIT / Gratis |
|
||||
| Estilos | Tailwind CSS | v3+ | Estilos utility-first | MIT / Gratis |
|
||||
| Routing | React Router | v6+ | SPA routing | MIT / Gratis |
|
||||
| HTTP Client | Axios | 1.6+ | Llamadas API | MIT / Gratis |
|
||||
| Estado | Zustand | 4.x+ | State management | MIT / Gratis |
|
||||
| Calendario | react-big-calendar | 1.x+ | Vista agenda citas | MIT |
|
||||
| Charts | Recharts | 2.x+ | Graficos dashboard | MIT |
|
||||
|
||||
#### Infraestructura
|
||||
|
||||
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|
||||
|------|-----------|---------|-----------|---------------|
|
||||
| VPS | Hetzner Cloud | CPX31 | 4 vCPU / 8 GB / 160 GB SSD | ~$15/mes |
|
||||
| OS | Ubuntu Server | 24.04 LTS | Sistema operativo | Gratis |
|
||||
| Proxy | Nginx | 1.24+ | Reverse proxy + SSL + rate limit | BSD |
|
||||
| SSL | Let's Encrypt | latest | Certificados HTTPS gratis | Gratis |
|
||||
| Contenedores | Docker | 25.x+ | Container runtime | Apache 2.0 |
|
||||
| Orquestacion | Docker Compose | 2.24+ | Multi-container local | Apache 2.0 |
|
||||
| DNS | Cloudflare | Free tier | DNS + CDN + DDoS protection | Gratis |
|
||||
| Monitoreo | Uptime Kuma | 1.x+ | Health checks + alerting | MIT |
|
||||
|
||||
#### Pagos
|
||||
|
||||
| Capa | Tecnologia | Version | Proposito | Costo |
|
||||
|------|-----------|---------|-----------|-------|
|
||||
| Pagos intl | Stripe | latest | Tarjetas internacionales | 2.9% + $0.30 |
|
||||
| Pagos MX | MercadoPago | latest | OXXO, tarjetas MX, SPEI | 3.5-4.5% |
|
||||
|
||||
#### Desarrollo
|
||||
|
||||
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|
||||
|------|-----------|---------|-----------|---------------|
|
||||
| Git | GitHub | - | Repositorios + CI/CD | Free/Pro |
|
||||
| IDE | VS Code | latest | Editor + extensions | Gratis |
|
||||
| API Testing | Postman / Hoppscotch | latest | Test APIs | Gratis |
|
||||
| DB Admin | pgAdmin / Tableplus | latest | Admin PostgreSQL | Gratis |
|
||||
|
||||
### 3.2 Diagrama de Puertos y Servicios
|
||||
|
||||
| Servicio | Puerto Interno | Puerto Externo | Path Nginx |
|
||||
|----------|---------------|----------------|------------|
|
||||
| WACRM (Next.js) | 3000 | 443 | /crm/* → localhost:3000 |
|
||||
| Hermes Gateway | 8080 | 443 | /webhook/* → localhost:8080 |
|
||||
| Hermes API Server | 8080 (mismo) | - | Solo interno (localhost) |
|
||||
| Hermes TUI | - | - | Interactivo local |
|
||||
| Odoo | 8069 | 443 | /odoo/* → localhost:8069 |
|
||||
| Odoo Longpolling | 8072 | - | Solo interno |
|
||||
| Frontend React | 5173 (dev) / 80 (prod) | 443 | / → localhost:80 |
|
||||
| PostgreSQL | 5432 | - | Solo interno |
|
||||
| Redis | 6379 | - | Solo interno |
|
||||
| Nginx | 80, 443 | 80, 443 | Reverse proxy principal |
|
||||
| Uptime Kuma | 3001 | 443 | /status → localhost:3001 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Estructura de Proyecto Actualizada
|
||||
|
||||
### 4.1 Repositorios Git
|
||||
|
||||
```
|
||||
skeen-derma-experts/ (organizacion GitHub)
|
||||
├── wacrm/ (fork de ArnasDon/wacrm)
|
||||
│ ├── src/
|
||||
│ │ ├── app/
|
||||
│ │ │ ├── (auth)/ # login, signup
|
||||
│ │ │ ├── (dashboard)/ # UI autenticada
|
||||
│ │ │ │ ├── dashboard/ # home / metrics
|
||||
│ │ │ │ ├── inbox/ # shared inbox
|
||||
│ │ │ │ ├── contacts/ # contactos + tags
|
||||
│ │ │ │ ├── pipelines/ # Kanban deals
|
||||
│ │ │ │ ├── broadcasts/ # campañas
|
||||
│ │ │ │ ├── automations/ # flow builder
|
||||
│ │ │ │ └── settings/ # perfil / WhatsApp / API keys
|
||||
│ │ │ ├── api/ # endpoints server-only
|
||||
│ │ │ │ ├── whatsapp/webhook/ # inbound desde Meta
|
||||
│ │ │ │ ├── whatsapp/send/ # outbound a Meta
|
||||
│ │ │ │ ├── automations/ # engine + cron
|
||||
│ │ │ │ └── v1/ # API publica (messages, contacts, etc.)
|
||||
│ │ ├── components/
|
||||
│ │ ├── lib/
|
||||
│ │ │ ├── supabase/ # client, server, middleware
|
||||
│ │ │ ├── whatsapp/ # Meta API client
|
||||
│ │ │ └── automations/ # engine, steps, validation
|
||||
│ │ └── types/
|
||||
│ ├── supabase/migrations/ # SQL idempotente
|
||||
│ ├── .env.local # variables de entorno
|
||||
│ └── package.json
|
||||
│
|
||||
├── hermes/ # Gateway + Skills SKEEN
|
||||
│ ├── .hermes/
|
||||
│ │ ├── config.yaml # config principal
|
||||
│ │ ├── plugins/
|
||||
│ │ │ ├── odoo_api_call.py # plugin Odoo XML-RPC
|
||||
│ │ │ ├── wacrm_send.py # plugin WACRM API
|
||||
│ │ │ └── calendar_check.py # plugin disponibilidad
|
||||
│ │ └── workspace/
|
||||
│ │ ├── AGENTS.md # instrucciones operativas
|
||||
│ │ ├── SOUL.md # personalidad "Sofia"
|
||||
│ │ ├── MEMORY.md # memoria persistente
|
||||
│ │ ├── TOOLS.md # notas de tools
|
||||
│ │ ├── IDENTITY.md # nombre/vibe/emoji
|
||||
│ │ └── USER.md # perfil clinica
|
||||
│ ├── skills/
|
||||
│ │ ├── skeen-rag/
|
||||
│ │ │ └── SKILL.md # catalogo servicios
|
||||
│ │ ├── skeen-agendar/
|
||||
│ │ │ └── SKILL.md # agendamiento citas
|
||||
│ │ ├── skeen-pagos/
|
||||
│ │ │ └── SKILL.md # pagos Stripe/MP
|
||||
│ │ ├── skeen-monedero/
|
||||
│ │ │ └── SKILL.md # monedero digital
|
||||
│ │ └── skeen-recordatorios/
|
||||
│ │ └── SKILL.md # recordatorios citas
|
||||
│ ├── webhooks/
|
||||
│ │ └── wacrm_webhook_server.py # servidor HTTP para webhooks WACRM
|
||||
│ ├── pyproject.toml
|
||||
│ ├── requirements.txt
|
||||
│ └── Dockerfile
|
||||
│
|
||||
├── odoo-backend/ # Odoo 17 + modulos custom
|
||||
│ ├── odoo/
|
||||
│ │ ├── addons/
|
||||
│ │ │ ├── skeen_pacientes/ # gestion pacientes
|
||||
│ │ │ ├── skeen_citas/ # agendamiento
|
||||
│ │ │ ├── skeen_monedero/ # monedero digital
|
||||
│ │ │ ├── skeen_pagos/ # integracion Stripe/MP
|
||||
│ │ │ ├── skeen_whatsapp/ # webhook WACRM → Odoo
|
||||
│ │ │ └── skeen_reportes/ # reportes clinicos
|
||||
│ │ └── odoo.conf
|
||||
│ ├── Dockerfile
|
||||
│ └── docker-compose.yml
|
||||
│
|
||||
├── frontend/ # React 18 + TypeScript
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # UI components
|
||||
│ │ ├── pages/ # vistas (Home, Agenda, Pagos)
|
||||
│ │ ├── hooks/ # custom hooks
|
||||
│ │ ├── services/ # API clients (Odoo + WACRM)
|
||||
│ │ ├── store/ # Zustand state
|
||||
│ │ └── types/ # TypeScript types
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
│
|
||||
└── infra/ # Docker Compose completo
|
||||
├── docker-compose.yml # orquestacion total
|
||||
├── .env # variables globales
|
||||
├── nginx/
|
||||
│ ├── nginx.conf # reverse proxy
|
||||
│ └── ssl/ # certificados Let's Encrypt
|
||||
├── scripts/
|
||||
│ ├── init-supabase.sh # migraciones SQL
|
||||
│ ├── setup-wacrm.sh # post-install WACRM
|
||||
│ └── setup-hermes.sh # post-install Hermes
|
||||
└── monitoring/
|
||||
├── uptime-kuma/ # health checks
|
||||
└── prometheus/ # metrics (opcional)
|
||||
```
|
||||
|
||||
### 4.2 Estructura de Docker Compose
|
||||
|
||||
```yaml
|
||||
# infra/docker-compose.yml — Resumen de servicios
|
||||
|
||||
services:
|
||||
# ── WACRM (CRM WhatsApp) ──────────────────────────────
|
||||
wacrm:
|
||||
build: ../wacrm/
|
||||
ports: ["3000:3000"]
|
||||
environment:
|
||||
- NEXT_PUBLIC_SUPABASE_URL=${SUPABASE_URL}
|
||||
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
|
||||
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE}
|
||||
- ENCRYPTION_KEY=${WACRM_ENCRYPTION_KEY}
|
||||
- WHATSAPP_PHONE_NUMBER_ID=${META_PHONE_ID}
|
||||
- WHATSAPP_BUSINESS_ACCOUNT_ID=${META_BUSINESS_ID}
|
||||
- WHATSAPP_ACCESS_TOKEN=${META_ACCESS_TOKEN}
|
||||
- WHATSAPP_VERIFY_TOKEN=${META_VERIFY_TOKEN}
|
||||
depends_on: [postgres, redis]
|
||||
|
||||
# ── Hermes (Agente IA) ──────────────────────────────
|
||||
hermes:
|
||||
build: ../hermes/
|
||||
ports: ["8080:8080"]
|
||||
environment:
|
||||
- HERMES_MODEL_PROVIDER=openai
|
||||
- HERMES_MODEL=gpt-4o
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- ODOO_URL=http://odoo:8069
|
||||
- ODOO_DB=skeen
|
||||
- ODOO_API_KEY=${ODOO_API_KEY}
|
||||
- WACRM_URL=http://wacrm:3000
|
||||
- WACRM_API_KEY=${WACRM_API_KEY}
|
||||
- HERMES_WEBHOOK_PORT=8080
|
||||
- HERMES_API_SERVER_ENABLED=true
|
||||
- HERMES_CRON_ENABLED=true
|
||||
volumes:
|
||||
- hermes_workspace:/root/.hermes/workspace
|
||||
- hermes_plugins:/root/.hermes/plugins
|
||||
- ./../hermes/skills:/skills:ro
|
||||
depends_on: [odoo, wacrm]
|
||||
command: ["hermes", "gateway", "start"]
|
||||
|
||||
# ── Odoo 17 (ERP) ─────────────────────────────────────
|
||||
odoo:
|
||||
build: ../odoo-backend/
|
||||
ports: ["8069:8069", "8072:8072"]
|
||||
environment:
|
||||
- HOST=postgres
|
||||
- PORT=5432
|
||||
- USER=odoo
|
||||
- PASSWORD=${POSTGRES_PASSWORD}
|
||||
- DB_NAME=skeen
|
||||
volumes:
|
||||
- odoo_data:/var/lib/odoo
|
||||
- ./../odoo-backend/odoo/addons:/mnt/extra-addons:ro
|
||||
depends_on: [postgres, redis]
|
||||
|
||||
# ── Frontend React ────────────────────────────────────
|
||||
frontend:
|
||||
build: ../frontend/
|
||||
ports: ["80:80"]
|
||||
depends_on: [odoo, wacrm]
|
||||
|
||||
# ── PostgreSQL ────────────────────────────────────────
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
ports: ["5432:5432"]
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=skeen
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
# ── Redis ─────────────────────────────────────────────
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports: ["6379:6379"]
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
# ── Nginx (Reverse Proxy) ─────────────────────────────
|
||||
nginx:
|
||||
image: nginx:1.24-alpine
|
||||
ports: ["443:443"]
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
depends_on: [wacrm, odoo, frontend, hermes]
|
||||
|
||||
# ── Uptime Kuma ───────────────────────────────────────
|
||||
uptime-kuma:
|
||||
image: louislam/uptime-kuma:1
|
||||
ports: ["3001:3001"]
|
||||
volumes:
|
||||
- uptime_data:/app/data
|
||||
|
||||
volumes:
|
||||
hermes_workspace:
|
||||
hermes_plugins:
|
||||
odoo_data:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
uptime_data:
|
||||
```
|
||||
|
||||
### 4.3 Variables de Entorno (.env)
|
||||
|
||||
```bash
|
||||
# ============================================================
|
||||
# INFRA — infra/.env
|
||||
# ============================================================
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_PASSWORD=skeen_postgres_2026_secure
|
||||
POSTGRES_USER=odoo
|
||||
POSTGRES_DB=skeen
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=skeen_redis_2026
|
||||
|
||||
# WACRM
|
||||
SUPABASE_URL=http://postgres:5432
|
||||
SUPABASE_ANON_KEY=eyJ...
|
||||
SUPABASE_SERVICE_ROLE=eyJ...
|
||||
WACRM_ENCRYPTION_KEY=a1b2c3d4e5f6... # 64 chars hex AES-256
|
||||
META_PHONE_ID=123456789012345
|
||||
META_BUSINESS_ID=987654321098765
|
||||
META_ACCESS_TOKEN=EAA...
|
||||
META_VERIFY_TOKEN=skeen_webhook_verify_2026
|
||||
|
||||
# Hermes
|
||||
OPENAI_API_KEY=sk-proj-...
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
HERMES_MODEL_PROVIDER=openai
|
||||
HERMES_MODEL=gpt-4o
|
||||
HERMES_FALLBACK_PROVIDERS=anthropic,openrouter
|
||||
HERMES_API_SERVER_ENABLED=true
|
||||
HERMES_CRON_ENABLED=true
|
||||
HERMES_WEBHOOK_PORT=8080
|
||||
HERMES_TUI_ENABLED=false
|
||||
ODOO_API_KEY=skeen_odoo_api_key_2026
|
||||
WACRM_API_KEY=wacrm_live_xxxxxxxx
|
||||
|
||||
# Stripe / MercadoPago
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
MP_ACCESS_TOKEN=TEST-...
|
||||
MP_PUBLIC_KEY=TEST-...
|
||||
|
||||
# Dominio
|
||||
DOMAIN=skeen.mx
|
||||
WACRM_SUBDOMAIN=crm.skeen.mx
|
||||
ODOO_SUBDOMAIN=erp.skeen.mx
|
||||
APP_SUBDOMAIN=app.skeen.mx
|
||||
HERMES_SUBDOMAIN=hermes.skeen.mx
|
||||
```
|
||||
1099
hermes/docs/ROADMAP_SKEEN_HERMES_APPENDIX.md
Normal file
1099
hermes/docs/ROADMAP_SKEEN_HERMES_APPENDIX.md
Normal file
File diff suppressed because it is too large
Load Diff
1858
hermes/docs/ROADMAP_SKEEN_HERMES_COMPLETE.md
Normal file
1858
hermes/docs/ROADMAP_SKEEN_HERMES_COMPLETE.md
Normal file
File diff suppressed because it is too large
Load Diff
159
hermes/hermes/.hermes/config.yaml
Normal file
159
hermes/hermes/.hermes/config.yaml
Normal file
@@ -0,0 +1,159 @@
|
||||
# Configuracion principal de Hermes para SKEEN Derma Experts
|
||||
# Guardar en: ~/.hermes/config.yaml
|
||||
|
||||
# ── Perfil ──────────────────────────────────────────────────────
|
||||
profile: skeen
|
||||
|
||||
# ── Modelo y Providers ─────────────────────────────────────────
|
||||
model:
|
||||
provider: openai
|
||||
primary: gpt-4o
|
||||
fallbacks:
|
||||
- anthropic:claude-3-5-sonnet
|
||||
- openrouter:openai/gpt-4o
|
||||
|
||||
# Pools de credenciales (rotacion automatica)
|
||||
credentials:
|
||||
openai:
|
||||
- key: ${OPENAI_API_KEY}
|
||||
label: primary
|
||||
- key: ${OPENAI_API_KEY_BACKUP}
|
||||
label: backup
|
||||
anthropic:
|
||||
- key: ${ANTHROPIC_API_KEY}
|
||||
label: primary
|
||||
|
||||
# ── Gateway ──────────────────────────────────────────────────────
|
||||
gateway:
|
||||
enabled: true
|
||||
port: 8080
|
||||
bind: 0.0.0.0
|
||||
|
||||
# Plataformas habilitadas (WACRM es el canal principal)
|
||||
platforms:
|
||||
webhook:
|
||||
enabled: true
|
||||
path: /webhook/wacrm
|
||||
secret: ${WACRM_WEBHOOK_SECRET}
|
||||
|
||||
# WhatsApp directo via Baileys (opcional, fallback)
|
||||
whatsapp:
|
||||
enabled: false # Usamos WACRM como gateway
|
||||
|
||||
# API Server OpenAI-compatible
|
||||
api_server:
|
||||
enabled: true
|
||||
port: 8080
|
||||
cors_origins:
|
||||
- https://crm.skeen.mx
|
||||
- https://app.skeen.mx
|
||||
|
||||
# ── Cron ─────────────────────────────────────────────────────────
|
||||
cron:
|
||||
enabled: true
|
||||
jobs:
|
||||
# Recordatorios de citas (24h antes)
|
||||
- name: recordatorios-citas
|
||||
schedule: "0 18 * * *" # 6 PM diario
|
||||
command: "skill skeen-recordatorios --action send"
|
||||
|
||||
# Generar recordatorios nuevos
|
||||
- name: generar-recordatorios
|
||||
schedule: "0 */6 * * *" # Cada 6 horas
|
||||
command: "skill skeen-recordatorios --action generate"
|
||||
|
||||
# Follow-ups post-tratamiento
|
||||
- name: followups-semanal
|
||||
schedule: "0 10 * * 1" # Lunes 10 AM
|
||||
command: "skill skeen-recordatorios --action followup"
|
||||
|
||||
# Re-engagement pacientes inactivos
|
||||
- name: reengagement-semanal
|
||||
schedule: "0 9 * * 1" # Lunes 9 AM
|
||||
command: "skill skeen-recordatorios --action reengage"
|
||||
|
||||
# ── Memoria ──────────────────────────────────────────────────────
|
||||
memory:
|
||||
provider: sqlite # built-in
|
||||
sqlite_path: ~/.hermes/workspace/memory.db
|
||||
|
||||
# Archivos de memoria persistente
|
||||
files:
|
||||
- ~/.hermes/workspace/MEMORY.md
|
||||
- ~/.hermes/workspace/SOUL.md
|
||||
- ~/.hermes/workspace/AGENTS.md
|
||||
|
||||
# ── Plugins ────────────────────────────────────────────────────
|
||||
plugins:
|
||||
enabled: true
|
||||
paths:
|
||||
- ~/.hermes/plugins
|
||||
|
||||
# Plugins custom de SKEEN
|
||||
custom:
|
||||
- name: odoo_api_call
|
||||
file: odoo_api_call.py
|
||||
enabled: true
|
||||
- name: wacrm_send
|
||||
file: wacrm_send.py
|
||||
enabled: true
|
||||
|
||||
# ── Skills ───────────────────────────────────────────────────────
|
||||
skills:
|
||||
enabled: true
|
||||
paths:
|
||||
- ~/.hermes/skills
|
||||
- /skills # Docker volume
|
||||
|
||||
# Skills de SKEEN
|
||||
active:
|
||||
- skeen-rag
|
||||
- skeen-agendar
|
||||
- skeen-pagos
|
||||
- skeen-monedero
|
||||
- skeen-recordatorios
|
||||
|
||||
# ── Tools ────────────────────────────────────────────────────────
|
||||
tools:
|
||||
allowed:
|
||||
- web_search
|
||||
- web_fetch
|
||||
- exec
|
||||
- shell
|
||||
- file_read
|
||||
- file_write
|
||||
- browser
|
||||
|
||||
# Overrides de tools built-in
|
||||
overrides:
|
||||
- tool: exec
|
||||
sandbox: docker
|
||||
|
||||
# ── Seguridad ────────────────────────────────────────────────────
|
||||
security:
|
||||
redact_secrets: true
|
||||
redact_pii: true
|
||||
|
||||
# Aprobaciones
|
||||
approvals:
|
||||
mode: smart # smart, always, never
|
||||
remember_duration: 3600 # segundos
|
||||
|
||||
# ── Logging ─────────────────────────────────────────────────────
|
||||
logging:
|
||||
level: info
|
||||
format: json
|
||||
file: ~/.hermes/logs/hermes.log
|
||||
|
||||
# ── Compresion de Contexto ──────────────────────────────────────
|
||||
compression:
|
||||
enabled: true
|
||||
threshold: 0.5 # 50% de la ventana de contexto
|
||||
protect_last_n: 20 # Proteger ultimos 20 mensajes
|
||||
|
||||
# ── Variables de Entorno ─────────────────────────────────────────
|
||||
# Estas se sobreescriben con variables de entorno del sistema
|
||||
env:
|
||||
ODOO_URL: http://odoo:8069
|
||||
ODOO_DB: skeen
|
||||
WACRM_URL: http://wacrm:3000
|
||||
108
hermes/hermes/.hermes/plugins/odoo_api_call.py
Normal file
108
hermes/hermes/.hermes/plugins/odoo_api_call.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import xmlrpc.client
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
ODOO_URL = os.environ.get("ODOO_URL", "http://odoo:8069")
|
||||
ODOO_DB = os.environ.get("ODOO_DB", "skeen")
|
||||
ODOO_API_KEY = os.environ.get("ODOO_API_KEY")
|
||||
|
||||
|
||||
def odoo_api_call(model: str, method: str, args: List = None, kwargs: Dict = None) -> Any:
|
||||
"""
|
||||
Plugin de Hermes para comunicarse con Odoo via XML-RPC.
|
||||
|
||||
Uso desde skills:
|
||||
odoo_api_call(model="salon.booking", method="get_available_slots",
|
||||
args=[service_id, date_from, date_to])
|
||||
|
||||
Args:
|
||||
model: Nombre del modelo Odoo (ej: 'res.partner', 'salon.booking')
|
||||
method: Metodo a ejecutar (ej: 'search_read', 'create', 'write')
|
||||
args: Lista de argumentos posicionales
|
||||
kwargs: Diccionario de argumentos nombrados
|
||||
|
||||
Returns:
|
||||
Resultado de la llamada XML-RPC
|
||||
"""
|
||||
if args is None:
|
||||
args = []
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
if not ODOO_API_KEY:
|
||||
raise ValueError("ODOO_API_KEY no configurada en variables de entorno")
|
||||
|
||||
# Autenticacion
|
||||
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
|
||||
uid = common.authenticate(ODOO_DB, 1, ODOO_API_KEY, {})
|
||||
|
||||
if not uid:
|
||||
raise ValueError("Autenticacion con Odoo fallida")
|
||||
|
||||
# Ejecutar metodo
|
||||
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_API_KEY,
|
||||
model, method, args, kwargs
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def odoo_search_read(model: str, domain: List = None, fields: List[str] = None,
|
||||
limit: int = 100) -> List[Dict]:
|
||||
"""
|
||||
Atajo para search_read en Odoo.
|
||||
|
||||
Uso:
|
||||
odoo_search_read(model="res.partner",
|
||||
domain=[["phone", "=", "+526641234567"]],
|
||||
fields=["name", "email", "phone"])
|
||||
"""
|
||||
if domain is None:
|
||||
domain = []
|
||||
if fields is None:
|
||||
fields = []
|
||||
|
||||
return odoo_api_call(
|
||||
model=model,
|
||||
method="search_read",
|
||||
args=[domain, fields],
|
||||
kwargs={"limit": limit}
|
||||
)
|
||||
|
||||
|
||||
def odoo_create(model: str, values: Dict) -> int:
|
||||
"""
|
||||
Atajo para create en Odoo.
|
||||
|
||||
Uso:
|
||||
odoo_create(model="res.partner",
|
||||
values={"name": "Sofia Garcia", "phone": "+526641234567"})
|
||||
"""
|
||||
return odoo_api_call(model=model, method="create", args=[values])
|
||||
|
||||
|
||||
def odoo_write(model: str, ids: List[int], values: Dict) -> bool:
|
||||
"""
|
||||
Atajo para write en Odoo.
|
||||
|
||||
Uso:
|
||||
odoo_write(model="salon.booking", ids=[123],
|
||||
values={"state": "cancelled"})
|
||||
"""
|
||||
return odoo_api_call(model=model, method="write", args=[ids, values])
|
||||
|
||||
|
||||
# Registro del plugin para Hermes
|
||||
PLUGIN_NAME = "odoo_api_call"
|
||||
PLUGIN_VERSION = "1.0.0"
|
||||
PLUGIN_DESCRIPTION = "Comunicacion con Odoo ERP via XML-RPC"
|
||||
|
||||
# Herramientas expuestas (para hermes tools)
|
||||
TOOLS = {
|
||||
"odoo_api_call": odoo_api_call,
|
||||
"odoo_search_read": odoo_search_read,
|
||||
"odoo_create": odoo_create,
|
||||
"odoo_write": odoo_write,
|
||||
}
|
||||
102
hermes/hermes/.hermes/plugins/wacrm_send.py
Normal file
102
hermes/hermes/.hermes/plugins/wacrm_send.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
WACRM_URL = os.environ.get("WACRM_URL", "http://wacrm:3000")
|
||||
WACRM_API_KEY = os.environ.get("WACRM_API_KEY")
|
||||
|
||||
|
||||
def wacrm_send(to: str, type: str = "text", text: str = None,
|
||||
template: Dict = None, media_url: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Plugin de Hermes para enviar mensajes via WACRM API.
|
||||
|
||||
Uso desde skills:
|
||||
wacrm_send(to="+526641234567", type="text",
|
||||
text="Hola! Tu cita esta confirmada")
|
||||
|
||||
Args:
|
||||
to: Numero de telefono destino (formato E.164: +52664...)
|
||||
type: Tipo de mensaje: 'text', 'template', 'image', 'document'
|
||||
text: Contenido del mensaje (para type='text')
|
||||
template: Dict con {name, language, params} (para type='template')
|
||||
media_url: URL del archivo (para type='image' o 'document')
|
||||
|
||||
Returns:
|
||||
Dict con la respuesta de WACRM API
|
||||
"""
|
||||
if not WACRM_API_KEY:
|
||||
raise ValueError("WACRM_API_KEY no configurada en variables de entorno")
|
||||
|
||||
payload = {"to": to, "type": type}
|
||||
|
||||
if type == "text" and text:
|
||||
payload["text"] = text
|
||||
elif type == "template" and template:
|
||||
payload["template"] = template
|
||||
elif type in ("image", "document") and media_url:
|
||||
payload["media_url"] = media_url
|
||||
|
||||
response = requests.post(
|
||||
f"{WACRM_URL}/api/v1/messages",
|
||||
headers={
|
||||
"Authorization": f"Bearer {WACRM_API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json=payload,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
error_data = response.json() if response.content else {}
|
||||
raise Exception(f"WACRM send failed: {error_data.get('error', {}).get('message', response.status_text)}")
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def wacrm_send_template(to: str, template_name: str, language: str = "es_MX",
|
||||
params: list = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Atajo para enviar templates de WhatsApp aprobados.
|
||||
|
||||
Uso:
|
||||
wacrm_send_template(to="+526641234567",
|
||||
template_name="recordatorio_cita_skeen",
|
||||
params=["Sofia", "5 de julio", "10:00 AM"])
|
||||
"""
|
||||
if params is None:
|
||||
params = []
|
||||
|
||||
return wacrm_send(
|
||||
to=to,
|
||||
type="template",
|
||||
template={
|
||||
"name": template_name,
|
||||
"language": language,
|
||||
"params": params
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def wacrm_send_text(to: str, text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Atajo para enviar mensaje de texto simple.
|
||||
|
||||
Uso:
|
||||
wacrm_send_text(to="+526641234567", text="Hola! Como estas?")
|
||||
"""
|
||||
return wacrm_send(to=to, type="text", text=text)
|
||||
|
||||
|
||||
# Registro del plugin para Hermes
|
||||
PLUGIN_NAME = "wacrm_send"
|
||||
PLUGIN_VERSION = "1.0.0"
|
||||
PLUGIN_DESCRIPTION = "Envio de mensajes WhatsApp via WACRM API"
|
||||
|
||||
# Herramientas expuestas
|
||||
TOOLS = {
|
||||
"wacrm_send": wacrm_send,
|
||||
"wacrm_send_template": wacrm_send_template,
|
||||
"wacrm_send_text": wacrm_send_text,
|
||||
}
|
||||
60
hermes/hermes/.hermes/workspace/AGENTS.md
Normal file
60
hermes/hermes/.hermes/workspace/AGENTS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Instrucciones Operativas — SKEEN
|
||||
|
||||
## Flujos de trabajo
|
||||
|
||||
1. **Agendar Cita:** Cargar skill `skeen-agendar` → Odoo slots → WACRM confirm → Programar cron recordatorio
|
||||
2. **Consultar Catalogo:** Cargar skill `skeen-rag` → Catalogo servicios → Ofrecer valoracion
|
||||
3. **Procesar Pago:** Cargar skill `skeen-pagos` → Odoo saldo → Stripe/MP link → Confirmar
|
||||
4. **Consultar Monedero:** Cargar skill `skeen-monedero` → Odoo balance → Historial
|
||||
5. **Recordatorios (Cron):** `skeen-recordatorios` ejecuta via `hermes cron` cada 6h/18h
|
||||
6. **Follow-ups:** `skeen-recordatorios` —action followup cada lunes 10 AM
|
||||
7. **Re-engagement:** `skeen-recordatorios` —action reengage cada lunes 9 AM
|
||||
|
||||
## Reglas de seguridad
|
||||
|
||||
- NUNCA compartir API keys en mensajes
|
||||
- NUNCA ejecutar comandos destructivos en produccion
|
||||
- SIEMPRE verificar identidad del paciente por telefono
|
||||
- Limitar redencion de puntos a 50% del total
|
||||
- Validar todos los inputs antes de ejecutar en Odoo
|
||||
|
||||
## Comandos de Hermes utiles
|
||||
|
||||
```bash
|
||||
# Ver estado del gateway
|
||||
hermes gateway status
|
||||
|
||||
# Ver logs
|
||||
hermes logs --follow
|
||||
|
||||
# Diagnosticar configuracion
|
||||
hermes doctor
|
||||
|
||||
# Listar cron jobs activos
|
||||
hermes cron list
|
||||
|
||||
# Ejecutar skill manualmente
|
||||
hermes skill skeen-agendar --phone +526641234567
|
||||
|
||||
# Ver memoria
|
||||
hermes memory read
|
||||
|
||||
# Comprimir contexto
|
||||
hermes compress
|
||||
|
||||
# Cambiar modelo
|
||||
hermes model
|
||||
|
||||
# Actualizar
|
||||
hermes update
|
||||
```
|
||||
|
||||
## Integraciones
|
||||
|
||||
| Sistema | Protocolo | Endpoint | Auth |
|
||||
|---------|-----------|----------|------|
|
||||
| WACRM | HTTPS REST | http://wacrm:3000/api/v1 | Bearer API Key |
|
||||
| Odoo | XML-RPC | http://odoo:8069/xmlrpc/2 | API Key |
|
||||
| Stripe | HTTPS REST | https://api.stripe.com | Secret Key |
|
||||
| MercadoPago | HTTPS REST | https://api.mercadopago.com | Access Token |
|
||||
| Meta API | HTTPS REST | https://graph.facebook.com/v18.0 | Access Token |
|
||||
45
hermes/hermes/.hermes/workspace/SOUL.md
Normal file
45
hermes/hermes/.hermes/workspace/SOUL.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Sofia — Asistente Virtual de SKEEN Derma Experts
|
||||
|
||||
## Identidad
|
||||
|
||||
- **Nombre:** Sofia
|
||||
- **Rol:** Asistente virtual de SKEEN Derma Experts
|
||||
- **Personalidad:** Calida, profesional, empatica, eficiente
|
||||
- **Idioma:** Espanol (Mexico)
|
||||
- **Emoji favorito:** 💙✨🌸
|
||||
|
||||
## Tonos de voz
|
||||
|
||||
- **Saludo:** "Hola! Soy Sofia de SKEEN 💙"
|
||||
- **Confirmacion:** "Perfecto! Tu cita esta confirmada ✨"
|
||||
- **Empatia:** "Entiendo, estamos aqui para cuidar tu piel 🌸"
|
||||
- **Urgencia:** "Voy a conectarte con un especialista de inmediato"
|
||||
|
||||
## Limites
|
||||
|
||||
- NO diagnosticar condiciones medicas
|
||||
- NO recomendar medicamentos especificos
|
||||
- SIEMPRE sugerir consulta presencial para evaluaciones
|
||||
- Max 3 parrafos por mensaje de WhatsApp
|
||||
- Max 2 emojis por mensaje
|
||||
|
||||
## Datos de la Clinica
|
||||
|
||||
- **Nombre:** SKEEN Derma Experts, Playas de Rosarito, BC
|
||||
- **Horario:** Lun-Vie 9:00-18:00, Sab 9:00-14:00
|
||||
- **Direccion:** Blvd. Popotla 123, Playas de Rosarito
|
||||
- **Telefono:** +52 (664) 123-4567
|
||||
|
||||
## Handoff a Humano
|
||||
|
||||
**Trigger:** "AGENTE", "HUMANO", "ESPECIALISTA", "DOCTOR"
|
||||
|
||||
**Respuesta:** "Te conecto con un especialista enseguida. Espera un momento 💙"
|
||||
|
||||
## Contexto de Hermes
|
||||
|
||||
- Sofia es el agente principal del perfil `skeen` en Hermes
|
||||
- Usa el modelo `gpt-4o` como primario, con fallback a Claude 3.5 Sonnet
|
||||
- Su memoria persiste en SQLite + archivos MEMORY.md
|
||||
- Puede delegar tareas a subagentes para tareas complejas
|
||||
- Se comunica con pacientes via WACRM (WhatsApp)
|
||||
33
hermes/hermes/Dockerfile
Normal file
33
hermes/hermes/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# Dockerfile para Hermes (Python)
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias del sistema
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instalar Hermes
|
||||
RUN pip install --no-cache-dir "hermes-agent[messaging,cron,web]"
|
||||
|
||||
# Instalar dependencias adicionales
|
||||
RUN pip install --no-cache-dir \
|
||||
flask \
|
||||
requests \
|
||||
xmlrpc.client
|
||||
|
||||
# Copiar plugins y skills
|
||||
COPY hermes/plugins/ /root/.hermes/plugins/
|
||||
COPY hermes/skills/ /skills/
|
||||
COPY hermes/workspace/ /root/.hermes/workspace/
|
||||
COPY hermes/config.yaml /root/.hermes/config.yaml
|
||||
COPY hermes/webhooks/ /app/webhooks/
|
||||
|
||||
# Exponer puertos
|
||||
EXPOSE 8080
|
||||
|
||||
# Comando de inicio
|
||||
CMD ["hermes", "gateway", "start"]
|
||||
159
hermes/hermes/config.yaml
Normal file
159
hermes/hermes/config.yaml
Normal file
@@ -0,0 +1,159 @@
|
||||
# Configuracion principal de Hermes para SKEEN Derma Experts
|
||||
# Guardar en: ~/.hermes/config.yaml
|
||||
|
||||
# ── Perfil ──────────────────────────────────────────────────────
|
||||
profile: skeen
|
||||
|
||||
# ── Modelo y Providers ─────────────────────────────────────────
|
||||
model:
|
||||
provider: openai
|
||||
primary: gpt-4o
|
||||
fallbacks:
|
||||
- anthropic:claude-3-5-sonnet
|
||||
- openrouter:openai/gpt-4o
|
||||
|
||||
# Pools de credenciales (rotacion automatica)
|
||||
credentials:
|
||||
openai:
|
||||
- key: ${OPENAI_API_KEY}
|
||||
label: primary
|
||||
- key: ${OPENAI_API_KEY_BACKUP}
|
||||
label: backup
|
||||
anthropic:
|
||||
- key: ${ANTHROPIC_API_KEY}
|
||||
label: primary
|
||||
|
||||
# ── Gateway ──────────────────────────────────────────────────────
|
||||
gateway:
|
||||
enabled: true
|
||||
port: 8080
|
||||
bind: 0.0.0.0
|
||||
|
||||
# Plataformas habilitadas (WACRM es el canal principal)
|
||||
platforms:
|
||||
webhook:
|
||||
enabled: true
|
||||
path: /webhook/wacrm
|
||||
secret: ${WACRM_WEBHOOK_SECRET}
|
||||
|
||||
# WhatsApp directo via Baileys (opcional, fallback)
|
||||
whatsapp:
|
||||
enabled: false # Usamos WACRM como gateway
|
||||
|
||||
# API Server OpenAI-compatible
|
||||
api_server:
|
||||
enabled: true
|
||||
port: 8080
|
||||
cors_origins:
|
||||
- https://crm.skeen.mx
|
||||
- https://app.skeen.mx
|
||||
|
||||
# ── Cron ─────────────────────────────────────────────────────────
|
||||
cron:
|
||||
enabled: true
|
||||
jobs:
|
||||
# Recordatorios de citas (24h antes)
|
||||
- name: recordatorios-citas
|
||||
schedule: "0 18 * * *" # 6 PM diario
|
||||
command: "skill skeen-recordatorios --action send"
|
||||
|
||||
# Generar recordatorios nuevos
|
||||
- name: generar-recordatorios
|
||||
schedule: "0 */6 * * *" # Cada 6 horas
|
||||
command: "skill skeen-recordatorios --action generate"
|
||||
|
||||
# Follow-ups post-tratamiento
|
||||
- name: followups-semanal
|
||||
schedule: "0 10 * * 1" # Lunes 10 AM
|
||||
command: "skill skeen-recordatorios --action followup"
|
||||
|
||||
# Re-engagement pacientes inactivos
|
||||
- name: reengagement-semanal
|
||||
schedule: "0 9 * * 1" # Lunes 9 AM
|
||||
command: "skill skeen-recordatorios --action reengage"
|
||||
|
||||
# ── Memoria ──────────────────────────────────────────────────────
|
||||
memory:
|
||||
provider: sqlite # built-in
|
||||
sqlite_path: ~/.hermes/workspace/memory.db
|
||||
|
||||
# Archivos de memoria persistente
|
||||
files:
|
||||
- ~/.hermes/workspace/MEMORY.md
|
||||
- ~/.hermes/workspace/SOUL.md
|
||||
- ~/.hermes/workspace/AGENTS.md
|
||||
|
||||
# ── Plugins ────────────────────────────────────────────────────
|
||||
plugins:
|
||||
enabled: true
|
||||
paths:
|
||||
- ~/.hermes/plugins
|
||||
|
||||
# Plugins custom de SKEEN
|
||||
custom:
|
||||
- name: odoo_api_call
|
||||
file: odoo_api_call.py
|
||||
enabled: true
|
||||
- name: wacrm_send
|
||||
file: wacrm_send.py
|
||||
enabled: true
|
||||
|
||||
# ── Skills ───────────────────────────────────────────────────────
|
||||
skills:
|
||||
enabled: true
|
||||
paths:
|
||||
- ~/.hermes/skills
|
||||
- /skills # Docker volume
|
||||
|
||||
# Skills de SKEEN
|
||||
active:
|
||||
- skeen-rag
|
||||
- skeen-agendar
|
||||
- skeen-pagos
|
||||
- skeen-monedero
|
||||
- skeen-recordatorios
|
||||
|
||||
# ── Tools ────────────────────────────────────────────────────────
|
||||
tools:
|
||||
allowed:
|
||||
- web_search
|
||||
- web_fetch
|
||||
- exec
|
||||
- shell
|
||||
- file_read
|
||||
- file_write
|
||||
- browser
|
||||
|
||||
# Overrides de tools built-in
|
||||
overrides:
|
||||
- tool: exec
|
||||
sandbox: docker
|
||||
|
||||
# ── Seguridad ────────────────────────────────────────────────────
|
||||
security:
|
||||
redact_secrets: true
|
||||
redact_pii: true
|
||||
|
||||
# Aprobaciones
|
||||
approvals:
|
||||
mode: smart # smart, always, never
|
||||
remember_duration: 3600 # segundos
|
||||
|
||||
# ── Logging ─────────────────────────────────────────────────────
|
||||
logging:
|
||||
level: info
|
||||
format: json
|
||||
file: ~/.hermes/logs/hermes.log
|
||||
|
||||
# ── Compresion de Contexto ──────────────────────────────────────
|
||||
compression:
|
||||
enabled: true
|
||||
threshold: 0.5 # 50% de la ventana de contexto
|
||||
protect_last_n: 20 # Proteger ultimos 20 mensajes
|
||||
|
||||
# ── Variables de Entorno ─────────────────────────────────────────
|
||||
# Estas se sobreescriben con variables de entorno del sistema
|
||||
env:
|
||||
ODOO_URL: http://odoo:8069
|
||||
ODOO_DB: skeen
|
||||
WACRM_URL: http://wacrm:3000
|
||||
112
hermes/hermes/memory/_5216642170990.json
Normal file
112
hermes/hermes/memory/_5216642170990.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"phone": "+5216642170990",
|
||||
"context": {},
|
||||
"history": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "me gustaria agendar",
|
||||
"ts": "2026-07-10T07:27:02.435Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Claro, con gusto te ayudo a agendar. ¿Qué servicio necesitas?\n\n• Consulta Dermatologica General — $800 MXN (30 min)\n• Tratamiento Facial para Acne — $1500 MXN (60 min)\n• Peel Quimico — $1500 MXN (30 min)\n• Depilacion Laser — $500 MXN (15 min)\n• Tratamiento Anti-Edad — $2500 MXN (75 min)\n\nTambién puedes pedir una **consulta de valoración gratuita** (15 min).",
|
||||
"ts": "2026-07-10T07:27:03.426Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Consulta Dermatologica General",
|
||||
"ts": "2026-07-10T07:27:15.760Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Perfecto, elegiste **Consulta Dermatologica General** (30 min).\n\n¿Qué día te gustaría agendar? Puedes escribir la fecha como \"mañana\", \"viernes\", o \"2026-07-15\".",
|
||||
"ts": "2026-07-10T07:27:16.602Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "el martes que viene",
|
||||
"ts": "2026-07-10T07:27:28.665Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "hola",
|
||||
"ts": "2026-07-10T07:45:21.611Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "¡Hola! 👋 ¿Cómo estás?\n\nPara continuar con tu cita de **Consulta Dermatológica General** el **martes que viene**, ¿a qué hora te gustaría asistir? Tenemos disponibilidad de 9:00 a 18:00 de lunes a viernes.\n\nQuedo atenta a tu respuesta para confirmar los detalles. 😊",
|
||||
"ts": "2026-07-10T07:45:27.127Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "a las 9 esta bien",
|
||||
"ts": "2026-07-10T07:45:47.333Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Claro, con gusto te ayudo a agendar. ¿Qué servicio necesitas?\n\n• ANESTESIOLOGA NO SHOW — $2000 MXN (30 min)\n• ANESTESIOLOGA SESION — $4000 MXN (30 min)\n• ANTICIPO CREMA HORMONAL — $1000 MXN (30 min)\n• Acné 1ra vez — $900 MXN (30 min)\n• Asesoria skincare — $600 MXN (30 min)\n• BIOESTIMULACION ADN SALMON — $3500 MXN (30 min)\n\nSi no sabes cuál elegir, agenda una **consulta** y te orientamos.",
|
||||
"ts": "2026-07-10T07:45:50.146Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "*consulta*",
|
||||
"ts": "2026-07-10T07:46:07.555Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Perfecto, elegiste **Consulta** (30 min).\n\n¿Qué día te gustaría agendar? Puedes escribir la fecha como \"mañana\", \"viernes\", o \"2026-07-15\".",
|
||||
"ts": "2026-07-10T07:46:08.469Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "el martes que viene",
|
||||
"ts": "2026-07-10T07:46:17.501Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Estos son los horarios disponibles para **Consulta** el **martes, 14 de julio de 2026**:\n\n1. 9:00 AM\n2. 9:30 AM\n3. 10:00 AM\n4. 10:30 AM\n5. 11:00 AM\n\nEscribe el número o la hora que prefieras.",
|
||||
"ts": "2026-07-10T07:46:18.513Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "a las 9 am esta bien",
|
||||
"ts": "2026-07-10T07:46:29.373Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "No entendí el horario. Por favor elige una opción de la lista (escribe el número o la hora, ej. 10:00). Puedes escribir \"cancelar\" para salir.",
|
||||
"ts": "2026-07-10T07:46:30.394Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "1. 9:00 AM",
|
||||
"ts": "2026-07-10T07:46:41.467Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Muy bien, horario seleccionado: **9:00 AM**.\n\n¿A qué nombre registramos la cita?",
|
||||
"ts": "2026-07-10T07:46:42.537Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Ivan Alcaraz",
|
||||
"ts": "2026-07-10T07:46:51.451Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Confirmemos tu cita:\n\n• Servicio: Consulta\n• Fecha: martes, 14 de julio de 2026\n• Hora: 9:00 AM\n• Paciente: Ivan Alcaraz\n• Teléfono: +5216642170990\n\n¿Confirmamos? (sí/no)",
|
||||
"ts": "2026-07-10T07:46:52.253Z"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Si",
|
||||
"ts": "2026-07-10T07:46:58.143Z"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "¡Perfecto! Tu cita está confirmada ✨\n\n• Servicio: Consulta\n• Fecha: martes, 14 de julio de 2026\n• Hora: 9:00 AM\n• Referencia: CITA-001\n\nDirección: Blvd. Popotla 123, Playas de Rosarito, BC, México\n\nTe enviaremos un recordatorio un día antes. Si necesitas cambiar o cancelar, solo escríbenos.",
|
||||
"ts": "2026-07-10T07:46:59.258Z"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-07-10T07:26:38.589Z"
|
||||
}
|
||||
52
hermes/hermes/memory/reminders_sent.json
Normal file
52
hermes/hermes/memory/reminders_sent.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"reengagement:526618504866:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611121459:2026-07-09": "2026-07-09",
|
||||
"reengagement:523312688280:2026-07-09": "2026-07-09",
|
||||
"reengagement:526645029513:2026-07-09": "2026-07-09",
|
||||
"reengagement:522094930605:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611077906:2026-07-09": "2026-07-09",
|
||||
"reengagement:526194197072:2026-07-09": "2026-07-09",
|
||||
"reengagement:526653164624:2026-07-09": "2026-07-09",
|
||||
"reengagement:526193955418:2026-07-09": "2026-07-09",
|
||||
"reengagement:526634404451:2026-07-09": "2026-07-09",
|
||||
"reengagement:526461164445:2026-07-09": "2026-07-09",
|
||||
"reengagement:526196225741:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611288180:2026-07-09": "2026-07-09",
|
||||
"reengagement:526642767839:2026-07-09": "2026-07-09",
|
||||
"reengagement:526197211181:2026-07-09": "2026-07-09",
|
||||
"reengagement:16197889299:2026-07-09": "2026-07-09",
|
||||
"reengagement:526643979843:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611167403:2026-07-09": "2026-07-09",
|
||||
"reengagement:526192745622:2026-07-09": "2026-07-09",
|
||||
"reengagement:526197480886:2026-07-09": "2026-07-09",
|
||||
"reengagement:16197699175:2026-07-09": "2026-07-09",
|
||||
"reengagement:526643757327:2026-07-09": "2026-07-09",
|
||||
"reengagement:526618501119:2026-07-09": "2026-07-09",
|
||||
"reengagement:526642324280:2026-07-09": "2026-07-09",
|
||||
"reengagement:526198794022:2026-07-09": "2026-07-09",
|
||||
"reengagement:526641798892:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611035281:2026-07-09": "2026-07-09",
|
||||
"reengagement:526648298490:2026-07-09": "2026-07-09",
|
||||
"reengagement:526641084869:2026-07-09": "2026-07-09",
|
||||
"reengagement:526643423016:2026-07-09": "2026-07-09",
|
||||
"reengagement:526612426301:2026-07-09": "2026-07-09",
|
||||
"reengagement:526631278823:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611269713:2026-07-09": "2026-07-09",
|
||||
"reengagement:527143128702:2026-07-09": "2026-07-09",
|
||||
"reengagement:526192105498:2026-07-09": "2026-07-09",
|
||||
"reengagement:526645612760:2026-07-09": "2026-07-09",
|
||||
"reengagement:526642947763:2026-07-09": "2026-07-09",
|
||||
"reengagement:522431306950:2026-07-09": "2026-07-09",
|
||||
"reengagement:526642572433:2026-07-09": "2026-07-09",
|
||||
"reengagement:526646574771:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611100452:2026-07-09": "2026-07-09",
|
||||
"reengagement:526642918451:2026-07-09": "2026-07-09",
|
||||
"reengagement:526611072980:2026-07-09": "2026-07-09",
|
||||
"reengagement:526644063990:2026-07-09": "2026-07-09",
|
||||
"reengagement:526642517280:2026-07-09": "2026-07-09",
|
||||
"reengagement:526612814494:2026-07-09": "2026-07-09",
|
||||
"reengagement:526647339185:2026-07-09": "2026-07-09",
|
||||
"reengagement:526861169131:2026-07-09": "2026-07-09",
|
||||
"reengagement:526616115762:2026-07-09": "2026-07-09",
|
||||
"reengagement:526641368921:2026-07-09": "2026-07-09"
|
||||
}
|
||||
905
hermes/hermes/package-lock.json
generated
Normal file
905
hermes/hermes/package-lock.json
generated
Normal file
@@ -0,0 +1,905 @@
|
||||
{
|
||||
"name": "hermes",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hermes",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/content-type": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.0.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/content-type": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
hermes/hermes/package.json
Normal file
16
hermes/hermes/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "hermes",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "webhook-bridge.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
}
|
||||
108
hermes/hermes/plugins/odoo_api_call.py
Normal file
108
hermes/hermes/plugins/odoo_api_call.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import xmlrpc.client
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
ODOO_URL = os.environ.get("ODOO_URL", "http://odoo:8069")
|
||||
ODOO_DB = os.environ.get("ODOO_DB", "skeen")
|
||||
ODOO_API_KEY = os.environ.get("ODOO_API_KEY")
|
||||
|
||||
|
||||
def odoo_api_call(model: str, method: str, args: List = None, kwargs: Dict = None) -> Any:
|
||||
"""
|
||||
Plugin de Hermes para comunicarse con Odoo via XML-RPC.
|
||||
|
||||
Uso desde skills:
|
||||
odoo_api_call(model="salon.booking", method="get_available_slots",
|
||||
args=[service_id, date_from, date_to])
|
||||
|
||||
Args:
|
||||
model: Nombre del modelo Odoo (ej: 'res.partner', 'salon.booking')
|
||||
method: Metodo a ejecutar (ej: 'search_read', 'create', 'write')
|
||||
args: Lista de argumentos posicionales
|
||||
kwargs: Diccionario de argumentos nombrados
|
||||
|
||||
Returns:
|
||||
Resultado de la llamada XML-RPC
|
||||
"""
|
||||
if args is None:
|
||||
args = []
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
if not ODOO_API_KEY:
|
||||
raise ValueError("ODOO_API_KEY no configurada en variables de entorno")
|
||||
|
||||
# Autenticacion
|
||||
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
|
||||
uid = common.authenticate(ODOO_DB, 1, ODOO_API_KEY, {})
|
||||
|
||||
if not uid:
|
||||
raise ValueError("Autenticacion con Odoo fallida")
|
||||
|
||||
# Ejecutar metodo
|
||||
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
|
||||
result = models.execute_kw(
|
||||
ODOO_DB, uid, ODOO_API_KEY,
|
||||
model, method, args, kwargs
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def odoo_search_read(model: str, domain: List = None, fields: List[str] = None,
|
||||
limit: int = 100) -> List[Dict]:
|
||||
"""
|
||||
Atajo para search_read en Odoo.
|
||||
|
||||
Uso:
|
||||
odoo_search_read(model="res.partner",
|
||||
domain=[["phone", "=", "+526641234567"]],
|
||||
fields=["name", "email", "phone"])
|
||||
"""
|
||||
if domain is None:
|
||||
domain = []
|
||||
if fields is None:
|
||||
fields = []
|
||||
|
||||
return odoo_api_call(
|
||||
model=model,
|
||||
method="search_read",
|
||||
args=[domain, fields],
|
||||
kwargs={"limit": limit}
|
||||
)
|
||||
|
||||
|
||||
def odoo_create(model: str, values: Dict) -> int:
|
||||
"""
|
||||
Atajo para create en Odoo.
|
||||
|
||||
Uso:
|
||||
odoo_create(model="res.partner",
|
||||
values={"name": "Sofia Garcia", "phone": "+526641234567"})
|
||||
"""
|
||||
return odoo_api_call(model=model, method="create", args=[values])
|
||||
|
||||
|
||||
def odoo_write(model: str, ids: List[int], values: Dict) -> bool:
|
||||
"""
|
||||
Atajo para write en Odoo.
|
||||
|
||||
Uso:
|
||||
odoo_write(model="salon.booking", ids=[123],
|
||||
values={"state": "cancelled"})
|
||||
"""
|
||||
return odoo_api_call(model=model, method="write", args=[ids, values])
|
||||
|
||||
|
||||
# Registro del plugin para Hermes
|
||||
PLUGIN_NAME = "odoo_api_call"
|
||||
PLUGIN_VERSION = "1.0.0"
|
||||
PLUGIN_DESCRIPTION = "Comunicacion con Odoo ERP via XML-RPC"
|
||||
|
||||
# Herramientas expuestas (para hermes tools)
|
||||
TOOLS = {
|
||||
"odoo_api_call": odoo_api_call,
|
||||
"odoo_search_read": odoo_search_read,
|
||||
"odoo_create": odoo_create,
|
||||
"odoo_write": odoo_write,
|
||||
}
|
||||
102
hermes/hermes/plugins/wacrm_send.py
Normal file
102
hermes/hermes/plugins/wacrm_send.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
WACRM_URL = os.environ.get("WACRM_URL", "http://wacrm:3000")
|
||||
WACRM_API_KEY = os.environ.get("WACRM_API_KEY")
|
||||
|
||||
|
||||
def wacrm_send(to: str, type: str = "text", text: str = None,
|
||||
template: Dict = None, media_url: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Plugin de Hermes para enviar mensajes via WACRM API.
|
||||
|
||||
Uso desde skills:
|
||||
wacrm_send(to="+526641234567", type="text",
|
||||
text="Hola! Tu cita esta confirmada")
|
||||
|
||||
Args:
|
||||
to: Numero de telefono destino (formato E.164: +52664...)
|
||||
type: Tipo de mensaje: 'text', 'template', 'image', 'document'
|
||||
text: Contenido del mensaje (para type='text')
|
||||
template: Dict con {name, language, params} (para type='template')
|
||||
media_url: URL del archivo (para type='image' o 'document')
|
||||
|
||||
Returns:
|
||||
Dict con la respuesta de WACRM API
|
||||
"""
|
||||
if not WACRM_API_KEY:
|
||||
raise ValueError("WACRM_API_KEY no configurada en variables de entorno")
|
||||
|
||||
payload = {"to": to, "type": type}
|
||||
|
||||
if type == "text" and text:
|
||||
payload["text"] = text
|
||||
elif type == "template" and template:
|
||||
payload["template"] = template
|
||||
elif type in ("image", "document") and media_url:
|
||||
payload["media_url"] = media_url
|
||||
|
||||
response = requests.post(
|
||||
f"{WACRM_URL}/api/v1/messages",
|
||||
headers={
|
||||
"Authorization": f"Bearer {WACRM_API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json=payload,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
error_data = response.json() if response.content else {}
|
||||
raise Exception(f"WACRM send failed: {error_data.get('error', {}).get('message', response.status_text)}")
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def wacrm_send_template(to: str, template_name: str, language: str = "es_MX",
|
||||
params: list = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Atajo para enviar templates de WhatsApp aprobados.
|
||||
|
||||
Uso:
|
||||
wacrm_send_template(to="+526641234567",
|
||||
template_name="recordatorio_cita_skeen",
|
||||
params=["Sofia", "5 de julio", "10:00 AM"])
|
||||
"""
|
||||
if params is None:
|
||||
params = []
|
||||
|
||||
return wacrm_send(
|
||||
to=to,
|
||||
type="template",
|
||||
template={
|
||||
"name": template_name,
|
||||
"language": language,
|
||||
"params": params
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def wacrm_send_text(to: str, text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Atajo para enviar mensaje de texto simple.
|
||||
|
||||
Uso:
|
||||
wacrm_send_text(to="+526641234567", text="Hola! Como estas?")
|
||||
"""
|
||||
return wacrm_send(to=to, type="text", text=text)
|
||||
|
||||
|
||||
# Registro del plugin para Hermes
|
||||
PLUGIN_NAME = "wacrm_send"
|
||||
PLUGIN_VERSION = "1.0.0"
|
||||
PLUGIN_DESCRIPTION = "Envio de mensajes WhatsApp via WACRM API"
|
||||
|
||||
# Herramientas expuestas
|
||||
TOOLS = {
|
||||
"wacrm_send": wacrm_send,
|
||||
"wacrm_send_template": wacrm_send_template,
|
||||
"wacrm_send_text": wacrm_send_text,
|
||||
}
|
||||
103
hermes/hermes/skills/skeen-agendar/SKILL.md
Normal file
103
hermes/hermes/skills/skeen-agendar/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: skeen-agendar
|
||||
description: >
|
||||
Agendamiento de citas para SKEEN Derma Experts.
|
||||
Consulta disponibilidad, crea citas, reagenda y cancela.
|
||||
Usar cuando el usuario quiera agendar, reagendar o cancelar una cita.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: [python]
|
||||
envVars:
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
- name: ODOO_DB
|
||||
required: true
|
||||
- name: ODOO_API_KEY
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["agendar", "cita", "hora", "reagendar", "cancelar", "disponibilidad"]
|
||||
---
|
||||
|
||||
# SKEEN — Agendamiento de Citas
|
||||
|
||||
## Instrucciones de Uso
|
||||
|
||||
### PASO 1: Identificar intencion
|
||||
|
||||
- "Quiero agendar" / "Necesito una cita" → NUEVA CITA
|
||||
- "Quiero cambiar mi cita" / "Reagendar" → REAGENDAR
|
||||
- "Cancelar mi cita" → CANCELAR
|
||||
- "Tengo una cita para manana?" → CONSULTAR
|
||||
|
||||
### PASO 2: Para NUEVA CITA
|
||||
|
||||
1. Pregunta que tipo de servicio desea (si no lo especifico):
|
||||
- Consulta dermatologica general ($800 MXN, 30 min)
|
||||
- Tratamiento facial ($1,200-$3,500 MXN, 45-90 min)
|
||||
- Depilacion laser (desde $500 MXN por zona, 15-60 min)
|
||||
- Peel quimico ($1,500 MXN, 30 min)
|
||||
- O consulta de valoracion gratuita (15 min)
|
||||
|
||||
2. Consulta disponibilidad usando el plugin `odoo_api_call`:
|
||||
Modelo: salon.booking, metodo: get_available_slots
|
||||
Argumentos: [service_id, date_from, date_to]
|
||||
|
||||
3. Presenta las 3 mejores opciones de horario
|
||||
|
||||
4. Cuando el paciente elige, confirma datos:
|
||||
- Nombre completo
|
||||
- Confirmar telefono
|
||||
- Alergias o condiciones especiales (opcional)
|
||||
|
||||
5. Crea la cita usando `odoo_api_call`:
|
||||
Modelo: salon.booking, metodo: create
|
||||
Datos: { name, phone, service_id, date, time, state: 'confirmed' }
|
||||
|
||||
6. Programa recordatorio via cron:
|
||||
```bash
|
||||
hermes cron schedule --name "recordatorio-cita-[ID]" \
|
||||
--at "[fecha_cita] 18:00 - 1 day" \
|
||||
--command "skill skeen-recordatorios --phone [PHONE] --cita [DATE]"
|
||||
```
|
||||
|
||||
7. Envia confirmacion via WACRM usando `wacrm_send`
|
||||
|
||||
### PASO 3: Para REAGENDAR
|
||||
|
||||
1. Busca citas del paciente por telefono via `odoo_api_call`
|
||||
2. Muestra citas activas
|
||||
3. Pide nueva fecha/hora
|
||||
4. Consulta disponibilidad
|
||||
5. Actualiza cita via `odoo_api_call` (model salon.booking, method write)
|
||||
6. Confirma cambio
|
||||
|
||||
### PASO 4: Para CANCELAR
|
||||
|
||||
1. Busca citas del paciente por telefono
|
||||
2. Confirma cual cita cancelar
|
||||
3. Pide motivo (opcional, para estadisticas)
|
||||
4. Cancela via `odoo_api_call` (state: 'cancelled')
|
||||
5. Ofrece reagendar
|
||||
|
||||
## Formato de Confirmacion
|
||||
|
||||
Perfecto! Tu cita esta confirmada:
|
||||
|
||||
Servicio: [Nombre servicio]
|
||||
Fecha: [Dia de semana], [DD] de [mes] de [AAAA]
|
||||
Hora: [HH:MM] AM/PM
|
||||
Duracion: [X] minutos
|
||||
Direccion: Blvd. Popotla 123, Playas de Rosarito
|
||||
|
||||
Te enviaremos un recordatorio un dia antes. Si necesitas cambiar o cancelar, solo escribenos!
|
||||
|
||||
## Reglas Importantes
|
||||
|
||||
- Solo agendar dentro del horario de la clinica
|
||||
- Validar que el paciente no tenga citas duplicadas
|
||||
- Consulta de valoracion gratuita tiene duracion de 15 min
|
||||
- Si no hay disponibilidad, ofrecer lista de espera
|
||||
- Guardar TODAS las citas en memoria para recordatorios
|
||||
55
hermes/hermes/skills/skeen-monedero/SKILL.md
Normal file
55
hermes/hermes/skills/skeen-monedero/SKILL.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: skeen-monedero
|
||||
description: >
|
||||
Monedero digital de fidelidad de SKEEN Derma Experts.
|
||||
Consulta saldo, redime puntos, muestra historial.
|
||||
1 punto = $1 MXN. Se acumula 1 punto por cada $10 gastados.
|
||||
Usar cuando el usuario pregunte por puntos, saldo, o redencion.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: []
|
||||
envVars:
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["puntos", "monedero", "saldo", "redimir", "fidelidad"]
|
||||
---
|
||||
|
||||
# SKEEN — Monedero Digital
|
||||
|
||||
## Consultar saldo
|
||||
|
||||
Usa `odoo_api_call`: modelo skeen.monedero, metodo get_balance, args: [phone_number]
|
||||
|
||||
Respuesta esperada: { points: 450, equivalent_mxn: 450, history: [...] }
|
||||
|
||||
Enviar via WACRM con formato:
|
||||
|
||||
Tu monedero SKEEN:
|
||||
Saldo actual: 450 puntos (= $450 MXN)
|
||||
Historial reciente:
|
||||
- 03/07/2026: +120 pts (Pago Tratamiento Acne — $1,200)
|
||||
- 28/06/2026: +80 pts (Pago Consulta + peel — $800)
|
||||
- 15/06/2026: +250 pts (Pago Depilacion laser — $2,500)
|
||||
|
||||
Puedes redimir tus puntos en tu proxima visita! Cada 10 puntos = $10 MXN de descuento.
|
||||
|
||||
## Redimir puntos
|
||||
|
||||
1. Verificar que tenga suficientes puntos
|
||||
2. Consultar monto a pagar de la cita/servicio
|
||||
3. Calcular descuento (maximo 50% del total)
|
||||
4. Confirmar redencion
|
||||
5. Aplicar via `odoo_api_call`: modelo skeen.monedero, metodo redeem
|
||||
6. Confirmar redencion via WACRM
|
||||
|
||||
## Reglas
|
||||
|
||||
- Maximo redimible: 50% del total de la compra
|
||||
- Puntos vencen a los 12 meses de inactividad
|
||||
- No transferibles entre pacientes
|
||||
- Se acumulan en pagos confirmados unicamente
|
||||
- En devoluciones, se restan los puntos correspondientes
|
||||
79
hermes/hermes/skills/skeen-pagos/SKILL.md
Normal file
79
hermes/hermes/skills/skeen-pagos/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: skeen-pagos
|
||||
description: >
|
||||
Gestion de pagos para SKEEN Derma Experts.
|
||||
Genera links de pago (Stripe/MercadoPago), consulta estados,
|
||||
y gestiona facturacion. Usar cuando el usuario quiera pagar,
|
||||
consultar saldo, o ver sus pagos pendientes.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: [python]
|
||||
env: [STRIPE_SECRET_KEY, MP_ACCESS_TOKEN]
|
||||
envVars:
|
||||
- name: STRIPE_SECRET_KEY
|
||||
required: true
|
||||
- name: MP_ACCESS_TOKEN
|
||||
required: true
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["pagar", "saldo", "deuda", "factura", "pago", "stripe", "mercadopago"]
|
||||
---
|
||||
|
||||
# SKEEN — Gestion de Pagos
|
||||
|
||||
## Instrucciones de Uso
|
||||
|
||||
### PASO 1: Identificar intencion de pago
|
||||
|
||||
- "Quiero pagar" → Generar link de pago
|
||||
- "Cuanto debo?" / "Saldo pendiente" → Consultar deuda
|
||||
- "Ya pague" → Verificar pago
|
||||
- "Necesito factura" → Generar factura (CFDI)
|
||||
|
||||
### PASO 2: Consultar servicios pendientes
|
||||
|
||||
Usa `odoo_api_call` para consultar modelo account.move:
|
||||
Dominio: [['partner_id.phone', '=', PACIENTE_PHONE], ['state', '=', 'posted'], ['payment_state', '!=', 'paid']]
|
||||
Campos: ['name', 'amount_residual', 'invoice_date_due']
|
||||
|
||||
### PASO 3: Generar link de pago
|
||||
|
||||
Para pagos en Mexico (MXN) — usa MercadoPago:
|
||||
Crear preference con items del servicio
|
||||
notification_url: https://skeen.mx/webhooks/mercadopago
|
||||
back_urls: { success: 'https://app.skeen.mx/pagos/exitoso' }
|
||||
|
||||
Para pagos internacionales — usa Stripe Checkout:
|
||||
Crear session con line_items
|
||||
mode: 'payment'
|
||||
success_url: 'https://app.skeen.mx/pagos/exitoso'
|
||||
metadata: { patient_phone: PACIENTE_PHONE }
|
||||
|
||||
### PASO 4: Enviar link de pago
|
||||
|
||||
Usa `wacrm_send` para enviar mensaje con resumen y link.
|
||||
|
||||
Incluir:
|
||||
- Listado de servicios con precios
|
||||
- Total a pagar
|
||||
- Link de pago
|
||||
- Metodos aceptados (tarjeta, OXXO, SPEI)
|
||||
- Puntos de monedero que acumulara
|
||||
|
||||
### PASO 5: Confirmacion de pago
|
||||
|
||||
Cuando el webhook confirme el pago:
|
||||
1. Registra pago en Odoo via `odoo_api_call`
|
||||
2. Calcula puntos de monedero (10% del monto)
|
||||
3. Envia confirmacion + puntos acumulados
|
||||
|
||||
## Reglas
|
||||
|
||||
- Siempre confirmar el monto antes de generar el link
|
||||
- Para pagos >$5,000 MXN, ofrecer opcion de meses sin intereses
|
||||
- Verificar que el telefono coincida con el paciente
|
||||
- Guardar referencia de pago en memoria
|
||||
59
hermes/hermes/skills/skeen-rag/SKILL.md
Normal file
59
hermes/hermes/skills/skeen-rag/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: skeen-rag
|
||||
description: >
|
||||
Catalogo de servicios dermatologicos de SKEEN Derma Experts.
|
||||
Proporciona informacion sobre tratamientos, precios, duracion y contraindicaciones.
|
||||
SOLO usar cuando el usuario pregunte por servicios, tratamientos, precios o el catalogo.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: []
|
||||
envVars:
|
||||
- name: SKEEN_SERVICES_JSON
|
||||
required: false
|
||||
description: "Path al archivo JSON con catalogo de servicios"
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["servicios", "tratamientos", "precios", "catalogo", "acne", "peel", "laser", "dermatologia"]
|
||||
---
|
||||
|
||||
# SKEEN Derma Experts — Catalogo de Servicios
|
||||
|
||||
## Instrucciones de Uso
|
||||
|
||||
Cuando un paciente pregunte por tratamientos, servicios, precios o recomendaciones dermatologicas:
|
||||
|
||||
1. Lee el archivo de catalogo: `${env:SKEEN_SERVICES_JSON:-/skills/data/skeen-servicios.json}`
|
||||
2. Filtra los servicios relevantes segun la pregunta
|
||||
3. Presenta la informacion de forma clara y amigable
|
||||
4. SIEMPRE incluye: nombre del tratamiento, precio aproximado, duracion, y una breve descripcion
|
||||
5. NUNCA diagnostiques condiciones medicas — invita al paciente a agendar una consulta
|
||||
6. Si no encuentras el servicio, ofrece agendar una consulta de valoracion
|
||||
|
||||
## Formato de Respuesta
|
||||
|
||||
Hola! En SKEEN contamos con estos tratamientos para [condicion]:
|
||||
|
||||
1. **[Nombre tratamiento]** — $[precio] MXN
|
||||
Duracion: [X] minutos
|
||||
[Descripcion breve]
|
||||
|
||||
2. **[Nombre tratamiento]** — $[precio] MXN
|
||||
...
|
||||
|
||||
Te gustaria agendar una consulta de valoracion? Podemos evaluar tu caso especifico y recomendarte el mejor tratamiento.
|
||||
|
||||
## Limites
|
||||
|
||||
- NO diagnosticar — solo informacion general
|
||||
- NO recomendar medicamentos especificos
|
||||
- SIEMPRE sugerir consulta presencial para evaluacion
|
||||
- Precios son aproximados — pueden variar segun evaluacion
|
||||
|
||||
## Datos de la Clinica
|
||||
|
||||
- Nombre: SKEEN Derma Experts
|
||||
- Direccion: Blvd. Popotla 123, Playas de Rosarito, BC, Mexico
|
||||
- Horario: Lun-Vie 9:00-18:00, Sab 9:00-14:00
|
||||
- Telefono: +52 (664) 123-4567
|
||||
89
hermes/hermes/skills/skeen-recordatorios/SKILL.md
Normal file
89
hermes/hermes/skills/skeen-recordatorios/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: skeen-recordatorios
|
||||
description: >
|
||||
Recordatorios de citas, follow-ups y re-engagement para SKEEN.
|
||||
Usar PRINCIPALMENTE via cron (hermes cron).
|
||||
Tambien puede responder a consultas sobre recordatorios.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
always: true
|
||||
requires:
|
||||
bins: []
|
||||
envVars:
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
- name: WACRM_URL
|
||||
required: true
|
||||
- name: WACRM_API_KEY
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: cron
|
||||
cron:
|
||||
- schedule: "0 */6 * * *" # Cada 6 horas: generar recordatorios
|
||||
command: "skill skeen-recordatorios --action generate"
|
||||
- schedule: "0 18 * * *" # 6 PM diario: enviar recordatorios
|
||||
command: "skill skeen-recordatorios --action send"
|
||||
- schedule: "0 9 * * 1" # Lunes 9 AM: re-engagement
|
||||
command: "skill skeen-recordatorios --action reengage"
|
||||
---
|
||||
|
||||
# SKEEN — Recordatorios y Follow-ups
|
||||
|
||||
## Ejecucion via Cron
|
||||
|
||||
Esta skill se ejecuta automaticamente via `hermes cron`.
|
||||
|
||||
### PASO 1: Generar recordatorios (cada 6 horas)
|
||||
|
||||
Recordatorios de citas (24h antes):
|
||||
Consulta Odoo: salon.booking con date=tomorrow, state=confirmed
|
||||
Para cada cita, programa cron:
|
||||
```bash
|
||||
hermes cron schedule --name "recordatorio-[booking_id]" \
|
||||
--at "[fecha_cita] 18:00 - 1 day" \
|
||||
--command "skill skeen-recordatorios --action send --phone [PHONE] --cita [DATE]"
|
||||
```
|
||||
|
||||
Follow-ups post-tratamiento (7 dias despues):
|
||||
Consulta Odoo: salon.booking con date=7_days_ago, state=done
|
||||
Programa cron para cada paciente:
|
||||
```bash
|
||||
hermes cron schedule --name "followup-[booking_id]" \
|
||||
--at "[fecha_cita + 7 days] 10:00" \
|
||||
--command "skill skeen-recordatorios --action followup --phone [PHONE]"
|
||||
```
|
||||
|
||||
Re-engagement (pacientes inactivos 90 dias):
|
||||
Consulta Odoo: res.partner con last_visit <= 90_days_ago
|
||||
Programa cron semanal:
|
||||
```bash
|
||||
hermes cron schedule --name "reengage-[partner_id]" \
|
||||
--at "lunes 9:00" \
|
||||
--command "skill skeen-recordatorios --action reengage --phone [PHONE]"
|
||||
```
|
||||
|
||||
### PASO 2: Enviar recordatorios (6 PM diario)
|
||||
|
||||
Para cada recordatorio programado para hoy:
|
||||
1. Enviar via WACRM usando `wacrm_send` con template aprobado
|
||||
2. Si es exitoso, marcar como enviado en Odoo
|
||||
3. Si falla, reintentar en siguiente ejecucion (max 3 intentos)
|
||||
|
||||
### PASO 3: Limpieza
|
||||
|
||||
Eliminar cron jobs completados con mas de 7 dias:
|
||||
```bash
|
||||
hermes cron list --completed | awk '$4 > 7 {print $1}' | xargs hermes cron delete
|
||||
```
|
||||
|
||||
## Templates de WhatsApp Aprobados
|
||||
|
||||
recordatorio_cita_skeen:
|
||||
"Hola {{1}}! Te recordamos tu cita manana {{2}} a las {{3}} en SKEEN Derma Experts."
|
||||
|
||||
followup_tratamiento_skeen:
|
||||
"Hola {{1}}! Han pasado {{2}} dias desde tu {{3}}. Como te sientes?"
|
||||
|
||||
reengage_skeen:
|
||||
"Hola {{1}}! Te extranamos en SKEEN. Agenda tu proxima visita y recibe 50 puntos."
|
||||
98
hermes/hermes/skills/skeen-services.json
Normal file
98
hermes/hermes/skills/skeen-services.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"clinica": {
|
||||
"nombre": "SKEEN Derma Experts",
|
||||
"direccion": "Blvd. Popotla 123, Playas de Rosarito, BC, Mexico",
|
||||
"telefono": "+52 (664) 123-4567",
|
||||
"horario": {
|
||||
"lunes_viernes": "9:00 - 18:00",
|
||||
"sabado": "9:00 - 14:00",
|
||||
"domingo": "Cerrado"
|
||||
}
|
||||
},
|
||||
"servicios": [
|
||||
{
|
||||
"id": "cons-derm",
|
||||
"nombre": "Consulta Dermatologica",
|
||||
"categoria": "consulta",
|
||||
"precio": 800.0,
|
||||
"duracion_min": 30,
|
||||
"precio_paquete": 0,
|
||||
"sesiones_paquete": 0,
|
||||
"notas_paquete": "",
|
||||
"descripcion": "Evaluacion completa de la piel con diagnostico y plan de tratamiento personalizado.",
|
||||
"contraindicaciones": []
|
||||
},
|
||||
{
|
||||
"id": "cons-val",
|
||||
"nombre": "Consulta de Valoracion",
|
||||
"categoria": "consulta",
|
||||
"precio": 0.0,
|
||||
"duracion_min": 15,
|
||||
"precio_paquete": 0,
|
||||
"sesiones_paquete": 0,
|
||||
"notas_paquete": "",
|
||||
"descripcion": "Evaluacion breve para recomendar el tratamiento mas adecuado. Sin compromiso.",
|
||||
"contraindicaciones": []
|
||||
},
|
||||
{
|
||||
"id": "laser-axi",
|
||||
"nombre": "Depilacion Laser Axila",
|
||||
"categoria": "tratamiento",
|
||||
"precio": 500.0,
|
||||
"duracion_min": 15,
|
||||
"precio_paquete": 0,
|
||||
"sesiones_paquete": 0,
|
||||
"notas_paquete": "",
|
||||
"descripcion": "Depilacion laser por zona pequena.",
|
||||
"contraindicaciones": []
|
||||
},
|
||||
{
|
||||
"id": "laser-ful",
|
||||
"nombre": "Depilacion Laser Full Body",
|
||||
"categoria": "tratamiento",
|
||||
"precio": 2500.0,
|
||||
"duracion_min": 90,
|
||||
"precio_paquete": 7000.0,
|
||||
"sesiones_paquete": 3,
|
||||
"notas_paquete": "Ahorro de $500",
|
||||
"descripcion": "Depilacion laser full body. Precio por sesion o paquete de 3 sesiones.",
|
||||
"contraindicaciones": []
|
||||
},
|
||||
{
|
||||
"id": "peel-qui",
|
||||
"nombre": "Peel Quimico",
|
||||
"categoria": "tratamiento",
|
||||
"precio": 1500.0,
|
||||
"duracion_min": 30,
|
||||
"precio_paquete": 0,
|
||||
"sesiones_paquete": 0,
|
||||
"notas_paquete": "",
|
||||
"descripcion": "Exfoliacion quimica para renovar la piel tratar manchas y mejorar textura.",
|
||||
"contraindicaciones": []
|
||||
},
|
||||
{
|
||||
"id": "anti-edad",
|
||||
"nombre": "Tratamiento Anti-Edad",
|
||||
"categoria": "tratamiento",
|
||||
"precio": 2500.0,
|
||||
"duracion_min": 75,
|
||||
"precio_paquete": 6500.0,
|
||||
"sesiones_paquete": 3,
|
||||
"notas_paquete": "Ahorro de $1000",
|
||||
"descripcion": "Combinacion de microdermoabrasion vitaminas y mascarilla reafirmante.",
|
||||
"contraindicaciones": []
|
||||
},
|
||||
{
|
||||
"id": "acne-fac",
|
||||
"nombre": "Tratamiento Facial para Acne",
|
||||
"categoria": "tratamiento",
|
||||
"precio": 1500.0,
|
||||
"duracion_min": 60,
|
||||
"precio_paquete": 0,
|
||||
"sesiones_paquete": 0,
|
||||
"notas_paquete": "",
|
||||
"descripcion": "Limpieza profunda extraccion de comedones y mascarilla calmante.",
|
||||
"contraindicaciones": []
|
||||
}
|
||||
]
|
||||
}
|
||||
1001
hermes/hermes/webhook-bridge.js
Normal file
1001
hermes/hermes/webhook-bridge.js
Normal file
File diff suppressed because it is too large
Load Diff
157
hermes/hermes/webhook-bridge.js.bak.20260707062846
Normal file
157
hermes/hermes/webhook-bridge.js.bak.20260707062846
Normal file
@@ -0,0 +1,157 @@
|
||||
const express = require('express');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const NAN_API_KEY = process.env.NAN_API_KEY || 'sk-gJ8OWbZVGeWy9ns_H00cxA';
|
||||
const NAN_BASE_URL = process.env.NAN_BASE_URL || 'https://api.nan.builders/v1';
|
||||
const WACRM_URL = process.env.WACRM_URL || 'http://localhost:3000';
|
||||
const WACRM_API_KEY = process.env.WACRM_API_KEY || 'wacrm_live_ffac5531d76645acb239dba71ace6d1c';
|
||||
|
||||
const SKEEN_SYSTEM_PROMPT = `Eres Sofia, la asistente virtual de SKEEN Derma Experts, una clínica dermatológica en Playas de Rosarito, BC, México.
|
||||
|
||||
Tu personalidad: cálida, profesional, empática y eficiente.
|
||||
Idioma: Español (México).
|
||||
|
||||
Datos de la clínica:
|
||||
- Nombre: SKEEN Derma Experts
|
||||
- Dirección: Blvd. Popotla 123, Playas de Rosarito, BC, México
|
||||
- Horario: Lun-Vie 9:00-18:00, Sáb 9:00-14:00
|
||||
- Teléfono: +52 (664) 123-4567
|
||||
|
||||
Límites importantes:
|
||||
- NO diagnosticar condiciones médicas.
|
||||
- NO recomendar medicamentos específicos.
|
||||
- SIEMPRE sugerir consulta presencial para evaluación.
|
||||
- Máximo 3 párrafos por mensaje.
|
||||
- Máximo 2 emojis por mensaje.
|
||||
|
||||
Servicios principales:
|
||||
- Consulta Dermatológica General — $800 MXN, 30 min
|
||||
- Consulta de Valoración Gratuita — 15 min
|
||||
- Tratamiento Facial para Acné — $1,500 MXN, 60 min
|
||||
- Peel Químico — $1,500 MXN, 30 min
|
||||
- Depilación Láser — desde $500 MXN por zona, 15-60 min
|
||||
- Tratamiento Anti-Edad — $2,500 MXN, 60 min
|
||||
|
||||
Cuando el usuario pregunte por servicios, dales información general y ofrece agendar una consulta de valoración.
|
||||
Cuando quiera agendar, pregunta tipo de servicio y ofrece opciones de horario.
|
||||
Si escribe AGENTE, HUMANO, ESPECIALISTA o DOCTOR, responde: "Te conecto con un especialista enseguida. Espera un momento 💙"`;
|
||||
|
||||
async function callNanBuilders(messages) {
|
||||
const response = await fetch(`${NAN_BASE_URL}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NAN_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-v4-flash',
|
||||
messages,
|
||||
temperature: 0.7,
|
||||
max_tokens: 500,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Nan API error: ${response.status} ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0]?.message?.content || 'Lo siento, no pude procesar tu mensaje.';
|
||||
}
|
||||
|
||||
async function sendWacrmMessage(to, text, simulate = false) {
|
||||
const endpoint = simulate
|
||||
? `${WACRM_URL}/api/v1/messages/simulate`
|
||||
: `${WACRM_URL}/api/v1/messages`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${WACRM_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ to, type: 'text', text }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`WACRM send error: ${response.status} ${error}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function sendWacrmMessageWithFallback(to, text) {
|
||||
try {
|
||||
return await sendWacrmMessage(to, text, false);
|
||||
} catch (error) {
|
||||
const msg = error.message || '';
|
||||
// If Meta is not connected, fall back to simulated injection so the
|
||||
// demo/development flow still works end-to-end.
|
||||
if (msg.includes('Meta API error') || msg.includes('Invalid OAuth')) {
|
||||
console.warn('[Bridge] Meta send failed, falling back to simulate endpoint');
|
||||
return await sendWacrmMessage(to, text, true);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', service: 'skeen-hermes-webhook-bridge' });
|
||||
});
|
||||
|
||||
app.post('/webhook/wacrm', async (req, res) => {
|
||||
try {
|
||||
// WACRM envía el payload con estructura { id, event, occurred_at, account_id, data }
|
||||
const payload = req.body;
|
||||
const eventType = payload.event;
|
||||
const data = payload.data || {};
|
||||
|
||||
console.log(`[WACRM] Webhook recibido: ${eventType}`, JSON.stringify(data).substring(0, 200));
|
||||
|
||||
// Solo procesar mensajes entrantes
|
||||
if (eventType !== 'message.received') {
|
||||
return res.json({ status: 'ignored', event: eventType });
|
||||
}
|
||||
|
||||
const from = data.contact_phone || data.from;
|
||||
const text = data.text;
|
||||
const conversation_id = data.conversation_id;
|
||||
const contact_id = data.contact_id;
|
||||
|
||||
if (!from || !text) {
|
||||
return res.status(400).json({ error: 'Missing from or text', received: payload });
|
||||
}
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: SKEEN_SYSTEM_PROMPT },
|
||||
{ role: 'user', content: text },
|
||||
];
|
||||
|
||||
const reply = await callNanBuilders(messages);
|
||||
|
||||
console.log(`[Sofia] Respuesta: ${reply.substring(0, 100)}...`);
|
||||
|
||||
try {
|
||||
await sendWacrmMessageWithFallback(from, reply);
|
||||
} catch (wacrmError) {
|
||||
console.error('Error enviando a WACRM:', wacrmError.message);
|
||||
}
|
||||
|
||||
res.json({ status: 'success', reply, from, conversation_id, contact_id });
|
||||
} catch (error) {
|
||||
console.error('Error procesando webhook:', error);
|
||||
res.status(500).json({ status: 'error', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 8090;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`SKEEN Hermes Webhook Bridge escuchando en http://0.0.0.0:${PORT}`);
|
||||
console.log(`Health: http://localhost:${PORT}/health`);
|
||||
console.log(`Webhook: http://localhost:${PORT}/webhook/wacrm`);
|
||||
});
|
||||
120
hermes/hermes/webhooks/wacrm_webhook_server.py
Normal file
120
hermes/hermes/webhooks/wacrm_webhook_server.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import os
|
||||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Dict, Any
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
WACRM_WEBHOOK_SECRET = os.environ.get("WACRM_WEBHOOK_SECRET", "skeen_webhook_secret_2026")
|
||||
HERMES_AGENT = None # Se inicializa al arrancar el gateway
|
||||
|
||||
|
||||
def verify_signature(payload: bytes, signature: str) -> bool:
|
||||
"""Verifica la firma HMAC-SHA256 del webhook de WACRM."""
|
||||
if not signature:
|
||||
return False
|
||||
|
||||
# Formato esperado: "t=<timestamp>,v1=<signature>"
|
||||
parts = {}
|
||||
for part in signature.split(','):
|
||||
if '=' in part:
|
||||
key, value = part.split('=', 1)
|
||||
parts[key.strip()] = value.strip()
|
||||
|
||||
timestamp = parts.get('t', '')
|
||||
expected_sig = parts.get('v1', '')
|
||||
|
||||
expected = hmac.new(
|
||||
WACRM_WEBHOOK_SECRET.encode(),
|
||||
f"{timestamp}.{payload.decode()}".encode(),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(expected_sig, expected)
|
||||
|
||||
|
||||
@app.route('/webhook/wacrm', methods=['POST'])
|
||||
def handle_wacrm_webhook():
|
||||
"""Recibe webhooks de WACRM y los procesa via Hermes agent."""
|
||||
payload = request.get_data()
|
||||
signature = request.headers.get('X-WACRM-Signature', '')
|
||||
|
||||
if not verify_signature(payload, signature):
|
||||
return jsonify({"error": "Invalid signature"}), 401
|
||||
|
||||
event = request.json
|
||||
event_type = event.get('event')
|
||||
data = event.get('data', {})
|
||||
|
||||
# Enrutar segun tipo de evento
|
||||
handlers = {
|
||||
'message.received': handle_message_received,
|
||||
'message.status_updated': handle_status_update,
|
||||
'conversation.created': handle_new_conversation,
|
||||
}
|
||||
|
||||
handler = handlers.get(event_type)
|
||||
if handler:
|
||||
handler(data)
|
||||
|
||||
return jsonify({"received": True}), 200
|
||||
|
||||
|
||||
def handle_message_received(data: Dict[str, Any]):
|
||||
"""Procesa mensaje entrante de WACRM."""
|
||||
from_number = data.get('from')
|
||||
text = data.get('text', '')
|
||||
conversation_id = data.get('conversation_id')
|
||||
contact_id = data.get('contact_id')
|
||||
|
||||
# Enviar al agent runtime de Hermes
|
||||
# Esto se conecta con el gateway principal de Hermes
|
||||
if HERMES_AGENT:
|
||||
HERMES_AGENT.process_message(
|
||||
channel='whatsapp',
|
||||
sender=from_number,
|
||||
text=text,
|
||||
metadata={
|
||||
'conversation_id': conversation_id,
|
||||
'contact_id': contact_id,
|
||||
'source': 'wacrm'
|
||||
}
|
||||
)
|
||||
|
||||
# Log para debugging
|
||||
print(f"[WACRM Webhook] Mensaje de {from_number}: {text[:50]}...")
|
||||
|
||||
|
||||
def handle_status_update(data: Dict[str, Any]):
|
||||
"""Procesa actualizacion de estado de mensaje."""
|
||||
message_id = data.get('message_id')
|
||||
status = data.get('status') # sent, delivered, read, failed
|
||||
|
||||
print(f"[WACRM Webhook] Estado mensaje {message_id}: {status}")
|
||||
|
||||
|
||||
def handle_new_conversation(data: Dict[str, Any]):
|
||||
"""Procesa nueva conversacion."""
|
||||
conversation_id = data.get('conversation_id')
|
||||
contact_id = data.get('contact_id')
|
||||
|
||||
print(f"[WACRM Webhook] Nueva conversacion: {conversation_id}")
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""Health check para monitoreo."""
|
||||
return jsonify({"status": "ok", "service": "wacrm-webhook-server"}), 200
|
||||
|
||||
|
||||
def init_hermes_agent(agent):
|
||||
"""Inicializa la referencia al agente de Hermes."""
|
||||
global HERMES_AGENT
|
||||
HERMES_AGENT = agent
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.environ.get('HERMES_WEBHOOK_PORT', 8080))
|
||||
app.run(host='0.0.0.0', port=port, debug=False)
|
||||
60
hermes/hermes/workspace/AGENTS.md
Normal file
60
hermes/hermes/workspace/AGENTS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Instrucciones Operativas — SKEEN
|
||||
|
||||
## Flujos de trabajo
|
||||
|
||||
1. **Agendar Cita:** Cargar skill `skeen-agendar` → Odoo slots → WACRM confirm → Programar cron recordatorio
|
||||
2. **Consultar Catalogo:** Cargar skill `skeen-rag` → Catalogo servicios → Ofrecer valoracion
|
||||
3. **Procesar Pago:** Cargar skill `skeen-pagos` → Odoo saldo → Stripe/MP link → Confirmar
|
||||
4. **Consultar Monedero:** Cargar skill `skeen-monedero` → Odoo balance → Historial
|
||||
5. **Recordatorios (Cron):** `skeen-recordatorios` ejecuta via `hermes cron` cada 6h/18h
|
||||
6. **Follow-ups:** `skeen-recordatorios` —action followup cada lunes 10 AM
|
||||
7. **Re-engagement:** `skeen-recordatorios` —action reengage cada lunes 9 AM
|
||||
|
||||
## Reglas de seguridad
|
||||
|
||||
- NUNCA compartir API keys en mensajes
|
||||
- NUNCA ejecutar comandos destructivos en produccion
|
||||
- SIEMPRE verificar identidad del paciente por telefono
|
||||
- Limitar redencion de puntos a 50% del total
|
||||
- Validar todos los inputs antes de ejecutar en Odoo
|
||||
|
||||
## Comandos de Hermes utiles
|
||||
|
||||
```bash
|
||||
# Ver estado del gateway
|
||||
hermes gateway status
|
||||
|
||||
# Ver logs
|
||||
hermes logs --follow
|
||||
|
||||
# Diagnosticar configuracion
|
||||
hermes doctor
|
||||
|
||||
# Listar cron jobs activos
|
||||
hermes cron list
|
||||
|
||||
# Ejecutar skill manualmente
|
||||
hermes skill skeen-agendar --phone +526641234567
|
||||
|
||||
# Ver memoria
|
||||
hermes memory read
|
||||
|
||||
# Comprimir contexto
|
||||
hermes compress
|
||||
|
||||
# Cambiar modelo
|
||||
hermes model
|
||||
|
||||
# Actualizar
|
||||
hermes update
|
||||
```
|
||||
|
||||
## Integraciones
|
||||
|
||||
| Sistema | Protocolo | Endpoint | Auth |
|
||||
|---------|-----------|----------|------|
|
||||
| WACRM | HTTPS REST | http://wacrm:3000/api/v1 | Bearer API Key |
|
||||
| Odoo | XML-RPC | http://odoo:8069/xmlrpc/2 | API Key |
|
||||
| Stripe | HTTPS REST | https://api.stripe.com | Secret Key |
|
||||
| MercadoPago | HTTPS REST | https://api.mercadopago.com | Access Token |
|
||||
| Meta API | HTTPS REST | https://graph.facebook.com/v18.0 | Access Token |
|
||||
45
hermes/hermes/workspace/SOUL.md
Normal file
45
hermes/hermes/workspace/SOUL.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Sofia — Asistente Virtual de SKEEN Derma Experts
|
||||
|
||||
## Identidad
|
||||
|
||||
- **Nombre:** Sofia
|
||||
- **Rol:** Asistente virtual de SKEEN Derma Experts
|
||||
- **Personalidad:** Calida, profesional, empatica, eficiente
|
||||
- **Idioma:** Espanol (Mexico)
|
||||
- **Emoji favorito:** 💙✨🌸
|
||||
|
||||
## Tonos de voz
|
||||
|
||||
- **Saludo:** "Hola! Soy Sofia de SKEEN 💙"
|
||||
- **Confirmacion:** "Perfecto! Tu cita esta confirmada ✨"
|
||||
- **Empatia:** "Entiendo, estamos aqui para cuidar tu piel 🌸"
|
||||
- **Urgencia:** "Voy a conectarte con un especialista de inmediato"
|
||||
|
||||
## Limites
|
||||
|
||||
- NO diagnosticar condiciones medicas
|
||||
- NO recomendar medicamentos especificos
|
||||
- SIEMPRE sugerir consulta presencial para evaluaciones
|
||||
- Max 3 parrafos por mensaje de WhatsApp
|
||||
- Max 2 emojis por mensaje
|
||||
|
||||
## Datos de la Clinica
|
||||
|
||||
- **Nombre:** SKEEN Derma Experts, Playas de Rosarito, BC
|
||||
- **Horario:** Lun-Vie 9:00-18:00, Sab 9:00-14:00
|
||||
- **Direccion:** Blvd. Popotla 123, Playas de Rosarito
|
||||
- **Telefono:** +52 (664) 123-4567
|
||||
|
||||
## Handoff a Humano
|
||||
|
||||
**Trigger:** "AGENTE", "HUMANO", "ESPECIALISTA", "DOCTOR"
|
||||
|
||||
**Respuesta:** "Te conecto con un especialista enseguida. Espera un momento 💙"
|
||||
|
||||
## Contexto de Hermes
|
||||
|
||||
- Sofia es el agente principal del perfil `skeen` en Hermes
|
||||
- Usa el modelo `gpt-4o` como primario, con fallback a Claude 3.5 Sonnet
|
||||
- Su memoria persiste en SQLite + archivos MEMORY.md
|
||||
- Puede delegar tareas a subagentes para tareas complejas
|
||||
- Se comunica con pacientes via WACRM (WhatsApp)
|
||||
74
hermes/scripts/setup-phase1.sh
Executable file
74
hermes/scripts/setup-phase1.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "FASE 1: Setup WACRM + Hermes"
|
||||
echo "SKEEN Derma Experts"
|
||||
echo "=========================================="
|
||||
|
||||
# Variables
|
||||
PROJECT_ROOT="/root/skeen-roadmap-hermes"
|
||||
WACRM_DIR="/root/skeen-wacrm"
|
||||
HERMES_DIR="$PROJECT_ROOT/hermes"
|
||||
|
||||
# 1. Verificar dependencias
|
||||
echo "[1/8] Verificando dependencias..."
|
||||
python3 --version
|
||||
node -v
|
||||
docker --version
|
||||
nginx -v
|
||||
|
||||
# 2. Configurar PostgreSQL
|
||||
echo "[2/8] Configurando PostgreSQL..."
|
||||
sudo -u postgres psql -c "CREATE DATABASE skeen_wacrm;" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "CREATE USER skeen WITH PASSWORD 'skeen_dev_2026';" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE skeen_wacrm TO skeen;" 2>/dev/null || true
|
||||
|
||||
# 3. Crear schemas de Supabase
|
||||
echo "[3/8] Creando schemas de Supabase..."
|
||||
sudo -u postgres psql -d skeen_wacrm -c "CREATE SCHEMA IF NOT EXISTS auth;" 2>/dev/null || true
|
||||
sudo -u postgres psql -d skeen_wacrm -c "CREATE SCHEMA IF NOT EXISTS storage;" 2>/dev/null || true
|
||||
sudo -u postgres psql -d skeen_wacrm -c "CREATE TABLE IF NOT EXISTS auth.users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT, encrypted_password TEXT, email_confirmed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW());" 2>/dev/null || true
|
||||
|
||||
# 4. Crear .env.local para WACRM
|
||||
echo "[4/8] Configurando WACRM..."
|
||||
cat > "$WACRM_DIR/.env.local" << 'EOF'
|
||||
# WACRM Environment - Development
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3NTc0MDAsImV4cCI6MTkxNzUxMzgwMH0.skeen_dev_key
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTc1NzQwMCwiZXhwIjoxOTE3NTEzODAwfQ.skeen_dev_service
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
WHATSAPP_VERIFY_TOKEN=skeen_webhook_verify_2026
|
||||
EOF
|
||||
|
||||
# 5. Crear directorios de Hermes
|
||||
echo "[5/8] Configurando Hermes..."
|
||||
mkdir -p "$HERMES_DIR/.hermes/workspace"
|
||||
mkdir -p "$HERMES_DIR/.hermes/plugins"
|
||||
mkdir -p "$HERMES_DIR/skills"
|
||||
|
||||
# 6. Copiar archivos de configuración
|
||||
echo "[6/8] Copiando archivos de configuración..."
|
||||
cp "$PROJECT_ROOT/hermes/config.yaml" "$HERMES_DIR/.hermes/config.yaml" 2>/dev/null || true
|
||||
cp "$PROJECT_ROOT/hermes/workspace/SOUL.md" "$HERMES_DIR/.hermes/workspace/SOUL.md" 2>/dev/null || true
|
||||
cp "$PROJECT_ROOT/hermes/workspace/AGENTS.md" "$HERMES_DIR/.hermes/workspace/AGENTS.md" 2>/dev/null || true
|
||||
|
||||
# 7. Copiar skills
|
||||
echo "[7/8] Copiando skills..."
|
||||
cp -r "$PROJECT_ROOT/skills/hermes-skeen/"* "$HERMES_DIR/skills/" 2>/dev/null || true
|
||||
|
||||
# 8. Copiar plugins
|
||||
echo "[8/8] Copiando plugins..."
|
||||
cp "$PROJECT_ROOT/hermes/plugins/"*.py "$HERMES_DIR/.hermes/plugins/" 2>/dev/null || true
|
||||
|
||||
echo "=========================================="
|
||||
echo "Setup completado!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Próximos pasos:"
|
||||
echo "1. Configurar variables de Meta (PHONE_ID, ACCESS_TOKEN)"
|
||||
echo "2. Instalar Hermes: pip install 'hermes-agent[messaging,cron,web]'"
|
||||
echo "3. Iniciar WACRM: cd $WACRM_DIR && npm run dev"
|
||||
echo "4. Iniciar Hermes: hermes gateway start"
|
||||
echo ""
|
||||
68
hermes/scripts/start-stack.sh
Executable file
68
hermes/scripts/start-stack.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Script de inicio para el stack SKEEN (WACRM + Hermes)
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_ROOT="/root/skeen-roadmap-hermes"
|
||||
WACRM_DIR="/root/skeen-wacrm"
|
||||
HERMES_VENV="/root/hermes-venv"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Iniciando Stack SKEEN"
|
||||
echo "=========================================="
|
||||
|
||||
# 1. Iniciar PostgreSQL
|
||||
echo "[1/4] Iniciando PostgreSQL..."
|
||||
systemctl start postgresql
|
||||
|
||||
# 2. Iniciar Redis
|
||||
echo "[2/4] Iniciando Redis..."
|
||||
systemctl start redis-server
|
||||
|
||||
# 3. Iniciar WACRM (en background)
|
||||
echo "[3/4] Iniciando WACRM..."
|
||||
cd "$WACRM_DIR"
|
||||
export NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
export NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||
export NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3NTc0MDAsImV4cCI6MTkxNzUxMzgwMH0.skeen_dev_key
|
||||
export SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTc1NzQwMCwiZXhwIjoxOTE3NTEzODAwfQ.skeen_dev_service
|
||||
export ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
export WHATSAPP_VERIFY_TOKEN=skeen_webhook_verify_2026
|
||||
|
||||
# Iniciar WACRM en background
|
||||
nohup npm run dev > /var/log/wacrm.log 2>&1 &
|
||||
echo "WACRM iniciado en puerto 3000 (PID: $!)"
|
||||
|
||||
# 4. Iniciar Hermes (en background)
|
||||
echo "[4/4] Iniciando Hermes..."
|
||||
source "$HERMES_VENV/bin/activate"
|
||||
export HERMES_MODEL_PROVIDER=openai
|
||||
export HERMES_MODEL=gpt-4o
|
||||
export OPENAI_API_KEY=${OPENAI_API_KEY:-sk-test}
|
||||
export HERMES_WEBHOOK_PORT=8080
|
||||
export HERMES_API_SERVER_ENABLED=true
|
||||
export HERMES_CRON_ENABLED=true
|
||||
export WACRM_URL=http://localhost:3000
|
||||
export WACRM_API_KEY=wacrm_dev_key
|
||||
export SKEEN_SERVICES_JSON=/root/skeen-roadmap-hermes/hermes/skills/skeen-services.json
|
||||
|
||||
# Iniciar Hermes gateway en background
|
||||
nohup hermes gateway start > /var/log/hermes.log 2>&1 &
|
||||
echo "Hermes iniciado en puerto 8080 (PID: $!)"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Stack SKEEN iniciado!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Servicios:"
|
||||
echo " - WACRM: http://localhost:3000"
|
||||
echo " - Hermes: http://localhost:8080"
|
||||
echo " - Nginx: http://localhost:80"
|
||||
echo ""
|
||||
echo "Logs:"
|
||||
echo " - WACRM: tail -f /var/log/wacrm.log"
|
||||
echo " - Hermes: tail -f /var/log/hermes.log"
|
||||
echo ""
|
||||
echo "Para detener:"
|
||||
echo " pkill -f 'next dev'"
|
||||
echo " pkill -f 'hermes gateway'"
|
||||
103
hermes/skills/hermes-skeen/skeen-agendar/SKILL.md
Normal file
103
hermes/skills/hermes-skeen/skeen-agendar/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: skeen-agendar
|
||||
description: >
|
||||
Agendamiento de citas para SKEEN Derma Experts.
|
||||
Consulta disponibilidad, crea citas, reagenda y cancela.
|
||||
Usar cuando el usuario quiera agendar, reagendar o cancelar una cita.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: [python]
|
||||
envVars:
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
- name: ODOO_DB
|
||||
required: true
|
||||
- name: ODOO_API_KEY
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["agendar", "cita", "hora", "reagendar", "cancelar", "disponibilidad"]
|
||||
---
|
||||
|
||||
# SKEEN — Agendamiento de Citas
|
||||
|
||||
## Instrucciones de Uso
|
||||
|
||||
### PASO 1: Identificar intencion
|
||||
|
||||
- "Quiero agendar" / "Necesito una cita" → NUEVA CITA
|
||||
- "Quiero cambiar mi cita" / "Reagendar" → REAGENDAR
|
||||
- "Cancelar mi cita" → CANCELAR
|
||||
- "Tengo una cita para manana?" → CONSULTAR
|
||||
|
||||
### PASO 2: Para NUEVA CITA
|
||||
|
||||
1. Pregunta que tipo de servicio desea (si no lo especifico):
|
||||
- Consulta dermatologica general ($800 MXN, 30 min)
|
||||
- Tratamiento facial ($1,200-$3,500 MXN, 45-90 min)
|
||||
- Depilacion laser (desde $500 MXN por zona, 15-60 min)
|
||||
- Peel quimico ($1,500 MXN, 30 min)
|
||||
- O consulta de valoracion gratuita (15 min)
|
||||
|
||||
2. Consulta disponibilidad usando el plugin `odoo_api_call`:
|
||||
Modelo: salon.booking, metodo: get_available_slots
|
||||
Argumentos: [service_id, date_from, date_to]
|
||||
|
||||
3. Presenta las 3 mejores opciones de horario
|
||||
|
||||
4. Cuando el paciente elige, confirma datos:
|
||||
- Nombre completo
|
||||
- Confirmar telefono
|
||||
- Alergias o condiciones especiales (opcional)
|
||||
|
||||
5. Crea la cita usando `odoo_api_call`:
|
||||
Modelo: salon.booking, metodo: create
|
||||
Datos: { name, phone, service_id, date, time, state: 'confirmed' }
|
||||
|
||||
6. Programa recordatorio via cron:
|
||||
```bash
|
||||
hermes cron schedule --name "recordatorio-cita-[ID]" \
|
||||
--at "[fecha_cita] 18:00 - 1 day" \
|
||||
--command "skill skeen-recordatorios --phone [PHONE] --cita [DATE]"
|
||||
```
|
||||
|
||||
7. Envia confirmacion via WACRM usando `wacrm_send`
|
||||
|
||||
### PASO 3: Para REAGENDAR
|
||||
|
||||
1. Busca citas del paciente por telefono via `odoo_api_call`
|
||||
2. Muestra citas activas
|
||||
3. Pide nueva fecha/hora
|
||||
4. Consulta disponibilidad
|
||||
5. Actualiza cita via `odoo_api_call` (model salon.booking, method write)
|
||||
6. Confirma cambio
|
||||
|
||||
### PASO 4: Para CANCELAR
|
||||
|
||||
1. Busca citas del paciente por telefono
|
||||
2. Confirma cual cita cancelar
|
||||
3. Pide motivo (opcional, para estadisticas)
|
||||
4. Cancela via `odoo_api_call` (state: 'cancelled')
|
||||
5. Ofrece reagendar
|
||||
|
||||
## Formato de Confirmacion
|
||||
|
||||
Perfecto! Tu cita esta confirmada:
|
||||
|
||||
Servicio: [Nombre servicio]
|
||||
Fecha: [Dia de semana], [DD] de [mes] de [AAAA]
|
||||
Hora: [HH:MM] AM/PM
|
||||
Duracion: [X] minutos
|
||||
Direccion: Blvd. Popotla 123, Playas de Rosarito
|
||||
|
||||
Te enviaremos un recordatorio un dia antes. Si necesitas cambiar o cancelar, solo escribenos!
|
||||
|
||||
## Reglas Importantes
|
||||
|
||||
- Solo agendar dentro del horario de la clinica
|
||||
- Validar que el paciente no tenga citas duplicadas
|
||||
- Consulta de valoracion gratuita tiene duracion de 15 min
|
||||
- Si no hay disponibilidad, ofrecer lista de espera
|
||||
- Guardar TODAS las citas en memoria para recordatorios
|
||||
55
hermes/skills/hermes-skeen/skeen-monedero/SKILL.md
Normal file
55
hermes/skills/hermes-skeen/skeen-monedero/SKILL.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: skeen-monedero
|
||||
description: >
|
||||
Monedero digital de fidelidad de SKEEN Derma Experts.
|
||||
Consulta saldo, redime puntos, muestra historial.
|
||||
1 punto = $1 MXN. Se acumula 1 punto por cada $10 gastados.
|
||||
Usar cuando el usuario pregunte por puntos, saldo, o redencion.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: []
|
||||
envVars:
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["puntos", "monedero", "saldo", "redimir", "fidelidad"]
|
||||
---
|
||||
|
||||
# SKEEN — Monedero Digital
|
||||
|
||||
## Consultar saldo
|
||||
|
||||
Usa `odoo_api_call`: modelo skeen.monedero, metodo get_balance, args: [phone_number]
|
||||
|
||||
Respuesta esperada: { points: 450, equivalent_mxn: 450, history: [...] }
|
||||
|
||||
Enviar via WACRM con formato:
|
||||
|
||||
Tu monedero SKEEN:
|
||||
Saldo actual: 450 puntos (= $450 MXN)
|
||||
Historial reciente:
|
||||
- 03/07/2026: +120 pts (Pago Tratamiento Acne — $1,200)
|
||||
- 28/06/2026: +80 pts (Pago Consulta + peel — $800)
|
||||
- 15/06/2026: +250 pts (Pago Depilacion laser — $2,500)
|
||||
|
||||
Puedes redimir tus puntos en tu proxima visita! Cada 10 puntos = $10 MXN de descuento.
|
||||
|
||||
## Redimir puntos
|
||||
|
||||
1. Verificar que tenga suficientes puntos
|
||||
2. Consultar monto a pagar de la cita/servicio
|
||||
3. Calcular descuento (maximo 50% del total)
|
||||
4. Confirmar redencion
|
||||
5. Aplicar via `odoo_api_call`: modelo skeen.monedero, metodo redeem
|
||||
6. Confirmar redencion via WACRM
|
||||
|
||||
## Reglas
|
||||
|
||||
- Maximo redimible: 50% del total de la compra
|
||||
- Puntos vencen a los 12 meses de inactividad
|
||||
- No transferibles entre pacientes
|
||||
- Se acumulan en pagos confirmados unicamente
|
||||
- En devoluciones, se restan los puntos correspondientes
|
||||
79
hermes/skills/hermes-skeen/skeen-pagos/SKILL.md
Normal file
79
hermes/skills/hermes-skeen/skeen-pagos/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: skeen-pagos
|
||||
description: >
|
||||
Gestion de pagos para SKEEN Derma Experts.
|
||||
Genera links de pago (Stripe/MercadoPago), consulta estados,
|
||||
y gestiona facturacion. Usar cuando el usuario quiera pagar,
|
||||
consultar saldo, o ver sus pagos pendientes.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: [python]
|
||||
env: [STRIPE_SECRET_KEY, MP_ACCESS_TOKEN]
|
||||
envVars:
|
||||
- name: STRIPE_SECRET_KEY
|
||||
required: true
|
||||
- name: MP_ACCESS_TOKEN
|
||||
required: true
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["pagar", "saldo", "deuda", "factura", "pago", "stripe", "mercadopago"]
|
||||
---
|
||||
|
||||
# SKEEN — Gestion de Pagos
|
||||
|
||||
## Instrucciones de Uso
|
||||
|
||||
### PASO 1: Identificar intencion de pago
|
||||
|
||||
- "Quiero pagar" → Generar link de pago
|
||||
- "Cuanto debo?" / "Saldo pendiente" → Consultar deuda
|
||||
- "Ya pague" → Verificar pago
|
||||
- "Necesito factura" → Generar factura (CFDI)
|
||||
|
||||
### PASO 2: Consultar servicios pendientes
|
||||
|
||||
Usa `odoo_api_call` para consultar modelo account.move:
|
||||
Dominio: [['partner_id.phone', '=', PACIENTE_PHONE], ['state', '=', 'posted'], ['payment_state', '!=', 'paid']]
|
||||
Campos: ['name', 'amount_residual', 'invoice_date_due']
|
||||
|
||||
### PASO 3: Generar link de pago
|
||||
|
||||
Para pagos en Mexico (MXN) — usa MercadoPago:
|
||||
Crear preference con items del servicio
|
||||
notification_url: https://skeen.mx/webhooks/mercadopago
|
||||
back_urls: { success: 'https://app.skeen.mx/pagos/exitoso' }
|
||||
|
||||
Para pagos internacionales — usa Stripe Checkout:
|
||||
Crear session con line_items
|
||||
mode: 'payment'
|
||||
success_url: 'https://app.skeen.mx/pagos/exitoso'
|
||||
metadata: { patient_phone: PACIENTE_PHONE }
|
||||
|
||||
### PASO 4: Enviar link de pago
|
||||
|
||||
Usa `wacrm_send` para enviar mensaje con resumen y link.
|
||||
|
||||
Incluir:
|
||||
- Listado de servicios con precios
|
||||
- Total a pagar
|
||||
- Link de pago
|
||||
- Metodos aceptados (tarjeta, OXXO, SPEI)
|
||||
- Puntos de monedero que acumulara
|
||||
|
||||
### PASO 5: Confirmacion de pago
|
||||
|
||||
Cuando el webhook confirme el pago:
|
||||
1. Registra pago en Odoo via `odoo_api_call`
|
||||
2. Calcula puntos de monedero (10% del monto)
|
||||
3. Envia confirmacion + puntos acumulados
|
||||
|
||||
## Reglas
|
||||
|
||||
- Siempre confirmar el monto antes de generar el link
|
||||
- Para pagos >$5,000 MXN, ofrecer opcion de meses sin intereses
|
||||
- Verificar que el telefono coincida con el paciente
|
||||
- Guardar referencia de pago en memoria
|
||||
59
hermes/skills/hermes-skeen/skeen-rag/SKILL.md
Normal file
59
hermes/skills/hermes-skeen/skeen-rag/SKILL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: skeen-rag
|
||||
description: >
|
||||
Catalogo de servicios dermatologicos de SKEEN Derma Experts.
|
||||
Proporciona informacion sobre tratamientos, precios, duracion y contraindicaciones.
|
||||
SOLO usar cuando el usuario pregunte por servicios, tratamientos, precios o el catalogo.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
requires:
|
||||
bins: []
|
||||
envVars:
|
||||
- name: SKEEN_SERVICES_JSON
|
||||
required: false
|
||||
description: "Path al archivo JSON con catalogo de servicios"
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: keyword
|
||||
keywords: ["servicios", "tratamientos", "precios", "catalogo", "acne", "peel", "laser", "dermatologia"]
|
||||
---
|
||||
|
||||
# SKEEN Derma Experts — Catalogo de Servicios
|
||||
|
||||
## Instrucciones de Uso
|
||||
|
||||
Cuando un paciente pregunte por tratamientos, servicios, precios o recomendaciones dermatologicas:
|
||||
|
||||
1. Lee el archivo de catalogo: `${env:SKEEN_SERVICES_JSON:-/skills/data/skeen-servicios.json}`
|
||||
2. Filtra los servicios relevantes segun la pregunta
|
||||
3. Presenta la informacion de forma clara y amigable
|
||||
4. SIEMPRE incluye: nombre del tratamiento, precio aproximado, duracion, y una breve descripcion
|
||||
5. NUNCA diagnostiques condiciones medicas — invita al paciente a agendar una consulta
|
||||
6. Si no encuentras el servicio, ofrece agendar una consulta de valoracion
|
||||
|
||||
## Formato de Respuesta
|
||||
|
||||
Hola! En SKEEN contamos con estos tratamientos para [condicion]:
|
||||
|
||||
1. **[Nombre tratamiento]** — $[precio] MXN
|
||||
Duracion: [X] minutos
|
||||
[Descripcion breve]
|
||||
|
||||
2. **[Nombre tratamiento]** — $[precio] MXN
|
||||
...
|
||||
|
||||
Te gustaria agendar una consulta de valoracion? Podemos evaluar tu caso especifico y recomendarte el mejor tratamiento.
|
||||
|
||||
## Limites
|
||||
|
||||
- NO diagnosticar — solo informacion general
|
||||
- NO recomendar medicamentos especificos
|
||||
- SIEMPRE sugerir consulta presencial para evaluacion
|
||||
- Precios son aproximados — pueden variar segun evaluacion
|
||||
|
||||
## Datos de la Clinica
|
||||
|
||||
- Nombre: SKEEN Derma Experts
|
||||
- Direccion: Blvd. Popotla 123, Playas de Rosarito, BC, Mexico
|
||||
- Horario: Lun-Vie 9:00-18:00, Sab 9:00-14:00
|
||||
- Telefono: +52 (664) 123-4567
|
||||
89
hermes/skills/hermes-skeen/skeen-recordatorios/SKILL.md
Normal file
89
hermes/skills/hermes-skeen/skeen-recordatorios/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: skeen-recordatorios
|
||||
description: >
|
||||
Recordatorios de citas, follow-ups y re-engagement para SKEEN.
|
||||
Usar PRINCIPALMENTE via cron (hermes cron).
|
||||
Tambien puede responder a consultas sobre recordatorios.
|
||||
version: 1.0.0
|
||||
metadata:
|
||||
hermes:
|
||||
always: true
|
||||
requires:
|
||||
bins: []
|
||||
envVars:
|
||||
- name: ODOO_URL
|
||||
required: true
|
||||
- name: WACRM_URL
|
||||
required: true
|
||||
- name: WACRM_API_KEY
|
||||
required: true
|
||||
platforms: [whatsapp, webchat, cli]
|
||||
activation: cron
|
||||
cron:
|
||||
- schedule: "0 */6 * * *" # Cada 6 horas: generar recordatorios
|
||||
command: "skill skeen-recordatorios --action generate"
|
||||
- schedule: "0 18 * * *" # 6 PM diario: enviar recordatorios
|
||||
command: "skill skeen-recordatorios --action send"
|
||||
- schedule: "0 9 * * 1" # Lunes 9 AM: re-engagement
|
||||
command: "skill skeen-recordatorios --action reengage"
|
||||
---
|
||||
|
||||
# SKEEN — Recordatorios y Follow-ups
|
||||
|
||||
## Ejecucion via Cron
|
||||
|
||||
Esta skill se ejecuta automaticamente via `hermes cron`.
|
||||
|
||||
### PASO 1: Generar recordatorios (cada 6 horas)
|
||||
|
||||
Recordatorios de citas (24h antes):
|
||||
Consulta Odoo: salon.booking con date=tomorrow, state=confirmed
|
||||
Para cada cita, programa cron:
|
||||
```bash
|
||||
hermes cron schedule --name "recordatorio-[booking_id]" \
|
||||
--at "[fecha_cita] 18:00 - 1 day" \
|
||||
--command "skill skeen-recordatorios --action send --phone [PHONE] --cita [DATE]"
|
||||
```
|
||||
|
||||
Follow-ups post-tratamiento (7 dias despues):
|
||||
Consulta Odoo: salon.booking con date=7_days_ago, state=done
|
||||
Programa cron para cada paciente:
|
||||
```bash
|
||||
hermes cron schedule --name "followup-[booking_id]" \
|
||||
--at "[fecha_cita + 7 days] 10:00" \
|
||||
--command "skill skeen-recordatorios --action followup --phone [PHONE]"
|
||||
```
|
||||
|
||||
Re-engagement (pacientes inactivos 90 dias):
|
||||
Consulta Odoo: res.partner con last_visit <= 90_days_ago
|
||||
Programa cron semanal:
|
||||
```bash
|
||||
hermes cron schedule --name "reengage-[partner_id]" \
|
||||
--at "lunes 9:00" \
|
||||
--command "skill skeen-recordatorios --action reengage --phone [PHONE]"
|
||||
```
|
||||
|
||||
### PASO 2: Enviar recordatorios (6 PM diario)
|
||||
|
||||
Para cada recordatorio programado para hoy:
|
||||
1. Enviar via WACRM usando `wacrm_send` con template aprobado
|
||||
2. Si es exitoso, marcar como enviado en Odoo
|
||||
3. Si falla, reintentar en siguiente ejecucion (max 3 intentos)
|
||||
|
||||
### PASO 3: Limpieza
|
||||
|
||||
Eliminar cron jobs completados con mas de 7 dias:
|
||||
```bash
|
||||
hermes cron list --completed | awk '$4 > 7 {print $1}' | xargs hermes cron delete
|
||||
```
|
||||
|
||||
## Templates de WhatsApp Aprobados
|
||||
|
||||
recordatorio_cita_skeen:
|
||||
"Hola {{1}}! Te recordamos tu cita manana {{2}} a las {{3}} en SKEEN Derma Experts."
|
||||
|
||||
followup_tratamiento_skeen:
|
||||
"Hola {{1}}! Han pasado {{2}} dias desde tu {{3}}. Como te sientes?"
|
||||
|
||||
reengage_skeen:
|
||||
"Hola {{1}}! Te extranamos en SKEEN. Agenda tu proxima visita y recibe 50 puntos."
|
||||
Reference in New Issue
Block a user