mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:20:43 +00:00
fix(telegram): separate progress drafts from final replies
This commit is contained in:
@@ -278,7 +278,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
Requirement:
|
||||
|
||||
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
|
||||
- `progress` keeps one editable status draft and updates it with tool progress until final delivery
|
||||
- `progress` keeps one editable status draft for tool progress, clears it at completion, and sends the final answer as a normal message
|
||||
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
|
||||
- `streaming.preview.commandText` controls command/exec detail inside those tool-progress lines: `raw` (default, preserves released behavior) or `status` (tool label only)
|
||||
- legacy `channels.telegram.streamMode` and boolean `streaming` values are detected; run `openclaw doctor --fix` to migrate them to `channels.telegram.streaming.mode`
|
||||
@@ -317,7 +317,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
}
|
||||
```
|
||||
|
||||
For progress-draft mode, put the same command-text policy under `streaming.progress`:
|
||||
Use `progress` mode when you want visible tool progress without editing the final answer into that same message. Put the command-text policy under `streaming.progress`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -345,6 +345,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
- short DM/group/topic previews: OpenClaw keeps the same preview message and performs the final edit in place
|
||||
- long text finals that split into multiple Telegram messages reuse the existing preview as the first final chunk when possible, then send only the remaining chunks
|
||||
- progress-mode finals clear the status draft and use normal final delivery instead of editing the draft into the answer
|
||||
- if the final edit fails before the completed text is confirmed, OpenClaw uses normal final delivery and cleans up the stale preview
|
||||
|
||||
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.
|
||||
|
||||
@@ -161,6 +161,7 @@ Telegram:
|
||||
|
||||
- Uses `sendMessage` + `editMessageText` preview updates across DMs and group/topics.
|
||||
- Final text edits the active preview in place; long finals reuse that message for the first chunk and send only the remaining chunks.
|
||||
- `progress` mode keeps tool progress in an editable status draft, clears that draft at completion, and sends the final answer through normal delivery.
|
||||
- If the final edit fails before the completed text is confirmed, OpenClaw uses normal final delivery and cleans up the stale preview.
|
||||
- Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming).
|
||||
- `/reasoning stream` can write reasoning to a transient preview that is deleted after final delivery.
|
||||
|
||||
@@ -919,6 +919,40 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(deliverReplies).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps progress updates in a draft and sends the final answer normally", async () => {
|
||||
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
async ({ dispatcherOptions, replyOptions }) => {
|
||||
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
await replyOptions?.onItemEvent?.({
|
||||
kind: "command",
|
||||
name: "exec",
|
||||
progressText: "git rev-parse --abbrev-ref HEAD",
|
||||
});
|
||||
await dispatcherOptions.deliver({ text: "Branch is up to date" }, { kind: "final" });
|
||||
return { queuedFinal: true };
|
||||
},
|
||||
);
|
||||
|
||||
await dispatchWithContext({
|
||||
context: createContext(),
|
||||
streamMode: "progress",
|
||||
telegramCfg: { streaming: { mode: "progress" } },
|
||||
});
|
||||
|
||||
expect(answerDraftStream.update).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/`🛠️ Exec: git rev-parse --abbrev-ref HEAD`$/),
|
||||
);
|
||||
expect(answerDraftStream.update).not.toHaveBeenCalledWith("Branch is up to date");
|
||||
expect(answerDraftStream.clear).toHaveBeenCalledTimes(1);
|
||||
expect(deliverReplies).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
replies: [expect.objectContaining({ text: "Branch is up to date" })],
|
||||
}),
|
||||
);
|
||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("streams the first long final chunk and sends follow-up chunks", async () => {
|
||||
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
|
||||
const longText = "one ".repeat(80);
|
||||
|
||||
@@ -901,6 +901,16 @@ export const dispatchTelegramMessage = async ({
|
||||
deliveryState.markDelivered();
|
||||
},
|
||||
});
|
||||
const deliverProgressModeFinalAnswer = async (
|
||||
payload: ReplyPayload,
|
||||
text: string,
|
||||
): Promise<LaneDeliveryResult> => {
|
||||
await answerLane.stream?.clear();
|
||||
resetDraftLaneState(answerLane);
|
||||
const delivered = await sendPayload(applyTextToPayload(payload, text), { durable: true });
|
||||
answerLane.finalized = true;
|
||||
return delivered ? { kind: "sent" } : { kind: "skipped" };
|
||||
};
|
||||
|
||||
if (isDmTopic) {
|
||||
try {
|
||||
@@ -1042,13 +1052,18 @@ export const dispatchTelegramMessage = async ({
|
||||
if (segment.lane === "reasoning") {
|
||||
reasoningStepState.noteReasoningHint();
|
||||
}
|
||||
const result = await deliverLaneText({
|
||||
laneName: segment.lane,
|
||||
text: segment.text,
|
||||
payload,
|
||||
infoKind: info.kind,
|
||||
buttons: telegramButtons,
|
||||
});
|
||||
const result =
|
||||
streamMode === "progress" &&
|
||||
segment.lane === "answer" &&
|
||||
info.kind === "final"
|
||||
? await deliverProgressModeFinalAnswer(payload, segment.text)
|
||||
: await deliverLaneText({
|
||||
laneName: segment.lane,
|
||||
text: segment.text,
|
||||
payload,
|
||||
infoKind: info.kind,
|
||||
buttons: telegramButtons,
|
||||
});
|
||||
if (info.kind === "final") {
|
||||
emitPreviewFinalizedHook(result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user