forked from EduCraft/curriculum-project-hub
fix(hub): send Feishu files explicitly
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
export interface PatchableTextSink {
|
||||
readonly create: (text: string) => Promise<string | null>;
|
||||
readonly patch: (messageId: string, text: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface PatchableTextStreamOptions {
|
||||
readonly patchIntervalMs?: number;
|
||||
readonly maxMessageLength?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_PATCH_INTERVAL_MS = 400;
|
||||
const DEFAULT_MAX_MESSAGE_LENGTH = 4000;
|
||||
|
||||
/**
|
||||
* Serializes text-card creation/patching for streaming output.
|
||||
*
|
||||
* Feishu returns the message id asynchronously. Without this queue, multiple
|
||||
* early text deltas can all observe "no message yet" and create duplicate
|
||||
* prefix messages before the first create call resolves.
|
||||
*/
|
||||
export class PatchableTextStream {
|
||||
private currentMessageId: string | null = null;
|
||||
private text = "";
|
||||
private lastPatchAt = 0;
|
||||
private flushChain: Promise<void> = Promise.resolve();
|
||||
private flushScheduled = false;
|
||||
|
||||
private readonly patchIntervalMs: number;
|
||||
private readonly maxMessageLength: number;
|
||||
|
||||
constructor(
|
||||
private readonly sink: PatchableTextSink,
|
||||
options: PatchableTextStreamOptions = {},
|
||||
) {
|
||||
this.patchIntervalMs = options.patchIntervalMs ?? DEFAULT_PATCH_INTERVAL_MS;
|
||||
this.maxMessageLength = options.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH;
|
||||
}
|
||||
|
||||
append(delta: string): void {
|
||||
if (delta === "") return;
|
||||
this.text += delta;
|
||||
this.scheduleFlush();
|
||||
}
|
||||
|
||||
async finish(fallbackText: string): Promise<void> {
|
||||
await this.flushChain;
|
||||
if (this.currentMessageId !== null && this.text.length > 0) {
|
||||
await this.sink.patch(this.currentMessageId, this.text);
|
||||
return;
|
||||
}
|
||||
if (this.currentMessageId === null && fallbackText.length > 0) {
|
||||
this.currentMessageId = await this.sink.create(fallbackText);
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleFlush(): void {
|
||||
if (this.flushScheduled) return;
|
||||
this.flushScheduled = true;
|
||||
this.flushChain = this.flushChain.then(async () => {
|
||||
this.flushScheduled = false;
|
||||
await this.flush();
|
||||
});
|
||||
}
|
||||
|
||||
private async flush(): Promise<void> {
|
||||
if (this.currentMessageId === null) {
|
||||
this.currentMessageId = await this.sink.create(this.text);
|
||||
this.lastPatchAt = Date.now();
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - this.lastPatchAt < this.patchIntervalMs) return;
|
||||
this.lastPatchAt = now;
|
||||
|
||||
if (this.text.length > this.maxMessageLength) {
|
||||
await this.sink.patch(this.currentMessageId, this.text.slice(0, this.maxMessageLength));
|
||||
this.text = this.text.slice(this.maxMessageLength);
|
||||
this.currentMessageId = await this.sink.create(this.text);
|
||||
} else {
|
||||
await this.sink.patch(this.currentMessageId, this.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user