























@@ -10,18 +10,36 @@ const timeoutMs = Number(process.env.OPENCLAW_QA_TELEGRAM_SCENARIO_TIMEOUT_MS ??
1010const canaryTimeoutMs = Number(
1111process.env.OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS ?? String(timeoutMs),
1212);
13-const scenarioIds = new Set(
14-(process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply")
15-.split(",")
16-.map((value) => value.trim())
17-.filter(Boolean),
18-);
13+const warmSampleCount = Number(process.env.OPENCLAW_NPM_TELEGRAM_WARM_SAMPLES ?? "20");
14+const sampleTimeoutMs = Number(process.env.OPENCLAW_NPM_TELEGRAM_SAMPLE_TIMEOUT_MS ?? "30000");
15+const maxWarmFailures = Number(process.env.OPENCLAW_NPM_TELEGRAM_MAX_FAILURES ?? "3");
16+const scenarioIds = (
17+process.env.OPENCLAW_NPM_TELEGRAM_SCENARIOS ?? "telegram-mentioned-message-reply"
18+)
19+.split(",")
20+.map((value) => value.trim())
21+.filter(Boolean);
19222023if (!groupId || !driverToken || !sutToken) {
2124throw new Error(
2225"missing Telegram env: OPENCLAW_QA_TELEGRAM_GROUP_ID, OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN, OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN",
2326);
2427}
28+if (!Number.isInteger(warmSampleCount) || warmSampleCount < 1) {
29+throw new Error(
30+`OPENCLAW_NPM_TELEGRAM_WARM_SAMPLES must be a positive integer; got: ${warmSampleCount}`,
31+);
32+}
33+if (!Number.isInteger(sampleTimeoutMs) || sampleTimeoutMs < 1) {
34+throw new Error(
35+`OPENCLAW_NPM_TELEGRAM_SAMPLE_TIMEOUT_MS must be a positive integer; got: ${sampleTimeoutMs}`,
36+);
37+}
38+if (!Number.isInteger(maxWarmFailures) || maxWarmFailures < 1) {
39+throw new Error(
40+`OPENCLAW_NPM_TELEGRAM_MAX_FAILURES must be a positive integer; got: ${maxWarmFailures}`,
41+);
42+}
25432644class TelegramBot {
2745constructor(token) {
@@ -101,6 +119,7 @@ async function waitForSutReply(params) {
101119text: messageText(message),
102120scenarioId: params.scenarioId,
103121scenarioTitle: params.scenarioTitle,
122+sampleIndex: params.sampleIndex,
104123});
105124if (message.from?.id !== params.sutId) {
106125continue;
@@ -137,6 +156,7 @@ async function runScenario(params) {
137156requestMessageId: request.message_id,
138157scenarioId: params.id,
139158scenarioTitle: params.title,
159+sampleIndex: params.sampleIndex,
140160 startedUnixSeconds,
141161sutId: params.sutId,
142162timeoutMs: params.timeoutMs,
@@ -159,6 +179,71 @@ async function runScenario(params) {
159179}
160180}
161181182+function percentile(sortedValues, percentileValue) {
183+if (sortedValues.length === 0) {
184+return undefined;
185+}
186+const index = Math.ceil((percentileValue / 100) * sortedValues.length) - 1;
187+return sortedValues[Math.min(Math.max(index, 0), sortedValues.length - 1)];
188+}
189+190+function summarizeSamples(samples) {
191+const passed = samples.filter((sample) => sample.status === "pass" && sample.rttMs !== undefined);
192+const sorted = passed.map((sample) => sample.rttMs).sort((a, b) => a - b);
193+const sum = sorted.reduce((total, value) => total + value, 0);
194+return {
195+total: samples.length,
196+passed: passed.length,
197+failed: samples.length - passed.length,
198+avgMs: sorted.length > 0 ? Math.round(sum / sorted.length) : undefined,
199+p50Ms: percentile(sorted, 50),
200+p95Ms: percentile(sorted, 95),
201+maxMs: sorted.at(-1),
202+};
203+}
204+205+async function runWarmScenario(params) {
206+const samples = [];
207+let failures = 0;
208+for (let index = 0; index < params.sampleCount; index += 1) {
209+const sample = await runScenario({
210+allowAnySutReply: true,
211+id: params.id,
212+input: `/status@${params.sutUsername}`,
213+sampleIndex: index + 1,
214+sutId: params.sutId,
215+timeoutMs: params.sampleTimeoutMs,
216+title: params.title,
217+});
218+if (sample.status === "fail") {
219+failures += 1;
220+}
221+samples.push({
222+index: index + 1,
223+status: sample.status,
224+details: sample.details,
225+ ...(sample.rttMs === undefined ? {} : { rttMs: sample.rttMs }),
226+});
227+if (failures >= params.maxFailures) {
228+break;
229+}
230+if (index + 1 < params.sampleCount) {
231+await sleep(500);
232+}
233+}
234+235+const stats = summarizeSamples(samples);
236+return {
237+id: params.id,
238+title: params.title,
239+status: stats.failed > 0 ? "fail" : "pass",
240+details: `${stats.passed}/${stats.total} warm samples passed`,
241+rttMs: stats.p50Ms,
242+ samples,
243+ stats,
244+};
245+}
246+162247function reportMarkdown(summary) {
163248const lines = ["# Telegram RTT", ""];
164249for (const scenario of summary.scenarios) {
@@ -168,6 +253,21 @@ function reportMarkdown(summary) {
168253if (scenario.rttMs !== undefined) {
169254lines.push(`- RTT: ${scenario.rttMs}ms`);
170255}
256+if (scenario.stats) {
257+lines.push(`- Samples: ${scenario.stats.passed}/${scenario.stats.total}`);
258+if (scenario.stats.avgMs !== undefined) {
259+lines.push(`- Avg: ${scenario.stats.avgMs}ms`);
260+}
261+if (scenario.stats.p50Ms !== undefined) {
262+lines.push(`- P50: ${scenario.stats.p50Ms}ms`);
263+}
264+if (scenario.stats.p95Ms !== undefined) {
265+lines.push(`- P95: ${scenario.stats.p95Ms}ms`);
266+}
267+if (scenario.stats.maxMs !== undefined) {
268+lines.push(`- Max: ${scenario.stats.maxMs}ms`);
269+}
270+}
171271lines.push("");
172272}
173273return lines.join("\n");
@@ -190,16 +290,15 @@ async function main() {
190290}),
191291);
192292193-if (scenarioIds.has("telegram-mentioned-message-reply")) {
194-const marker = `OPENCLAW_RTT_${Date.now().toString(36)}`;
293+if (scenarioIds.includes("telegram-mentioned-message-reply")) {
195294scenarios.push(
196-await runScenario({
197-allowAnySutReply: true,
295+await runWarmScenario({
198296id: "telegram-mentioned-message-reply",
199-input: `/status@${sutMe.username} RTT marker ${marker}`,
200-matchText: "OPENCLAW_RTT_OK",
297+maxFailures: maxWarmFailures,
298+sampleCount: warmSampleCount,
299+ sampleTimeoutMs,
201300sutId: sutMe.id,
202-timeoutMs,
301+sutUsername: sutMe.username,
203302title: "Telegram status command reply",
204303}),
205304);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。