feat(feishu): add /addwriter /removewriter /writers commands

Adds parity with Telegram and Discord for file-writer management
commands, closing the UX gap where users saw an error mentioning
/addwriter but the Feishu channel had no handler.

- New maybeHandleWriterCommand routes /addwriter, /removewriter,
  /writers from the Feishu inbound flow. Runs at step 5a — after
  checkGroupPolicy — so commands never bypass allowlist or pairing
  enforcement. Step 2a rejects slash commands in DM chats early so
  users get a clear hint without waking the agent pipeline.
- Target user is identified via reply-to (fetches parent message
  sender) or first non-bot @mention. A bare /addwriter with no
  target shows the usage hint instead of silently self-granting,
  preventing accidental privilege capture in empty-writer groups.
- Refuses to run while botOpenID is unresolved so a @mention of
  the bot itself cannot be mistaken for a human target.
- 10s context timeout on each handler bounds worst-case Lark API
  latency (parent message lookup, permission store access).
- feishu.New() gains variadic Option parameter with WithAgentStore
  and WithConfigPermStore mirroring Telegram's pattern. The gateway
  now wires pgStores.Agents and pgStores.ConfigPermissions into
  Feishu channel on startup.
- 12 new unit tests with fakeConfigPermStore and httptest Lark
  server cover DM rejection, nil-store graceful degradation, bootstrap
  via self-mention, bare-command usage hint, bot-probe race refusal,
  non-writer rejection, grant via mention, remove last-writer guard,
  empty and populated list output, reply-to target resolution via
  Lark im/v1/messages lookup, and non-command passthrough. 40 total
  tests in the feishu package, all green with -race.

Closes #818.
This commit is contained in:
viettranx committed 2026-04-11 21:45:37 +07:00
1 parent 934ab7e485
commit 20de0e332e
7 files changed
+824 -29

No files matched your search

+11 -5
View File
@@ -126,12 +126,18 @@ func registerConfigChannels(cfg *config.Config, channelMgr *channels.Manager, ms
if cfg.Channels.Feishu.Enabled {
if cfg.Channels.Feishu.AppID == "" {
recordMissingConfig(channels.TypeFeishu, "Set channels.feishu.app_id in config.")
} else if f, err := feishu.New(cfg.Channels.Feishu, msgBus, pgStores.Pairing, nil); err != nil {
channelMgr.RecordFailure(channels.TypeFeishu, "", err)
slog.Error("failed to initialize feishu channel", "error", err)
} else {
channelMgr.RegisterChannel(channels.TypeFeishu, f)
slog.Info("feishu/lark channel enabled (config)")
feishuOpts := []feishu.Option{
feishu.WithAgentStore(pgStores.Agents),
feishu.WithConfigPermStore(pgStores.ConfigPermissions),
}
if f, err := feishu.New(cfg.Channels.Feishu, msgBus, pgStores.Pairing, nil, feishuOpts...); err != nil {
channelMgr.RecordFailure(channels.TypeFeishu, "", err)
slog.Error("failed to initialize feishu channel", "error", err)
} else {
channelMgr.RegisterChannel(channels.TypeFeishu, f)
slog.Info("feishu/lark channel enabled (config)")
}
}
}
}