Files
xuhdandClaude Opus 4.7 8162719e36 feat(skills/notifications): 多租户阶段 1 — 持久化层
新增两套 persistence 模块,仿 persistence/agents/ 模板:

1. persistence/skills/
   - SkillRow ORM (PK=name, owner_user_id, published, timestamps)
   - SkillStore 抽象:list_visible / get_visible / get_any /
     get_owned / create / ensure_legacy / update / update_admin /
     delete / delete_admin
   - SqlAlchemy 实现 SkillRepository
   - MemorySkillStore 用于测试
   - 可见性 = owner OR published OR builtin

2. persistence/notifications/
   - NotificationRow ORM (id, user_id, type, title, body, payload JSON,
     read, created_at)
   - NotificationStore 抽象:create / list_for_user / unread_count /
     mark_read / mark_all_read / delete
   - SqlAlchemy 实现 NotificationRepository
   - MemoryNotificationStore 用于测试
   - sender.notify(user_id, type, ...) 全局接口;harness 不能 import
     app.*,所以用 set_notification_store / get_notification_store
     注入模式

3. persistence/models/__init__.py 注册 SkillRow + NotificationRow

后续阶段会接入路由、cascade、curator per-user 重构、前端。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:44:36 +08:00

73 lines
2.1 KiB
Python

"""Helper for writing notifications without callers having to thread a store.
The harness can NOT import ``app.*`` (firewall), so the Gateway app injects
its ``NotificationStore`` instance via ``set_notification_store(...)`` at
startup. Until that happens, every call is a no-op (silently logged) so
non-Gateway entry points (tests, embedded client) don't crash on missing
infrastructure.
"""
from __future__ import annotations
import logging
import uuid
from typing import Any
from deerflow.persistence.notifications.base import NotificationStore
logger = logging.getLogger(__name__)
_store: NotificationStore | None = None
def set_notification_store(store: NotificationStore | None) -> None:
"""Inject the active ``NotificationStore`` instance for the process.
Pass ``None`` to clear (e.g. on shutdown or in tests).
"""
global _store
_store = store
def get_notification_store() -> NotificationStore | None:
return _store
async def notify(
user_id: str,
*,
type: str,
title: str = "",
body: str = "",
payload: dict[str, Any] | None = None,
) -> None:
"""Write a notification for ``user_id``. Never raises — failures are logged.
Args:
user_id: Recipient.
type: Stable identifier (e.g. ``agent_skill_removed``, ``skill_taken_down``).
title: One-line title shown in the inbox.
body: Longer description shown when the user expands the entry.
payload: Structured context (skill_name, agent_id, actor_user_id, ...).
"""
try:
if _store is None:
logger.warning(
"notify(user_id=%s, type=%s) dropped — NotificationStore not configured",
user_id,
type,
)
return
await _store.create(
{
"id": str(uuid.uuid4()),
"user_id": user_id,
"type": type,
"title": title,
"body": body,
"payload": payload or {},
}
)
except Exception:
logger.exception("Failed to write notification (user_id=%s, type=%s)", user_id, type)