notify

package
v0.7.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package notify is the host-agnostic notification provider abstraction shared by deepwork-terminal and deepwork-pro (imported like agentintel). It defines:

  • Event: the structured "what happened", rendered per-provider (no rendering logic leaks into the data contract).
  • Provider: a delivery channel (WeChat iLink / web push / Feishu / DingTalk / WeCom). Stateless w.r.t. config — its ProviderConfig is passed in per call by the one config owner (Coordinator), so there is a single join point.
  • Config / Store: the provider on/off + credentials SSOT. The host supplies a concrete Store (terminal = encrypted local file; pro = per-user DB; a per-user store instance is bound at construction so Load/Save need no userID param).
  • Coordinator: fans an Event out to every ENABLED provider (no priority/fallback orchestration — fan-out is the model), records one EventRecord per event.

The package is pure: no HTTP server, no tmux, no global state. Webhook providers (Feishu/DingTalk/WeCom) live here too (generic HTTP, reusable by pro); iLink and web push are terminal-specific and implement Provider in package terminal.

Index

Constants

View Source
const ConfigVersion = 1

ConfigVersion is the current schema version (Load upgrades older in place, §12.11).

View Source
const TestCooldown = 8 * time.Second

TestCooldown rate-limits the manual "send test" PER provider (so testing WeChat doesn't block testing Feishu, and rapid taps can't drain a channel's quota).

Variables

This section is empty.

Functions

func PlainText

func PlainText(e Event) string

PlainText renders an Event to the plain-text BODY used by text-only channels (WeChat iLink) and as the web-push body. The Title is a SEPARATE notification field (the caller supplies it), so it is not repeated here. Header counts + the grouped, capped session lists derive from the SAME Event (single source — §12.2): Counts is the true tally, Sessions is the recency-sorted full list this caps.

func RedactWebhook

func RedactWebhook(raw json.RawMessage) json.RawMessage

RedactWebhook returns a display-safe copy of settings: URL/secret reduced to a masked tail so the config API and logs never expose the credential (§12.8).

func SessionLine

func SessionLine(s SessionRef) string

SessionLine renders one session as a plain-text list item (also reused by rich providers that fall back to text). 🆕 marks a session that just changed state.

Types

type Config

type Config struct {
	Version   int              `json:"version"`
	Providers []ProviderConfig `json:"providers"`
}

Config is the provider config SSOT (shared data schema; persisted by the host's Store).

func (Config) Enabled

func (c Config) Enabled(kind string) bool

Enabled reports whether a kind is present AND enabled.

func (Config) Get

func (c Config) Get(kind string) (ProviderConfig, bool)

Get returns the config for a kind (zero value + false when absent).

type Coordinator

type Coordinator struct {
	// contains filtered or unexported fields
}

Coordinator is the one fan-out point and the one config-join point.

func NewCoordinator

func NewCoordinator(store Store, now func() time.Time, providers ...Provider) *Coordinator

NewCoordinator builds a coordinator. providers are registered in the given order (display + send order). now defaults to time.Now when nil (overridable in tests).

func (*Coordinator) AnyEnabled added in v0.7.6

func (c *Coordinator) AnyEnabled(ctx context.Context) bool

AnyEnabled reports whether at least one provider is toggled on — the gate for whether the background notifier is worth running at all (there is a channel to deliver to). A load error reads as "none" so a broken store can't spin the poller up pointlessly.

func (*Coordinator) Config

func (c *Coordinator) Config(ctx context.Context) (Config, error)

Config returns the current config with schema defaults applied.

func (*Coordinator) Kinds

func (c *Coordinator) Kinds() []string

Knownkinds returns the registered provider kinds in display order.

func (*Coordinator) Metrics

func (c *Coordinator) Metrics() MetricsView

Metrics returns the metrics view for the status API.

func (*Coordinator) MetricsSnapshot

func (c *Coordinator) MetricsSnapshot() MetricsState

MetricsSnapshot exports the metrics state for the host to persist (the package stays disk-agnostic). RestoreMetrics imports it on startup so send history / counters survive a restart (the "可排障" intent: see WHY a channel failed last).

func (*Coordinator) RestoreMetrics

func (c *Coordinator) RestoreMetrics(s MetricsState)

func (*Coordinator) Send

func (c *Coordinator) Send(ctx context.Context, e Event) EventRecord

Send fans e out to every ENABLED provider CONCURRENTLY and records one EventRecord. Disabled providers are skipped (invariant §8.2: "关了不推" is enforced here, not in docs). Each provider runs isolated (safeSend): one slow or panicking channel never blocks the others past its own timeout, nor kills the caller (the notifier goroutine). Wall time ≈ the slowest single provider, not the sum.

func (*Coordinator) SetEnabled

func (c *Coordinator) SetEnabled(ctx context.Context, kind string, enabled bool) error

SetEnabled flips one provider on/off and persists (the UI toggle path).

func (*Coordinator) SetSettings

func (c *Coordinator) SetSettings(ctx context.Context, kind string, settings json.RawMessage) error

SetSettings replaces one provider's per-kind Settings (webhook url/secret) and persists. Settings are write-only from the UI's perspective (GET redacts them).

func (*Coordinator) Statuses

func (c *Coordinator) Statuses(ctx context.Context) []Status

Statuses is the SINGLE join point: provider runtime Status ⨝ Config.Enabled ⨝ metrics TodaySent. Nothing else joins these sources (§12.3).

func (*Coordinator) Test

func (c *Coordinator) Test(ctx context.Context, kind string, e Event) (res Result, allowed bool)

Test sends a single KindTest event to one provider (manual UI "发测试"). It is per-provider cooldown-gated and records to metrics like a real event. Returns the honest Result (never a fake "sent"). cooldown reports false when gated.

type Counts

type Counts struct {
	Waiting int `json:"waiting"`
	Idle    int `json:"idle"`
	Running int `json:"running"`
}

Counts is the live header tally across ALL tracked sessions (never capped — the list may be truncated for display but the counts stay true; §12.2 single source).

type Event

type Event struct {
	Title    string            `json:"title"`
	Kind     Kind              `json:"kind"`
	Counts   Counts            `json:"counts"`
	Sessions []SessionRef      `json:"sessions"` // recency-sorted; ALL live (renderer caps for display)
	Summary  string            `json:"summary"`  // optional pre-rolled stat blocks (delta/today), host-built
	DeepURL  string            `json:"deepUrl"`  // tap target; may be empty
	Extra    map[string]string `json:"extra"`    // free fields for non-session hosts (pro: platform/count)
}

Event is the structured notification. No rendering methods, no provider knowledge — PlainText(Event) (package fn) and per-provider renderers consume it.

type EventRecord

type EventRecord struct {
	At      time.Time `json:"at"`
	Kind    Kind      `json:"kind"`
	Results []Result  `json:"results"`
}

EventRecord is exactly one fanned-out event with each enabled provider's result. Replaces the old two-valued record(channel, fellBack): under fan-out there is no primary/fallback, so "fellBack" is gone.

type Kind

type Kind int

Kind classifies what happened so a provider can pick an icon / urgency and a non-session host (pro) can map its own events onto a shared vocabulary.

const (
	KindInfo    Kind = iota // generic
	KindWaiting             // an agent needs user input (most urgent)
	KindDone                // an agent finished a turn (idle, can continue)
	KindRunning             // an agent is working
	KindTest                // a manual "send test" from the settings UI
)

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics is the per-provider delivery tally (one record per fanned-out event). SSOT for "what was notified and how" — no second tally to drift against.

type MetricsState

type MetricsState struct {
	Events      int                     `json:"events"`
	LastMs      int64                   `json:"lastMs"`
	TodayDay    int                     `json:"todayDay"`
	Sent        map[string]int          `json:"sent"`
	Dormant     map[string]int          `json:"dormant"`
	Failed      map[string]int          `json:"failed"`
	Today       map[string]int          `json:"today"`
	LastSuccess map[string]int64        `json:"lastSuccess"` // provider → unix ms
	Recent      map[string][]RecentSend `json:"recent"`
}

MetricsState is the serializable metrics snapshot the host persists to disk.

type MetricsView

type MetricsView struct {
	Events      int              `json:"events"`
	LastAtMs    int64            `json:"lastAtMs"` // 0 = never
	PerProvider []ProviderMetric `json:"perProvider"`
}

MetricsView is the JSON-friendly metrics snapshot for the status API.

type Outcome

type Outcome int

Outcome is one provider's result for one Send. Generalized from ilinkOutcome.

const (
	OutcomeNotConfigured Outcome = iota // channel not set up (no creds / not logged in)
	OutcomeSent                         // accepted by the channel
	OutcomeDormant                      // configured but temporarily unable (quota/session window)
	OutcomeFailed                       // attempted and rejected/errored
)

func (Outcome) String

func (o Outcome) String() string

type Provider

type Provider interface {
	Kind() string // stable id: "ilink" | "webpush" | "feishu" | "dingtalk" | "wecom"
	Name() string // display: "微信" | "浏览器" | "飞书" | "钉钉" | "企业微信"
	// Send renders e to the channel's native format and delivers it. cfg is this
	// provider's current config (Settings carries webhook url/secret etc.). It returns
	// the Outcome plus a short human-readable detail for troubleshooting — the failure
	// reason on failure (e.g. "订阅失效(410 Gone)", "BadJwtToken", "休眠:配额满"), "" on
	// clean success. The detail flows into the recent-3 history shown in the UI.
	Send(ctx context.Context, e Event, cfg ProviderConfig) (Outcome, string)
	// Status reports runtime health. The provider does NOT set Status.Enabled
	// (Coordinator joins it from Config) nor TodaySent (Coordinator joins metrics).
	Status(ctx context.Context, cfg ProviderConfig) Status
}

Provider is one delivery channel. STATELESS w.r.t. config: the matching ProviderConfig is passed in per call by the Coordinator (the one config owner).

func NewDingTalkProvider

func NewDingTalkProvider(now func() time.Time) Provider

func NewFeishuProvider

func NewFeishuProvider(now func() time.Time) Provider

func NewSlackProvider added in v0.3.2

func NewSlackProvider(now func() time.Time) Provider

func NewWeComProvider

func NewWeComProvider(now func() time.Time) Provider

type ProviderConfig

type ProviderConfig struct {
	Kind     string          `json:"kind"`
	Enabled  bool            `json:"enabled"`
	Settings json.RawMessage `json:"settings,omitempty"` // per-kind (webhook: {url,secret})
}

ProviderConfig is one channel's on/off + per-kind typed Settings.

type ProviderMetric

type ProviderMetric struct {
	Provider        string       `json:"provider"`
	Sent            int          `json:"sent"`
	Dormant         int          `json:"dormant"`
	Failed          int          `json:"failed"`
	LastSuccessAtMs int64        `json:"lastSuccessAtMs"` // 0 = never succeeded
	Recent          []RecentSend `json:"recent"`          // last recentCap attempts (newest last)
}

ProviderMetric is one provider's tally + troubleshooting trail for the status view. Today's count is NOT here — it lives once on Status.TodaySent (single source); this carries the lifetime tally + last-success + the recent-3 trail.

type Quota

type Quota struct {
	Used int `json:"used"`
	Max  int `json:"max"`
}

Quota is a channel's send budget within its current window (iLink ~10/window).

type RecentSend

type RecentSend struct {
	AtMs    int64   `json:"atMs"`
	Outcome Outcome `json:"outcome"`
	Detail  string  `json:"detail,omitempty"` // failure reason ("订阅失效(410)", "BadJwtToken"…)
}

RecentSend is one historical send outcome — the recent-3 troubleshooting trail.

type Result

type Result struct {
	Provider string  `json:"provider"`
	Outcome  Outcome `json:"outcome"`
	Detail   string  `json:"detail,omitempty"` // optional ("429", "BadJwtToken", "dormant: quota")
}

Result is one provider's outcome for one event.

type SessionRef

type SessionRef struct {
	Tool        string `json:"tool"`        // "claude" | "codex"
	Location    string `json:"location"`    // readable "where": "main · 窗口3 TERMINAL · 面板1"
	Status      string `json:"status"`      // "waiting" | "idle" | "running"
	JustChanged bool   `json:"justChanged"` // transitioned in this batch → 🆕 marker
	Turns       int    `json:"turns"`
	Stats       string `json:"stats"` // optional compact stats ("in 29k out 532k cc 2.3M cr 152.9M ~$112")
}

SessionRef is one agent session referenced by a notification. Pure semantic data — providers render it to their native format (plain text vs interactive card).

type Status

type Status struct {
	Kind           string `json:"kind"`
	Name           string `json:"name"`
	Enabled        bool   `json:"enabled"`    // filled by Coordinator from Config, not by the provider
	Configured     bool   `json:"configured"` // has creds / logged in / webhook set
	Healthy        bool   `json:"healthy"`
	Quota          *Quota `json:"quota,omitempty"` // nil for channels without a quota
	TodaySent      int    `json:"todaySent"`       // filled by Coordinator from metrics, not the provider
	ActivationHint string `json:"activationHint"`  // e.g. "回任意字符续订" / "未配置 webhook"(no creds)
}

Status is a provider's runtime health, joined with config Enabled by the Coordinator (the SINGLE join point — providers MUST NOT set Enabled themselves). It carries NO credential plaintext (§12.8).

type Store

type Store interface {
	Load(ctx context.Context) (Config, error)
	Save(ctx context.Context, c Config) error
}

Store persists the Config. A per-user store instance is bound at construction (pro), so Load/Save carry no userID — the user dimension is the host's concern.

type WebhookSettings

type WebhookSettings struct {
	URL    string `json:"url"`
	Secret string `json:"secret,omitempty"` // optional signing secret (Feishu/DingTalk 加签)
}

WebhookSettings is the per-provider config for the IM-bot webhook channels (Feishu / DingTalk / WeCom). URL is itself a credential (it embeds a token), so it is encrypted at rest and redacted in any status/log output (§12.8).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL