Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 27 additions & 45 deletions cmd/anna/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,25 +119,19 @@ func runServer(ctx context.Context, s *setupResult, listFn channel.ModelListFunc
slog.Info("starting telegram bot")

tgBot, err := telegram.New(telegram.Config{
Token: tgCfg.Token,
NotifyChat: tgCfg.NotifyChat,
ChannelID: tgCfg.ChannelID,
GroupMode: tgCfg.GroupMode,
AllowedIDs: tgCfg.AllowedIDs,
Token: tgCfg.Token,
ChannelID: tgCfg.ChannelID,
GroupMode: tgCfg.GroupMode,
}, s.poolManager, s.store, listFn, switchFn,
telegram.WithAuth(as, engine, linkCodes),
)
if err != nil {
return fmt.Errorf("create telegram bot: %w", err)
}

defaultChat := tgCfg.NotifyChat
if defaultChat == "" {
defaultChat = tgCfg.ChannelID
}
channels = append(channels, tgBot)
if tgCfg.EnableNotify {
s.notifier.Register(tgBot, defaultChat)
s.notifier.Register(tgBot)
}
}

Expand All @@ -146,10 +140,9 @@ func runServer(ctx context.Context, s *setupResult, listFn channel.ModelListFunc
slog.Info("starting qq bot")

qqBot, err := qq.New(qq.Config{
AppID: qqCfg.AppID,
AppSecret: qqCfg.AppSecret,
GroupMode: qqCfg.GroupMode,
AllowedIDs: qqCfg.AllowedIDs,
AppID: qqCfg.AppID,
AppSecret: qqCfg.AppSecret,
GroupMode: qqCfg.GroupMode,
}, s.poolManager, s.store, listFn, switchFn,
qq.WithAuth(as, engine, linkCodes),
)
Expand All @@ -159,7 +152,7 @@ func runServer(ctx context.Context, s *setupResult, listFn channel.ModelListFunc

channels = append(channels, qqBot)
if qqCfg.EnableNotify {
s.notifier.Register(qqBot, "")
s.notifier.Register(qqBot)
}
}

Expand All @@ -179,9 +172,7 @@ func runServer(ctx context.Context, s *setupResult, listFn channel.ModelListFunc
AppSecret: fsCfg.AppSecret,
EncryptKey: fsCfg.EncryptKey,
VerificationToken: fsCfg.VerificationToken,
NotifyChat: fsCfg.NotifyChat,
GroupMode: fsCfg.GroupMode,
AllowedIDs: fsCfg.AllowedIDs,
Groups: fsCfg.Groups,
RedirectURI: fsCfg.RedirectURI,
}, s.poolManager, s.store, listFn, switchFn,
Expand All @@ -193,7 +184,7 @@ func runServer(ctx context.Context, s *setupResult, listFn channel.ModelListFunc

channels = append(channels, fsBot)
if fsCfg.EnableNotify {
s.notifier.Register(fsBot, fsCfg.NotifyChat)
s.notifier.Register(fsBot)
}
}

Expand All @@ -202,12 +193,10 @@ func runServer(ctx context.Context, s *setupResult, listFn channel.ModelListFunc
slog.Info("starting weixin bot")

wxBot, err := weixin.New(weixin.Config{
BotToken: wxCfg.BotToken,
BaseURL: wxCfg.BaseURL,
BotID: wxCfg.BotID,
UserID: wxCfg.UserID,
NotifyChat: wxCfg.NotifyChat,
AllowedIDs: wxCfg.AllowedIDs,
BotToken: wxCfg.BotToken,
BaseURL: wxCfg.BaseURL,
BotID: wxCfg.BotID,
UserID: wxCfg.UserID,
}, s.poolManager, s.store, listFn, switchFn,
weixin.WithAuth(as, engine, linkCodes),
)
Expand All @@ -217,7 +206,7 @@ func runServer(ctx context.Context, s *setupResult, listFn channel.ModelListFunc

channels = append(channels, wxBot)
if wxCfg.EnableNotify {
s.notifier.Register(wxBot, wxCfg.NotifyChat)
s.notifier.Register(wxBot)
}
}

Expand Down Expand Up @@ -338,43 +327,36 @@ func launchBrowser(url string) {
// --- Channel config types for JSON deserialization ---

type telegramChannelConfig struct {
Token string `json:"token"`
NotifyChat string `json:"notify_chat"`
ChannelID string `json:"channel_id"`
GroupMode string `json:"group_mode"`
AllowedIDs []int64 `json:"allowed_ids"`
EnableNotify bool `json:"enable_notify"`
Token string `json:"token"`
ChannelID string `json:"channel_id"`
GroupMode string `json:"group_mode"`
EnableNotify bool `json:"enable_notify"`
}

type qqChannelConfig struct {
AppID string `json:"app_id"`
AppSecret string `json:"app_secret"`
GroupMode string `json:"group_mode"`
AllowedIDs []string `json:"allowed_ids"`
EnableNotify bool `json:"enable_notify"`
AppID string `json:"app_id"`
AppSecret string `json:"app_secret"`
GroupMode string `json:"group_mode"`
EnableNotify bool `json:"enable_notify"`
}

type feishuChannelConfig struct {
AppID string `json:"app_id"`
AppSecret string `json:"app_secret"`
EncryptKey string `json:"encrypt_key"`
VerificationToken string `json:"verification_token"`
NotifyChat string `json:"notify_chat"`
GroupMode string `json:"group_mode"`
AllowedIDs []string `json:"allowed_ids"`
Groups map[string]feishu.GroupConfig `json:"groups"`
RedirectURI string `json:"redirect_uri"`
EnableNotify bool `json:"enable_notify"`
}

type weixinChannelConfig struct {
BotToken string `json:"bot_token"`
BaseURL string `json:"base_url"`
BotID string `json:"bot_id"`
UserID string `json:"user_id"`
NotifyChat string `json:"notify_chat"`
AllowedIDs []string `json:"allowed_ids"`
EnableNotify bool `json:"enable_notify"`
BotToken string `json:"bot_token"`
BaseURL string `json:"base_url"`
BotID string `json:"bot_id"`
UserID string `json:"user_id"`
EnableNotify bool `json:"enable_notify"`
}

// loadChannelConfig loads a channel's JSON config from the store and
Expand Down
16 changes: 8 additions & 8 deletions docs/content/docs/features/notification-system.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ type Channel interface {

```go
d := channel.NewDispatcher()
d.Register(tgBot, "136345060") // デフォルトチャット付きtelegramチャネル
d.Register(qqBot, "") // qqチャネル
d.Register(tgBot) // telegramチャネル
d.Register(qqBot) // qqチャネル

// すべてのチャネルにブロードキャスト(各チャネルはデフォルトチャットを使用):
d.Notify(ctx, channel.Notification{Text: "hello"})
Expand Down Expand Up @@ -125,7 +125,7 @@ setup()

runGateway()
+-- Create telegram.Bot
+-- dispatcher.Register(tgBot, notifyChat) <- チャネル登録
+-- dispatcher.Register(tgBot) <- チャネル登録
+-- wireSchedulerNotifier(schedulerSvc, poolManager, dispatcher) <- スケジューラー出力 -> ディスパッチャー
+-- tgBot.Start(ctx) <- ポーリング開始
```
Expand All @@ -146,15 +146,15 @@ CLIモード(`anna chat`)では、通知チャネルは登録されないた

## 設定

チャネル設定は管理パネルで管理されます。各チャネルの設定(トークン、チャットID、グループモード、許可されたID)は、データベースにJSONとして保存されます。設定ファイルを直接編集するのではなく、管理パネルUIから通知チャネルを設定してください。
チャネル設定は管理パネルで管理されます。各チャネルの設定(トークン、チャットID、グループモード)は、データベースにJSONとして保存されます。設定ファイルを直接編集するのではなく、管理パネルUIから通知チャネルを設定してください。

### 通知ターゲット解決

`Notify()`が呼び出されると、ターゲットチャットは次の順序で解決されます。

1. `Notification.ChatID`(呼び出しで明示的)
2. チャネルの登録されたデフォルトチャット(`dispatcher.Register`から
3. Telegramの場合: `notify_chat` -> `channel_id` -> エラー
2. チャネルのデフォルトターゲット(チャネル設定から
3. Telegramの場合: `channel_id` -> エラー

## 新しいチャネルの追加

Expand Down Expand Up @@ -182,7 +182,7 @@ func (b *Bot) Notify(ctx context.Context, n channel.Notification) error {
if slackCfg.Token != "" {
slackBot := slack.New(slackCfg)
channels = append(channels, slackBot)
s.notifier.Register(slackBot, slackCfg.NotifyChannel)
s.notifier.Register(slackBot)
}
```

Expand All @@ -204,7 +204,7 @@ if slackCfg.Token != "" {

### アクセス制御

`allowed_ids`は、ボットとのインタラクションを特定のユーザーID(Telegram数値ID、QQ OpenID、Feishu open_id、WeChat iLinkユーザーID)に制限します。リストが空の場合、すべてのユーザーが許可されます。権限のないユーザーは静かに無視されます。すべてのハンドラー(コマンド、コールバック、テキスト)はアクセスチェックでラップされます。ユーザーは自分のIDを確認するために`/whoami`をボットに送信できます。
アクセス制御はRBACシステムで管理されます。ユーザーはプラットフォームID(Telegram数値ID、QQ OpenID、Feishu open_id、WeChat iLinkユーザーID)を通じて認証システムに自動的に関連付けられます。権限のないユーザーは静かに無視されます。すべてのハンドラー(コマンド、コールバック、テキスト)はアクセスチェックでラップされます。ユーザーは自分のIDを確認するために`/whoami`をボットに送信できます。

### 通知配信

Expand Down
22 changes: 9 additions & 13 deletions docs/content/docs/features/notification-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type Notification struct {

- `Channel` empty -- broadcast to **all** registered channels
- `Channel` set -- route to that specific channel only
- `ChatID` empty -- each channel uses its configured default
- `ChatID` empty -- resolved from auth identities via `NotifyUser`, or channel-specific fallback

### `channel.Channel`

Expand All @@ -73,16 +73,16 @@ Routes notifications to registered channels:

```go
d := channel.NewDispatcher()
d.Register(tgBot, "136345060") // telegram channel with default chat
d.Register(qqBot, "") // qq channel
d.Register(tgBot) // telegram channel
d.Register(qqBot) // qq channel

// Broadcast to all channels (each uses its default chat):
// Broadcast to all channels:
d.Notify(ctx, channel.Notification{Text: "hello"})

// Route to specific channel:
d.Notify(ctx, channel.Notification{Channel: "telegram", Text: "hello"})

// Override the default chat:
// Specify target chat:
d.Notify(ctx, channel.Notification{Channel: "telegram", ChatID: "999", Text: "hello"})
```

Expand Down Expand Up @@ -125,7 +125,7 @@ setup()

runGateway()
+-- Create telegram.Bot
+-- dispatcher.Register(tgBot, notifyChat) <- channel registered
+-- dispatcher.Register(tgBot) <- channel registered
+-- wireSchedulerNotifier(schedulerSvc, poolManager, dispatcher) <- scheduler output -> dispatcher
+-- tgBot.Start(ctx) <- begin polling
```
Expand All @@ -150,11 +150,7 @@ Channel configuration is managed through the admin panel. Each channel's setting

### Notify Target Resolution

When `Notify()` is called, the target chat is resolved in this order:

1. `Notification.ChatID` (explicit in the call)
2. Channel's registered default chat (from `dispatcher.Register`)
3. For Telegram: `notify_chat` -> `channel_id` -> error
For user-owned jobs, `NotifyUser()` resolves the target via `auth_identities` — each user's linked platform identity provides the chat ID. For system jobs, `Notify()` broadcasts to all registered channels using the explicit `ChatID` in the notification.

## Adding a New Channel

Expand Down Expand Up @@ -182,7 +178,7 @@ Use `channel.NewCommander(pool, listFn, switchFn)` for shared `/new`, `/compact`
if slackCfg.Token != "" {
slackBot := slack.New(slackCfg)
channels = append(channels, slackBot)
s.notifier.Register(slackBot, slackCfg.NotifyChannel)
s.notifier.Register(slackBot)
}
```

Expand All @@ -204,7 +200,7 @@ Session ID for groups = group chat ID (shared context per group).

### Access Control

`allowed_ids` restricts bot interaction to specific user IDs (Telegram numeric IDs, QQ OpenIDs, Feishu open_ids, WeChat iLink user IDs). When the list is empty, all users are allowed. Unauthorized users are silently ignored -- all handlers (commands, callbacks, text) are wrapped in the access check. Users can send `/whoami` to the bot to discover their ID.
Access control is managed through the RBAC system. Users are authenticated via `auth_identities` when they send messages, and agent access is enforced by the policy engine. Use the admin panel to manage user roles and agent assignments.

### Notification Delivery

Expand Down
16 changes: 8 additions & 8 deletions docs/content/docs/features/notification-system.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ type Channel interface {

```go
d := channel.NewDispatcher()
d.Register(tgBot, "136345060") // telegram 通道,带默认聊天
d.Register(qqBot, "") // qq 通道
d.Register(tgBot) // telegram 通道
d.Register(qqBot) // qq 通道

// 广播到所有通道(每个使用其默认聊天):
d.Notify(ctx, channel.Notification{Text: "hello"})
Expand Down Expand Up @@ -125,7 +125,7 @@ setup()

runGateway()
+-- Create telegram.Bot
+-- dispatcher.Register(tgBot, notifyChat) <- 通道已注册
+-- dispatcher.Register(tgBot) <- 通道已注册
+-- wireSchedulerNotifier(schedulerSvc, poolManager, dispatcher) <- 调度器输出 -> 分发器
+-- tgBot.Start(ctx) <- 开始轮询
```
Expand All @@ -146,15 +146,15 @@ runGateway()

## 配置

通道配置通过管理面板管理。每个通道的设置(令牌、聊天 ID、群组模式、允许的 ID)作为 JSON 存储在数据库中。从管理面板 UI 配置通知通道,而不是直接编辑配置文件。
通道配置通过管理面板管理。每个通道的设置(令牌、聊天 ID、群组模式)作为 JSON 存储在数据库中。从管理面板 UI 配置通知通道,而不是直接编辑配置文件。

### 通知目标解析

当调用 `Notify()` 时,目标聊天按以下顺序解析:

1. `Notification.ChatID`(调用中的显式值)
2. 通道注册的默认聊天(来自 `dispatcher.Register`)
3. 对于 Telegram: `notify_chat` -> `channel_id` -> 错误
2. 通道的默认目标(来自通道配置)
3. 对于 Telegram: `channel_id` -> 错误

## 添加新通道

Expand Down Expand Up @@ -182,7 +182,7 @@ func (b *Bot) Notify(ctx context.Context, n channel.Notification) error {
if slackCfg.Token != "" {
slackBot := slack.New(slackCfg)
channels = append(channels, slackBot)
s.notifier.Register(slackBot, slackCfg.NotifyChannel)
s.notifier.Register(slackBot)
}
```

Expand All @@ -204,7 +204,7 @@ if slackCfg.Token != "" {

### 访问控制

`allowed_ids` 限制机器人交互到特定用户 ID(Telegram 数字 ID、QQ OpenID、Feishu open_id、微信 iLink 用户 ID)。当列表为空时,允许所有用户。未授权用户会被静默忽略——所有处理器(命令、回调、文本)都包装在访问检查中。用户可以向机器人发送 `/whoami` 来发现他们的 ID。
访问控制通过 RBAC 系统管理。用户通过平台身份(Telegram 数字 ID、QQ OpenID、Feishu open_id、微信 iLink 用户 ID)自动关联到认证系统。未授权用户会被静默忽略——所有处理器(命令、回调、文本)都包装在访问检查中。用户可以向机器人发送 `/whoami` 来发现他们的 ID。

### 通知传递

Expand Down
1 change: 1 addition & 0 deletions internal/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func New(store config.Store, authStore auth.AuthStore, engine *auth.PolicyEngine

// User APIs (admin-only) — memory management and default agent.
s.mux.Handle("PUT /api/users/{id}/default-agent", adminAPI(s.updateUserDefaultAgent))
s.mux.Handle("PUT /api/users/{id}/notify-identity", adminAPI(s.updateUserNotifyIdentity))
s.mux.Handle("GET /api/users/{id}/memories", adminAPI(s.listUserMemories))
s.mux.Handle("PUT /api/users/{id}/memories/{agentId}", adminAPI(s.setUserMemory))
s.mux.Handle("DELETE /api/users/{id}/memories/{agentId}", adminAPI(s.deleteUserMemory))
Expand Down
Loading
Loading