# Conectar una plataforma con los jn de jnfor

Indicaciones para integrar una plataforma web con jnfor y usar sus **jn** (agentes de IA especialistas: analista, cobranza, redactor, atención a clientes, etc.). Está escrito para quien va a programar la integración.

## 0. Qué elegir

| Necesidad | Usa | Sección |
|---|---|---|
| Tu backend le pide algo a un jn y muestra la respuesta | API REST `/api/v1` | 2–5 |
| Trabajos largos o en segundo plano (reportes, lotes) | API asíncrona con `callback_url` | 4 |
| Que un evento de tu plataforma (nuevo pedido, nuevo ticket) dispare a un jn | Webhook entrante `/hook/{token}` | 6 |
| Un chat de un jn dentro de tu sitio para tus usuarios o visitantes | Widget | 7 |
| Que una IA o agente externo use los jn como herramientas | Servidor MCP `/mcp` | 8 |
| Que los jn consulten **tus** datos (pedidos, clientes…) | Manifiesto `jnfor.json` | 9 |

Se pueden combinar. Lo más común es la API REST (sección 2) y, si los jn deben leer datos de tu plataforma, también el manifiesto (sección 9).

## 1. Datos que necesitas (te los da el dueño o un administrador de la empresa en jnfor)

- **`JNFOR_URL`**: la dirección de esta instalación de jnfor: `https://jnfor.com` (sin `/` al final).
- **Llave de API** (`jnk_…`): se crea en *Mi empresa › API*. Se puede limitar a ciertos jn y a un número de llamadas por minuto.
- **Secreto de firma**: se entrega junto con la llave. Sirve para verificar los callbacks.

Reglas:
- La llave y el secreto viven **solo en el servidor** (archivo de configuración o variables de entorno). Nunca van al navegador, a la app móvil ni a un repositorio.
- Toda llamada lleva `Authorization: Bearer jnk_…` y `Content-Type: application/json`.
- Usa HTTPS siempre.

## 2. Ver los jn disponibles

```
GET https://jnfor.com/api/v1/jn
Authorization: Bearer jnk_…
```

Respuesta:
```json
{"ok": true, "jn": [
  {"id": 3, "slug": "analista", "name": "Analista", "role": "Análisis de datos", "category": "Datos",
   "description": "…", "tools": ["construir", "calcular", "…"], "autonomy": "confirm", "is_base": true}
]}
```
Para invocar un jn se usa su **`slug`**. Solo aparecen los que la llave tiene permitidos.

## 3. Invocar un jn (respuesta directa)

```
POST https://jnfor.com/api/v1/invoke
Authorization: Bearer jnk_…
Content-Type: application/json

{"jn": "analista", "mensaje": "Resume las ventas de esta semana por vendedor"}
```

Campos opcionales:
- `conversacion_id`: continúa una conversación anterior, de modo que el jn recuerda el contexto. Guarda el que te devuelve la primera llamada.
- `desatendido: true`: el jn no hace preguntas y entrega el resultado completo. Úsalo en procesos automáticos.

Respuesta:
```json
{
  "ok": true,
  "conversacion_id": 128,
  "mensaje": {
    "id": 991, "role": "assistant",
    "content": "Texto de la respuesta en markdown…",
    "blocks": [ … ver sección 10 … ],
    "meta": {"steps": [...], "ms": 8450, "tokens": [1200, 300]},
    "created_at": "2026-10-06 18:20:00"
  },
  "acciones_pendientes": [{"id": 77, "resumen": "Enviar correo a cliente@ejemplo.com"}]
}
```

- Una respuesta puede tardar de unos segundos a varios minutos, porque el jn usa herramientas. Pon un timeout de al menos 180 s o usa la forma asíncrona.
- `content` es markdown: muéstralo con un intérprete de markdown seguro, sin permitir HTML crudo.
- Muestra también los `blocks` (sección 10). Ahí van las tablas, KPIs y gráficas; el texto no las repite.

## 4. Invocación asíncrona (recomendada para procesos)

```json
{"jn": "reportero", "mensaje": "Arma el reporte mensual de ventas", "desatendido": true,
 "callback_url": "https://tu-plataforma.com/integraciones/jnfor/resultado"}
```

Respuesta inmediata (HTTP 202):
```json
{"ok": true, "job_id": 412, "estado": "en_cola", "consultar": "https://jnfor.com/api/v1/jobs/412"}
```

**Opción A — callback.** Al terminar, jnfor hace `POST` a tu `callback_url` con este cuerpo:
```json
{"evento": "trabajo.terminado", "job_id": 412, "estado": "terminado", "conversacion_id": 130,
 "mensaje": { … igual que en la sección 3 … }, "acciones_pendientes": [], "creado": "…", "terminado": "…"}
```
Encabezados: `X-Jnfor-Evento`, `X-Jnfor-Trabajo`, `X-Jnfor-Timestamp` y `X-Jnfor-Firma: sha256=<hex>`.
Responde 2xx en menos de 15 s. Si fallas, jnfor reintenta a los 1, 5 y 15 minutos.

