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
- func PlainText(e Event) string
- func RedactWebhook(raw json.RawMessage) json.RawMessage
- func SessionLine(s SessionRef) string
- type Config
- type Coordinator
- func (c *Coordinator) AnyEnabled(ctx context.Context) bool
- func (c *Coordinator) Config(ctx context.Context) (Config, error)
- func (c *Coordinator) Kinds() []string
- func (c *Coordinator) Metrics() MetricsView
- func (c *Coordinator) MetricsSnapshot() MetricsState
- func (c *Coordinator) RestoreMetrics(s MetricsState)
- func (c *Coordinator) Send(ctx context.Context, e Event) EventRecord
- func (c *Coordinator) SetEnabled(ctx context.Context, kind string, enabled bool) error
- func (c *Coordinator) SetSettings(ctx context.Context, kind string, settings json.RawMessage) error
- func (c *Coordinator) Statuses(ctx context.Context) []Status
- func (c *Coordinator) Test(ctx context.Context, kind string, e Event) (res Result, allowed bool)
- type Counts
- type Event
- type EventRecord
- type Kind
- type Metrics
- type MetricsState
- type MetricsView
- type Outcome
- type Provider
- type ProviderConfig
- type ProviderMetric
- type Quota
- type RecentSend
- type Result
- type SessionRef
- type Status
- type Store
- type WebhookSettings
Constants ¶
const ConfigVersion = 1
ConfigVersion is the current schema version (Load upgrades older in place, §12.11).
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 ¶
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).
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 ¶
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).
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.
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.
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 NewFeishuProvider ¶
func NewSlackProvider ¶ added in v0.3.2
func NewWeComProvider ¶
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 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).