7.80.0 — Émission d'événements vers le journal du service d'agents (Lot 2)
This commit is contained in:
@@ -102,6 +102,9 @@ def create_collection_v2(request: Request, authorization: str | None = Header(de
|
||||
conn.commit()
|
||||
row = conn.execute("SELECT * FROM collections WHERE id=?", (cid,)).fetchone()
|
||||
audit_log(user, "collection.create", "collection", cid, name, request)
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.collection.created", f"collection:{cid}", name,
|
||||
url=url_collection(cid), actor="user")
|
||||
data = {"id": cid, "name": name, "status": "created", "collection": row_to_dict(row)}
|
||||
key = (request.headers.get("Idempotency-Key") or "").strip()
|
||||
if key:
|
||||
@@ -349,6 +352,9 @@ def create_collection_page_v2(collection_id: int, request: Request, authorizatio
|
||||
conn.commit()
|
||||
row = conn.execute("SELECT * FROM collection_pages WHERE id=?", (pid,)).fetchone()
|
||||
audit_log(user, "page.create", "collection_page", pid, title, request)
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.created", f"page:{pid}", title,
|
||||
url=url_collection(collection_id), actor="user")
|
||||
try:
|
||||
run_event_sync(_fire_event("collection.page.created", {"page_id": pid, "collection_id": collection_id, "title": title}))
|
||||
except Exception:
|
||||
@@ -426,6 +432,9 @@ def patch_page_v2(page_id: int, request: Request, authorization: str | None = He
|
||||
conn.execute("UPDATE collection_pages SET title=?, icon=?, position=?, parent_id=?, property_values_json=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (title, icon, pos, parent_id, json.dumps(stored), page_id))
|
||||
conn.commit()
|
||||
audit_log(user, "page.update", "collection_page", page_id, "", request)
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.updated", f"page:{page_id}", title,
|
||||
url=url_collection(row["collection_id"]), actor="user")
|
||||
try:
|
||||
run_event_sync(_fire_event("collection.page.updated", {"page_id": page_id, "collection_id": row["collection_id"], "title": title}))
|
||||
except Exception:
|
||||
@@ -444,6 +453,9 @@ def delete_page_v2(page_id: int, request: Request, authorization: str | None = H
|
||||
conn.execute("DELETE FROM collection_pages WHERE id=?", (page_id,))
|
||||
conn.commit()
|
||||
audit_log(user, "page.delete", "collection_page", page_id, "", request)
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.deleted", f"page:{page_id}", row["title"],
|
||||
url=url_collection(row["collection_id"]), actor="user")
|
||||
try:
|
||||
run_event_sync(_fire_event("collection.page.deleted", {"page_id": page_id, "collection_id": row["collection_id"]}))
|
||||
except Exception:
|
||||
|
||||
@@ -86,6 +86,9 @@ def delete_page(request: Request, page_id: int):
|
||||
conn.execute("UPDATE pages SET deleted_at=? WHERE id=?", (datetime.datetime.now(datetime.UTC).replace(tzinfo=None).isoformat(), page_id,))
|
||||
conn.commit()
|
||||
run_event_sync(fire_event("page.deleted", {"page_id": page_id, "title": row["title"] or ""}))
|
||||
from app.services.agent_evenements import emettre, url_page
|
||||
emettre("flowdeck.page.deleted", f"page:{page_id}", row["title"] or "",
|
||||
url=url_page(page_id), actor="user")
|
||||
return {"status": "ok", "deleted": page_id, "title": row["title"]}
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ def create_page(request: Request, title: str = Query(default=""),
|
||||
page_id = cur.lastrowid
|
||||
run_event_sync(fire_event("page.created", {"page_id": page_id, "title": page_title,
|
||||
"workspace": ws_key, "parent_id": parent_id}))
|
||||
from app.services.agent_evenements import emettre, url_page
|
||||
emettre("flowdeck.page.created", f"page:{page_id}", page_title,
|
||||
url=url_page(page_id), actor="user")
|
||||
return {"status": "ok", "id": page_id, "title": page_title, "workspace": ws_key, "parent_id": parent_id}
|
||||
except Exception as e:
|
||||
logger.error("create_page failed: %s", e)
|
||||
@@ -109,9 +112,14 @@ def update_page(request: Request, page_id: int, title: str = Query(default=""),
|
||||
if content_format:
|
||||
conn.execute("UPDATE pages SET content_format=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (content_format, page_id))
|
||||
conn.commit()
|
||||
trow = conn.execute("SELECT title FROM pages WHERE id=?", (page_id,)).fetchone()
|
||||
page_label = (trow["title"] if trow else title) or ""
|
||||
run_event_sync(fire_event("page.updated", {"page_id": page_id, "title": title,
|
||||
"content_format": content_format or "markdown",
|
||||
"actor_id": user.get("id")}))
|
||||
from app.services.agent_evenements import emettre, url_page
|
||||
emettre("flowdeck.page.updated", f"page:{page_id}", page_label,
|
||||
url=url_page(page_id), actor="user")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
|
||||
@@ -118,6 +118,9 @@ def create_collection_api(request: Request, body: dict = Body(default={})):
|
||||
conn.commit()
|
||||
|
||||
run_event_sync(fire_event("collection.created", {"collection_id": collection_id, "name": name, "icon": icon}))
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.collection.created", f"collection:{collection_id}", name,
|
||||
url=url_collection(collection_id), actor="user")
|
||||
return {"id": collection_id, "name": name, "status": "created"}
|
||||
|
||||
|
||||
|
||||
@@ -119,4 +119,7 @@ def create_page_api(request: Request, collection_id: int, body: dict = Body(defa
|
||||
"collection_id": collection_id,
|
||||
"title": title,
|
||||
}))
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.created", f"page:{page_id}", title,
|
||||
url=url_collection(collection_id), actor="user")
|
||||
return {"id": page_id, "title": title, "status": "created"}
|
||||
|
||||
@@ -149,6 +149,9 @@ def update_page_api(request: Request, page_id: int, body: dict = Body(default={}
|
||||
user = _current_user(request)
|
||||
notify_assignment(existing["collection_id"], page_id, title,
|
||||
stored, props, user.get("id"))
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.updated", f"page:{page_id}", title,
|
||||
url=url_collection(existing["collection_id"]), actor="user")
|
||||
return {"id": page_id, "status": "updated"}
|
||||
|
||||
|
||||
@@ -178,6 +181,9 @@ def delete_page_api(request: Request, page_id: int):
|
||||
"page_id": page_id,
|
||||
"collection_id": existing["collection_id"],
|
||||
}))
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.deleted", f"page:{page_id}", existing["title"],
|
||||
url=url_collection(existing["collection_id"]), actor="user")
|
||||
return {"id": page_id, "status": "deleted"}
|
||||
|
||||
|
||||
|
||||
@@ -237,6 +237,9 @@ def _appel_outil(nom: str, args: dict, user_id: int):
|
||||
conn.commit()
|
||||
page = conn.execute("SELECT * FROM collection_pages WHERE id = ?",
|
||||
(cur.lastrowid,)).fetchone()
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.created", f"page:{page['id']}", title,
|
||||
url=url_collection(collection_id), actor="agent:mcp")
|
||||
return _page_resume(page)
|
||||
if nom == "update_page":
|
||||
try:
|
||||
@@ -265,6 +268,9 @@ def _appel_outil(nom: str, args: dict, user_id: int):
|
||||
conn.commit()
|
||||
updated = conn.execute("SELECT * FROM collection_pages WHERE id = ?",
|
||||
(page_id,)).fetchone()
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.page.updated", f"page:{page_id}", title,
|
||||
url=url_collection(page["collection_id"]), actor="agent:mcp")
|
||||
return _page_resume(updated)
|
||||
if nom == "create_collection":
|
||||
name = str(args.get("name") or "").strip()
|
||||
@@ -287,6 +293,9 @@ def _appel_outil(nom: str, args: dict, user_id: int):
|
||||
conn.commit()
|
||||
coll = conn.execute("SELECT * FROM collections WHERE id = ?",
|
||||
(new_id,)).fetchone()
|
||||
from app.services.agent_evenements import emettre, url_collection
|
||||
emettre("flowdeck.collection.created", f"collection:{new_id}", name,
|
||||
url=url_collection(new_id), actor="agent:mcp")
|
||||
return {"id": coll["id"], "name": coll["name"],
|
||||
"description": coll["description"], "icon": coll["icon"]}
|
||||
return {"erreur": f"outil inconnu : {nom}"}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Émission d'événements vers le journal du service d'agents
|
||||
(Fusion, Lot 2 — contrat ACC-0.2).
|
||||
|
||||
Flowdeck émet ses mutations significatives (pages et collections)
|
||||
vers `POST /api/v1/evenements` du service d'agents, qui les expose
|
||||
dans le « Journal d'aujourd'hui » de la console. L'enveloppe ne
|
||||
contient qu'une référence, un libellé et un lien — jamais le
|
||||
contenu de l'objet : {id, type, occurred_at, actor, domain,
|
||||
source_app, object{ref, label, url}}.
|
||||
|
||||
Authentification : signature HMAC-SHA256 du corps brut, en-tête
|
||||
`X-Draco-Signature`, avec la clé propre à Flowdeck
|
||||
(`AGENT_EVENEMENTS_CLE`, jamais en dur). Sans clé, le module est
|
||||
INACTIF : aucun appel, aucun bruit.
|
||||
|
||||
Discipline « au mieux » (même esprit que le journal du service) :
|
||||
l'envoi part dans un fil d'arrière-plan démon, délai maximal 2 s,
|
||||
et toute erreur est avalée avec une ligne de journal — une panne
|
||||
du récepteur ne doit jamais casser ni ralentir le métier.
|
||||
|
||||
Aucun événement « meeting » n'est émis ici : la décision A/B de
|
||||
Bruno sur le bloc meeting de Flowdeck est toujours ouverte.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
|
||||
log = logging.getLogger("flowdeck.evenements")
|
||||
|
||||
SOURCE_APP = "flowdeck"
|
||||
URL_DEFAUT = "https://agent.dracodev.net/api/v1/evenements"
|
||||
DELAI_S = 2.0
|
||||
|
||||
|
||||
def _cle() -> str:
|
||||
return os.environ.get("AGENT_EVENEMENTS_CLE", "").strip()
|
||||
|
||||
|
||||
def _url() -> str:
|
||||
return os.environ.get("AGENT_EVENEMENTS_URL", "").strip() or URL_DEFAUT
|
||||
|
||||
|
||||
def actif() -> bool:
|
||||
"""L'émission n'existe que si la clé de Flowdeck est configurée."""
|
||||
return bool(_cle())
|
||||
|
||||
|
||||
def url_page(page_id: int) -> str:
|
||||
"""Lien profond d'une page d'espace de travail (vue HTML du board)."""
|
||||
return f"/board/pages/{page_id}"
|
||||
|
||||
|
||||
def url_collection(collection_id: int) -> str:
|
||||
"""Lien profond d'une collection (sa vue tableau). Une page de
|
||||
collection n'a pas d'URL autonome : elle s'ouvre dans cette vue."""
|
||||
return f"/db/{collection_id}/view/table"
|
||||
|
||||
|
||||
def construire_enveloppe(type_: str, objet_ref: str, label: str,
|
||||
url: str = "/", domain: str = "pro",
|
||||
actor: str | None = None) -> tuple[dict, bytes]:
|
||||
"""L'enveloppe ACC-0.2 et son corps JSON exact (celui qui est
|
||||
signé puis envoyé — la signature porte sur ces octets-là)."""
|
||||
env = {
|
||||
"id": uuid.uuid4().hex,
|
||||
"type": type_,
|
||||
"occurred_at": datetime.now(timezone.utc).isoformat(),
|
||||
"actor": actor or "user",
|
||||
"domain": domain or "pro",
|
||||
"source_app": SOURCE_APP,
|
||||
"object": {"ref": objet_ref, "label": label or "", "url": url or "/"},
|
||||
}
|
||||
corps = json.dumps(env, ensure_ascii=False).encode("utf-8")
|
||||
return env, corps
|
||||
|
||||
|
||||
def _signer(corps: bytes) -> str:
|
||||
return hmac.new(_cle().encode("utf-8"), corps, hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def _envoyer(corps: bytes) -> None:
|
||||
"""Le POST signé, dans le fil d'arrière-plan. Avale tout : le
|
||||
journal distant est un confort, jamais une dépendance."""
|
||||
try:
|
||||
with httpx.Client(timeout=DELAI_S, trust_env=False) as client:
|
||||
client.post(
|
||||
_url(), content=corps,
|
||||
headers={"Content-Type": "application/json",
|
||||
"X-Draco-Signature": _signer(corps)},
|
||||
)
|
||||
except Exception as exc: # panne, délai, DNS… : on log et on passe
|
||||
log.warning("événement non livré au journal du service (%s)", exc)
|
||||
|
||||
|
||||
def emettre(type_: str, objet_ref: str, label: str, url: str = "/",
|
||||
domain: str = "pro", actor: str | None = None) -> bool:
|
||||
"""Émet un événement en tâche de fond. Retourne True si l'envoi
|
||||
a été déclenché, False si le module est inactif (pas de clé) ou
|
||||
si l'enveloppe n'a pas pu être construite. Ne lève jamais."""
|
||||
if not actif():
|
||||
return False
|
||||
try:
|
||||
_env, corps = construire_enveloppe(type_, objet_ref, label, url,
|
||||
domain, actor)
|
||||
except Exception:
|
||||
log.warning("événement %s non construit", type_, exc_info=True)
|
||||
return False
|
||||
threading.Thread(target=_envoyer, args=(corps,), daemon=True,
|
||||
name="flowdeck-evenements").start()
|
||||
return True
|
||||
Reference in New Issue
Block a user