feat(channel): structured tool-call IM display with edit-in-place

Introduce a new `show_tool_calls_in_im` bot setting plus a full overhaul of
how tool calls are surfaced in IM channels:

- Add per-bot setting + migration (0072) and expose through settings API /
  handlers / frontend SDK.
- Introduce a `toolCallDroppingStream` wrapper that filters tool_call_* events
  when the setting is off, keeping the rest of the stream intact.
- Add a shared `ToolCallPresentation` model (Header / Body blocks / Footer)
  with plain and Markdown renderers, and a per-tool formatter registry that
  produces rich output (e.g. `web_search` link lists, `list` directory
  previews, `exec` stdout/stderr tails) instead of raw JSON dumps.
- High-capability adapters (Telegram, Feishu, Matrix, Slack, Discord) now
  flush pre-text and then send ONE tool-call message per call, editing it
  in-place from `running` to `completed` / `failed`; mapping from callID to
  platform message ID is tracked per stream, with a fallback to a new
  message if the edit fails. Low-capability adapters (WeCom, QQ, DingTalk)
  keep posting a single final message, but now benefit from the same rich
  per-tool formatting.
- Suppress the early duplicate `EventToolCallStart` (from
  `sdk.ToolInputStartPart`) so that the SDK's final `StreamToolCallPart`
  remains the single source of truth for tool call start, preventing
  duplicated "running" bubbles in IM.
- Stop auto-populating `InputSummary` / `ResultSummary` after a per-tool
  formatter runs, which previously leaked the raw JSON result as a
  fallback footer underneath the formatted body.

Add regression tests for the formatters, the Markdown renderer, the
edit-in-place flow on Telegram/Matrix, and the JSON-leak guard on `list`.
This commit is contained in:
Acbox
2026-04-23 20:49:44 +08:00
parent 35118a81ad
commit 473d559042
36 changed files with 3688 additions and 77 deletions
+13
View File
@@ -374,6 +374,7 @@ func provideChannelRouter(
processor.SetDispatcher(inbound.NewRouteDispatcher(log))
processor.SetSpeechService(audioService, &settingsSpeechModelResolver{settings: settingsService})
processor.SetTranscriptionService(&settingsTranscriptionAdapter{audio: audioService}, &settingsTranscriptionModelResolver{settings: settingsService})
processor.SetIMDisplayOptions(&settingsIMDisplayOptions{settings: settingsService})
cmdHandler := command.NewHandler(
log,
&command.BotMemberRoleAdapter{BotService: botService},
@@ -597,6 +598,18 @@ func (r *settingsSpeechModelResolver) ResolveSpeechModelID(ctx context.Context,
return s.TtsModelID, nil
}
type settingsIMDisplayOptions struct {
settings *settings.Service
}
func (r *settingsIMDisplayOptions) ShowToolCallsInIM(ctx context.Context, botID string) (bool, error) {
s, err := r.settings.GetBot(ctx, botID)
if err != nil {
return false, err
}
return s.ShowToolCallsInIM, nil
}
type settingsTranscriptionModelResolver struct {
settings *settings.Service
}