@femtomc/mu-server 26.2.106 → 26.2.108
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 +6 -10
- package/dist/api/control_plane.js +65 -1
- package/dist/control_plane.d.ts +20 -2
- package/dist/control_plane.js +293 -163
- package/dist/control_plane_adapter_registry.d.ts +2 -1
- package/dist/control_plane_adapter_registry.js +4 -0
- package/dist/control_plane_telegram_generation.d.ts +2 -1
- package/dist/control_plane_telegram_generation.js +3 -0
- package/dist/server_routing.js +1 -1
- package/package.json +35 -35
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@ HTTP API server for mu control-plane infrastructure.
|
|
|
4
4
|
Powers `mu serve`, messaging frontend transport routes, and
|
|
5
5
|
control-plane/session coordination endpoints.
|
|
6
6
|
|
|
7
|
-
> Scope note: server-routed
|
|
8
|
-
>
|
|
9
|
-
>
|
|
7
|
+
> Scope note: server-routed endpoints focus on runtime coordination only.
|
|
8
|
+
> Business reads/writes are CLI-first, while long-lived runtime coordination
|
|
9
|
+
> (runs/heartbeats/cron) stays server-owned.
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -44,7 +44,7 @@ Use `mu store paths --pretty` to resolve `<store>` for the active repo/workspace
|
|
|
44
44
|
|
|
45
45
|
### Health Check
|
|
46
46
|
|
|
47
|
-
- `GET /healthz`
|
|
47
|
+
- `GET /healthz` - Returns 200 OK
|
|
48
48
|
|
|
49
49
|
### Status
|
|
50
50
|
|
|
@@ -194,11 +194,7 @@ Operational fallbacks:
|
|
|
194
194
|
- `telegram_reply_to_message_id` metadata anchors replies when parseable.
|
|
195
195
|
- Missing Slack/Telegram bot tokens surface capability reason codes (`*_bot_token_missing`) and retry behavior.
|
|
196
196
|
|
|
197
|
-
Server channel renderers consume canonical `
|
|
198
|
-
rendering + actions. Optional HUD presentation hints (`title_style`, `snapshot_style`, chip/item styles)
|
|
199
|
-
and metadata presets (`metadata.style_preset`) may be used by richer renderers and safely ignored by
|
|
200
|
-
plain-text channels. New features should extend the shared HUD contract path instead of bespoke
|
|
201
|
-
channel-specific HUD payload formats.
|
|
197
|
+
Server channel renderers consume canonical `ui_docs` metadata (`UiDoc`) for Slack/Telegram/Discord/Neovim delivery. Interactive actions use callback tokens (`mu-ui:*`) with deterministic text fallbacks when tokens cannot be issued or rendered.
|
|
202
198
|
|
|
203
199
|
## Running the Server
|
|
204
200
|
|
|
@@ -256,6 +252,6 @@ The server uses:
|
|
|
256
252
|
- Filesystem-backed JSONL event storage (FsJsonlStore)
|
|
257
253
|
- Bun's built-in HTTP server
|
|
258
254
|
- Control-plane adapter/webhook transport + session coordination routes
|
|
259
|
-
- Generation-supervised control-plane hot reload lifecycle
|
|
255
|
+
- Generation-supervised control-plane hot reload lifecycle
|
|
260
256
|
|
|
261
257
|
Control-plane/coordination data is persisted under `<store>/` (for example `<store>/events.jsonl` and `<store>/control-plane/*`). Use `mu store paths` to resolve `<store>` for your repo.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CONTROL_PLANE_CHANNEL_ADAPTER_SPECS, ingressModeForValue, summarizeInboundAttachmentPolicy, } from "@femtomc/mu-control-plane";
|
|
2
|
+
import { UI_ACTIONS_UNSUPPORTED_REASON, UI_COMPONENT_SUPPORT } from "../control_plane.js";
|
|
2
3
|
import { configRoutes } from "./config.js";
|
|
3
4
|
import { eventRoutes } from "./events.js";
|
|
4
5
|
import { identityRoutes } from "./identities.js";
|
|
@@ -92,9 +93,71 @@ function mediaInboundAttachmentCapability(config, channel) {
|
|
|
92
93
|
reason: hasToken ? null : "telegram_bot_token_missing",
|
|
93
94
|
};
|
|
94
95
|
}
|
|
96
|
+
const TEXT_ONLY_UI_COMPONENT_SUPPORT = {
|
|
97
|
+
text: true,
|
|
98
|
+
list: false,
|
|
99
|
+
key_value: false,
|
|
100
|
+
divider: false,
|
|
101
|
+
};
|
|
102
|
+
const STATUS_PROFILE_ACTIONS_FALLBACK_REASON = "status_profile_actions_degrade_to_text";
|
|
103
|
+
const CHANNEL_UI_CAPABILITIES = {
|
|
104
|
+
slack: {
|
|
105
|
+
supported: true,
|
|
106
|
+
reason: null,
|
|
107
|
+
components: UI_COMPONENT_SUPPORT,
|
|
108
|
+
actions: {
|
|
109
|
+
supported: true,
|
|
110
|
+
reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
discord: {
|
|
114
|
+
supported: true,
|
|
115
|
+
reason: null,
|
|
116
|
+
components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
|
|
117
|
+
actions: {
|
|
118
|
+
supported: true,
|
|
119
|
+
reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
telegram: {
|
|
123
|
+
supported: true,
|
|
124
|
+
reason: null,
|
|
125
|
+
components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
|
|
126
|
+
actions: {
|
|
127
|
+
supported: true,
|
|
128
|
+
reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
neovim: {
|
|
132
|
+
supported: true,
|
|
133
|
+
reason: null,
|
|
134
|
+
components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
|
|
135
|
+
actions: {
|
|
136
|
+
supported: true,
|
|
137
|
+
reason: STATUS_PROFILE_ACTIONS_FALLBACK_REASON,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
terminal: {
|
|
141
|
+
supported: true,
|
|
142
|
+
reason: null,
|
|
143
|
+
components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
|
|
144
|
+
actions: {
|
|
145
|
+
supported: false,
|
|
146
|
+
reason: UI_ACTIONS_UNSUPPORTED_REASON,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
function uiCapabilityForChannel(channel) {
|
|
151
|
+
return CHANNEL_UI_CAPABILITIES[channel] ?? {
|
|
152
|
+
supported: true,
|
|
153
|
+
reason: null,
|
|
154
|
+
components: TEXT_ONLY_UI_COMPONENT_SUPPORT,
|
|
155
|
+
actions: { supported: false, reason: UI_ACTIONS_UNSUPPORTED_REASON },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
95
158
|
export async function controlPlaneRoutes(request, url, deps, headers) {
|
|
96
159
|
const path = url.pathname;
|
|
97
|
-
if (path === "/api/control-plane
|
|
160
|
+
if (path === "/api/control-plane/status") {
|
|
98
161
|
if (request.method !== "GET") {
|
|
99
162
|
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
100
163
|
}
|
|
@@ -164,6 +227,7 @@ export async function controlPlaneRoutes(request, url, deps, headers) {
|
|
|
164
227
|
outbound_delivery: mediaOutboundCapability(config, spec.channel),
|
|
165
228
|
inbound_attachment_download: mediaInboundAttachmentCapability(config, spec.channel),
|
|
166
229
|
},
|
|
230
|
+
ui: uiCapabilityForChannel(spec.channel),
|
|
167
231
|
}));
|
|
168
232
|
return Response.json({
|
|
169
233
|
ok: true,
|
package/dist/control_plane.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
|
|
2
|
-
import { type
|
|
2
|
+
import { type UiDoc, type UiEvent } from "@femtomc/mu-core";
|
|
3
|
+
import { type GenerationTelemetryRecorder, type OutboxDeliveryHandlerResult, type OutboxRecord, UiCallbackTokenStore } from "@femtomc/mu-control-plane";
|
|
3
4
|
import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type TelegramGenerationSwapHooks, type WakeDeliveryObserver } from "./control_plane_contract.js";
|
|
4
5
|
import { detectAdapters } from "./control_plane_adapter_registry.js";
|
|
5
6
|
export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
|
|
@@ -52,8 +53,22 @@ export declare function buildTelegramSendMessagePayload(opts: {
|
|
|
52
53
|
richFormatting: boolean;
|
|
53
54
|
}): TelegramSendMessagePayload;
|
|
54
55
|
export declare function splitTelegramMessageText(text: string, maxLen?: number): string[];
|
|
56
|
+
export declare const UI_COMPONENT_SUPPORT: {
|
|
57
|
+
readonly text: true;
|
|
58
|
+
readonly list: true;
|
|
59
|
+
readonly key_value: true;
|
|
60
|
+
readonly divider: true;
|
|
61
|
+
};
|
|
62
|
+
export declare const UI_ACTIONS_UNSUPPORTED_REASON = "ui_actions_not_implemented";
|
|
63
|
+
export declare function uiDocTextLines(doc: UiDoc, opts?: {
|
|
64
|
+
includeActions?: boolean;
|
|
65
|
+
}): string[];
|
|
66
|
+
export declare function uiDocsTextFallback(uiDocs: readonly UiDoc[]): string;
|
|
55
67
|
export declare function splitSlackMessageText(text: string, maxLen?: number): string[];
|
|
56
|
-
type TelegramCallbackDataEncoder = (commandText: string
|
|
68
|
+
type TelegramCallbackDataEncoder = (commandText: string, opts: {
|
|
69
|
+
record: OutboxRecord;
|
|
70
|
+
uiEvent?: UiEvent;
|
|
71
|
+
}) => Promise<string>;
|
|
57
72
|
export declare function deliverTelegramOutboxRecord(opts: {
|
|
58
73
|
botToken: string;
|
|
59
74
|
record: OutboxRecord;
|
|
@@ -62,6 +77,9 @@ export declare function deliverTelegramOutboxRecord(opts: {
|
|
|
62
77
|
export declare function deliverSlackOutboxRecord(opts: {
|
|
63
78
|
botToken: string;
|
|
64
79
|
record: OutboxRecord;
|
|
80
|
+
uiCallbackTokenStore?: UiCallbackTokenStore;
|
|
81
|
+
nowMs?: () => number;
|
|
82
|
+
fetchImpl?: typeof fetch;
|
|
65
83
|
}): Promise<OutboxDeliveryHandlerResult>;
|
|
66
84
|
export type BootstrapControlPlaneOpts = {
|
|
67
85
|
repoRoot: string;
|
package/dist/control_plane.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime,
|
|
1
|
+
import { normalizeUiDocs, resolveUiStatusProfileName, } from "@femtomc/mu-core";
|
|
2
|
+
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, buildSanitizedUiEventForAction, buildSlackUiActionId, getControlPlanePaths, issueUiDocActionPayloads, TelegramControlPlaneAdapterSpec, uiActionPayloadContextFromOutboxRecord, uiDocActionPayloadKey, UiCallbackTokenStore, } from "@femtomc/mu-control-plane";
|
|
3
3
|
import { DEFAULT_MU_CONFIG } from "./config.js";
|
|
4
4
|
import { buildMessagingOperatorRuntime, createOutboxDrainLoop } from "./control_plane_bootstrap_helpers.js";
|
|
5
5
|
import { buildWakeOutboundEnvelope, resolveWakeFanoutCapability, wakeDeliveryMetadataFromOutboxRecord, wakeDispatchReasonCode, wakeFanoutDedupeKey, } from "./control_plane_wake_delivery.js";
|
|
@@ -146,11 +146,17 @@ const SLACK_BLOCK_TEXT_MAX_LEN = 3_000;
|
|
|
146
146
|
const SLACK_BLOCKS_MAX = 50;
|
|
147
147
|
const SLACK_ACTIONS_MAX_PER_BLOCK = 5;
|
|
148
148
|
const SLACK_ACTIONS_MAX_TOTAL = 20;
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
const SLACK_ACTION_VALUE_MAX_CHARS = 2_000;
|
|
150
|
+
const SLACK_UI_EVENT_ACTION_ID = buildSlackUiActionId("ui_event");
|
|
151
|
+
const UI_DOCS_MAX = 3;
|
|
152
|
+
const UI_CALLBACK_TOKEN_TTL_MS = 15 * 60_000;
|
|
153
|
+
export const UI_COMPONENT_SUPPORT = {
|
|
154
|
+
text: true,
|
|
155
|
+
list: true,
|
|
156
|
+
key_value: true,
|
|
157
|
+
divider: true,
|
|
158
|
+
};
|
|
159
|
+
export const UI_ACTIONS_UNSUPPORTED_REASON = "ui_actions_not_implemented";
|
|
154
160
|
function truncateSlackText(text, maxLen = SLACK_BLOCK_TEXT_MAX_LEN) {
|
|
155
161
|
if (text.length <= maxLen) {
|
|
156
162
|
return text;
|
|
@@ -160,113 +166,146 @@ function truncateSlackText(text, maxLen = SLACK_BLOCK_TEXT_MAX_LEN) {
|
|
|
160
166
|
}
|
|
161
167
|
return `${text.slice(0, maxLen - 1)}…`;
|
|
162
168
|
}
|
|
163
|
-
function
|
|
164
|
-
switch (tone) {
|
|
165
|
-
case "success":
|
|
166
|
-
return "✅";
|
|
167
|
-
case "warning":
|
|
168
|
-
return "⚠️";
|
|
169
|
-
case "error":
|
|
170
|
-
return "⛔";
|
|
171
|
-
case "accent":
|
|
172
|
-
return "🔹";
|
|
173
|
-
case "muted":
|
|
174
|
-
return "▫️";
|
|
175
|
-
case "dim":
|
|
176
|
-
return "·";
|
|
177
|
-
case "info":
|
|
178
|
-
default:
|
|
179
|
-
return "ℹ️";
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
function applySlackHudTextStyle(text, style, opts = {}) {
|
|
183
|
-
const weight = style?.weight ?? opts.defaultWeight;
|
|
184
|
-
const italic = style?.italic ?? opts.defaultItalic ?? false;
|
|
185
|
-
const code = style?.code ?? opts.defaultCode ?? false;
|
|
186
|
-
let out = text;
|
|
187
|
-
if (code) {
|
|
188
|
-
out = `\`${out}\``;
|
|
189
|
-
}
|
|
190
|
-
if (italic) {
|
|
191
|
-
out = `_${out}_`;
|
|
192
|
-
}
|
|
193
|
-
if (weight === "strong") {
|
|
194
|
-
out = `*${out}*`;
|
|
195
|
-
}
|
|
196
|
-
return out;
|
|
197
|
-
}
|
|
198
|
-
function stripSlackMrkdwn(text) {
|
|
199
|
-
return text.replace(/[*_`~]/g, "");
|
|
200
|
-
}
|
|
201
|
-
function hudDocSectionLines(doc) {
|
|
169
|
+
function uiDocComponentLines(doc) {
|
|
202
170
|
const lines = [];
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
})
|
|
209
|
-
.join(" · ");
|
|
210
|
-
if (chipsLine.length > 0) {
|
|
211
|
-
lines.push(chipsLine);
|
|
212
|
-
}
|
|
213
|
-
for (const section of doc.sections) {
|
|
214
|
-
switch (section.kind) {
|
|
215
|
-
case "kv": {
|
|
216
|
-
const title = applySlackHudTextStyle(section.title ?? "Details", section.title_style, { defaultWeight: "strong" });
|
|
217
|
-
const items = section.items.slice(0, SLACK_SECTION_LINE_MAX).map((item) => {
|
|
218
|
-
const value = applySlackHudTextStyle(item.value, item.value_style);
|
|
219
|
-
return `• *${item.label}:* ${value}`;
|
|
220
|
-
});
|
|
221
|
-
if (items.length > 0) {
|
|
222
|
-
lines.push([title, ...items].join("\n"));
|
|
223
|
-
}
|
|
171
|
+
const components = [...doc.components].sort((a, b) => a.id.localeCompare(b.id));
|
|
172
|
+
for (const component of components) {
|
|
173
|
+
switch (component.kind) {
|
|
174
|
+
case "text": {
|
|
175
|
+
lines.push(component.text);
|
|
224
176
|
break;
|
|
225
177
|
}
|
|
226
|
-
case "
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
lines.push([title, ...items].join("\n"));
|
|
178
|
+
case "list": {
|
|
179
|
+
if (component.title) {
|
|
180
|
+
lines.push(component.title);
|
|
181
|
+
}
|
|
182
|
+
for (const item of component.items) {
|
|
183
|
+
lines.push(`• ${item.label}${item.detail ? ` · ${item.detail}` : ""}`);
|
|
233
184
|
}
|
|
234
185
|
break;
|
|
235
186
|
}
|
|
236
|
-
case "
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
187
|
+
case "key_value": {
|
|
188
|
+
if (component.title) {
|
|
189
|
+
lines.push(component.title);
|
|
190
|
+
}
|
|
191
|
+
for (const row of component.rows) {
|
|
192
|
+
lines.push(`• ${row.key}: ${row.value}`);
|
|
241
193
|
}
|
|
242
194
|
break;
|
|
243
195
|
}
|
|
244
|
-
case "
|
|
245
|
-
|
|
246
|
-
? `${applySlackHudTextStyle(section.title, section.title_style, { defaultWeight: "strong" })}\n`
|
|
247
|
-
: "";
|
|
248
|
-
const text = applySlackHudTextStyle(section.text, section.style);
|
|
249
|
-
lines.push(`${title}${text}`);
|
|
196
|
+
case "divider": {
|
|
197
|
+
lines.push("────────");
|
|
250
198
|
break;
|
|
251
199
|
}
|
|
252
200
|
}
|
|
253
201
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
function uiDocActionLines(doc) {
|
|
205
|
+
const actions = [...doc.actions].sort((a, b) => a.id.localeCompare(b.id));
|
|
206
|
+
return actions.map((action) => {
|
|
207
|
+
const parts = [`• ${action.label}`];
|
|
208
|
+
if (action.description) {
|
|
209
|
+
parts.push(action.description);
|
|
210
|
+
}
|
|
211
|
+
parts.push(`(id=${action.id})`);
|
|
212
|
+
return parts.join(" ");
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
export function uiDocTextLines(doc, opts = {}) {
|
|
216
|
+
const lines = [`UI · ${doc.title}`];
|
|
217
|
+
if (doc.summary) {
|
|
218
|
+
lines.push(doc.summary);
|
|
219
|
+
}
|
|
220
|
+
const componentLines = uiDocComponentLines(doc);
|
|
221
|
+
if (componentLines.length > 0) {
|
|
222
|
+
lines.push(...componentLines);
|
|
223
|
+
}
|
|
224
|
+
if (opts.includeActions !== false) {
|
|
225
|
+
const actionLines = uiDocActionLines(doc);
|
|
226
|
+
if (actionLines.length > 0) {
|
|
227
|
+
lines.push("Actions:");
|
|
228
|
+
lines.push(...actionLines);
|
|
229
|
+
}
|
|
257
230
|
}
|
|
258
231
|
return lines;
|
|
259
232
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
233
|
+
export function uiDocsTextFallback(uiDocs) {
|
|
234
|
+
if (uiDocs.length === 0) {
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
const sections = uiDocs.map((doc) => uiDocTextLines(doc).join("\n"));
|
|
238
|
+
return sections.join("\n\n");
|
|
239
|
+
}
|
|
240
|
+
function appendUiDocText(body, uiDocs) {
|
|
241
|
+
const fallback = uiDocsTextFallback(uiDocs);
|
|
242
|
+
if (!fallback) {
|
|
243
|
+
return body;
|
|
244
|
+
}
|
|
245
|
+
const trimmed = body.trim();
|
|
246
|
+
if (trimmed.length === 0) {
|
|
247
|
+
return fallback;
|
|
248
|
+
}
|
|
249
|
+
return `${trimmed}\n\n${fallback}`;
|
|
250
|
+
}
|
|
251
|
+
function uiDocActionTextLine(action, opts = {}) {
|
|
252
|
+
const parts = [`• ${action.label}`];
|
|
253
|
+
if (action.description) {
|
|
254
|
+
parts.push(action.description);
|
|
255
|
+
}
|
|
256
|
+
parts.push(`(id=${action.id}${opts.suffix ? `; ${opts.suffix}` : ""})`);
|
|
257
|
+
return parts.join(" ");
|
|
258
|
+
}
|
|
259
|
+
function statusProfileVariant(doc) {
|
|
260
|
+
const profile = doc.metadata.profile;
|
|
261
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
|
|
262
|
+
return "status";
|
|
263
|
+
}
|
|
264
|
+
const rawVariant = typeof profile.variant === "string"
|
|
265
|
+
? profile.variant.trim().toLowerCase()
|
|
266
|
+
: "";
|
|
267
|
+
return rawVariant.length > 0 ? rawVariant : "status";
|
|
268
|
+
}
|
|
269
|
+
function isStatusProfileStatusVariant(doc) {
|
|
270
|
+
return resolveUiStatusProfileName(doc) !== null && statusProfileVariant(doc) === "status";
|
|
271
|
+
}
|
|
272
|
+
function uiDocDeterministicActionFallbackLine(action) {
|
|
273
|
+
const commandText = commandTextForUiDocAction(action);
|
|
274
|
+
if (commandText) {
|
|
275
|
+
return `• ${action.label}: ${commandText}`;
|
|
276
|
+
}
|
|
277
|
+
return `• ${action.label}: interactive unavailable (missing command_text)`;
|
|
278
|
+
}
|
|
279
|
+
function uiDocActionButtons(doc, actionPayloadsByKey) {
|
|
280
|
+
const buttons = [];
|
|
281
|
+
const fallbackLines = [];
|
|
282
|
+
const statusProfile = isStatusProfileStatusVariant(doc);
|
|
283
|
+
const actions = [...doc.actions].sort((a, b) => a.id.localeCompare(b.id)).slice(0, SLACK_ACTIONS_MAX_TOTAL);
|
|
284
|
+
for (const action of actions) {
|
|
285
|
+
if (statusProfile) {
|
|
286
|
+
fallbackLines.push(uiDocDeterministicActionFallbackLine(action));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const payload = actionPayloadsByKey.get(uiDocActionPayloadKey(doc.ui_id, action.id));
|
|
290
|
+
if (!payload) {
|
|
291
|
+
fallbackLines.push(uiDocActionTextLine(action, { suffix: "interactive unavailable" }));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (payload.length > SLACK_ACTION_VALUE_MAX_CHARS) {
|
|
295
|
+
fallbackLines.push(uiDocActionTextLine(action, { suffix: "interactive payload too large" }));
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
buttons.push({
|
|
299
|
+
type: "button",
|
|
300
|
+
text: {
|
|
301
|
+
type: "plain_text",
|
|
302
|
+
text: truncateSlackText(action.label, 75),
|
|
303
|
+
},
|
|
304
|
+
value: payload,
|
|
305
|
+
action_id: SLACK_UI_EVENT_ACTION_ID,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return { buttons, fallbackLines };
|
|
270
309
|
}
|
|
271
310
|
export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
|
|
272
311
|
if (text.length <= maxLen) {
|
|
@@ -288,26 +327,27 @@ export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
|
|
|
288
327
|
}
|
|
289
328
|
return chunks;
|
|
290
329
|
}
|
|
291
|
-
function slackBlocksForOutboxRecord(
|
|
292
|
-
|
|
293
|
-
if (hudDocs.length === 0) {
|
|
330
|
+
function slackBlocksForOutboxRecord(body, uiDocs, opts = {}) {
|
|
331
|
+
if (uiDocs.length === 0) {
|
|
294
332
|
return undefined;
|
|
295
333
|
}
|
|
334
|
+
const uiDocActionPayloadsByKey = opts.uiDocActionPayloadsByKey ?? new Map();
|
|
296
335
|
const blocks = [];
|
|
336
|
+
const headerText = body.trim().length > 0 ? body : "Update";
|
|
297
337
|
blocks.push({
|
|
298
338
|
type: "section",
|
|
299
|
-
text: { type: "mrkdwn", text: truncateSlackText(
|
|
339
|
+
text: { type: "mrkdwn", text: truncateSlackText(headerText) },
|
|
300
340
|
});
|
|
301
|
-
for (const doc of
|
|
341
|
+
for (const doc of uiDocs) {
|
|
302
342
|
if (blocks.length >= SLACK_BLOCKS_MAX) {
|
|
303
343
|
break;
|
|
304
344
|
}
|
|
305
|
-
const
|
|
345
|
+
const lines = uiDocTextLines(doc, { includeActions: false });
|
|
306
346
|
blocks.push({
|
|
307
347
|
type: "context",
|
|
308
|
-
elements: [{ type: "mrkdwn", text: truncateSlackText(
|
|
348
|
+
elements: [{ type: "mrkdwn", text: truncateSlackText(lines[0]) }],
|
|
309
349
|
});
|
|
310
|
-
for (const line of
|
|
350
|
+
for (const line of lines.slice(1)) {
|
|
311
351
|
if (blocks.length >= SLACK_BLOCKS_MAX) {
|
|
312
352
|
break;
|
|
313
353
|
}
|
|
@@ -316,27 +356,35 @@ function slackBlocksForOutboxRecord(record, body) {
|
|
|
316
356
|
text: { type: "mrkdwn", text: truncateSlackText(line) },
|
|
317
357
|
});
|
|
318
358
|
}
|
|
319
|
-
const
|
|
320
|
-
for (let idx = 0; idx < buttons.length; idx += SLACK_ACTIONS_MAX_PER_BLOCK) {
|
|
359
|
+
const actionRender = uiDocActionButtons(doc, uiDocActionPayloadsByKey);
|
|
360
|
+
for (let idx = 0; idx < actionRender.buttons.length; idx += SLACK_ACTIONS_MAX_PER_BLOCK) {
|
|
321
361
|
if (blocks.length >= SLACK_BLOCKS_MAX) {
|
|
322
362
|
break;
|
|
323
363
|
}
|
|
324
364
|
blocks.push({
|
|
325
365
|
type: "actions",
|
|
326
|
-
elements: buttons.slice(idx, idx + SLACK_ACTIONS_MAX_PER_BLOCK),
|
|
366
|
+
elements: actionRender.buttons.slice(idx, idx + SLACK_ACTIONS_MAX_PER_BLOCK),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (actionRender.fallbackLines.length > 0 && blocks.length < SLACK_BLOCKS_MAX) {
|
|
370
|
+
blocks.push({
|
|
371
|
+
type: "section",
|
|
372
|
+
text: {
|
|
373
|
+
type: "mrkdwn",
|
|
374
|
+
text: truncateSlackText(`Actions:\n${actionRender.fallbackLines.join("\n")}`),
|
|
375
|
+
},
|
|
327
376
|
});
|
|
328
377
|
}
|
|
329
378
|
}
|
|
330
379
|
return blocks.slice(0, SLACK_BLOCKS_MAX);
|
|
331
380
|
}
|
|
332
381
|
function slackThreadTsFromMetadata(metadata) {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return value.trim();
|
|
337
|
-
}
|
|
382
|
+
const value = metadata?.slack_thread_ts;
|
|
383
|
+
if (typeof value !== "string") {
|
|
384
|
+
return undefined;
|
|
338
385
|
}
|
|
339
|
-
|
|
386
|
+
const trimmed = value.trim();
|
|
387
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
340
388
|
}
|
|
341
389
|
function slackStatusMessageTsFromMetadata(metadata) {
|
|
342
390
|
const value = metadata?.slack_status_message_ts;
|
|
@@ -349,60 +397,96 @@ function slackStatusMessageTsFromMetadata(metadata) {
|
|
|
349
397
|
const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
|
|
350
398
|
const TELEGRAM_ACTIONS_MAX_TOTAL = 20;
|
|
351
399
|
const TELEGRAM_ACTIONS_PER_ROW = 3;
|
|
352
|
-
const TELEGRAM_DOCS_MAX = 2;
|
|
353
400
|
function utf8ByteLength(value) {
|
|
354
401
|
return new TextEncoder().encode(value).length;
|
|
355
402
|
}
|
|
356
|
-
function telegramTextForOutboxRecord(
|
|
357
|
-
const hudDocs = hudDocsForPresentation(record.envelope.metadata?.hud_docs, TELEGRAM_DOCS_MAX);
|
|
358
|
-
if (hudDocs.length === 0) {
|
|
359
|
-
return body;
|
|
360
|
-
}
|
|
403
|
+
function telegramTextForOutboxRecord(body, uiDocs) {
|
|
361
404
|
const lines = [body.trim()];
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
lines.push(stripSlackMrkdwn(sectionLine));
|
|
366
|
-
}
|
|
405
|
+
const uiFallback = uiDocsTextFallback(uiDocs);
|
|
406
|
+
if (uiFallback) {
|
|
407
|
+
lines.push("", uiFallback);
|
|
367
408
|
}
|
|
368
409
|
return lines.join("\n").trim();
|
|
369
410
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return { overflowText: "" };
|
|
411
|
+
function telegramReplyMarkupFromButtons(buttons) {
|
|
412
|
+
if (buttons.length === 0) {
|
|
413
|
+
return undefined;
|
|
374
414
|
}
|
|
375
|
-
const
|
|
415
|
+
const inline_keyboard = [];
|
|
416
|
+
for (let idx = 0; idx < buttons.length; idx += TELEGRAM_ACTIONS_PER_ROW) {
|
|
417
|
+
inline_keyboard.push(buttons.slice(idx, idx + TELEGRAM_ACTIONS_PER_ROW));
|
|
418
|
+
}
|
|
419
|
+
return { inline_keyboard };
|
|
420
|
+
}
|
|
421
|
+
function telegramOverflowText(lines) {
|
|
422
|
+
if (lines.length === 0) {
|
|
423
|
+
return "";
|
|
424
|
+
}
|
|
425
|
+
return `\n\nActions:\n${lines.join("\n")}`;
|
|
426
|
+
}
|
|
427
|
+
function commandTextForUiDocAction(action) {
|
|
428
|
+
const fromMetadata = typeof action.metadata.command_text === "string" ? action.metadata.command_text.trim() : "";
|
|
429
|
+
if (fromMetadata.length === 0) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
return fromMetadata;
|
|
433
|
+
}
|
|
434
|
+
async function compileTelegramUiDocActions(opts) {
|
|
435
|
+
if (opts.uiDocs.length === 0) {
|
|
436
|
+
return { buttonEntries: [], overflowLines: [] };
|
|
437
|
+
}
|
|
438
|
+
const buttonEntries = [];
|
|
376
439
|
const overflowLines = [];
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
440
|
+
for (const doc of opts.uiDocs) {
|
|
441
|
+
const statusProfile = isStatusProfileStatusVariant(doc);
|
|
442
|
+
const actions = [...doc.actions].sort((a, b) => a.id.localeCompare(b.id));
|
|
443
|
+
for (const action of actions) {
|
|
444
|
+
const commandText = commandTextForUiDocAction(action);
|
|
445
|
+
const fallbackLine = commandText
|
|
446
|
+
? `• ${action.label}: ${commandText}`
|
|
447
|
+
: `• ${action.label}: interactive unavailable (missing command_text)`;
|
|
448
|
+
if (statusProfile) {
|
|
449
|
+
overflowLines.push(fallbackLine);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const uiEvent = buildSanitizedUiEventForAction({
|
|
453
|
+
doc,
|
|
454
|
+
action,
|
|
455
|
+
createdAtMs: opts.nowMs,
|
|
456
|
+
});
|
|
457
|
+
if (!commandText || !uiEvent) {
|
|
458
|
+
overflowLines.push(fallbackLine);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (!opts.encodeCallbackData) {
|
|
462
|
+
overflowLines.push(fallbackLine);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
let callbackData;
|
|
381
466
|
try {
|
|
382
|
-
callbackData = await opts.encodeCallbackData(
|
|
467
|
+
callbackData = await opts.encodeCallbackData(commandText, {
|
|
468
|
+
record: opts.record,
|
|
469
|
+
uiEvent,
|
|
470
|
+
});
|
|
383
471
|
}
|
|
384
472
|
catch {
|
|
385
|
-
overflowLines.push(
|
|
473
|
+
overflowLines.push(fallbackLine);
|
|
386
474
|
continue;
|
|
387
475
|
}
|
|
476
|
+
if (utf8ByteLength(callbackData) > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
|
|
477
|
+
overflowLines.push(fallbackLine);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
buttonEntries.push({
|
|
481
|
+
button: {
|
|
482
|
+
text: action.label.slice(0, 64),
|
|
483
|
+
callback_data: callbackData,
|
|
484
|
+
},
|
|
485
|
+
fallbackLine,
|
|
486
|
+
});
|
|
388
487
|
}
|
|
389
|
-
if (utf8ByteLength(callbackData) > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
|
|
390
|
-
overflowLines.push(`• ${action.label}: ${action.command_text}`);
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
buttons.push({
|
|
394
|
-
text: action.label.slice(0, 64),
|
|
395
|
-
callback_data: callbackData,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
const inline_keyboard = [];
|
|
399
|
-
for (let idx = 0; idx < buttons.length; idx += TELEGRAM_ACTIONS_PER_ROW) {
|
|
400
|
-
inline_keyboard.push(buttons.slice(idx, idx + TELEGRAM_ACTIONS_PER_ROW));
|
|
401
488
|
}
|
|
402
|
-
return {
|
|
403
|
-
replyMarkup: inline_keyboard.length > 0 ? { inline_keyboard } : undefined,
|
|
404
|
-
overflowText: overflowLines.length > 0 ? `\n\nActions:\n${overflowLines.join("\n")}` : "",
|
|
405
|
-
};
|
|
489
|
+
return { buttonEntries, overflowLines };
|
|
406
490
|
}
|
|
407
491
|
async function postTelegramMessage(botToken, payload) {
|
|
408
492
|
return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
@@ -492,13 +576,22 @@ async function sendTelegramTextChunks(opts) {
|
|
|
492
576
|
}
|
|
493
577
|
export async function deliverTelegramOutboxRecord(opts) {
|
|
494
578
|
const { botToken, record } = opts;
|
|
495
|
-
const
|
|
579
|
+
const uiDocs = normalizeUiDocs(record.envelope.metadata?.ui_docs, { maxDocs: UI_DOCS_MAX });
|
|
580
|
+
const nowMs = Math.trunc(Date.now());
|
|
581
|
+
const uiActions = await compileTelegramUiDocActions({
|
|
496
582
|
record,
|
|
583
|
+
uiDocs,
|
|
584
|
+
nowMs,
|
|
497
585
|
encodeCallbackData: opts.encodeCallbackData,
|
|
498
586
|
});
|
|
499
|
-
const
|
|
587
|
+
const visibleEntries = uiActions.buttonEntries.slice(0, TELEGRAM_ACTIONS_MAX_TOTAL);
|
|
588
|
+
const overflowLines = [
|
|
589
|
+
...uiActions.overflowLines,
|
|
590
|
+
...uiActions.buttonEntries.slice(TELEGRAM_ACTIONS_MAX_TOTAL).map((entry) => entry.fallbackLine),
|
|
591
|
+
];
|
|
592
|
+
const replyMarkup = telegramReplyMarkupFromButtons(visibleEntries.map((entry) => entry.button));
|
|
500
593
|
const replyToMessageId = maybeParseTelegramMessageId(record.envelope.metadata?.telegram_reply_to_message_id);
|
|
501
|
-
const telegramText = `${telegramTextForOutboxRecord(record
|
|
594
|
+
const telegramText = `${telegramTextForOutboxRecord(record.envelope.body, uiDocs)}${telegramOverflowText(overflowLines)}`.trim();
|
|
502
595
|
const fallbackMessagePayload = {
|
|
503
596
|
...buildTelegramSendMessagePayload({
|
|
504
597
|
chatId: record.envelope.channel_conversation_id,
|
|
@@ -640,7 +733,8 @@ export async function deliverTelegramOutboxRecord(opts) {
|
|
|
640
733
|
};
|
|
641
734
|
}
|
|
642
735
|
async function postSlackJson(opts) {
|
|
643
|
-
const
|
|
736
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
737
|
+
const response = await fetchImpl(`https://slack.com/api/${opts.method}`, {
|
|
644
738
|
method: "POST",
|
|
645
739
|
headers: {
|
|
646
740
|
Authorization: `Bearer ${opts.botToken}`,
|
|
@@ -653,10 +747,32 @@ async function postSlackJson(opts) {
|
|
|
653
747
|
}
|
|
654
748
|
export async function deliverSlackOutboxRecord(opts) {
|
|
655
749
|
const { botToken, record } = opts;
|
|
750
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
656
751
|
const attachments = record.envelope.attachments ?? [];
|
|
657
|
-
const
|
|
752
|
+
const uiDocs = normalizeUiDocs(record.envelope.metadata?.ui_docs, { maxDocs: UI_DOCS_MAX });
|
|
753
|
+
const interactiveUiDocs = uiDocs.filter((doc) => !isStatusProfileStatusVariant(doc));
|
|
754
|
+
let uiDocActionPayloadsByKey = new Map();
|
|
755
|
+
if (opts.uiCallbackTokenStore && interactiveUiDocs.some((doc) => doc.actions.length > 0)) {
|
|
756
|
+
const issued = await issueUiDocActionPayloads({
|
|
757
|
+
uiDocs: interactiveUiDocs,
|
|
758
|
+
tokenStore: opts.uiCallbackTokenStore,
|
|
759
|
+
context: uiActionPayloadContextFromOutboxRecord(record),
|
|
760
|
+
ttlMs: UI_CALLBACK_TOKEN_TTL_MS,
|
|
761
|
+
nowMs: Math.trunc((opts.nowMs ?? Date.now)()),
|
|
762
|
+
});
|
|
763
|
+
uiDocActionPayloadsByKey = new Map(issued.map((entry) => [entry.key, entry.payload_json]));
|
|
764
|
+
}
|
|
765
|
+
const renderedBodyForBlocks = renderSlackMarkdown(record.envelope.body);
|
|
766
|
+
const blocks = slackBlocksForOutboxRecord(renderedBodyForBlocks, uiDocs, {
|
|
767
|
+
uiDocActionPayloadsByKey,
|
|
768
|
+
});
|
|
769
|
+
const bodyForText = blocks
|
|
770
|
+
? record.envelope.body.trim().length > 0
|
|
771
|
+
? record.envelope.body
|
|
772
|
+
: "Update"
|
|
773
|
+
: appendUiDocText(record.envelope.body, uiDocs);
|
|
774
|
+
const renderedBody = renderSlackMarkdown(bodyForText);
|
|
658
775
|
const textChunks = splitSlackMessageText(renderedBody);
|
|
659
|
-
const blocks = slackBlocksForOutboxRecord(record, renderedBody);
|
|
660
776
|
const threadTs = slackThreadTsFromMetadata(record.envelope.metadata);
|
|
661
777
|
const statusMessageTs = slackStatusMessageTsFromMetadata(record.envelope.metadata);
|
|
662
778
|
if (attachments.length === 0) {
|
|
@@ -673,6 +789,7 @@ export async function deliverSlackOutboxRecord(opts) {
|
|
|
673
789
|
unfurl_media: false,
|
|
674
790
|
...(blocks ? { blocks } : {}),
|
|
675
791
|
},
|
|
792
|
+
fetchImpl,
|
|
676
793
|
});
|
|
677
794
|
if (updated.response.ok && updated.payload?.ok) {
|
|
678
795
|
chunkStartIndex = 1;
|
|
@@ -707,6 +824,7 @@ export async function deliverSlackOutboxRecord(opts) {
|
|
|
707
824
|
...(index === 0 && blocks ? { blocks } : {}),
|
|
708
825
|
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
709
826
|
},
|
|
827
|
+
fetchImpl,
|
|
710
828
|
});
|
|
711
829
|
if (delivered.response.ok && delivered.payload?.ok) {
|
|
712
830
|
continue;
|
|
@@ -733,7 +851,7 @@ export async function deliverSlackOutboxRecord(opts) {
|
|
|
733
851
|
error: `slack attachment ${index + 1} missing reference.url`,
|
|
734
852
|
};
|
|
735
853
|
}
|
|
736
|
-
const source = await
|
|
854
|
+
const source = await fetchImpl(referenceUrl);
|
|
737
855
|
if (!source.ok) {
|
|
738
856
|
const sourceErr = await source.text().catch(() => "");
|
|
739
857
|
if (source.status === 429 || source.status >= 500) {
|
|
@@ -759,7 +877,7 @@ export async function deliverSlackOutboxRecord(opts) {
|
|
|
759
877
|
form.set("thread_ts", threadTs);
|
|
760
878
|
}
|
|
761
879
|
form.set("file", new Blob([bytes], { type: contentType }), filename);
|
|
762
|
-
const uploaded = await
|
|
880
|
+
const uploaded = await fetchImpl("https://slack.com/api/files.upload", {
|
|
763
881
|
method: "POST",
|
|
764
882
|
headers: {
|
|
765
883
|
Authorization: `Bearer ${botToken}`,
|
|
@@ -797,6 +915,7 @@ export async function deliverSlackOutboxRecord(opts) {
|
|
|
797
915
|
unfurl_media: false,
|
|
798
916
|
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
799
917
|
},
|
|
918
|
+
fetchImpl,
|
|
800
919
|
});
|
|
801
920
|
if (fallback.response.ok && fallback.payload?.ok) {
|
|
802
921
|
continue;
|
|
@@ -840,6 +959,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
840
959
|
return null;
|
|
841
960
|
}
|
|
842
961
|
const paths = getControlPlanePaths(opts.repoRoot);
|
|
962
|
+
const uiCallbackTokenStore = new UiCallbackTokenStore(paths.uiCallbackTokenPath);
|
|
843
963
|
const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
|
|
844
964
|
let pipeline = null;
|
|
845
965
|
let outboxDrainLoop = null;
|
|
@@ -847,6 +967,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
847
967
|
const outboundDeliveryChannels = new Set();
|
|
848
968
|
const adapterMap = new Map();
|
|
849
969
|
try {
|
|
970
|
+
await uiCallbackTokenStore.load();
|
|
850
971
|
await runtime.start();
|
|
851
972
|
const operator = opts.operatorRuntime !== undefined
|
|
852
973
|
? opts.operatorRuntime
|
|
@@ -868,6 +989,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
868
989
|
const telegramManager = new TelegramAdapterGenerationManager({
|
|
869
990
|
pipeline,
|
|
870
991
|
outbox,
|
|
992
|
+
uiCallbackTokenStore,
|
|
871
993
|
initialConfig: controlPlaneConfig,
|
|
872
994
|
onOutboxEnqueued: () => {
|
|
873
995
|
scheduleOutboxDrainRef?.();
|
|
@@ -881,6 +1003,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
881
1003
|
config: controlPlaneConfig,
|
|
882
1004
|
pipeline,
|
|
883
1005
|
outbox,
|
|
1006
|
+
uiCallbackTokenStore,
|
|
884
1007
|
})) {
|
|
885
1008
|
const route = adapter.spec.route;
|
|
886
1009
|
if (adapterMap.has(route)) {
|
|
@@ -939,6 +1062,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
939
1062
|
return await deliverSlackOutboxRecord({
|
|
940
1063
|
botToken: slackBotToken,
|
|
941
1064
|
record,
|
|
1065
|
+
uiCallbackTokenStore,
|
|
942
1066
|
});
|
|
943
1067
|
},
|
|
944
1068
|
},
|
|
@@ -952,12 +1076,18 @@ export async function bootstrapControlPlane(opts) {
|
|
|
952
1076
|
return await deliverTelegramOutboxRecord({
|
|
953
1077
|
botToken: telegramBotToken,
|
|
954
1078
|
record,
|
|
955
|
-
encodeCallbackData: async (commandText) => {
|
|
1079
|
+
encodeCallbackData: async (commandText, encodeOpts) => {
|
|
956
1080
|
const active = telegramManager.activeAdapter();
|
|
957
1081
|
if (!active) {
|
|
958
1082
|
return commandText;
|
|
959
1083
|
}
|
|
960
|
-
return await active.issueCallbackToken({
|
|
1084
|
+
return await active.issueCallbackToken({
|
|
1085
|
+
commandText,
|
|
1086
|
+
actorId: encodeOpts.record.envelope.correlation.actor_id,
|
|
1087
|
+
actorBindingId: encodeOpts.record.envelope.correlation.actor_binding_id,
|
|
1088
|
+
conversationId: encodeOpts.record.envelope.channel_conversation_id,
|
|
1089
|
+
uiEvent: encodeOpts.uiEvent,
|
|
1090
|
+
});
|
|
961
1091
|
},
|
|
962
1092
|
});
|
|
963
1093
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ControlPlaneAdapter, type ControlPlaneCommandPipeline, type ControlPlaneOutbox } from "@femtomc/mu-control-plane";
|
|
1
|
+
import { type ControlPlaneAdapter, type ControlPlaneCommandPipeline, type ControlPlaneOutbox, UiCallbackTokenStore } from "@femtomc/mu-control-plane";
|
|
2
2
|
import type { ControlPlaneConfig } from "./control_plane_contract.js";
|
|
3
3
|
export type DetectedStaticAdapter = {
|
|
4
4
|
name: "slack" | "discord" | "neovim";
|
|
@@ -16,4 +16,5 @@ export declare function createStaticAdaptersFromDetected(opts: {
|
|
|
16
16
|
config: ControlPlaneConfig;
|
|
17
17
|
pipeline: ControlPlaneCommandPipeline;
|
|
18
18
|
outbox: ControlPlaneOutbox;
|
|
19
|
+
uiCallbackTokenStore: UiCallbackTokenStore;
|
|
19
20
|
}): ControlPlaneAdapter[];
|
|
@@ -8,6 +8,7 @@ const STATIC_ADAPTER_MODULES = [
|
|
|
8
8
|
outbox: opts.outbox,
|
|
9
9
|
signingSecret: opts.secret,
|
|
10
10
|
botToken: opts.config.adapters.slack.bot_token,
|
|
11
|
+
uiCallbackTokenStore: opts.uiCallbackTokenStore,
|
|
11
12
|
}),
|
|
12
13
|
},
|
|
13
14
|
{
|
|
@@ -17,6 +18,7 @@ const STATIC_ADAPTER_MODULES = [
|
|
|
17
18
|
pipeline: opts.pipeline,
|
|
18
19
|
outbox: opts.outbox,
|
|
19
20
|
signingSecret: opts.secret,
|
|
21
|
+
uiCallbackTokenStore: opts.uiCallbackTokenStore,
|
|
20
22
|
}),
|
|
21
23
|
},
|
|
22
24
|
{
|
|
@@ -25,6 +27,7 @@ const STATIC_ADAPTER_MODULES = [
|
|
|
25
27
|
create: (opts) => new NeovimControlPlaneAdapter({
|
|
26
28
|
pipeline: opts.pipeline,
|
|
27
29
|
sharedSecret: opts.secret,
|
|
30
|
+
uiCallbackTokenStore: opts.uiCallbackTokenStore,
|
|
28
31
|
}),
|
|
29
32
|
},
|
|
30
33
|
];
|
|
@@ -69,6 +72,7 @@ export function createStaticAdaptersFromDetected(opts) {
|
|
|
69
72
|
outbox: opts.outbox,
|
|
70
73
|
secret: detected.secret,
|
|
71
74
|
config: opts.config,
|
|
75
|
+
uiCallbackTokenStore: opts.uiCallbackTokenStore,
|
|
72
76
|
}));
|
|
73
77
|
}
|
|
74
78
|
return adapters;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, type ControlPlaneSignalObserver, type ReloadableGenerationIdentity, TelegramControlPlaneAdapter } from "@femtomc/mu-control-plane";
|
|
1
|
+
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, type ControlPlaneSignalObserver, type ReloadableGenerationIdentity, TelegramControlPlaneAdapter, UiCallbackTokenStore } from "@femtomc/mu-control-plane";
|
|
2
2
|
import type { ControlPlaneConfig, TelegramGenerationReloadResult, TelegramGenerationSwapHooks } from "./control_plane_contract.js";
|
|
3
3
|
export declare class TelegramAdapterGenerationManager {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(opts: {
|
|
6
6
|
pipeline: ControlPlaneCommandPipeline;
|
|
7
7
|
outbox: ControlPlaneOutbox;
|
|
8
|
+
uiCallbackTokenStore: UiCallbackTokenStore;
|
|
8
9
|
initialConfig: ControlPlaneConfig;
|
|
9
10
|
nowMs?: () => number;
|
|
10
11
|
onOutboxEnqueued?: () => void;
|
|
@@ -79,6 +79,7 @@ async function runWithTimeout(opts) {
|
|
|
79
79
|
export class TelegramAdapterGenerationManager {
|
|
80
80
|
#pipeline;
|
|
81
81
|
#outbox;
|
|
82
|
+
#uiCallbackTokenStore;
|
|
82
83
|
#nowMs;
|
|
83
84
|
#onOutboxEnqueued;
|
|
84
85
|
#signalObserver;
|
|
@@ -90,6 +91,7 @@ export class TelegramAdapterGenerationManager {
|
|
|
90
91
|
constructor(opts) {
|
|
91
92
|
this.#pipeline = opts.pipeline;
|
|
92
93
|
this.#outbox = opts.outbox;
|
|
94
|
+
this.#uiCallbackTokenStore = opts.uiCallbackTokenStore;
|
|
93
95
|
this.#nowMs = opts.nowMs ?? Date.now;
|
|
94
96
|
this.#onOutboxEnqueued = opts.onOutboxEnqueued ?? null;
|
|
95
97
|
this.#signalObserver = opts.signalObserver ?? null;
|
|
@@ -115,6 +117,7 @@ export class TelegramAdapterGenerationManager {
|
|
|
115
117
|
acceptIngress: opts.acceptIngress,
|
|
116
118
|
ingressDrainEnabled: opts.ingressDrainEnabled,
|
|
117
119
|
nowMs: this.#nowMs,
|
|
120
|
+
uiCallbackTokenStore: this.#uiCallbackTokenStore,
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
123
|
async initialize() {
|
package/dist/server_routing.js
CHANGED
|
@@ -13,7 +13,7 @@ export function createServerRequestHandler(deps) {
|
|
|
13
13
|
if (request.method === "OPTIONS") {
|
|
14
14
|
return new Response(null, { status: 204, headers });
|
|
15
15
|
}
|
|
16
|
-
if (path === "/healthz"
|
|
16
|
+
if (path === "/healthz") {
|
|
17
17
|
return new Response("ok", { status: 200, headers });
|
|
18
18
|
}
|
|
19
19
|
if (path === "/api/server/shutdown") {
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
2
|
+
"name": "@femtomc/mu-server",
|
|
3
|
+
"version": "26.2.108",
|
|
4
|
+
"description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mu",
|
|
7
|
+
"server",
|
|
8
|
+
"api",
|
|
9
|
+
"web",
|
|
10
|
+
"automation"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"bin": {
|
|
16
|
+
"mu-server": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/**"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.build.json",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"start": "bun run dist/cli.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@femtomc/mu-agent": "26.2.108",
|
|
34
|
+
"@femtomc/mu-control-plane": "26.2.108",
|
|
35
|
+
"@femtomc/mu-core": "26.2.108"
|
|
36
|
+
}
|
|
37
37
|
}
|