Skip to content

Commit 111df16

Browse files
committed
fix(feishu): share streaming tool progress labels
1 parent 1fe2b8b commit 111df16

3 files changed

Lines changed: 87 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
5757
- Agents/messaging: deliver distinct final commentary after same-target `message` tool sends while still deduping text/media already sent by the tool, so short closing remarks are no longer silently dropped. Fixes #76915. Thanks @hclsys.
5858
- Agents/messaging: preserve string thread IDs when matching message-tool reply dedupe routes, avoiding precision loss on numeric-looking topic IDs before channel plugin comparison. Thanks @vincentkoc.
5959
- Channels/streaming: honor `agents.defaults.toolProgressDetail: "raw"` in Slack, Discord, Telegram, Matrix, and Microsoft Teams progress drafts, so tool-start lines include raw command/detail output when debugging. Thanks @vincentkoc.
60+
- Feishu: use the shared channel progress formatter for streaming-card tool status lines, including raw command/detail output and message-tool filtering. Thanks @vincentkoc.
6061
- Mattermost: use the shared progress draft formatter for tool status previews, including raw command/detail output when `agents.defaults.toolProgressDetail: "raw"` is enabled. Thanks @vincentkoc.
6162
- OpenAI Codex: honor `auth.order.openai-codex` when starting app-server clients without an explicit auth profile, so status/model probes and implicit startup use the configured Codex account instead of falling back to the default profile. Thanks @vincentkoc.
6263
- OpenAI Codex: let SSRF-guarded provider requests inherit OpenClaw's undici IPv4/IPv6 fallback policy, so ChatGPT-backed Codex runs recover on IPv4-working hosts when DNS still returns unreachable IPv6 addresses. Fixes #76857. Thanks @jplavoiemtl and @SymbolStar.

extensions/feishu/src/reply-dispatcher.test.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
11011101
);
11021102
});
11031103

1104-
it("shows transient tool status on streaming cards but omits it from the final close", async () => {
1104+
it("shows shared transient tool status on streaming cards but omits it from the final close", async () => {
11051105
resolveFeishuAccountMock.mockReturnValue({
11061106
accountId: "main",
11071107
appId: "app_id",
@@ -1124,12 +1124,70 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
11241124
const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) =>
11251125
typeof call[0] === "string" ? call[0] : "",
11261126
);
1127-
expect(updateTexts.some((text) => text.includes("Using: web_search"))).toBe(true);
1127+
expect(updateTexts.some((text) => text.includes("🔎 Web Search"))).toBe(true);
11281128
expect(streamingInstances[0].close).toHaveBeenCalledWith("final answer", {
11291129
note: "Agent: agent",
11301130
});
11311131
});
11321132

1133+
it("shows raw command detail in streaming card tool status", async () => {
1134+
resolveFeishuAccountMock.mockReturnValue({
1135+
accountId: "main",
1136+
appId: "app_id",
1137+
appSecret: "app_secret",
1138+
domain: "feishu",
1139+
config: {
1140+
renderMode: "card",
1141+
streaming: true,
1142+
},
1143+
});
1144+
1145+
const { result, options } = createDispatcherHarness({
1146+
runtime: createRuntimeLogger(),
1147+
});
1148+
await options.onReplyStart?.();
1149+
result.replyOptions.onToolStart?.({
1150+
name: "exec",
1151+
args: { command: "pnpm test -- --watch=false" },
1152+
detailMode: "raw",
1153+
});
1154+
result.replyOptions.onPartialReply?.({ text: "final answer" });
1155+
await options.onIdle?.();
1156+
1157+
const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) =>
1158+
typeof call[0] === "string" ? call[0] : "",
1159+
);
1160+
expect(
1161+
updateTexts.some((text) => text.includes("🛠️ Exec: run tests, `pnpm test -- --watch=false`")),
1162+
).toBe(true);
1163+
});
1164+
1165+
it("omits message-like tools from streaming card status", async () => {
1166+
resolveFeishuAccountMock.mockReturnValue({
1167+
accountId: "main",
1168+
appId: "app_id",
1169+
appSecret: "app_secret",
1170+
domain: "feishu",
1171+
config: {
1172+
renderMode: "card",
1173+
streaming: true,
1174+
},
1175+
});
1176+
1177+
const { result, options } = createDispatcherHarness({
1178+
runtime: createRuntimeLogger(),
1179+
});
1180+
await options.onReplyStart?.();
1181+
result.replyOptions.onToolStart?.({ name: "message" });
1182+
result.replyOptions.onPartialReply?.({ text: "final answer" });
1183+
await options.onIdle?.();
1184+
1185+
const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) =>
1186+
typeof call[0] === "string" ? call[0] : "",
1187+
);
1188+
expect(updateTexts.some((text) => text.includes("Message"))).toBe(false);
1189+
});
1190+
11331191
it("does not suppress a later final after error closeout", async () => {
11341192
resolveFeishuAccountMock.mockReturnValue({
11351193
accountId: "main",

extensions/feishu/src/reply-dispatcher.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback";
22
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
3+
import {
4+
formatChannelProgressDraftLine,
5+
isChannelProgressDraftWorkToolName,
6+
} from "openclaw/plugin-sdk/channel-streaming";
37
import {
48
resolveSendableOutboundReplyParts,
59
resolveTextChunksWithFallback,
@@ -695,10 +699,29 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
695699
: undefined,
696700
onReasoningEnd: reasoningPreviewEnabled ? () => {} : undefined,
697701
onToolStart: streamingEnabled
698-
? (payload: { name?: string; phase?: string }) => {
699-
updateStreamingStatusLine(
700-
`🔧 **Using: ${payload.name ?? payload.phase ?? "tool"}...**`,
702+
? (payload: {
703+
name?: string;
704+
phase?: string;
705+
args?: Record<string, unknown>;
706+
detailMode?: "explain" | "raw";
707+
}) => {
708+
if (!isChannelProgressDraftWorkToolName(payload.name)) {
709+
return;
710+
}
711+
const statusLine = formatChannelProgressDraftLine(
712+
{
713+
event: "tool",
714+
name: payload.name,
715+
phase: payload.phase,
716+
args: payload.args,
717+
},
718+
{
719+
detailMode: payload.detailMode,
720+
},
701721
);
722+
if (statusLine) {
723+
updateStreamingStatusLine(statusLine);
724+
}
702725
}
703726
: undefined,
704727
onAssistantMessageStart: streamingEnabled

0 commit comments

Comments
 (0)