jkes6203 commited on
Commit
f85569d
·
1 Parent(s): a6a2418

Add Telegram multi-transport startup diagnostics

Browse files
Files changed (3) hide show
  1. Dockerfile +6 -1
  2. README.md +6 -2
  3. startup-notify.mjs +328 -25
Dockerfile CHANGED
@@ -6,7 +6,7 @@ FROM node:22-bookworm
6
  ARG OPENCLAW_VERSION=2026.3.11
7
 
8
  RUN apt-get update && \
9
- DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates git && \
10
  apt-get clean && rm -rf /var/lib/apt/lists/*
11
 
12
  ENV NODE_ENV=production
@@ -35,6 +35,11 @@ RUN printf '%s\n' \
35
  ' export OPENCLAW_HOME=/home/user' \
36
  ' mkdir -p /home/user/.openclaw' \
37
  'fi' \
 
 
 
 
 
38
  'node /app/setup-hf-config.mjs' \
39
  'node /app/import-auth-profiles.mjs' \
40
  'node /app/startup-notify.mjs >/tmp/openclaw-startup-notify.log 2>&1 || true' \
 
6
  ARG OPENCLAW_VERSION=2026.3.11
7
 
8
  RUN apt-get update && \
9
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl git && \
10
  apt-get clean && rm -rf /var/lib/apt/lists/*
11
 
12
  ENV NODE_ENV=production
 
35
  ' export OPENCLAW_HOME=/home/user' \
36
  ' mkdir -p /home/user/.openclaw' \
37
  'fi' \
38
+ 'if [ -n "${OPENCLAW_TELEGRAM_PROXY}" ]; then' \
39
+ ' export HTTPS_PROXY="${HTTPS_PROXY:-$OPENCLAW_TELEGRAM_PROXY}"' \
40
+ ' export HTTP_PROXY="${HTTP_PROXY:-$OPENCLAW_TELEGRAM_PROXY}"' \
41
+ ' export ALL_PROXY="${ALL_PROXY:-$OPENCLAW_TELEGRAM_PROXY}"' \
42
+ 'fi' \
43
  'node /app/setup-hf-config.mjs' \
44
  'node /app/import-auth-profiles.mjs' \
45
  'node /app/startup-notify.mjs >/tmp/openclaw-startup-notify.log 2>&1 || true' \
README.md CHANGED
@@ -37,6 +37,7 @@ This Space runs the [OpenClaw](https://github.com/openclaw/openclaw) gateway so
37
  - **`OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS`** — (optional) Comma-separated chat IDs to receive startup notifications from the init script. Recommended if you want an explicit "boot completed" signal.
38
  - **`OPENCLAW_TELEGRAM_INIT_NOTIFY_MESSAGE`** — (optional) Extra plain-text line appended to the startup notification.
39
  - **`OPENCLAW_TELEGRAM_DROP_PENDING_UPDATES_ON_START`** — (optional) `1` / `0`; when startup finds a webhook and removes it, also drops queued Telegram updates. Default `0`.
 
40
  - **`OPENCLAW_TELEGRAM_DM_POLICY`** / **`OPENCLAW_TELEGRAM_ALLOW_FROM`** / **`OPENCLAW_TELEGRAM_GROUP_POLICY`** / **`OPENCLAW_TELEGRAM_GROUP_ALLOW_FROM`** / **`OPENCLAW_TELEGRAM_REQUIRE_MENTION`** — (optional) Advanced Telegram access-control settings.
41
  4. **Build and run** — push to the Space repo; the Space will build and start the gateway. Startup writes config so the default model chain is `Qwen3-8B -> DeepSeek-R1 -> openai-codex/gpt-5.4`, the Control UI accepts token-only connections (no device pairing), and Supabase sync starts automatically when `SUPABASE_URL` + `SUPABASE_KEY` are present.
42
 
@@ -72,6 +73,7 @@ OPENCLAW_MODEL_FALLBACK_1=huggingface/deepseek-ai/DeepSeek-R1
72
  OPENCLAW_MODEL_FALLBACK_2=openai-codex/gpt-5.4
73
  OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS=https://your-space.hf.space
74
  OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS=123456789,-1001234567890
 
75
  OPENCLAW_TELEGRAM_INIT_NOTIFY_MESSAGE=HF Space boot completed
76
  OPENCLAW_TELEGRAM_DROP_PENDING_UPDATES_ON_START=0
77
  OPENCLAW_SUPABASE_TABLE=openclaw_state
@@ -82,8 +84,9 @@ Telegram startup notification notes:
82
 
83
  1. Ask the target user to start the bot first, or add the bot to the target group/channel before using that chat ID.
84
  2. Use numeric chat IDs when possible; they are less fragile than usernames.
85
- 3. The init script validates the bot with `getMe`, clears any stale webhook, probes `getUpdates`, then sends multiple `sendMessage` variants tagged with their version name.
86
- 4. Startup currently tests these send variants and includes the variant name in the message body: `json-html`, `json-plain`, `form-html`, `form-plain`, `get-plain`.
 
87
 
88
  ## Keep it running 24/7
89
 
@@ -178,6 +181,7 @@ The startup script `setup-hf-config.mjs` reads the following from **Secrets** or
178
  | Telegram command menu | `channels.telegram.commands.native = off` | This HF wrapper disables Telegram command-menu registration to reduce noisy `setMyCommands` retries on constrained hosts |
179
  | `OPENCLAW_STARTUP_NOTIFY` | startup notify helper | `1` / `0`; send a best-effort Telegram startup notification |
180
  | `OPENCLAW_STARTUP_NOTIFY_TELEGRAM_TO` | startup notify helper | Optional explicit Telegram chat id; defaults to first `OPENCLAW_TELEGRAM_ALLOW_FROM` entry |
 
181
  | `OPENCLAW_AUTH_PROFILES_JSON_B64` | startup auth import | Base64-encoded full `auth-profiles.json` exported from another OpenClaw host |
182
  | `OPENCLAW_AUTH_PROFILES_IMPORT_B64` | startup auth import | Alternate secret name for the same full `auth-profiles.json` import |
183
  | `OPENCLAW_OPENAI_CODEX_PROFILE_B64` | startup auth import | Base64-encoded JSON object for one `openai-codex:default` profile |
 
37
  - **`OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS`** — (optional) Comma-separated chat IDs to receive startup notifications from the init script. Recommended if you want an explicit "boot completed" signal.
38
  - **`OPENCLAW_TELEGRAM_INIT_NOTIFY_MESSAGE`** — (optional) Extra plain-text line appended to the startup notification.
39
  - **`OPENCLAW_TELEGRAM_DROP_PENDING_UPDATES_ON_START`** — (optional) `1` / `0`; when startup finds a webhook and removes it, also drops queued Telegram updates. Default `0`.
40
+ - **`OPENCLAW_STARTUP_NOTIFY_TELEGRAM_TO`** — (optional) Single fallback chat ID for startup notifier when `OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS` is not set.
41
  - **`OPENCLAW_TELEGRAM_DM_POLICY`** / **`OPENCLAW_TELEGRAM_ALLOW_FROM`** / **`OPENCLAW_TELEGRAM_GROUP_POLICY`** / **`OPENCLAW_TELEGRAM_GROUP_ALLOW_FROM`** / **`OPENCLAW_TELEGRAM_REQUIRE_MENTION`** — (optional) Advanced Telegram access-control settings.
42
  4. **Build and run** — push to the Space repo; the Space will build and start the gateway. Startup writes config so the default model chain is `Qwen3-8B -> DeepSeek-R1 -> openai-codex/gpt-5.4`, the Control UI accepts token-only connections (no device pairing), and Supabase sync starts automatically when `SUPABASE_URL` + `SUPABASE_KEY` are present.
43
 
 
73
  OPENCLAW_MODEL_FALLBACK_2=openai-codex/gpt-5.4
74
  OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS=https://your-space.hf.space
75
  OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS=123456789,-1001234567890
76
+ OPENCLAW_STARTUP_NOTIFY_TELEGRAM_TO=123456789
77
  OPENCLAW_TELEGRAM_INIT_NOTIFY_MESSAGE=HF Space boot completed
78
  OPENCLAW_TELEGRAM_DROP_PENDING_UPDATES_ON_START=0
79
  OPENCLAW_SUPABASE_TABLE=openclaw_state
 
84
 
85
  1. Ask the target user to start the bot first, or add the bot to the target group/channel before using that chat ID.
86
  2. Use numeric chat IDs when possible; they are less fragile than usernames.
87
+ 3. The startup notifier now tries several network transports: `fetch-json`, `https-family4-json`, `https-family4-form`, `https-resolve4-json`, and `curl-json`.
88
+ 4. If no explicit startup chat ID is configured, the notifier falls back to `OPENCLAW_STARTUP_NOTIFY_TELEGRAM_TO`, then `OPENCLAW_TELEGRAM_ALLOW_FROM`, then recent `getUpdates` chat IDs.
89
+ 5. Startup messages are tagged with both `transport=` and `payload=` in the message body so you can report exactly which path worked.
90
 
91
  ## Keep it running 24/7
92
 
 
181
  | Telegram command menu | `channels.telegram.commands.native = off` | This HF wrapper disables Telegram command-menu registration to reduce noisy `setMyCommands` retries on constrained hosts |
182
  | `OPENCLAW_STARTUP_NOTIFY` | startup notify helper | `1` / `0`; send a best-effort Telegram startup notification |
183
  | `OPENCLAW_STARTUP_NOTIFY_TELEGRAM_TO` | startup notify helper | Optional explicit Telegram chat id; defaults to first `OPENCLAW_TELEGRAM_ALLOW_FROM` entry |
184
+ | `OPENCLAW_TELEGRAM_API_HOST` | startup notify helper | Optional alternate Telegram Bot API host; default `api.telegram.org` |
185
  | `OPENCLAW_AUTH_PROFILES_JSON_B64` | startup auth import | Base64-encoded full `auth-profiles.json` exported from another OpenClaw host |
186
  | `OPENCLAW_AUTH_PROFILES_IMPORT_B64` | startup auth import | Alternate secret name for the same full `auth-profiles.json` import |
187
  | `OPENCLAW_OPENAI_CODEX_PROFILE_B64` | startup auth import | Base64-encoded JSON object for one `openai-codex:default` profile |
startup-notify.mjs CHANGED
@@ -1,34 +1,337 @@
1
  #!/usr/bin/env node
2
- import os from 'node:os';
 
 
 
3
 
 
4
  const token = process.env.TELEGRAM_BOT_TOKEN?.trim();
5
- const explicitChatId = process.env.OPENCLAW_STARTUP_NOTIFY_TELEGRAM_TO?.trim();
6
- const allowFrom = process.env.OPENCLAW_TELEGRAM_ALLOW_FROM?.split(',').map(s => s.trim()).filter(Boolean) || [];
7
- const chatId = explicitChatId || allowFrom[0];
8
- const initNotifyChatIds = process.env.OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS?.trim();
9
- const spaceHost = process.env.SPACE_HOST?.trim() || process.env.HF_SPACE_HOST?.trim() || '';
10
- const model = process.env.OPENCLAW_MODEL_PRIMARY?.trim() || process.env.OPENCLAW_DEFAULT_MODEL?.trim() || process.env.OPENCLAW_HF_DEFAULT_MODEL?.trim() || 'unknown';
11
- const where = process.env.HF_SPACE_ID?.trim() || process.env.SPACE_ID?.trim() || 'HF Space';
12
- const enabled = (process.env.OPENCLAW_STARTUP_NOTIFY ?? '1').trim().toLowerCase();
13
- if (['0','false','no','off'].includes(enabled)) process.exit(0);
14
- if (initNotifyChatIds) {
15
- console.log('[openclaw-startup-notify] skipped legacy notifier because OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS is set');
16
  process.exit(0);
17
  }
18
- if (!token || !chatId) {
19
- console.log(`[openclaw-startup-notify] skipped token=${token ? 1 : 0} chat=${chatId ? 1 : 0}`);
20
- process.exit(0);
21
  }
22
- const text = `OpenClaw 上線:${where}${spaceHost ? `\nHost: ${spaceHost}` : ''}\nModel: ${model}`;
23
- try {
24
- const r = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
25
- method: 'POST',
26
- headers: { 'content-type': 'application/json' },
27
- body: JSON.stringify({ chat_id: chatId, text, disable_web_page_preview: true }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  });
29
- const body = await r.text();
30
- console.log(`[openclaw-startup-notify] status=${r.status} body=${body.slice(0,300)}`);
31
- } catch (err) {
32
- console.error(`[openclaw-startup-notify] failed ${err?.name || 'Error'}: ${err?.message || err}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  process.exit(0);
34
  }
 
1
  #!/usr/bin/env node
2
+ import dns from "node:dns/promises";
3
+ import https from "node:https";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
 
7
+ const execFileAsync = promisify(execFile);
8
  const token = process.env.TELEGRAM_BOT_TOKEN?.trim();
9
+ const enabled = (process.env.OPENCLAW_STARTUP_NOTIFY ?? "1").trim().toLowerCase();
10
+ const apiHost = process.env.OPENCLAW_TELEGRAM_API_HOST?.trim() || "api.telegram.org";
11
+ const apiBase = `https://${apiHost}`;
12
+ const startupTimestamp = new Date().toISOString();
13
+
14
+ if (["0", "false", "no", "off"].includes(enabled)) process.exit(0);
15
+ if (!token) {
16
+ console.log("[openclaw-startup-notify] skipped token=0");
 
 
 
17
  process.exit(0);
18
  }
19
+
20
+ function parseCsv(raw) {
21
+ return raw ? raw.split(",").map((item) => item.trim()).filter(Boolean) : [];
22
  }
23
+
24
+ function dedupe(values) {
25
+ return [...new Set(values.filter(Boolean))];
26
+ }
27
+
28
+ function delay(ms) {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+
32
+ function extractChatIdsFromUpdates(updates) {
33
+ const chatIds = [];
34
+ for (const update of updates || []) {
35
+ const messageChatId = update?.message?.chat?.id;
36
+ const editedMessageChatId = update?.edited_message?.chat?.id;
37
+ const callbackChatId = update?.callback_query?.message?.chat?.id;
38
+ const memberChatId = update?.my_chat_member?.chat?.id;
39
+ for (const value of [messageChatId, editedMessageChatId, callbackChatId, memberChatId]) {
40
+ if (value !== undefined && value !== null) chatIds.push(String(value));
41
+ }
42
+ }
43
+ return dedupe(chatIds);
44
+ }
45
+
46
+ function escapeHtml(raw) {
47
+ return String(raw)
48
+ .replaceAll("&", "&")
49
+ .replaceAll("<", "&lt;")
50
+ .replaceAll(">", "&gt;");
51
+ }
52
+
53
+ function buildMessages() {
54
+ const where = process.env.HF_SPACE_ID?.trim() || process.env.SPACE_ID?.trim() || "HF Space";
55
+ const spaceHost = process.env.SPACE_HOST?.trim() || process.env.HF_SPACE_HOST?.trim() || "";
56
+ const model =
57
+ process.env.OPENCLAW_MODEL_PRIMARY?.trim() ||
58
+ process.env.OPENCLAW_DEFAULT_MODEL?.trim() ||
59
+ process.env.OPENCLAW_HF_DEFAULT_MODEL?.trim() ||
60
+ "unknown";
61
+ const customMessage = process.env.OPENCLAW_TELEGRAM_INIT_NOTIFY_MESSAGE?.trim();
62
+ const htmlLines = [
63
+ "<b>OpenClaw startup notify</b>",
64
+ `Space: <code>${escapeHtml(where)}</code>`,
65
+ `Started: <code>${escapeHtml(startupTimestamp)}</code>`,
66
+ `Model: <code>${escapeHtml(model)}</code>`,
67
+ ];
68
+ if (spaceHost) htmlLines.push(`Host: <code>${escapeHtml(spaceHost)}</code>`);
69
+ if (customMessage) {
70
+ htmlLines.push("");
71
+ htmlLines.push(escapeHtml(customMessage));
72
+ }
73
+ const html = htmlLines.join("\n");
74
+ const plain = html
75
+ .replace(/<[^>]+>/g, "")
76
+ .replaceAll("&lt;", "<")
77
+ .replaceAll("&gt;", ">")
78
+ .replaceAll("&amp;", "&");
79
+ return { html, plain };
80
+ }
81
+
82
+ async function fetchJson(url, init) {
83
+ const response = await fetch(url, init);
84
+ const data = await response.json().catch(() => null);
85
+ if (!response.ok || !data?.ok) {
86
+ throw new Error(data?.description || `${response.status} ${response.statusText}`.trim());
87
+ }
88
+ return data.result;
89
+ }
90
+
91
+ function httpsJsonRequest(url, { method = "POST", headers = {}, body, family, lookup, servername } = {}) {
92
+ return new Promise((resolve, reject) => {
93
+ const parsed = new URL(url);
94
+ const req = https.request(
95
+ {
96
+ protocol: parsed.protocol,
97
+ hostname: parsed.hostname,
98
+ host: parsed.hostname,
99
+ port: parsed.port || 443,
100
+ path: `${parsed.pathname}${parsed.search}`,
101
+ method,
102
+ headers,
103
+ family,
104
+ lookup,
105
+ servername,
106
+ },
107
+ (res) => {
108
+ let raw = "";
109
+ res.setEncoding("utf8");
110
+ res.on("data", (chunk) => {
111
+ raw += chunk;
112
+ });
113
+ res.on("end", () => {
114
+ try {
115
+ const data = JSON.parse(raw || "null");
116
+ if (res.statusCode < 200 || res.statusCode >= 300 || !data?.ok) {
117
+ reject(new Error(data?.description || `${res.statusCode || 0} ${res.statusMessage || ""}`.trim()));
118
+ return;
119
+ }
120
+ resolve(data.result);
121
+ } catch (error) {
122
+ reject(error);
123
+ }
124
+ });
125
+ },
126
+ );
127
+ req.on("error", reject);
128
+ req.setTimeout(15000, () => req.destroy(new Error("request timeout")));
129
+ if (body) req.write(body);
130
+ req.end();
131
  });
132
+ }
133
+
134
+ async function curlJsonRequest(url, payload) {
135
+ const args = [
136
+ "--silent",
137
+ "--show-error",
138
+ "--max-time",
139
+ "20",
140
+ "-H",
141
+ "content-type: application/json",
142
+ "-d",
143
+ JSON.stringify(payload),
144
+ url,
145
+ ];
146
+ const { stdout } = await execFileAsync("curl", args, { timeout: 25000 });
147
+ const data = JSON.parse(stdout || "null");
148
+ if (!data?.ok) throw new Error(data?.description || "curl request failed");
149
+ return data.result;
150
+ }
151
+
152
+ function methodUrl(method) {
153
+ return `${apiBase}/bot${token}/${method}`;
154
+ }
155
+
156
+ const transports = {
157
+ "fetch-json": {
158
+ async call(method, payload = {}) {
159
+ return fetchJson(methodUrl(method), {
160
+ method: "POST",
161
+ headers: { "content-type": "application/json" },
162
+ body: JSON.stringify(payload),
163
+ });
164
+ },
165
+ },
166
+ "https-family4-json": {
167
+ async call(method, payload = {}) {
168
+ return httpsJsonRequest(methodUrl(method), {
169
+ method: "POST",
170
+ headers: { "content-type": "application/json" },
171
+ body: JSON.stringify(payload),
172
+ family: 4,
173
+ });
174
+ },
175
+ },
176
+ "https-family4-form": {
177
+ async call(method, payload = {}) {
178
+ const body = new URLSearchParams(
179
+ Object.entries(payload).map(([key, value]) => [key, Array.isArray(value) ? JSON.stringify(value) : String(value)]),
180
+ ).toString();
181
+ return httpsJsonRequest(methodUrl(method), {
182
+ method: "POST",
183
+ headers: { "content-type": "application/x-www-form-urlencoded" },
184
+ body,
185
+ family: 4,
186
+ });
187
+ },
188
+ },
189
+ "https-resolve4-json": {
190
+ async call(method, payload = {}) {
191
+ const addresses = await dns.resolve4(apiHost);
192
+ const ip = addresses[0];
193
+ if (!ip) throw new Error("no IPv4 address from resolve4");
194
+ const directUrl = `https://${ip}/bot${token}/${method}`;
195
+ return httpsJsonRequest(directUrl, {
196
+ method: "POST",
197
+ headers: {
198
+ "content-type": "application/json",
199
+ host: apiHost,
200
+ },
201
+ body: JSON.stringify(payload),
202
+ lookup(hostname, options, callback) {
203
+ callback(null, ip, 4);
204
+ },
205
+ servername: apiHost,
206
+ });
207
+ },
208
+ },
209
+ "curl-json": {
210
+ async call(method, payload = {}) {
211
+ return curlJsonRequest(methodUrl(method), payload);
212
+ },
213
+ },
214
+ };
215
+
216
+ async function tryTransport(name, method, payload) {
217
+ try {
218
+ const result = await transports[name].call(method, payload);
219
+ console.log(`[openclaw-startup-notify] transport=${name} method=${method} status=ok`);
220
+ return { ok: true, result };
221
+ } catch (error) {
222
+ console.warn(
223
+ `[openclaw-startup-notify] transport=${name} method=${method} status=failed reason=${
224
+ error instanceof Error ? error.message : String(error)
225
+ }`,
226
+ );
227
+ return { ok: false, error };
228
+ }
229
+ }
230
+
231
+ async function discoverWorkingTransports() {
232
+ const order = Object.keys(transports);
233
+ const working = [];
234
+ for (const name of order) {
235
+ const probe = await tryTransport(name, "getMe", {});
236
+ if (probe.ok) working.push(name);
237
+ }
238
+ return working;
239
+ }
240
+
241
+ async function discoverChatIds(workingTransports) {
242
+ const configured = dedupe([
243
+ ...parseCsv(process.env.OPENCLAW_TELEGRAM_INIT_NOTIFY_CHAT_IDS?.trim()),
244
+ process.env.OPENCLAW_STARTUP_NOTIFY_TELEGRAM_TO?.trim(),
245
+ ...parseCsv(process.env.OPENCLAW_TELEGRAM_ALLOW_FROM?.trim()),
246
+ ]);
247
+ if (configured.length > 0) {
248
+ console.log(`[openclaw-startup-notify] chat_ids_source=configured count=${configured.length}`);
249
+ return configured;
250
+ }
251
+
252
+ for (const transport of workingTransports) {
253
+ const webhookInfo = await tryTransport(transport, "getWebhookInfo", {});
254
+ if (webhookInfo.ok) {
255
+ console.log(
256
+ `[openclaw-startup-notify] transport=${transport} webhook_url=${webhookInfo.result?.url || "-"} pending_updates=${
257
+ webhookInfo.result?.pending_update_count ?? 0
258
+ }`,
259
+ );
260
+ }
261
+ await tryTransport(transport, "deleteWebhook", { drop_pending_updates: false });
262
+ const updates = await tryTransport(transport, "getUpdates", {
263
+ timeout: 0,
264
+ limit: 10,
265
+ allowed_updates: ["message", "edited_message", "callback_query", "my_chat_member"],
266
+ });
267
+ if (updates.ok) {
268
+ const discovered = extractChatIdsFromUpdates(updates.result);
269
+ if (discovered.length > 0) {
270
+ console.log(`[openclaw-startup-notify] chat_ids_source=getUpdates transport=${transport} count=${discovered.length}`);
271
+ return discovered;
272
+ }
273
+ }
274
+ }
275
+
276
+ console.log("[openclaw-startup-notify] chat_ids_source=none count=0");
277
+ return [];
278
+ }
279
+
280
+ async function sendNotifications(workingTransports, chatIds) {
281
+ const { html, plain } = buildMessages();
282
+ const payloadVariants = [
283
+ {
284
+ name: "json-html",
285
+ build: (chatId, transport) => ({
286
+ chat_id: chatId,
287
+ text: `[startup transport=${transport} payload=json-html]\n${html}`,
288
+ parse_mode: "HTML",
289
+ disable_web_page_preview: true,
290
+ }),
291
+ },
292
+ {
293
+ name: "json-plain",
294
+ build: (chatId, transport) => ({
295
+ chat_id: chatId,
296
+ text: `[startup transport=${transport} payload=json-plain]\n${plain}`,
297
+ disable_web_page_preview: true,
298
+ }),
299
+ },
300
+ ];
301
+
302
+ for (const transport of workingTransports) {
303
+ for (const chatId of chatIds) {
304
+ for (const variant of payloadVariants) {
305
+ await delay(250);
306
+ const result = await tryTransport(transport, "sendMessage", variant.build(chatId, transport));
307
+ console.log(
308
+ `[openclaw-startup-notify] transport=${transport} chat=${chatId} payload=${variant.name} sent=${
309
+ result.ok ? "1" : "0"
310
+ }`,
311
+ );
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ try {
318
+ const workingTransports = await discoverWorkingTransports();
319
+ if (workingTransports.length === 0) {
320
+ console.warn("[openclaw-startup-notify] no working Telegram transport");
321
+ process.exit(0);
322
+ }
323
+ console.log(`[openclaw-startup-notify] working_transports=${workingTransports.join(",")}`);
324
+ const chatIds = await discoverChatIds(workingTransports);
325
+ if (chatIds.length === 0) {
326
+ console.warn("[openclaw-startup-notify] no chat ids available for startup notification");
327
+ process.exit(0);
328
+ }
329
+ await sendNotifications(workingTransports, chatIds);
330
+ } catch (error) {
331
+ console.error(
332
+ `[openclaw-startup-notify] fatal ${
333
+ error instanceof Error ? `${error.name}: ${error.message}` : String(error)
334
+ }`,
335
+ );
336
  process.exit(0);
337
  }