mirror of https://github.com/openclaw/openclaw.git
fix(feishu): non-blocking WS ACK and preserve full streaming card content (#29616)
* fix(feishu): non-blocking ws ack and preserve streaming card full content * fix(feishu): preserve fragmented streaming text without newline artifacts --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
parent
a5a6952bf2
commit
a5a7239182
|
|
@ -245,6 +245,7 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
### Fixes
|
||||
|
||||
- Feishu/Streaming card text fidelity: merge throttled/fragmented partial updates without dropping content and avoid newline injection when stitching chunk-style deltas so card-stream output matches final reply text. (#29616) Thanks @HaoHuaqing.
|
||||
- Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.
|
||||
- Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc.
|
||||
- Web tools/RFC2544 fake-IP compatibility: allow RFC2544 benchmark range (`198.18.0.0/15`) for trusted web-tool fetch endpoints so proxy fake-IP networking modes do not trigger false SSRF blocks. Landed from contributor PR #31176 by @sunkinux. Thanks @sunkinux.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { mergeStreamingText } from "./streaming-card.js";
|
||||
|
||||
describe("mergeStreamingText", () => {
|
||||
it("prefers the latest full text when it already includes prior text", () => {
|
||||
expect(mergeStreamingText("hello", "hello world")).toBe("hello world");
|
||||
});
|
||||
|
||||
it("keeps previous text when the next partial is empty or redundant", () => {
|
||||
expect(mergeStreamingText("hello", "")).toBe("hello");
|
||||
expect(mergeStreamingText("hello world", "hello")).toBe("hello world");
|
||||
});
|
||||
|
||||
it("appends fragmented chunks without injecting newlines", () => {
|
||||
expect(mergeStreamingText("hello wor", "ld")).toBe("hello world");
|
||||
expect(mergeStreamingText("line1", "line2")).toBe("line1line2");
|
||||
});
|
||||
});
|
||||
|
|
@ -85,6 +85,25 @@ function truncateSummary(text: string, max = 50): string {
|
|||
return clean.length <= max ? clean : clean.slice(0, max - 3) + "...";
|
||||
}
|
||||
|
||||
export function mergeStreamingText(
|
||||
previousText: string | undefined,
|
||||
nextText: string | undefined,
|
||||
): string {
|
||||
const previous = typeof previousText === "string" ? previousText : "";
|
||||
const next = typeof nextText === "string" ? nextText : "";
|
||||
if (!next) {
|
||||
return previous;
|
||||
}
|
||||
if (!previous || next === previous || next.includes(previous)) {
|
||||
return next;
|
||||
}
|
||||
if (previous.includes(next)) {
|
||||
return previous;
|
||||
}
|
||||
// Fallback for fragmented partial chunks: append as-is to avoid losing tokens.
|
||||
return `${previous}${next}`;
|
||||
}
|
||||
|
||||
/** Streaming card session manager */
|
||||
export class FeishuStreamingSession {
|
||||
private client: Client;
|
||||
|
|
@ -235,10 +254,15 @@ export class FeishuStreamingSession {
|
|||
if (!this.state || this.closed) {
|
||||
return;
|
||||
}
|
||||
const mergedInput = mergeStreamingText(this.pendingText ?? this.state.currentText, text);
|
||||
if (!mergedInput || mergedInput === this.state.currentText) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Throttle: skip if updated recently, but remember pending text
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdateTime < this.updateThrottleMs) {
|
||||
this.pendingText = text;
|
||||
this.pendingText = mergedInput;
|
||||
return;
|
||||
}
|
||||
this.pendingText = null;
|
||||
|
|
@ -248,8 +272,12 @@ export class FeishuStreamingSession {
|
|||
if (!this.state || this.closed) {
|
||||
return;
|
||||
}
|
||||
this.state.currentText = text;
|
||||
await this.updateCardContent(text, (e) => this.log?.(`Update failed: ${String(e)}`));
|
||||
const mergedText = mergeStreamingText(this.state.currentText, mergedInput);
|
||||
if (!mergedText || mergedText === this.state.currentText) {
|
||||
return;
|
||||
}
|
||||
this.state.currentText = mergedText;
|
||||
await this.updateCardContent(mergedText, (e) => this.log?.(`Update failed: ${String(e)}`));
|
||||
});
|
||||
await this.queue;
|
||||
}
|
||||
|
|
@ -261,8 +289,8 @@ export class FeishuStreamingSession {
|
|||
this.closed = true;
|
||||
await this.queue;
|
||||
|
||||
// Use finalText, or pending throttled text, or current text
|
||||
const text = finalText ?? this.pendingText ?? this.state.currentText;
|
||||
const pendingMerged = mergeStreamingText(this.state.currentText, this.pendingText ?? undefined);
|
||||
const text = finalText ? mergeStreamingText(pendingMerged, finalText) : pendingMerged;
|
||||
const apiBase = resolveApiBase(this.creds.domain);
|
||||
|
||||
// Only send final update if content differs from what's already displayed
|
||||
|
|
|
|||
Loading…
Reference in New Issue