
























@@ -0,0 +1,238 @@
1+#!/usr/bin/env node
2+import fs from "node:fs/promises";
3+import path from "node:path";
4+5+const groupId = process.env.OPENCLAW_QA_TELEGRAM_GROUP_ID;
6+const driverToken = process.env.OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN;
7+const sutToken = process.env.OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN;
8+const outputDir = process.env.OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR ?? ".artifacts/rtt/raw";
9+const timeoutMs = Number(process.env.OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS ?? "180000");
10+const canaryTimeoutMs = Number(
11+process.env.OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS ?? String(timeoutMs),
12+);
13+const scenarioIds = (
14+process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply"
15+)
16+.split(",")
17+.map((value) => value.trim())
18+.filter(Boolean);
19+20+if (!groupId || !driverToken || !sutToken) {
21+throw new Error(
22+"missing Telegram env: OPENCLAW_QA_TELEGRAM_GROUP_ID, OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN, OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN",
23+);
24+}
25+26+class TelegramBot {
27+constructor(token) {
28+this.baseUrl = `https://api.telegram.org/bot${token}`;
29+}
30+31+async call(method, body) {
32+const response = await fetch(`${this.baseUrl}/${method}`, {
33+method: "POST",
34+headers: { "content-type": "application/json" },
35+body: JSON.stringify(body),
36+});
37+const payload = await response.json();
38+if (!response.ok || payload.ok !== true) {
39+throw new Error(`${method} failed: ${JSON.stringify(payload)}`);
40+}
41+return payload.result;
42+}
43+44+getMe() {
45+return this.call("getMe", {});
46+}
47+48+sendMessage(params) {
49+return this.call("sendMessage", params);
50+}
51+52+getUpdates(params) {
53+return this.call("getUpdates", params);
54+}
55+}
56+57+const driver = new TelegramBot(driverToken);
58+const sut = new TelegramBot(sutToken);
59+const observedMessages = [];
60+let driverUpdateOffset = 0;
61+62+function messageText(message) {
63+return message.text ?? message.caption ?? "";
64+}
65+66+function sleep(ms) {
67+return new Promise((resolve) => setTimeout(resolve, ms));
68+}
69+70+async function flushUpdates(bot) {
71+let updates = await bot.getUpdates({ timeout: 0, allowed_updates: ["message"] });
72+let nextOffset;
73+while (updates.length > 0) {
74+const lastUpdateId = updates.at(-1).update_id;
75+nextOffset = lastUpdateId + 1;
76+updates = await bot.getUpdates({
77+offset: nextOffset,
78+timeout: 0,
79+allowed_updates: ["message"],
80+});
81+}
82+return nextOffset;
83+}
84+85+async function waitForSutReply(params) {
86+const deadline = Date.now() + params.timeoutMs;
87+while (Date.now() < deadline) {
88+const updates = await driver.getUpdates({
89+offset: driverUpdateOffset,
90+timeout: 5,
91+allowed_updates: ["message"],
92+});
93+for (const update of updates) {
94+driverUpdateOffset = Math.max(driverUpdateOffset, update.update_id + 1);
95+const message = update.message;
96+if (!message || String(message.chat?.id) !== String(groupId)) {
97+continue;
98+}
99+observedMessages.push({
100+updateId: update.update_id,
101+messageId: message.message_id,
102+fromId: message.from?.id,
103+fromUsername: message.from?.username,
104+replyToMessageId: message.reply_to_message?.message_id,
105+text: messageText(message),
106+scenarioId: params.scenarioId,
107+scenarioTitle: params.scenarioTitle,
108+});
109+if (message.from?.id !== params.sutId) {
110+continue;
111+}
112+if (message.date < params.startedUnixSeconds) {
113+continue;
114+}
115+const text = messageText(message);
116+const replyMatches = message.reply_to_message?.message_id === params.requestMessageId;
117+const markerMatches = params.matchText ? text.includes(params.matchText) : false;
118+const anySutReplyMatches = params.allowAnySutReply;
119+if (replyMatches || markerMatches || anySutReplyMatches) {
120+return message;
121+}
122+}
123+}
124+125+throw new Error(`timed out after ${params.timeoutMs}ms waiting for Telegram message`);
126+}
127+128+async function runScenario(params) {
129+const startedAt = new Date();
130+const startedUnixSeconds = Math.floor(startedAt.getTime() / 1000);
131+const request = await driver.sendMessage({
132+chat_id: groupId,
133+text: params.input,
134+disable_notification: true,
135+});
136+137+try {
138+const reply = await waitForSutReply({
139+allowAnySutReply: params.allowAnySutReply,
140+matchText: params.matchText,
141+requestMessageId: request.message_id,
142+scenarioId: params.id,
143+scenarioTitle: params.title,
144+ startedUnixSeconds,
145+sutId: params.sutId,
146+timeoutMs: params.timeoutMs,
147+});
148+const rttMs = Date.now() - startedAt.getTime();
149+return {
150+id: params.id,
151+title: params.title,
152+status: "pass",
153+details: `observed SUT message ${reply.message_id}`,
154+ rttMs,
155+};
156+} catch (error) {
157+return {
158+id: params.id,
159+title: params.title,
160+status: "fail",
161+details: error instanceof Error ? error.message : String(error),
162+};
163+}
164+}
165+166+function reportMarkdown(summary) {
167+const lines = ["# Telegram RTT", ""];
168+for (const scenario of summary.scenarios) {
169+lines.push(`## ${scenario.title}`, "");
170+lines.push(`- Status: ${scenario.status}`);
171+lines.push(`- Details: ${scenario.details}`);
172+if (scenario.rttMs !== undefined) {
173+lines.push(`- RTT: ${scenario.rttMs}ms`);
174+}
175+lines.push("");
176+}
177+return lines.join("\n");
178+}
179+180+async function main() {
181+await fs.mkdir(outputDir, { recursive: true });
182+const [driverMe, sutMe] = await Promise.all([driver.getMe(), sut.getMe()]);
183+driverUpdateOffset = (await flushUpdates(driver)) ?? driverUpdateOffset;
184+185+const scenarios = [];
186+scenarios.push(
187+await runScenario({
188+allowAnySutReply: true,
189+id: "telegram-canary",
190+input: `/status@${sutMe.username}`,
191+sutId: sutMe.id,
192+timeoutMs: canaryTimeoutMs,
193+title: "Telegram canary",
194+}),
195+);
196+197+if (scenarioIds.includes("telegram-mentioned-message-reply")) {
198+const marker = `OPENCLAW_RTT_${Date.now().toString(36)}`;
199+scenarios.push(
200+await runScenario({
201+allowAnySutReply: true,
202+id: "telegram-mentioned-message-reply",
203+input: `/status@${sutMe.username} RTT marker ${marker}`,
204+matchText: "OPENCLAW_RTT_OK",
205+sutId: sutMe.id,
206+ timeoutMs,
207+title: "Telegram status command reply",
208+}),
209+);
210+}
211+212+const failed = scenarios.filter((scenario) => scenario.status === "fail").length;
213+const summary = {
214+provider: "telegram",
215+driver: { id: driverMe.id, username: driverMe.username },
216+sut: { id: sutMe.id, username: sutMe.username },
217+startedAt: new Date().toISOString(),
218+status: failed > 0 ? "fail" : "pass",
219+totals: { total: scenarios.length, failed, passed: scenarios.length - failed },
220+ scenarios,
221+};
222+223+await fs.writeFile(
224+path.join(outputDir, "telegram-qa-summary.json"),
225+`${JSON.stringify(summary, null, 2)}\n`,
226+);
227+await fs.writeFile(path.join(outputDir, "telegram-qa-report.md"), reportMarkdown(summary));
228+await fs.writeFile(
229+path.join(outputDir, "telegram-qa-observed-messages.json"),
230+`${JSON.stringify(observedMessages, null, 2)}\n`,
231+);
232+233+if (failed > 0) {
234+process.exitCode = 1;
235+}
236+}
237+238+await main();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。