mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 22:14:37 +00:00
Show an ephemeral "agent is working" indicator (thinking/searching/ generating/analyzing…) in Bitrix24 chat while the agent processes, so users on this non-streaming channel aren't left staring at silence until the final reply. Send behavior is unchanged; no LLM call, no DB. Generic layer (internal/channels): - New optional ActivityIndicatorChannel interface. - HandleAgentEvent routes run.started->THINKING, tool.call->mapped status, tool.result->ANALYZING; static tool->status mapping. - Conditional heartbeat ticker fills LLM-inference gaps (re-sends only when idle), 5s per-run throttle caps call rate; ticker stopped on terminal events AND in UnregisterRun (safety net for missed terminals). Bitrix24 (internal/channels/bitrix24): - OnActivityEvent calls imbot.v2.Chat.InputAction.notify best-effort, drop-on-limit (raw Client.Call, no retry) so cosmetic notifies never steal leaky-bucket capacity from real message sends. - Per-channel activity_indicator toggle (default on). Web UI + docs: dashboard toggle, i18n en/vi/zh, channels doc section. Tests: 26 unit tests incl. UnregisterRun-stops-ticker regression.
72 lines
2.6 KiB
Go
72 lines
2.6 KiB
Go
package bitrix24
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/channels"
|
|
)
|
|
|
|
// Compile-time guard: *Channel must satisfy ActivityIndicatorChannel so
|
|
// HandleAgentEvent's type assertion routes activity events to Bitrix24.
|
|
var _ channels.ActivityIndicatorChannel = (*Channel)(nil)
|
|
|
|
// Activity indicator tuning. The platform auto-expires the indicator after
|
|
// activityDurationSeconds; the channel layer (Manager heartbeat) re-sends before it
|
|
// lapses. The call uses a short timeout so a slow REST round-trip cannot pile up.
|
|
const (
|
|
activityDurationSeconds = 30
|
|
activityCallTimeout = 4 * time.Second
|
|
)
|
|
|
|
// activityIndicatorEnabled reports whether the ephemeral activity indicator is on.
|
|
// Default is on (nil config value).
|
|
func (c *Channel) activityIndicatorEnabled() bool {
|
|
return c.cfg.ActivityIndicator == nil || *c.cfg.ActivityIndicator
|
|
}
|
|
|
|
// OnActivityEvent implements channels.ActivityIndicatorChannel. It shows a native
|
|
// Bitrix24 "agent is working" indicator via imbot.v2.Chat.InputAction.notify.
|
|
//
|
|
// This is COSMETIC and BEST-EFFORT: it deliberately does NOT use
|
|
// callWithRateLimitRetry. On QUERY_LIMIT_EXCEEDED (or any error) the notify is dropped
|
|
// silently so it never retries into — and never steals leaky-bucket capacity from — the
|
|
// real message sends. Rate limits are per-portal/shared-per-IP, so yielding here keeps
|
|
// imbot.v2.Chat.Message.send unaffected.
|
|
//
|
|
// statusCode is a Bitrix action code (IMBOT_AGENT_ACTION_*). Empty → default typing.
|
|
//
|
|
// MCP-DERIVED: imbot.v2.Chat.InputAction.notify param casing (botId/dialogId/
|
|
// statusMessageCode/duration) mirrors imbot.v2.Chat.Message.send; verify live vs the
|
|
// portal before release (Hard Rule #13).
|
|
func (c *Channel) OnActivityEvent(ctx context.Context, chatID, statusCode string) error {
|
|
if !c.activityIndicatorEnabled() {
|
|
return nil
|
|
}
|
|
botID := c.BotID()
|
|
// Liveness guard — mirror Send (send.go): skip when not fully started.
|
|
if c.Client() == nil || botID <= 0 || strings.TrimSpace(chatID) == "" {
|
|
return nil
|
|
}
|
|
|
|
params := map[string]any{
|
|
"botId": botID,
|
|
"dialogId": chatID,
|
|
"duration": activityDurationSeconds,
|
|
}
|
|
if statusCode != "" {
|
|
params["statusMessageCode"] = statusCode
|
|
}
|
|
|
|
cctx, cancel := context.WithTimeout(ctx, activityCallTimeout)
|
|
defer cancel()
|
|
if _, err := c.Client().Call(cctx, "imbot.v2.Chat.InputAction.notify", params); err != nil {
|
|
// Drop-on-limit: cosmetic indicator must never disturb real traffic.
|
|
slog.Debug("bitrix24.activity.notify dropped",
|
|
"chat_id", chatID, "code", statusCode, "err", err)
|
|
}
|
|
return nil
|
|
}
|