diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d153a1056e..a4f2783021f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai - 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. - 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. - 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. +- Feishu: use the shared channel progress formatter for streaming-card tool status lines, including raw command/detail output and message-tool filtering. Thanks @vincentkoc. - 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. - 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. - 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. diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index 94e6f7bdbb0..b85e7ae2950 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -1101,7 +1101,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { ); }); - it("shows transient tool status on streaming cards but omits it from the final close", async () => { + it("shows shared transient tool status on streaming cards but omits it from the final close", async () => { resolveFeishuAccountMock.mockReturnValue({ accountId: "main", appId: "app_id", @@ -1124,12 +1124,70 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) => typeof call[0] === "string" ? call[0] : "", ); - expect(updateTexts.some((text) => text.includes("Using: web_search"))).toBe(true); + expect(updateTexts.some((text) => text.includes("🔎 Web Search"))).toBe(true); expect(streamingInstances[0].close).toHaveBeenCalledWith("final answer", { note: "Agent: agent", }); }); + it("shows raw command detail in streaming card tool status", async () => { + resolveFeishuAccountMock.mockReturnValue({ + accountId: "main", + appId: "app_id", + appSecret: "app_secret", + domain: "feishu", + config: { + renderMode: "card", + streaming: true, + }, + }); + + const { result, options } = createDispatcherHarness({ + runtime: createRuntimeLogger(), + }); + await options.onReplyStart?.(); + result.replyOptions.onToolStart?.({ + name: "exec", + args: { command: "pnpm test -- --watch=false" }, + detailMode: "raw", + }); + result.replyOptions.onPartialReply?.({ text: "final answer" }); + await options.onIdle?.(); + + const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) => + typeof call[0] === "string" ? call[0] : "", + ); + expect( + updateTexts.some((text) => text.includes("🛠️ Exec: run tests, `pnpm test -- --watch=false`")), + ).toBe(true); + }); + + it("omits message-like tools from streaming card status", async () => { + resolveFeishuAccountMock.mockReturnValue({ + accountId: "main", + appId: "app_id", + appSecret: "app_secret", + domain: "feishu", + config: { + renderMode: "card", + streaming: true, + }, + }); + + const { result, options } = createDispatcherHarness({ + runtime: createRuntimeLogger(), + }); + await options.onReplyStart?.(); + result.replyOptions.onToolStart?.({ name: "message" }); + result.replyOptions.onPartialReply?.({ text: "final answer" }); + await options.onIdle?.(); + + const updateTexts = streamingInstances[0].update.mock.calls.map((call: unknown[]) => + typeof call[0] === "string" ? call[0] : "", + ); + expect(updateTexts.some((text) => text.includes("Message"))).toBe(false); + }); + it("does not suppress a later final after error closeout", async () => { resolveFeishuAccountMock.mockReturnValue({ accountId: "main", diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index 7c8347b04d2..4866f813ca1 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -1,5 +1,9 @@ import { logTypingFailure } from "openclaw/plugin-sdk/channel-feedback"; import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline"; +import { + formatChannelProgressDraftLine, + isChannelProgressDraftWorkToolName, +} from "openclaw/plugin-sdk/channel-streaming"; import { resolveSendableOutboundReplyParts, resolveTextChunksWithFallback, @@ -695,10 +699,29 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP : undefined, onReasoningEnd: reasoningPreviewEnabled ? () => {} : undefined, onToolStart: streamingEnabled - ? (payload: { name?: string; phase?: string }) => { - updateStreamingStatusLine( - `🔧 **Using: ${payload.name ?? payload.phase ?? "tool"}...**`, + ? (payload: { + name?: string; + phase?: string; + args?: Record; + detailMode?: "explain" | "raw"; + }) => { + if (!isChannelProgressDraftWorkToolName(payload.name)) { + return; + } + const statusLine = formatChannelProgressDraftLine( + { + event: "tool", + name: payload.name, + phase: payload.phase, + args: payload.args, + }, + { + detailMode: payload.detailMode, + }, ); + if (statusLine) { + updateStreamingStatusLine(statusLine); + } } : undefined, onAssistantMessageStart: streamingEnabled