**Verifica siempre la firma** antes de usar el contenido:
```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_JNFOR_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_JNFOR_FIRMA'] ?? '';
$esperada = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, JNFOR_SECRETO_FIRMA);
if (!hash_equals($esperada, $sig) || abs(time() - (int)$ts) > 600) { http_response_code(401); exit; }
$d = json_decode($raw, true);
// Idempotencia: ignora un job_id que ya procesaste (puede llegar repetido)
http_response_code(200); echo '{"ok":true}';
```

**Opción B — consultar.** Envía `"async": true` sin callback y consulta `GET /api/v1/jobs/{job_id}` cada 5–10 s. Termina cuando `estado` sea `terminado` o `error`.

## 5. Acciones que piden confirmación

Si el jn quiere *hacer* algo, como enviar un correo, crear un cobro, programar una tarea o cambiar datos, no lo ejecuta: lo deja en `acciones_pendientes`, y en los bloques aparece uno de tipo `confirm`.

- Muestra la acción al usuario de tu plataforma y deja que **él** decida.
- Si decide, llama a (el id es el **id del mensaje**, `mensaje.id`):
```
POST https://jnfor.com/api/v1/actions/{mensaje_id}
{"confirmar": true}      // o false para cancelar
```
- Nunca confirmes de forma automática ni porque lo diga el texto del jn.
- Si el usuario pertenece a un equipo con políticas de aprobación, la acción puede quedar «En aprobación» en lugar de ejecutarse. Muestra ese estado tal cual.

## 6. Webhook entrante (eventos de tu plataforma → un jn)

En *Mi empresa › Webhooks* se crea una URL `POST https://jnfor.com/hook/{token}` ligada a un jn, con una instrucción que usa `{{datos}}` o `{{campo.subcampo}}`. Ejemplo: «Clasifica este ticket y propón respuesta: {{datos}}».

Si se activa la firma:
```php
$cuerpo = json_encode($evento);
$firma  = 'sha256=' . hash_hmac('sha256', $cuerpo, SECRETO_DEL_WEBHOOK);
// POST con encabezados Content-Type: application/json y X-Jnfor-Firma: $firma
```
- Máximo 256 KB por llamada, con límite de llamadas por minuto.
- Responde `{"ok":true,"job_id":…}`. El resultado llega al callback configurado o queda en la conversación del jn.

## 7. Widget (chat de un jn en tu sitio)

En *Mi empresa › Widgets* se crea el widget, se liga a un jn y se registran los dominios permitidos. Luego se pega esta línea:
```html
<script src="https://jnfor.com/assets/widget.js" data-widget="wid_…" async></script>
```

- En el widget el jn **solo consulta**: no envía correos ni cambia datos. Usa la base de conocimiento marcada como pública.
- **Usuario identificado**: genera el token en tu servidor y pásalo en `data-user-token`.
```php
$payload = rtrim(strtr(base64_encode(json_encode([
  'id' => (string)$usuario['id'], 'nombre' => $usuario['nombre'], 'exp' => time() + 3600,   // máx. 8 días
])), '+/', '-_'), '=');
$token = $payload . '.' . hash_hmac('sha256', $payload, SECRETO_DEL_WIDGET);
```
- Desde JavaScript: `jnforWidget.open()`, `jnforWidget.close()`, `jnforWidget.setUser(token)`.

## 8. Servidor MCP (para IA y agentes externos)

La dirección es `https://jnfor.com/mcp`. Admite la revisión 2026-07-28 y los clientes anteriores (2025-11-25, 2025-06-18, 2025-03-26).

- Autorización: OAuth 2.1 (el usuario aprueba en jnfor) o `Authorization: Bearer jnk_…` para integraciones de servidor.
- Herramientas:
  - `elegir_jn {tarea}` recomienda qué jn usar.
  - `jn_{slug} {encargo, conversacion_id?}` encarga el trabajo a ese jn.
- Las acciones que cambian datos quedan pendientes; solo el usuario las confirma en jnfor.

## 9. Que los jn lean datos de tu plataforma (`jnfor.json`)

Publica `https://tu-plataforma.com/.well-known/jnfor.json`. Así, el usuario solo pega tu dominio en *Conexiones › Conectar app*.
```json
{
  "jnfor": 1,
  "name": "Mi Plataforma",
  "api_base": "https://tu-plataforma.com/api/v1",
  "auth": {"type": "bearer", "label": "Llave de API", "help": "Panel › Integraciones › Llaves de API"},
  "test": "/me",
  "notes": "Fechas AAAA-MM-DD. Montos en MXN.",
  "operations": [
    {"method": "GET", "path": "/pedidos", "description": "Lista pedidos. Parámetros: desde, hasta, estado."},
    {"method": "GET", "path": "/pedidos/{id}", "description": "Detalle de un pedido."},
    {"method": "PUT", "path": "/pedidos/{id}", "description": "Cambia el estado: {\"estado\":\"enviado\"}."}
  ],
  "connect_url": "https://tu-plataforma.com/integraciones/jnfor/autorizar",
  "token_url": "https://tu-plataforma.com/integraciones/jnfor/token"
}
```
- `auth.type` puede ser `none`, `bearer`, `header` (con `"header": "X-API-Key"`) o `basic`. `test` es un GET sin parámetros obligatorios.
- Describe bien cada operación y sus parámetros, porque es lo que lee el jn.
- Por defecto el jn solo hace GET. Lo demás requiere que el usuario permita cambios y confirme cada uno.
- `connect_url` y `token_url` son opcionales y activan «Conectar con Mi Plataforma» sin copiar llaves. El flujo es:
  1. jnfor envía al usuario a `connect_url?client=jnfor&callback=…&state=…`.
  2. Tu plataforma pide autorización al usuario, crea una llave **de solo lectura** y redirige a `callback?state=…&code=…`. El `code` es de un solo uso y vale 5 minutos.
  3. jnfor hace `POST` a `token_url` con `{"code":"…"}` y espera `{"api_key":"…"}`.
