mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 18:18:28 +00:00
feat(slack): add native text streaming support
Adds support for Slack's Agents & AI Apps text streaming APIs (chat.startStream, chat.appendStream, chat.stopStream) to deliver LLM responses as a single updating message instead of separate messages per block. Changes: - New src/slack/streaming.ts with stream lifecycle helpers using the SDK's ChatStreamer (client.chatStream()) - New 'streaming' config option on SlackAccountConfig - Updated dispatch.ts to route block replies through the stream when enabled, with graceful fallback to normal delivery - Docs in docs/channels/slack.md covering setup and requirements The streaming integration works by intercepting the deliver callback in the reply dispatcher. When streaming is enabled and a thread context exists, the first text delivery starts a stream, subsequent deliveries append to it, and the stream is finalized after dispatch completes. Media payloads and error cases fall back to normal message delivery. Refs: - https://docs.slack.dev/ai/developing-ai-apps#streaming - https://docs.slack.dev/reference/methods/chat.startStream - https://docs.slack.dev/reference/methods/chat.appendStream - https://docs.slack.dev/reference/methods/chat.stopStream
This commit is contained in:
136
src/slack/streaming.ts
Normal file
136
src/slack/streaming.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Slack native text streaming helpers.
|
||||
*
|
||||
* Uses the Slack SDK's `ChatStreamer` (via `client.chatStream()`) to stream
|
||||
* text responses word-by-word in a single updating message, matching Slack's
|
||||
* "Agents & AI Apps" streaming UX.
|
||||
*
|
||||
* @see https://docs.slack.dev/ai/developing-ai-apps#streaming
|
||||
* @see https://docs.slack.dev/reference/methods/chat.startStream
|
||||
* @see https://docs.slack.dev/reference/methods/chat.appendStream
|
||||
* @see https://docs.slack.dev/reference/methods/chat.stopStream
|
||||
*/
|
||||
|
||||
import type { ChatStreamer, WebClient } from "@slack/web-api";
|
||||
import { logVerbose } from "../globals.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type SlackStreamSession = {
|
||||
/** The SDK ChatStreamer instance managing this stream. */
|
||||
streamer: ChatStreamer;
|
||||
/** Channel this stream lives in. */
|
||||
channel: string;
|
||||
/** Thread timestamp (required for streaming). */
|
||||
threadTs: string;
|
||||
/** True once stop() has been called. */
|
||||
stopped: boolean;
|
||||
};
|
||||
|
||||
export type StartSlackStreamParams = {
|
||||
client: WebClient;
|
||||
channel: string;
|
||||
threadTs: string;
|
||||
/** Optional initial markdown text to include in the stream start. */
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export type AppendSlackStreamParams = {
|
||||
session: SlackStreamSession;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type StopSlackStreamParams = {
|
||||
session: SlackStreamSession;
|
||||
/** Optional final markdown text to append before stopping. */
|
||||
text?: string;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stream lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Start a new Slack text stream.
|
||||
*
|
||||
* Returns a {@link SlackStreamSession} that should be passed to
|
||||
* {@link appendSlackStream} and {@link stopSlackStream}.
|
||||
*
|
||||
* The first chunk of text can optionally be included via `text`.
|
||||
*/
|
||||
export async function startSlackStream(
|
||||
params: StartSlackStreamParams,
|
||||
): Promise<SlackStreamSession> {
|
||||
const { client, channel, threadTs, text } = params;
|
||||
|
||||
logVerbose(`slack-stream: starting stream in ${channel} thread=${threadTs}`);
|
||||
|
||||
const streamer = client.chatStream({
|
||||
channel,
|
||||
thread_ts: threadTs,
|
||||
});
|
||||
|
||||
const session: SlackStreamSession = {
|
||||
streamer,
|
||||
channel,
|
||||
threadTs,
|
||||
stopped: false,
|
||||
};
|
||||
|
||||
// If initial text is provided, send it as the first append which will
|
||||
// trigger the ChatStreamer to call chat.startStream under the hood.
|
||||
if (text) {
|
||||
await streamer.append({ markdown_text: text });
|
||||
logVerbose(`slack-stream: appended initial text (${text.length} chars)`);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append markdown text to an active Slack stream.
|
||||
*/
|
||||
export async function appendSlackStream(params: AppendSlackStreamParams): Promise<void> {
|
||||
const { session, text } = params;
|
||||
|
||||
if (session.stopped) {
|
||||
logVerbose("slack-stream: attempted to append to a stopped stream, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
await session.streamer.append({ markdown_text: text });
|
||||
logVerbose(`slack-stream: appended ${text.length} chars`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop (finalize) a Slack stream.
|
||||
*
|
||||
* After calling this the stream message becomes a normal Slack message.
|
||||
* Optionally include final text to append before stopping.
|
||||
*/
|
||||
export async function stopSlackStream(params: StopSlackStreamParams): Promise<void> {
|
||||
const { session, text } = params;
|
||||
|
||||
if (session.stopped) {
|
||||
logVerbose("slack-stream: stream already stopped, ignoring duplicate stop");
|
||||
return;
|
||||
}
|
||||
|
||||
session.stopped = true;
|
||||
|
||||
logVerbose(
|
||||
`slack-stream: stopping stream in ${session.channel} thread=${session.threadTs}${
|
||||
text ? ` (final text: ${text.length} chars)` : ""
|
||||
}`,
|
||||
);
|
||||
|
||||
await session.streamer.stop(text ? { markdown_text: text } : undefined);
|
||||
|
||||
logVerbose("slack-stream: stream stopped");
|
||||
}
|
||||
Reference in New Issue
Block a user