新增两套 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>
73 lines
2.1 KiB
Python
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)
|