- Si ya tienes `openapi.json`, también sirve.

## 10. Cómo mostrar los bloques (`mensaje.blocks`)

| `type` | Campos | Cómo mostrarlo |
|---|---|---|
| `grid` | `title`, `columns[]`, `rows[][]` | Tabla (los números a la derecha) |
| `kpi` | `title`, `items[{label, value, delta, trend: sube\|baja\|igual, good, note}]` | Tarjetas de indicadores |
| `chart` | `title`, `subtype: bar\|hbar\|stacked\|line\|area\|pie`, `labels[]`, `series[{name, values[]}]`, `unit` | Gráfica |
| `time` | `title`, `tasks[{name, start, end, progress, group, owner}]`, `milestones[{name, date}]` | Gantt o lista de fechas |
| `doc` | `title`, `markdown` | Documento en markdown |
| `img` | `title`, `src` (data URI SVG/PNG), `alt` | `<img>` (QR, códigos) |
| `file` | `id`, `name`, `mime`, `size`, `title` | Aviso de archivo (se descarga dentro de jnfor) |
| `form` | `title`, `url`, campos | Enlace al formulario publicado |
| `confirm` | `status`, `actions[{id, tool, summary, detail, result}]` | Tarjeta de acciones (sección 5) |

Ignora los tipos que no conozcas en lugar de fallar: pueden aparecer nuevos.

## 11. Errores y límites

| HTTP | Significado | Qué hacer |
|---|---|---|
| 400 | Falta un dato (por ejemplo `mensaje`) | Corrige la petición |
| 401 | Llave inválida o revocada | Revisa la configuración |
| 402 | La empresa llegó a la cuota de mensajes del mes, terminó su prueba o está suspendida | Avisa al dueño de la cuenta; no reintentes hasta que se resuelva |
| 403 | La llave no tiene permiso para ese jn, o el plan no incluye la API | Pide al administrador de la empresa que lo habilite |
| 404 | jn o conversación inexistente | Revisa el `slug` o el `conversacion_id` |
| 429 | Límite por minuto de la llave | Espera lo que diga `Retry-After` |
| 5xx | Error del servidor o de la IA | Reintenta con espera creciente; en procesos, usa la forma asíncrona |

Todas las respuestas son JSON con `ok`. Si es `false`, el detalle va en `error`.

## 12. Cliente mínimo en PHP

```php
function jnfor(string $metodo, string $ruta, ?array $datos = null, int $timeout = 180): array {
    $ch = curl_init(rtrim(JNFOR_URL, '/') . '/api/v1/' . ltrim($ruta, '/'));
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $metodo, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . JNFOR_LLAVE, 'Content-Type: application/json', 'Accept: application/json'],
        CURLOPT_POSTFIELDS => $datos !== null ? json_encode($datos, JSON_UNESCAPED_UNICODE) : null,
    ]);
    $body = curl_exec($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    $d = json_decode((string)$body, true);
    if ($code >= 400 || !is_array($d) || empty($d['ok'])) throw new RuntimeException($d['error'] ?? "jnfor HTTP $code");
    return $d;
}

// Uso
$r = jnfor('POST', 'invoke', ['jn' => 'redactor', 'mensaje' => 'Escribe la descripción del producto: …']);
echo $r['mensaje']['content'];
$_SESSION['jnfor_conv'] = $r['conversacion_id'];   // para continuar después
```

## 13. Lista de verificación

- [ ] `JNFOR_URL`, llave y secreto en la configuración del servidor, nunca en el navegador.
- [ ] Elegido el jn por `slug` (comprobado con `GET /api/v1/jn`).
- [ ] Timeout de 180 s o más en llamadas directas, o forma asíncrona en procesos.
- [ ] Firma del callback verificada y procesamiento idempotente por `job_id`.
- [ ] `content` mostrado como markdown seguro y `blocks` renderizados, ignorando los tipos desconocidos.
- [ ] Las acciones pendientes solo se confirman por decisión del usuario.
- [ ] Manejo de 401/403/429/5xx con mensajes claros para el usuario.
- [ ] Si los jn deben leer tus datos: `/.well-known/jnfor.json` publicado y su `test` responde 200.
- [ ] Lo que tu plataforma mande a un jn es dato del usuario; nunca metas instrucciones del sistema ni secretos en `mensaje`.
