From bd1da4c0dac4d6a1c4269d8f19d91ff264a920ee Mon Sep 17 00:00:00 2001 From: "Alice (OpenClaw)" Date: Wed, 13 May 2026 12:02:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20reusable=20Telegram=20PoC=20helpe?= =?UTF-8?q?r=20module=20/=20=E6=96=B0=E5=A2=9E=E5=8F=AF=E9=87=8D=E7=94=A8?= =?UTF-8?q?=20Telegram=20PoC=20helper=20=E6=A8=A1=E7=B5=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/runtime/openclaw-telegram-poc.ts | 36 +++++++++++++++++ tests/runtime-helper.test.mjs | 58 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/runtime/openclaw-telegram-poc.ts create mode 100644 tests/runtime-helper.test.mjs diff --git a/src/runtime/openclaw-telegram-poc.ts b/src/runtime/openclaw-telegram-poc.ts new file mode 100644 index 0000000..e1cb6f1 --- /dev/null +++ b/src/runtime/openclaw-telegram-poc.ts @@ -0,0 +1,36 @@ +import type { ReplyEndChoice, ReplyEndState, TelegramInlineButton } from "../types.js" + +export function buildDefaultReplyEndButtons(): TelegramInlineButton[][] { + return [[ + { text: "A. 繼續", callback_data: "rec:continue" }, + { text: "B. 就這樣吧,不需要額外處理", callback_data: "rec:stop" }, + ]] +} + +export function buildResolvedReplyEndButtons(choice: ReplyEndChoice): TelegramInlineButton[][] { + if (choice === "continue") { + return [[ + { text: "✅ A. 繼續", callback_data: "rec:continue" }, + { text: "B. 就這樣吧,不需要額外處理", callback_data: "rec:stop" }, + ]] + } + + return [[ + { text: "A. 繼續", callback_data: "rec:continue" }, + { text: "⏹ B. 就這樣吧,不需要額外處理", callback_data: "rec:stop" }, + ]] +} + +export function buildReplyEndAcknowledgement(choice: ReplyEndChoice): string { + return choice === "continue" + ? "reply-end-controls: continue received" + : "reply-end-controls: stop received" +} + +export function applyStopStateToInboundText(text: string, state: ReplyEndState | null): string { + if (!state || state.lastChoice !== "stop" || state.active !== true) { + return text + } + + return `${text}\n\n[System: The user previously pressed reply-end-controls Stop for this conversation. Treat the previous thread as closed. Unless the user's new message contains a clear new instruction or explicit request for more work, you must NOT proactively continue, must NOT add extra checks, must NOT propose next steps, must NOT extend the task, and should answer briefly and close cleanly.]`.trim() +} diff --git a/tests/runtime-helper.test.mjs b/tests/runtime-helper.test.mjs new file mode 100644 index 0000000..cb04554 --- /dev/null +++ b/tests/runtime-helper.test.mjs @@ -0,0 +1,58 @@ +import test from "node:test" +import assert from "node:assert/strict" + +import { + applyStopStateToInboundText, + buildDefaultReplyEndButtons, + buildReplyEndAcknowledgement, + buildResolvedReplyEndButtons, +} from "../src/runtime/openclaw-telegram-poc.ts" + +test("default reply-end buttons contain continue and stop", () => { + const buttons = buildDefaultReplyEndButtons() + assert.equal(buttons[0][0].callback_data, "rec:continue") + assert.equal(buttons[0][1].callback_data, "rec:stop") +}) + +test("resolved buttons mark continue selection", () => { + const buttons = buildResolvedReplyEndButtons("continue") + assert.match(buttons[0][0].text, /^✅/) + assert.doesNotMatch(buttons[0][1].text, /^⏹/) +}) + +test("resolved buttons mark stop selection", () => { + const buttons = buildResolvedReplyEndButtons("stop") + assert.match(buttons[0][1].text, /^⏹/) + assert.doesNotMatch(buttons[0][0].text, /^✅/) +}) + +test("acknowledgement text matches choice", () => { + assert.equal(buildReplyEndAcknowledgement("continue"), "reply-end-controls: continue received") + assert.equal(buildReplyEndAcknowledgement("stop"), "reply-end-controls: stop received") +}) + +test("stop state injects control text into inbound message", () => { + const result = applyStopStateToInboundText("我先這樣", { + lastChoice: "stop", + lastChoiceAt: "2026-05-13T00:00:00Z", + sourceMessageId: "1", + sourceCallbackId: "cb-1", + active: true, + }) + assert.match(result, /reply-end-controls Stop/) +}) + +test("continue or null state leaves inbound text unchanged", () => { + const base = "我先這樣" + assert.equal(applyStopStateToInboundText(base, null), base) + assert.equal( + applyStopStateToInboundText(base, { + lastChoice: "continue", + lastChoiceAt: "2026-05-13T00:00:00Z", + sourceMessageId: "1", + sourceCallbackId: "cb-1", + active: true, + }), + base + ) +})