@femtomc/mu-server 26.2.95 → 26.2.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/control_plane.d.ts +16 -0
- package/dist/control_plane.js +301 -76
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -189,6 +189,13 @@ When validating attachment support end-to-end, use this sequence:
|
|
|
189
189
|
Operational fallback expectations:
|
|
190
190
|
|
|
191
191
|
- If media upload fails, Telegram delivery falls back to text `sendMessage`.
|
|
192
|
+
- Telegram text delivery chunks long messages into deterministic in-order `sendMessage` calls to stay below API size limits.
|
|
193
|
+
- When outbound metadata includes `telegram_reply_to_message_id`, Telegram delivery anchors replies to the originating chat message.
|
|
194
|
+
- Invalid/non-integer `telegram_reply_to_message_id` metadata is ignored so delivery degrades gracefully to non-anchored sends.
|
|
195
|
+
- Awaiting-confirmation envelopes include Telegram inline `Confirm`/`Cancel` callback buttons when interaction metadata provides confirmation actions.
|
|
196
|
+
- Telegram callback payloads are contract-limited to `confirm:<command_id>` and `cancel:<command_id>`; unsupported payloads are explicitly rejected.
|
|
197
|
+
- Callback buttons keep parity with command fallback: `/mu confirm <id>` and `/mu cancel <id>` remain valid.
|
|
198
|
+
- Group/supergroup Telegram freeform text is deterministic no-op with guidance; explicit `/mu ...` commands stay actionable.
|
|
192
199
|
- If Slack/Telegram bot token is missing, channel capability reason codes should report `*_bot_token_missing` and outbound delivery retries rather than hard-crashing runtime.
|
|
193
200
|
|
|
194
201
|
## Running the Server
|
package/dist/control_plane.d.ts
CHANGED
|
@@ -4,21 +4,35 @@ import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type Contr
|
|
|
4
4
|
import { detectAdapters } from "./control_plane_adapter_registry.js";
|
|
5
5
|
export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
|
|
6
6
|
export { detectAdapters };
|
|
7
|
+
type TelegramInlineKeyboardButton = {
|
|
8
|
+
text: string;
|
|
9
|
+
callback_data: string;
|
|
10
|
+
};
|
|
11
|
+
type TelegramInlineKeyboardMarkup = {
|
|
12
|
+
inline_keyboard: TelegramInlineKeyboardButton[][];
|
|
13
|
+
};
|
|
7
14
|
export type TelegramSendMessagePayload = {
|
|
8
15
|
chat_id: string;
|
|
9
16
|
text: string;
|
|
10
17
|
parse_mode?: "Markdown";
|
|
11
18
|
disable_web_page_preview?: boolean;
|
|
19
|
+
reply_markup?: TelegramInlineKeyboardMarkup;
|
|
20
|
+
reply_to_message_id?: number;
|
|
21
|
+
allow_sending_without_reply?: boolean;
|
|
12
22
|
};
|
|
13
23
|
export type TelegramSendPhotoPayload = {
|
|
14
24
|
chat_id: string;
|
|
15
25
|
photo: string;
|
|
16
26
|
caption?: string;
|
|
27
|
+
reply_to_message_id?: number;
|
|
28
|
+
allow_sending_without_reply?: boolean;
|
|
17
29
|
};
|
|
18
30
|
export type TelegramSendDocumentPayload = {
|
|
19
31
|
chat_id: string;
|
|
20
32
|
document: string;
|
|
21
33
|
caption?: string;
|
|
34
|
+
reply_to_message_id?: number;
|
|
35
|
+
allow_sending_without_reply?: boolean;
|
|
22
36
|
};
|
|
23
37
|
/**
|
|
24
38
|
* Telegram supports a markdown dialect that uses single markers for emphasis.
|
|
@@ -32,6 +46,8 @@ export declare function buildTelegramSendMessagePayload(opts: {
|
|
|
32
46
|
text: string;
|
|
33
47
|
richFormatting: boolean;
|
|
34
48
|
}): TelegramSendMessagePayload;
|
|
49
|
+
export declare function splitTelegramMessageText(text: string, maxLen?: number): string[];
|
|
50
|
+
export declare function splitSlackMessageText(text: string, maxLen?: number): string[];
|
|
35
51
|
export declare function deliverTelegramOutboxRecord(opts: {
|
|
36
52
|
botToken: string;
|
|
37
53
|
record: OutboxRecord;
|
package/dist/control_plane.js
CHANGED
|
@@ -79,6 +79,147 @@ export function buildTelegramSendMessagePayload(opts) {
|
|
|
79
79
|
disable_web_page_preview: true,
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
const TELEGRAM_MESSAGE_MAX_LEN = 4_096;
|
|
83
|
+
function maybeParseTelegramMessageId(value) {
|
|
84
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
85
|
+
return Math.trunc(value);
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
88
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
89
|
+
if (Number.isFinite(parsed)) {
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
export function splitTelegramMessageText(text, maxLen = TELEGRAM_MESSAGE_MAX_LEN) {
|
|
96
|
+
if (text.length <= maxLen) {
|
|
97
|
+
return [text];
|
|
98
|
+
}
|
|
99
|
+
const chunks = [];
|
|
100
|
+
let cursor = 0;
|
|
101
|
+
while (cursor < text.length) {
|
|
102
|
+
const end = Math.min(text.length, cursor + maxLen);
|
|
103
|
+
if (end === text.length) {
|
|
104
|
+
chunks.push(text.slice(cursor));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
const window = text.slice(cursor, end);
|
|
108
|
+
const splitAtNewline = window.lastIndexOf("\n");
|
|
109
|
+
const splitPoint = splitAtNewline >= Math.floor(maxLen * 0.5) ? cursor + splitAtNewline + 1 : end;
|
|
110
|
+
chunks.push(text.slice(cursor, splitPoint));
|
|
111
|
+
cursor = splitPoint;
|
|
112
|
+
}
|
|
113
|
+
return chunks;
|
|
114
|
+
}
|
|
115
|
+
const SLACK_MESSAGE_MAX_LEN = 3_500;
|
|
116
|
+
export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
|
|
117
|
+
if (text.length <= maxLen) {
|
|
118
|
+
return [text];
|
|
119
|
+
}
|
|
120
|
+
const chunks = [];
|
|
121
|
+
let cursor = 0;
|
|
122
|
+
while (cursor < text.length) {
|
|
123
|
+
const end = Math.min(text.length, cursor + maxLen);
|
|
124
|
+
if (end === text.length) {
|
|
125
|
+
chunks.push(text.slice(cursor));
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
const window = text.slice(cursor, end);
|
|
129
|
+
const splitAtNewline = window.lastIndexOf("\n");
|
|
130
|
+
const splitPoint = splitAtNewline >= Math.floor(maxLen * 0.5) ? cursor + splitAtNewline + 1 : end;
|
|
131
|
+
chunks.push(text.slice(cursor, splitPoint));
|
|
132
|
+
cursor = splitPoint;
|
|
133
|
+
}
|
|
134
|
+
return chunks;
|
|
135
|
+
}
|
|
136
|
+
function slackBlocksForOutboxRecord(record) {
|
|
137
|
+
const interactionMessage = record.envelope.metadata?.interaction_message;
|
|
138
|
+
if (!interactionMessage || typeof interactionMessage !== "object") {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const rawActions = interactionMessage.actions;
|
|
142
|
+
if (!Array.isArray(rawActions) || rawActions.length === 0) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const buttons = [];
|
|
146
|
+
for (const action of rawActions) {
|
|
147
|
+
if (!action || typeof action !== "object") {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const candidate = action;
|
|
151
|
+
if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
|
|
155
|
+
const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
|
|
156
|
+
if (confirm?.[1]) {
|
|
157
|
+
buttons.push({
|
|
158
|
+
type: "button",
|
|
159
|
+
text: { type: "plain_text", text: candidate.label },
|
|
160
|
+
value: `confirm:${confirm[1]}`,
|
|
161
|
+
action_id: `confirm_${confirm[1]}`,
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
|
|
166
|
+
if (cancel?.[1]) {
|
|
167
|
+
buttons.push({
|
|
168
|
+
type: "button",
|
|
169
|
+
text: { type: "plain_text", text: candidate.label },
|
|
170
|
+
value: `cancel:${cancel[1]}`,
|
|
171
|
+
action_id: `cancel_${cancel[1]}`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (buttons.length === 0) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return [
|
|
179
|
+
{ type: "section", text: { type: "mrkdwn", text: record.envelope.body } },
|
|
180
|
+
{ type: "actions", elements: buttons },
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
function slackThreadTsFromMetadata(metadata) {
|
|
184
|
+
const candidates = [metadata?.slack_thread_ts, metadata?.slack_message_ts, metadata?.thread_ts];
|
|
185
|
+
for (const value of candidates) {
|
|
186
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
187
|
+
return value.trim();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
function telegramReplyMarkupForOutboxRecord(record) {
|
|
193
|
+
const interactionMessage = record.envelope.metadata?.interaction_message;
|
|
194
|
+
if (!interactionMessage || typeof interactionMessage !== "object") {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const rawActions = interactionMessage.actions;
|
|
198
|
+
if (!Array.isArray(rawActions) || rawActions.length === 0) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const row = [];
|
|
202
|
+
for (const action of rawActions) {
|
|
203
|
+
if (!action || typeof action !== "object") {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const candidate = action;
|
|
207
|
+
if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
|
|
211
|
+
const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
|
|
212
|
+
if (confirm?.[1]) {
|
|
213
|
+
row.push({ text: candidate.label, callback_data: `confirm:${confirm[1]}` });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
|
|
217
|
+
if (cancel?.[1]) {
|
|
218
|
+
row.push({ text: candidate.label, callback_data: `cancel:${cancel[1]}` });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return row.length > 0 ? { inline_keyboard: [row] } : undefined;
|
|
222
|
+
}
|
|
82
223
|
async function postTelegramMessage(botToken, payload) {
|
|
83
224
|
return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
84
225
|
method: "POST",
|
|
@@ -133,37 +274,76 @@ function parseRetryDelayMs(res) {
|
|
|
133
274
|
}
|
|
134
275
|
return parsed * 1000;
|
|
135
276
|
}
|
|
277
|
+
async function sendTelegramTextChunks(opts) {
|
|
278
|
+
const chunks = splitTelegramMessageText(opts.basePayload.text);
|
|
279
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
280
|
+
const payload = {
|
|
281
|
+
...opts.basePayload,
|
|
282
|
+
text: chunk,
|
|
283
|
+
...(index === 0 ? {} : { reply_markup: undefined, reply_to_message_id: undefined, allow_sending_without_reply: undefined }),
|
|
284
|
+
};
|
|
285
|
+
let res = await postTelegramMessage(opts.botToken, payload);
|
|
286
|
+
if (!res.ok && res.status === 400 && payload.parse_mode && opts.fallbackToPlainOnMarkdownError) {
|
|
287
|
+
const plainPayload = buildTelegramSendMessagePayload({
|
|
288
|
+
chatId: payload.chat_id,
|
|
289
|
+
text: payload.text,
|
|
290
|
+
richFormatting: false,
|
|
291
|
+
});
|
|
292
|
+
res = await postTelegramMessage(opts.botToken, {
|
|
293
|
+
...plainPayload,
|
|
294
|
+
...(payload.reply_markup ? { reply_markup: payload.reply_markup } : {}),
|
|
295
|
+
...(payload.reply_to_message_id != null
|
|
296
|
+
? {
|
|
297
|
+
reply_to_message_id: payload.reply_to_message_id,
|
|
298
|
+
allow_sending_without_reply: payload.allow_sending_without_reply,
|
|
299
|
+
}
|
|
300
|
+
: {}),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (!res.ok) {
|
|
304
|
+
return { ok: false, response: res, body: await res.text().catch(() => "") };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { ok: true };
|
|
308
|
+
}
|
|
136
309
|
export async function deliverTelegramOutboxRecord(opts) {
|
|
137
310
|
const { botToken, record } = opts;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
311
|
+
const replyMarkup = telegramReplyMarkupForOutboxRecord(record);
|
|
312
|
+
const replyToMessageId = maybeParseTelegramMessageId(record.envelope.metadata?.telegram_reply_to_message_id);
|
|
313
|
+
const fallbackMessagePayload = {
|
|
314
|
+
...buildTelegramSendMessagePayload({
|
|
315
|
+
chatId: record.envelope.channel_conversation_id,
|
|
316
|
+
text: record.envelope.body,
|
|
317
|
+
richFormatting: true,
|
|
318
|
+
}),
|
|
319
|
+
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
|
320
|
+
...(replyToMessageId != null
|
|
321
|
+
? {
|
|
322
|
+
reply_to_message_id: replyToMessageId,
|
|
323
|
+
allow_sending_without_reply: true,
|
|
324
|
+
}
|
|
325
|
+
: {}),
|
|
326
|
+
};
|
|
143
327
|
const firstAttachment = record.envelope.attachments?.[0] ?? null;
|
|
144
328
|
if (!firstAttachment) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
if (res.ok) {
|
|
329
|
+
const sent = await sendTelegramTextChunks({
|
|
330
|
+
botToken,
|
|
331
|
+
basePayload: fallbackMessagePayload,
|
|
332
|
+
fallbackToPlainOnMarkdownError: true,
|
|
333
|
+
});
|
|
334
|
+
if (sent.ok) {
|
|
154
335
|
return { kind: "delivered" };
|
|
155
336
|
}
|
|
156
|
-
|
|
157
|
-
if (res.status === 429 || res.status >= 500) {
|
|
337
|
+
if (sent.response.status === 429 || sent.response.status >= 500) {
|
|
158
338
|
return {
|
|
159
339
|
kind: "retry",
|
|
160
|
-
error: `telegram sendMessage ${
|
|
161
|
-
retryDelayMs: parseRetryDelayMs(
|
|
340
|
+
error: `telegram sendMessage ${sent.response.status}: ${sent.body}`,
|
|
341
|
+
retryDelayMs: parseRetryDelayMs(sent.response),
|
|
162
342
|
};
|
|
163
343
|
}
|
|
164
344
|
return {
|
|
165
345
|
kind: "retry",
|
|
166
|
-
error: `telegram sendMessage ${
|
|
346
|
+
error: `telegram sendMessage ${sent.response.status}: ${sent.body}`,
|
|
167
347
|
};
|
|
168
348
|
}
|
|
169
349
|
const mediaMethod = chooseTelegramMediaMethod(firstAttachment);
|
|
@@ -180,11 +360,23 @@ export async function deliverTelegramOutboxRecord(opts) {
|
|
|
180
360
|
chat_id: record.envelope.channel_conversation_id,
|
|
181
361
|
photo: firstAttachment.reference.file_id,
|
|
182
362
|
caption: mediaCaption,
|
|
363
|
+
...(replyToMessageId != null
|
|
364
|
+
? {
|
|
365
|
+
reply_to_message_id: replyToMessageId,
|
|
366
|
+
allow_sending_without_reply: true,
|
|
367
|
+
}
|
|
368
|
+
: {}),
|
|
183
369
|
}
|
|
184
370
|
: {
|
|
185
371
|
chat_id: record.envelope.channel_conversation_id,
|
|
186
372
|
document: firstAttachment.reference.file_id,
|
|
187
373
|
caption: mediaCaption,
|
|
374
|
+
...(replyToMessageId != null
|
|
375
|
+
? {
|
|
376
|
+
reply_to_message_id: replyToMessageId,
|
|
377
|
+
allow_sending_without_reply: true,
|
|
378
|
+
}
|
|
379
|
+
: {}),
|
|
188
380
|
});
|
|
189
381
|
}
|
|
190
382
|
else {
|
|
@@ -206,6 +398,10 @@ export async function deliverTelegramOutboxRecord(opts) {
|
|
|
206
398
|
if (mediaCaption.length > 0) {
|
|
207
399
|
form.append("caption", mediaCaption);
|
|
208
400
|
}
|
|
401
|
+
if (replyToMessageId != null) {
|
|
402
|
+
form.append("reply_to_message_id", String(replyToMessageId));
|
|
403
|
+
form.append("allow_sending_without_reply", "true");
|
|
404
|
+
}
|
|
209
405
|
form.append(mediaField, new Blob([body], { type: contentType }), filename);
|
|
210
406
|
mediaResponse = await postTelegramApiMultipart(botToken, mediaMethod, form);
|
|
211
407
|
}
|
|
@@ -220,26 +416,38 @@ export async function deliverTelegramOutboxRecord(opts) {
|
|
|
220
416
|
retryDelayMs: parseRetryDelayMs(mediaResponse),
|
|
221
417
|
};
|
|
222
418
|
}
|
|
223
|
-
const fallbackPlainPayload =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
419
|
+
const fallbackPlainPayload = {
|
|
420
|
+
...buildTelegramSendMessagePayload({
|
|
421
|
+
chatId: record.envelope.channel_conversation_id,
|
|
422
|
+
text: record.envelope.body,
|
|
423
|
+
richFormatting: false,
|
|
424
|
+
}),
|
|
425
|
+
...(replyMarkup ? { reply_markup: replyMarkup } : {}),
|
|
426
|
+
...(replyToMessageId != null
|
|
427
|
+
? {
|
|
428
|
+
reply_to_message_id: replyToMessageId,
|
|
429
|
+
allow_sending_without_reply: true,
|
|
430
|
+
}
|
|
431
|
+
: {}),
|
|
432
|
+
};
|
|
433
|
+
const fallbackSent = await sendTelegramTextChunks({
|
|
434
|
+
botToken,
|
|
435
|
+
basePayload: fallbackPlainPayload,
|
|
436
|
+
fallbackToPlainOnMarkdownError: false,
|
|
227
437
|
});
|
|
228
|
-
|
|
229
|
-
if (fallbackRes.ok) {
|
|
438
|
+
if (fallbackSent.ok) {
|
|
230
439
|
return { kind: "delivered" };
|
|
231
440
|
}
|
|
232
|
-
|
|
233
|
-
if (fallbackRes.status === 429 || fallbackRes.status >= 500) {
|
|
441
|
+
if (fallbackSent.response.status === 429 || fallbackSent.response.status >= 500) {
|
|
234
442
|
return {
|
|
235
443
|
kind: "retry",
|
|
236
|
-
error: `telegram media fallback sendMessage ${
|
|
237
|
-
retryDelayMs: parseRetryDelayMs(
|
|
444
|
+
error: `telegram media fallback sendMessage ${fallbackSent.response.status}: ${fallbackSent.body}`,
|
|
445
|
+
retryDelayMs: parseRetryDelayMs(fallbackSent.response),
|
|
238
446
|
};
|
|
239
447
|
}
|
|
240
448
|
return {
|
|
241
449
|
kind: "retry",
|
|
242
|
-
error: `telegram media fallback sendMessage ${
|
|
450
|
+
error: `telegram media fallback sendMessage ${fallbackSent.response.status}: ${fallbackSent.body} (media_error=${mediaMethod} ${mediaResponse.status}: ${mediaBody})`,
|
|
243
451
|
};
|
|
244
452
|
}
|
|
245
453
|
async function postSlackJson(opts) {
|
|
@@ -257,30 +465,38 @@ async function postSlackJson(opts) {
|
|
|
257
465
|
export async function deliverSlackOutboxRecord(opts) {
|
|
258
466
|
const { botToken, record } = opts;
|
|
259
467
|
const attachments = record.envelope.attachments ?? [];
|
|
468
|
+
const textChunks = splitSlackMessageText(record.envelope.body);
|
|
469
|
+
const blocks = slackBlocksForOutboxRecord(record);
|
|
470
|
+
const threadTs = slackThreadTsFromMetadata(record.envelope.metadata);
|
|
260
471
|
if (attachments.length === 0) {
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
472
|
+
for (const [index, chunk] of textChunks.entries()) {
|
|
473
|
+
const delivered = await postSlackJson({
|
|
474
|
+
botToken,
|
|
475
|
+
method: "chat.postMessage",
|
|
476
|
+
payload: {
|
|
477
|
+
channel: record.envelope.channel_conversation_id,
|
|
478
|
+
text: chunk,
|
|
479
|
+
unfurl_links: false,
|
|
480
|
+
unfurl_media: false,
|
|
481
|
+
...(index === 0 && blocks ? { blocks } : {}),
|
|
482
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
if (delivered.response.ok && delivered.payload?.ok) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const status = delivered.response.status;
|
|
489
|
+
const err = delivered.payload?.error ?? "unknown_error";
|
|
490
|
+
if (status === 429 || status >= 500) {
|
|
491
|
+
return {
|
|
492
|
+
kind: "retry",
|
|
493
|
+
error: `slack chat.postMessage ${status}: ${err}`,
|
|
494
|
+
retryDelayMs: parseRetryDelayMs(delivered.response),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return { kind: "retry", error: `slack chat.postMessage ${status}: ${err}` };
|
|
282
498
|
}
|
|
283
|
-
return { kind: "
|
|
499
|
+
return { kind: "delivered" };
|
|
284
500
|
}
|
|
285
501
|
let firstError = null;
|
|
286
502
|
for (const [index, attachment] of attachments.entries()) {
|
|
@@ -311,7 +527,10 @@ export async function deliverSlackOutboxRecord(opts) {
|
|
|
311
527
|
form.set("filename", filename);
|
|
312
528
|
form.set("title", filename);
|
|
313
529
|
if (index === 0 && record.envelope.body.trim().length > 0) {
|
|
314
|
-
form.set("initial_comment", record.envelope.body);
|
|
530
|
+
form.set("initial_comment", textChunks[0] ?? record.envelope.body);
|
|
531
|
+
}
|
|
532
|
+
if (threadTs) {
|
|
533
|
+
form.set("thread_ts", threadTs);
|
|
315
534
|
}
|
|
316
535
|
form.set("file", new Blob([bytes], { type: contentType }), filename);
|
|
317
536
|
const uploaded = await fetch("https://slack.com/api/files.upload", {
|
|
@@ -337,33 +556,39 @@ export async function deliverSlackOutboxRecord(opts) {
|
|
|
337
556
|
}
|
|
338
557
|
}
|
|
339
558
|
}
|
|
340
|
-
if (firstError) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
559
|
+
if (firstError || textChunks.length > 1) {
|
|
560
|
+
for (const [index, chunk] of textChunks.entries()) {
|
|
561
|
+
if (index === 0 && !firstError) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const fallback = await postSlackJson({
|
|
565
|
+
botToken,
|
|
566
|
+
method: "chat.postMessage",
|
|
567
|
+
payload: {
|
|
568
|
+
channel: record.envelope.channel_conversation_id,
|
|
569
|
+
text: chunk,
|
|
570
|
+
unfurl_links: false,
|
|
571
|
+
unfurl_media: false,
|
|
572
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
if (fallback.response.ok && fallback.payload?.ok) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const status = fallback.response.status;
|
|
579
|
+
const err = fallback.payload?.error ?? "unknown_error";
|
|
580
|
+
if (status === 429 || status >= 500) {
|
|
581
|
+
return {
|
|
582
|
+
kind: "retry",
|
|
583
|
+
error: `slack chat.postMessage fallback ${status}: ${err}${firstError ? ` (upload_error=${firstError})` : ""}`,
|
|
584
|
+
retryDelayMs: parseRetryDelayMs(fallback.response),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
357
587
|
return {
|
|
358
588
|
kind: "retry",
|
|
359
|
-
error: `slack chat.postMessage fallback ${status}: ${err} (upload_error=${firstError})`,
|
|
360
|
-
retryDelayMs: parseRetryDelayMs(fallback.response),
|
|
589
|
+
error: `slack chat.postMessage fallback ${status}: ${err}${firstError ? ` (upload_error=${firstError})` : ""}`,
|
|
361
590
|
};
|
|
362
591
|
}
|
|
363
|
-
return {
|
|
364
|
-
kind: "retry",
|
|
365
|
-
error: `slack chat.postMessage fallback ${status}: ${err} (upload_error=${firstError})`,
|
|
366
|
-
};
|
|
367
592
|
}
|
|
368
593
|
return { kind: "delivered" };
|
|
369
594
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-server",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.96",
|
|
4
4
|
"description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"start": "bun run dist/cli.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@femtomc/mu-agent": "26.2.
|
|
34
|
-
"@femtomc/mu-control-plane": "26.2.
|
|
35
|
-
"@femtomc/mu-core": "26.2.
|
|
33
|
+
"@femtomc/mu-agent": "26.2.96",
|
|
34
|
+
"@femtomc/mu-control-plane": "26.2.96",
|
|
35
|
+
"@femtomc/mu-core": "26.2.96"
|
|
36
36
|
}
|
|
37
37
|
}
|