mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(msteams): stream progress tool lines
This commit is contained in:
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Mattermost: accept the documented `channels.mattermost.streaming` config and honor `streaming: "off"` by disabling draft preview posts. Thanks @vincentkoc.
|
||||
- Mattermost: expose streaming progress config labels and help text in generated channel config metadata so Control UI/docs can explain the new `channels.mattermost.streaming.progress.*` fields. Thanks @vincentkoc.
|
||||
- Mattermost: honor `channels.mattermost.streaming.progress.toolProgress=false` in progress draft mode so compact tool status lines stay hidden until final delivery. Thanks @vincentkoc.
|
||||
- Microsoft Teams: honor progress draft tool lines in native Teams progress streams and suppress standalone tool messages when `channels.msteams.streaming.progress.toolProgress=false`. Thanks @vincentkoc.
|
||||
- Discord: keep progress draft boundary callbacks bound during streaming replies, so extension lint stays green while progress previews transition between assistant and reasoning blocks. Thanks @vincentkoc.
|
||||
- Discord: resolve SecretRef-backed bot tokens from the active runtime snapshot for named accounts and keep unresolved configured tokens from crashing status or health checks. (#76987) Thanks @joshavant.
|
||||
- Channels/streaming: expose `streaming.progress.label`, `labels`, `maxLines`, and `toolProgress` in bundled channel config metadata so progress draft settings appear in config, docs, and control surfaces. Thanks @vincentkoc.
|
||||
|
||||
@@ -332,6 +332,39 @@ describe("createMSTeamsReplyDispatcher", () => {
|
||||
expect(streamInstances[0]?.update).toHaveBeenCalledWith("partial response");
|
||||
});
|
||||
|
||||
it("surfaces Teams progress tool lines through native stream updates", async () => {
|
||||
const dispatcher = createDispatcher("personal", {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: {
|
||||
label: "Working",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dispatcher.replyOptions.suppressDefaultToolProgressMessages).toBe(true);
|
||||
await dispatcher.replyOptions.onToolStart?.({ name: "web_search" });
|
||||
|
||||
expect(streamInstances[0]?.sendInformativeUpdate).toHaveBeenCalledWith(
|
||||
"Working\n- tool: web_search",
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses standalone Teams progress messages when progress tool lines are disabled", async () => {
|
||||
const dispatcher = createDispatcher("personal", {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: {
|
||||
toolProgress: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dispatcher.replyOptions.suppressDefaultToolProgressMessages).toBe(true);
|
||||
expect(dispatcher.replyOptions.onToolStart).toBeUndefined();
|
||||
expect(streamInstances[0]?.sendInformativeUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not create a stream for channel conversations", async () => {
|
||||
createDispatcher("channel");
|
||||
|
||||
@@ -446,8 +479,8 @@ describe("createMSTeamsReplyDispatcher", () => {
|
||||
|
||||
describe("pickInformativeStatusText", () => {
|
||||
it("selects a deterministic status line for a fixed random source", () => {
|
||||
expect(pickInformativeStatusText(() => 0)).toBe("Thinking");
|
||||
expect(pickInformativeStatusText(() => 0.99)).toBe("Surfacing");
|
||||
expect(pickInformativeStatusText(() => 0)).toBe("Thinking...");
|
||||
expect(pickInformativeStatusText(() => 0.99)).toBe("Surfacing...");
|
||||
});
|
||||
|
||||
it("honors disabled progress labels", () => {
|
||||
|
||||
@@ -345,6 +345,66 @@ export function createMSTeamsReplyDispatcher(params: {
|
||||
streamController.onPartialReply(payload),
|
||||
}
|
||||
: {}),
|
||||
...(streamController.shouldSuppressDefaultToolProgressMessages()
|
||||
? { suppressDefaultToolProgressMessages: true }
|
||||
: {}),
|
||||
...(streamController.shouldStreamPreviewToolProgress()
|
||||
? {
|
||||
onToolStart: async (payload: { name?: string; phase?: string }) => {
|
||||
await streamController.pushProgressLine(
|
||||
payload.name ? `tool: ${payload.name}` : (payload.phase ?? "tool running"),
|
||||
);
|
||||
},
|
||||
onItemEvent: async (payload: {
|
||||
progressText?: string;
|
||||
summary?: string;
|
||||
title?: string;
|
||||
name?: string;
|
||||
}) => {
|
||||
await streamController.pushProgressLine(
|
||||
payload.progressText ?? payload.summary ?? payload.title ?? payload.name,
|
||||
);
|
||||
},
|
||||
onPlanUpdate: async (payload: {
|
||||
phase?: string;
|
||||
explanation?: string;
|
||||
steps?: string[];
|
||||
}) => {
|
||||
if (payload.phase !== "update") {
|
||||
return;
|
||||
}
|
||||
await streamController.pushProgressLine(
|
||||
payload.explanation ?? payload.steps?.[0] ?? "planning",
|
||||
);
|
||||
},
|
||||
onApprovalEvent: async (payload: { phase?: string; command?: string }) => {
|
||||
if (payload.phase !== "requested") {
|
||||
return;
|
||||
}
|
||||
await streamController.pushProgressLine(
|
||||
payload.command ? `approval: ${payload.command}` : "approval requested",
|
||||
);
|
||||
},
|
||||
onCommandOutput: async (payload: { phase?: string; summary?: string }) => {
|
||||
if (payload.phase !== "end") {
|
||||
return;
|
||||
}
|
||||
await streamController.pushProgressLine(payload.summary ?? "command output ready");
|
||||
},
|
||||
onPatchSummary: async (payload: {
|
||||
phase?: string;
|
||||
summary?: string;
|
||||
title?: string;
|
||||
}) => {
|
||||
if (payload.phase !== "end") {
|
||||
return;
|
||||
}
|
||||
await streamController.pushProgressLine(
|
||||
payload.summary ?? payload.title ?? "patch applied",
|
||||
);
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
disableBlockStreaming:
|
||||
typeof resolvedBlockStreamingEnabled === "boolean"
|
||||
? !resolvedBlockStreamingEnabled
|
||||
|
||||
@@ -268,6 +268,58 @@ describe("createTeamsReplyStreamController", () => {
|
||||
expect(streamInstances[0]?.sendInformativeUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("streams compact Teams progress lines when tool progress is enabled", async () => {
|
||||
streamInstances.length = 0;
|
||||
const ctrl = createTeamsReplyStreamController({
|
||||
conversationType: "personal",
|
||||
context: { sendActivity: vi.fn(async () => ({ id: "a" })) } as never,
|
||||
feedbackLoopEnabled: false,
|
||||
log: { debug: vi.fn() } as never,
|
||||
msteamsConfig: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: {
|
||||
label: "Working",
|
||||
maxLines: 1,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await ctrl.pushProgressLine("tool: search");
|
||||
await ctrl.pushProgressLine("tool: exec");
|
||||
|
||||
expect(ctrl.shouldSuppressDefaultToolProgressMessages()).toBe(true);
|
||||
expect(ctrl.shouldStreamPreviewToolProgress()).toBe(true);
|
||||
expect(streamInstances[0]?.sendInformativeUpdate).toHaveBeenLastCalledWith(
|
||||
"Working\n- tool: exec",
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses Teams default progress messages without stream lines when tool progress is disabled", async () => {
|
||||
streamInstances.length = 0;
|
||||
const ctrl = createTeamsReplyStreamController({
|
||||
conversationType: "personal",
|
||||
context: { sendActivity: vi.fn(async () => ({ id: "a" })) } as never,
|
||||
feedbackLoopEnabled: false,
|
||||
log: { debug: vi.fn() } as never,
|
||||
msteamsConfig: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: {
|
||||
toolProgress: false,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await ctrl.pushProgressLine("tool: search");
|
||||
|
||||
expect(ctrl.shouldSuppressDefaultToolProgressMessages()).toBe(true);
|
||||
expect(ctrl.shouldStreamPreviewToolProgress()).toBe(false);
|
||||
expect(streamInstances[0]?.sendInformativeUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not start native streaming for Teams block mode", async () => {
|
||||
streamInstances.length = 0;
|
||||
const ctrl = createTeamsReplyStreamController({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
formatChannelProgressDraftText,
|
||||
resolveChannelPreviewStreamMode,
|
||||
resolveChannelProgressDraftMaxLines,
|
||||
resolveChannelProgressDraftLabel,
|
||||
resolveChannelStreamingPreviewToolProgress,
|
||||
} from "openclaw/plugin-sdk/channel-streaming";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { MSTeamsConfig, ReplyPayload } from "../runtime-api.js";
|
||||
@@ -40,6 +43,11 @@ export function createTeamsReplyStreamController(params: {
|
||||
const streamMode = resolveChannelPreviewStreamMode(params.msteamsConfig, "partial");
|
||||
const shouldUseNativeStream =
|
||||
isPersonal && (streamMode === "partial" || streamMode === "progress");
|
||||
const shouldSuppressDefaultToolProgressMessages =
|
||||
shouldUseNativeStream && streamMode === "progress";
|
||||
const shouldStreamPreviewToolProgress =
|
||||
shouldSuppressDefaultToolProgressMessages &&
|
||||
resolveChannelStreamingPreviewToolProgress(params.msteamsConfig);
|
||||
const stream = shouldUseNativeStream
|
||||
? new TeamsHttpStream({
|
||||
sendActivity: (activity) => params.context.sendActivity(activity),
|
||||
@@ -52,8 +60,35 @@ export function createTeamsReplyStreamController(params: {
|
||||
|
||||
let streamReceivedTokens = false;
|
||||
let informativeUpdateSent = false;
|
||||
let progressLines: string[] = [];
|
||||
let pendingFinalize: Promise<void> | undefined;
|
||||
|
||||
const pushProgressLine = async (line?: string): Promise<void> => {
|
||||
if (!stream || !shouldStreamPreviewToolProgress) {
|
||||
return;
|
||||
}
|
||||
const normalized = line?.replace(/\s+/g, " ").trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const previous = progressLines.at(-1);
|
||||
if (previous === normalized) {
|
||||
return;
|
||||
}
|
||||
progressLines = [...progressLines, normalized].slice(
|
||||
-resolveChannelProgressDraftMaxLines(params.msteamsConfig),
|
||||
);
|
||||
informativeUpdateSent = true;
|
||||
await stream.sendInformativeUpdate(
|
||||
formatChannelProgressDraftText({
|
||||
entry: params.msteamsConfig,
|
||||
lines: progressLines,
|
||||
seed: params.progressSeed,
|
||||
bullet: "-",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const fallbackAfterStreamFailure = (
|
||||
payload: ReplyPayload,
|
||||
hasMedia: boolean,
|
||||
@@ -100,6 +135,18 @@ export function createTeamsReplyStreamController(params: {
|
||||
stream.update(payload.text);
|
||||
},
|
||||
|
||||
async pushProgressLine(line?: string): Promise<void> {
|
||||
await pushProgressLine(line);
|
||||
},
|
||||
|
||||
shouldSuppressDefaultToolProgressMessages(): boolean {
|
||||
return shouldSuppressDefaultToolProgressMessages;
|
||||
},
|
||||
|
||||
shouldStreamPreviewToolProgress(): boolean {
|
||||
return shouldStreamPreviewToolProgress;
|
||||
},
|
||||
|
||||
async preparePayload(payload: ReplyPayload): Promise<Maybe<ReplyPayload>> {
|
||||
const hasMedia = Boolean(payload.mediaUrl || payload.mediaUrls?.length);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user