"""Modèle Frappe/ERPNext v15 : DocType natif `Custom Block` (carrier du Chat OTOIA). Pourquoi un `Custom Block` (et pas un widget externe) ----------------------------------------------------- La roadmap Sprint 6 demande « Chat OTOIA embedded dans CHAQUE portail ». Contrainte #1 « ERPNext natif = priorité absolue avant tout outil externe » : dans ERPNext v15, un bloc de contenu réutilisable inséré dans un `Workspace` EST le DocType `Custom Block` (`frappe/desk/doctype/custom_block`). Le desk référence un Custom Block depuis le `content` (editor.js) d'un Workspace via un bloc de type `custom_block`. On produit donc un `Custom Block` par portail — aucun framework de chat externe côté worker. Champs `Custom Block` utilisés (v15) : - `block_name` (Data, unique, requis) : identifiant lisible du bloc. - `html` (HTML) : le markup rendu. Ici, UNIQUEMENT un `
` de montage vide et déterministe (id dérivé de la clé du portail). Le web-component OTOIA réel et son JS sont chargés par le thème desk (VPS · agent Frontend · #8) ; le worker ne fabrique aucun script ni endpoint. Anti-invention (#6) : ce module ne connaît AUCUN portail ni rôle en dur, n'émet aucun chiffre et aucune URL. Le `
` de montage porte la clé du portail et la persona (sourcée CLAUDE.md) — rien d'autre. La config runtime (rôles autorisés, portée de connaissance, endpoint) vit dans `chat_mount.json`, pas dans le HTML. Ce module ne touche JAMAIS le VPS : il retourne des dicts sérialisables. """ from __future__ import annotations from typing import Any CUSTOM_BLOCK_DOCTYPE = "Custom Block" # Type de bloc editor.js référençant un Custom Block dans le `content` d'un # Workspace (v15). Fourni au hand-off VPS ; la clé exacte de payload editor.js # peut varier selon le patch de version → marquée `a_confirmer` dans le manifeste. WORKSPACE_CONTENT_BLOCK_TYPE = "custom_block" def mount_html(html_id: str, portail_key: str, persona: str, css_class: str) -> str: """`
` de montage vide, déterministe (aucun chiffre, aucune URL).""" return ( f'
' ) def custom_block_fixture(block_name: str, html: str) -> dict[str, Any]: """Fixture `Custom Block` importable (bench). Champs standard posés par Frappe à l'import (name/owner/idx) volontairement non fabriqués.""" return { "doctype": CUSTOM_BLOCK_DOCTYPE, "block_name": block_name, "html": html, }