@0xmaxma/claude-gateway 1.8.9 → 1.8.11
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 +46 -6
- package/config.template.json +2 -1
- package/dist/agent/runner.d.ts +86 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +396 -9
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +1028 -6
- package/dist/api/router.js.map +1 -1
- package/dist/api/share-router.d.ts.map +1 -1
- package/dist/api/share-router.js +24 -0
- package/dist/api/share-router.js.map +1 -1
- package/dist/api/webhooks-router.d.ts +2 -0
- package/dist/api/webhooks-router.d.ts.map +1 -1
- package/dist/api/webhooks-router.js +2 -0
- package/dist/api/webhooks-router.js.map +1 -1
- package/dist/api/whatsapp-access.d.ts +123 -0
- package/dist/api/whatsapp-access.d.ts.map +1 -0
- package/dist/api/whatsapp-access.js +135 -0
- package/dist/api/whatsapp-access.js.map +1 -0
- package/dist/api/whatsapp-cloud-access.d.ts +48 -0
- package/dist/api/whatsapp-cloud-access.d.ts.map +1 -0
- package/dist/api/whatsapp-cloud-access.js +58 -0
- package/dist/api/whatsapp-cloud-access.js.map +1 -0
- package/dist/api/whatsapp-cloud-client.d.ts +143 -0
- package/dist/api/whatsapp-cloud-client.d.ts.map +1 -0
- package/dist/api/whatsapp-cloud-client.js +298 -0
- package/dist/api/whatsapp-cloud-client.js.map +1 -0
- package/dist/api/whatsapp-cloud-webhook-router.d.ts +132 -0
- package/dist/api/whatsapp-cloud-webhook-router.d.ts.map +1 -0
- package/dist/api/whatsapp-cloud-webhook-router.js +494 -0
- package/dist/api/whatsapp-cloud-webhook-router.js.map +1 -0
- package/dist/cli/args.d.ts +34 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +55 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/app.d.ts +28 -0
- package/dist/cli/commands/app.d.ts.map +1 -0
- package/dist/cli/commands/app.js +411 -0
- package/dist/cli/commands/app.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +77 -16
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/service.d.ts.map +1 -1
- package/dist/cli/commands/service.js +373 -56
- package/dist/cli/commands/service.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +1 -17
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/http-client.d.ts +8 -0
- package/dist/cli/http-client.d.ts.map +1 -1
- package/dist/cli/http-client.js +5 -2
- package/dist/cli/http-client.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +16 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt.d.ts +17 -2
- package/dist/cli/prompt.d.ts.map +1 -1
- package/dist/cli/prompt.js +33 -2
- package/dist/cli/prompt.js.map +1 -1
- package/dist/config/loader.d.ts +8 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +22 -4
- package/dist/config/loader.js.map +1 -1
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +12 -0
- package/dist/config/watcher.js.map +1 -1
- package/dist/config/whatsapp-accounts.d.ts +77 -0
- package/dist/config/whatsapp-accounts.d.ts.map +1 -0
- package/dist/config/whatsapp-accounts.js +218 -0
- package/dist/config/whatsapp-accounts.js.map +1 -0
- package/dist/history/db.d.ts.map +1 -1
- package/dist/history/db.js +43 -11
- package/dist/history/db.js.map +1 -1
- package/dist/history/types.d.ts +18 -1
- package/dist/history/types.d.ts.map +1 -1
- package/dist/history/types.js +1 -1
- package/dist/history/types.js.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +37 -0
- package/dist/session/process.js.map +1 -1
- package/dist/session/store.d.ts +3 -2
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +1 -1
- package/dist/session/store.js.map +1 -1
- package/dist/share/session-video-catalog.d.ts +19 -0
- package/dist/share/session-video-catalog.d.ts.map +1 -0
- package/dist/share/session-video-catalog.js +119 -0
- package/dist/share/session-video-catalog.js.map +1 -0
- package/dist/shared/image-optimize.d.ts +32 -0
- package/dist/shared/image-optimize.d.ts.map +1 -0
- package/dist/shared/image-optimize.js +197 -0
- package/dist/shared/image-optimize.js.map +1 -0
- package/dist/shared/image-sniff.d.ts +9 -0
- package/dist/shared/image-sniff.d.ts.map +1 -1
- package/dist/shared/image-sniff.js +13 -0
- package/dist/shared/image-sniff.js.map +1 -1
- package/dist/shared/text-chunk.d.ts +37 -0
- package/dist/shared/text-chunk.d.ts.map +1 -0
- package/dist/shared/text-chunk.js +105 -0
- package/dist/shared/text-chunk.js.map +1 -0
- package/dist/shared/whatsapp-ack.d.ts +13 -0
- package/dist/shared/whatsapp-ack.d.ts.map +1 -0
- package/dist/shared/whatsapp-ack.js +16 -0
- package/dist/shared/whatsapp-ack.js.map +1 -0
- package/dist/types.d.ts +183 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/whatsapp/manager.d.ts +173 -0
- package/dist/whatsapp/manager.d.ts.map +1 -0
- package/dist/whatsapp/manager.js +838 -0
- package/dist/whatsapp/manager.js.map +1 -0
- package/mcp/server.ts +4 -0
- package/mcp/tools/image/module.ts +6 -1
- package/mcp/tools/telegram/receiver-server.ts +15 -8
- package/mcp/tools/telegram/reply-attachment.ts +81 -0
- package/mcp/tools/video/module.ts +6 -1
- package/mcp/tools/whatsapp/module.ts +162 -0
- package/mcp/tools/whatsapp-cloud/module.ts +536 -0
- package/package.json +7 -2
package/dist/agent/runner.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.AgentRunner = exports.CHANNEL_COALESCE_WINDOW_MS = exports.DEFAULT_MODELS = exports.MAX_IMAGE_SIZE_BYTES = void 0;
|
|
36
|
+
exports.AgentRunner = exports.CHANNEL_COALESCE_WINDOW_MS = exports.WHATSAPP_CLOUD_WINDOW_NOTE = exports.DEFAULT_MODELS = exports.MAX_IMAGE_SIZE_BYTES = void 0;
|
|
37
37
|
exports.toRelMediaFiles = toRelMediaFiles;
|
|
38
38
|
const crypto_1 = require("crypto");
|
|
39
39
|
const events_1 = require("events");
|
|
@@ -51,6 +51,9 @@ const receiver_1 = require("../telegram/receiver");
|
|
|
51
51
|
const receiver_2 = require("../discord/receiver");
|
|
52
52
|
const line_reply_manager_1 = require("./line-reply-manager");
|
|
53
53
|
const slack_client_1 = require("../api/slack-client");
|
|
54
|
+
const whatsapp_cloud_client_1 = require("../api/whatsapp-cloud-client");
|
|
55
|
+
const manager_1 = require("../whatsapp/manager");
|
|
56
|
+
const whatsapp_accounts_1 = require("../config/whatsapp-accounts");
|
|
54
57
|
const markdown_1 = require("../telegram/markdown");
|
|
55
58
|
const skills_1 = require("../skills");
|
|
56
59
|
const builtin_commands_1 = require("./builtin-commands");
|
|
@@ -125,6 +128,15 @@ const PROTECTED_WORKSPACE_FILES = [
|
|
|
125
128
|
'AGENTS.md', 'SOUL.md', 'MEMORY.md', 'CLAUDE.md',
|
|
126
129
|
'IDENTITY.md', 'USER.md', 'HEARTBEAT.md',
|
|
127
130
|
];
|
|
131
|
+
/**
|
|
132
|
+
* One-line reminder injected into every `whatsapp_cloud` turn's <channel>
|
|
133
|
+
* block (see buildChannelXml) — the 24h customer-service window is a
|
|
134
|
+
* WhatsApp-Business-only rule the agent cannot infer from the turn itself.
|
|
135
|
+
*
|
|
136
|
+
* Must stay free of `--` so it remains a well-formed XML comment.
|
|
137
|
+
*/
|
|
138
|
+
exports.WHATSAPP_CLOUD_WINDOW_NOTE = '[WhatsApp Business: free-form replies only work within 24h of the user\'s last message. ' +
|
|
139
|
+
'Outside that window, whatsapp_cloud_reply requires template_name + template_language (see templatesEnabled).]';
|
|
128
140
|
const MAX_API_IMAGES = 5;
|
|
129
141
|
/** Extra time an api turn may keep running AFTER its soft timeout already
|
|
130
142
|
* answered the caller (#75). The soft timeout only abandons the WAIT — the
|
|
@@ -236,6 +248,29 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
236
248
|
// it exists only so writeAutoForward's fallback/command-reply path (below) has
|
|
237
249
|
// somewhere to actually deliver Slack messages instead of silently dropping them.
|
|
238
250
|
this.slackOutbound = null;
|
|
251
|
+
// WhatsApp Cloud mirrors Slack exactly: webhook-based, real credentials, no
|
|
252
|
+
// reply-token TTL to work around — a plain client so writeAutoForward's
|
|
253
|
+
// fallback/command-reply path has somewhere to deliver messages instead of
|
|
254
|
+
// silently dropping them (see the Slack comment just above).
|
|
255
|
+
this.whatsAppCloudOutbound = null;
|
|
256
|
+
// One live Baileys socket per linked WhatsApp account, keyed by account id
|
|
257
|
+
// (Phase 1 of the WhatsApp feature-parity plan — this used to be a single
|
|
258
|
+
// `whatsapp: WhatsAppManager | null`). Populated in start() (needs
|
|
259
|
+
// this.callbackPort, only resolved once startCallbackServer() runs — same
|
|
260
|
+
// reason DiscordReceiver/TelegramReceiver are built there too) and then
|
|
261
|
+
// DIFFED, never rebuilt wholesale, by syncWhatsAppAccounts(): unlike
|
|
262
|
+
// SlackClient/DiscordReceiver there is no credential to re-read on a config
|
|
263
|
+
// change, since the credential IS the on-disk session (see
|
|
264
|
+
// WhatsAppAccountConfig's doc comment), so tearing a linked socket down on
|
|
265
|
+
// an unrelated config edit would be pure downtime. Each manager decides for
|
|
266
|
+
// itself whether there's anything to do on start (resumeIfLinked() no-ops
|
|
267
|
+
// if that account was never linked).
|
|
268
|
+
this.whatsappAccounts = new Map();
|
|
269
|
+
// chatId → the account id its last inbound message arrived on. Mirrors
|
|
270
|
+
// slackThreadTs above: it lets writeAutoForward (and a whatsapp_reply call
|
|
271
|
+
// that omits account_id) answer on the SAME number the user wrote to
|
|
272
|
+
// instead of guessing 'default' and replying from the wrong line.
|
|
273
|
+
this.whatsappAccountForChat = new Map();
|
|
239
274
|
this.idleCleanerTimer = null;
|
|
240
275
|
// Tracks session IDs with an in-flight API request (prevents concurrent turns)
|
|
241
276
|
this.pendingApiSessions = new Set();
|
|
@@ -439,7 +474,11 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
439
474
|
? 'line'
|
|
440
475
|
: meta['source'] === 'slack'
|
|
441
476
|
? 'slack'
|
|
442
|
-
: '
|
|
477
|
+
: meta['source'] === 'whatsapp'
|
|
478
|
+
? 'whatsapp'
|
|
479
|
+
: meta['source'] === 'whatsapp_cloud'
|
|
480
|
+
? 'whatsapp_cloud'
|
|
481
|
+
: 'telegram');
|
|
443
482
|
this.channelSourceMap.set(chatId, channelSource);
|
|
444
483
|
// Slack: remember the current message's thread context so the
|
|
445
484
|
// auto-forward fallback (writeAutoForward) can stay in-thread. A
|
|
@@ -452,6 +491,14 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
452
491
|
else
|
|
453
492
|
this.slackThreadTs.delete(chatId);
|
|
454
493
|
}
|
|
494
|
+
// WhatsApp: remember which linked number this chat last came in on,
|
|
495
|
+
// so a reply that doesn't name an account_id (the auto-forward
|
|
496
|
+
// fallback, or a whatsapp_reply call that omitted it) still goes out
|
|
497
|
+
// on the number the user actually wrote to. Same shape as the Slack
|
|
498
|
+
// thread-context stash just above.
|
|
499
|
+
if (channelSource === 'whatsapp' && meta['account_id']) {
|
|
500
|
+
this.whatsappAccountForChat.set(chatId, meta['account_id']);
|
|
501
|
+
}
|
|
455
502
|
// LINE slow-LLM postback: stash this turn's reply token + arm the
|
|
456
503
|
// button timer before the token expires. In groups/rooms the shared
|
|
457
504
|
// button is disabled (armButton:false) — the token is still stashed so
|
|
@@ -1027,7 +1074,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1027
1074
|
for (const entry of entries) {
|
|
1028
1075
|
const meta = entry.meta ?? {};
|
|
1029
1076
|
const content = entry.content ?? '';
|
|
1030
|
-
const userContent = content || (meta['attachment_file_id'] || meta['image_path'] ? '(photo)' : '');
|
|
1077
|
+
const userContent = content || (meta['attachment_file_id'] || meta['image_path'] ? '(photo)' : meta['document_path'] ? '(document)' : meta['sticker_path'] ? '(sticker)' : '');
|
|
1031
1078
|
const userTs = Date.now();
|
|
1032
1079
|
await this.sessionStore.appendTelegramMessage(this.agentConfig.id, chatId, sessionId, {
|
|
1033
1080
|
role: 'user',
|
|
@@ -1058,6 +1105,33 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1058
1105
|
// Non-fatal — leave the original path so host agents still read it
|
|
1059
1106
|
}
|
|
1060
1107
|
}
|
|
1108
|
+
// WhatsApp Cloud documents (PDF today — see MediaStore.isAllowedMime):
|
|
1109
|
+
// same MediaStore copy + path-rewrite as image_path above, parallel
|
|
1110
|
+
// key so an inbound document doesn't collide with an inbound image
|
|
1111
|
+
// in the same turn.
|
|
1112
|
+
if (meta['document_path']) {
|
|
1113
|
+
try {
|
|
1114
|
+
const rel = media_store_1.MediaStore.copyToMedia(this.agentsBaseDir, this.agentConfig.id, `${channelSource}-${chatId}`, meta['document_path']);
|
|
1115
|
+
mediaFiles.push(rel);
|
|
1116
|
+
meta['document_path'] = media_store_1.MediaStore.resolvePath(this.agentsBaseDir, this.agentConfig.id, rel);
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
// Non-fatal — leave the original path so host agents still read it
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
// WhatsApp stickers (both channels — image/webp): same MediaStore
|
|
1123
|
+
// copy + path-rewrite as image_path above, on its own key so an
|
|
1124
|
+
// inbound sticker never masquerades as a photo.
|
|
1125
|
+
if (meta['sticker_path']) {
|
|
1126
|
+
try {
|
|
1127
|
+
const rel = media_store_1.MediaStore.copyToMedia(this.agentsBaseDir, this.agentConfig.id, `${channelSource}-${chatId}`, meta['sticker_path']);
|
|
1128
|
+
mediaFiles.push(rel);
|
|
1129
|
+
meta['sticker_path'] = media_store_1.MediaStore.resolvePath(this.agentsBaseDir, this.agentConfig.id, rel);
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
// Non-fatal — leave the original path so host agents still read it
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1061
1135
|
this.historyDb.insertMessage({
|
|
1062
1136
|
chatId: `${channelSource}-${chatId}`,
|
|
1063
1137
|
sessionId,
|
|
@@ -1068,6 +1142,14 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1068
1142
|
senderId: meta['user_id'] ?? meta['chat_id'] ?? undefined,
|
|
1069
1143
|
platformMessageId: meta['message_id'] ?? undefined,
|
|
1070
1144
|
mediaFiles: mediaFiles.length > 0 ? mediaFiles : undefined,
|
|
1145
|
+
// Reply context (WhatsApp Phase 2) — the same meta keys buildChannelXml
|
|
1146
|
+
// renders into <replied> for the agent, persisted so the web dashboard
|
|
1147
|
+
// can show "in reply to X" instead of losing it after the turn.
|
|
1148
|
+
// whatsapp_cloud only ever sets replied_message_id (its webhook reports
|
|
1149
|
+
// the quoted id and nothing else), so the other two stay undefined there.
|
|
1150
|
+
repliedToMessageId: meta['replied_message_id'] ?? undefined,
|
|
1151
|
+
repliedToText: meta['replied_text'] ?? undefined,
|
|
1152
|
+
repliedToUser: meta['replied_user'] ?? undefined,
|
|
1071
1153
|
ts: userTs,
|
|
1072
1154
|
});
|
|
1073
1155
|
}
|
|
@@ -1219,6 +1301,54 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1219
1301
|
`Do NOT open or Read the referenced files first — the image model receives the actual files; ` +
|
|
1220
1302
|
`reading them wastes minutes and can push the request past its timeout. Go straight to generate_image.\n`);
|
|
1221
1303
|
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Render composer-selected video options as a directive the agent reads and
|
|
1306
|
+
* forwards to the generate_video MCP tool. Mirrors buildImageParamsNote. Returns
|
|
1307
|
+
* '' when no usable options are present. Without this the agent gets no composer
|
|
1308
|
+
* context for video and invents duration/aspect (a phantom 10s cap, an 8+8 scene
|
|
1309
|
+
* split, or a landscape clip when 9:16 was picked).
|
|
1310
|
+
*/
|
|
1311
|
+
static buildVideoParamsNote(p) {
|
|
1312
|
+
const attrs = [
|
|
1313
|
+
p.model ? `model="${AgentRunner.escapeXmlAttr(p.model)}"` : '',
|
|
1314
|
+
p.resolution ? `resolution="${AgentRunner.escapeXmlAttr(p.resolution)}"` : '',
|
|
1315
|
+
p.aspect_ratio ? `aspect_ratio="${AgentRunner.escapeXmlAttr(p.aspect_ratio)}"` : '',
|
|
1316
|
+
typeof p.duration === 'number' ? `duration="${p.duration}"` : '',
|
|
1317
|
+
p.image_ref ? `image_ref="${AgentRunner.escapeXmlAttr(p.image_ref)}"` : '',
|
|
1318
|
+
].filter(Boolean);
|
|
1319
|
+
if (!attrs.length)
|
|
1320
|
+
return '';
|
|
1321
|
+
// The generate_video schema lets the agent pick model/duration itself; when the
|
|
1322
|
+
// composer selection isn't made authoritative the agent falls back to
|
|
1323
|
+
// action="list" and self-selects, or invents a duration cap and splits the clip
|
|
1324
|
+
// into multiple scenes. Nail every field down.
|
|
1325
|
+
const modelNote = p.model
|
|
1326
|
+
? `The user explicitly SELECTED model="${AgentRunner.escapeXmlAttr(p.model)}" in the composer. ` +
|
|
1327
|
+
`Call generate_video with that exact model — do NOT call action="list" to second-guess an ` +
|
|
1328
|
+
`explicit selection or substitute a different model.\n`
|
|
1329
|
+
: '';
|
|
1330
|
+
const durationNote = typeof p.duration === 'number'
|
|
1331
|
+
? `duration=${p.duration} is a valid length for the selected model — pass it verbatim as the "duration" ` +
|
|
1332
|
+
`argument. Do NOT invent a maximum, do NOT clamp it to a smaller value, and do NOT split the request ` +
|
|
1333
|
+
`into multiple shorter scenes/clips. Generate exactly ONE clip of this duration.\n`
|
|
1334
|
+
: '';
|
|
1335
|
+
const aspectNote = p.aspect_ratio
|
|
1336
|
+
? `Pass aspect_ratio="${AgentRunner.escapeXmlAttr(p.aspect_ratio)}" verbatim — do NOT change the orientation.\n`
|
|
1337
|
+
: '';
|
|
1338
|
+
const refNote = p.image_ref
|
|
1339
|
+
? `The user selected a source image for image-to-video. Pass image_ref="${AgentRunner.escapeXmlAttr(p.image_ref)}" ` +
|
|
1340
|
+
`as the "image" argument of generate_video (the source frame's own aspect then wins). ` +
|
|
1341
|
+
`Do NOT call list_refs to second-guess it, and do NOT open or Read the file first.\n`
|
|
1342
|
+
: '';
|
|
1343
|
+
return (`<video-params ${attrs.join(' ')} />\n` +
|
|
1344
|
+
`The user selected the video-generation options above in the composer. When the request involves ` +
|
|
1345
|
+
`creating a video, call the generate_video tool (action="generate") using these exact values, then ` +
|
|
1346
|
+
`deliver the returned video with your reply tool.\n` +
|
|
1347
|
+
modelNote +
|
|
1348
|
+
durationNote +
|
|
1349
|
+
aspectNote +
|
|
1350
|
+
refNote);
|
|
1351
|
+
}
|
|
1222
1352
|
/**
|
|
1223
1353
|
* The durable slice of the composer image options — everything except
|
|
1224
1354
|
* `image_refs`, which is a per-turn explicit selection (#73) and must never be
|
|
@@ -1238,16 +1368,26 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1238
1368
|
* session path; refs that were never staged (catalog refs, artifact:<id>)
|
|
1239
1369
|
* pass through untouched. (#74)
|
|
1240
1370
|
*/
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1371
|
+
// Build the staging→promoted path map: promoteUiUploads moves a turn's
|
|
1372
|
+
// ui-upload files into per-session storage, so any ref built from a staging
|
|
1373
|
+
// path must follow the file to its new location (#74). Empty map = nothing
|
|
1374
|
+
// moved (or no uploads), so callers leave their refs untouched.
|
|
1375
|
+
static buildRefRemap(stagedPaths, promotedPaths) {
|
|
1244
1376
|
const map = new Map();
|
|
1377
|
+
if (!stagedPaths?.length || !promotedPaths?.length)
|
|
1378
|
+
return map;
|
|
1245
1379
|
for (let i = 0; i < Math.min(stagedPaths.length, promotedPaths.length); i++) {
|
|
1246
1380
|
const from = stagedPaths[i];
|
|
1247
1381
|
const to = promotedPaths[i];
|
|
1248
1382
|
if (from !== to)
|
|
1249
1383
|
map.set(from, to);
|
|
1250
1384
|
}
|
|
1385
|
+
return map;
|
|
1386
|
+
}
|
|
1387
|
+
static remapImageParamsRefs(p, stagedPaths, promotedPaths) {
|
|
1388
|
+
if (!p)
|
|
1389
|
+
return p;
|
|
1390
|
+
const map = AgentRunner.buildRefRemap(stagedPaths, promotedPaths);
|
|
1251
1391
|
if (!map.size)
|
|
1252
1392
|
return p;
|
|
1253
1393
|
const remap = (r) => map.get(r) ?? r;
|
|
@@ -1257,10 +1397,26 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1257
1397
|
...(p.image_refs?.length ? { image_refs: p.image_refs.map(remap) } : {}),
|
|
1258
1398
|
};
|
|
1259
1399
|
}
|
|
1400
|
+
// Video analogue of remapImageParamsRefs. The composer's image-to-video
|
|
1401
|
+
// source frame (VideoParams.image_ref) is built from a staging path; without
|
|
1402
|
+
// this remap the raw ref dangles after promoteUiUploads moves the file, and
|
|
1403
|
+
// generate_video fails share_ref_not_found. artifact:/catalog refs aren't in
|
|
1404
|
+
// the map, so they pass through untouched.
|
|
1405
|
+
static remapVideoParamsRefs(p, stagedPaths, promotedPaths) {
|
|
1406
|
+
if (!p?.image_ref)
|
|
1407
|
+
return p;
|
|
1408
|
+
const to = AgentRunner.buildRefRemap(stagedPaths, promotedPaths).get(p.image_ref);
|
|
1409
|
+
return to ? { ...p, image_ref: to } : p;
|
|
1410
|
+
}
|
|
1260
1411
|
static buildChannelXml(params) {
|
|
1261
1412
|
const meta = params.meta ?? {};
|
|
1262
1413
|
const optionalAttrs = [
|
|
1263
1414
|
'image_path',
|
|
1415
|
+
'document_path', // WhatsApp Cloud: inbound PDF document (see MediaStore.isAllowedMime)
|
|
1416
|
+
'sticker_path', // WhatsApp (both channels): inbound sticker — kept distinct from image_path so the agent can tell a sticker from a photo
|
|
1417
|
+
'location_lat', // WhatsApp (both channels): inbound location pin
|
|
1418
|
+
'location_lng',
|
|
1419
|
+
'vcard', // WhatsApp: inbound contact card — raw vCard on Baileys, synthesized from Meta's structured payload on Cloud
|
|
1264
1420
|
'attachment_file_id',
|
|
1265
1421
|
'attachment_kind',
|
|
1266
1422
|
'attachment_mime',
|
|
@@ -1268,6 +1424,8 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1268
1424
|
'user_id', // LINE: the userId the session passes back to line_reply
|
|
1269
1425
|
'reply_token', // LINE: single-use reply token (push is preferred; surfaced for completeness)
|
|
1270
1426
|
'thread_ts', // Slack: set when the inbound message is inside a thread — pass back as thread_id to slack_reply to reply in-thread
|
|
1427
|
+
'account_id', // WhatsApp: which linked number this arrived on — pass back to whatsapp_reply to answer on the same one
|
|
1428
|
+
'interactive_id', // WhatsApp Cloud: the id of the tapped button / picked list row (content already carries its title)
|
|
1271
1429
|
// NOTE: message_id is NOT listed here — the base <channel> template below
|
|
1272
1430
|
// already unconditionally emits it; adding it here would duplicate the
|
|
1273
1431
|
// attribute in the XML whenever meta.message_id is set (any channel).
|
|
@@ -1280,6 +1438,10 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1280
1438
|
if (meta['replied_message_id']) {
|
|
1281
1439
|
const repliedAttrs = [
|
|
1282
1440
|
'replied_image_path',
|
|
1441
|
+
'replied_attachment_file_id',
|
|
1442
|
+
'replied_attachment_kind',
|
|
1443
|
+
'replied_attachment_mime',
|
|
1444
|
+
'replied_attachment_name',
|
|
1283
1445
|
]
|
|
1284
1446
|
.filter(k => meta[k])
|
|
1285
1447
|
.map(k => ` ${k}="${meta[k].replace(/"/g, '"')}"`)
|
|
@@ -1291,9 +1453,18 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1291
1453
|
`</replied>`;
|
|
1292
1454
|
}
|
|
1293
1455
|
const source = meta['source'] ?? 'telegram';
|
|
1456
|
+
// Per-turn channel note (Phase 3). WhatsApp Business enforces a 24h
|
|
1457
|
+
// customer-service window that no other channel here has, and the agent
|
|
1458
|
+
// cannot discover it from the turn itself — it only shows up as an opaque
|
|
1459
|
+
// Meta error at send time. Emitted as an XML COMMENT so it can never be
|
|
1460
|
+
// mistaken for part of the user's message, and kept to ONE short line
|
|
1461
|
+
// because it repeats on every single whatsapp_cloud turn. (A frozen,
|
|
1462
|
+
// system-prompt-level injection would be new infra for one warning; the
|
|
1463
|
+
// per-turn wrapper is the existing seam.)
|
|
1464
|
+
const channelNote = source === 'whatsapp_cloud' ? `<!-- ${exports.WHATSAPP_CLOUD_WINDOW_NOTE} -->` : '';
|
|
1294
1465
|
return (`<channel source="${source}" chat_id="${meta['chat_id'] ?? ''}" ` +
|
|
1295
1466
|
`message_id="${meta['message_id'] ?? ''}" user="${AgentRunner.escapeXmlAttr(meta['user'] ?? '')}" ` +
|
|
1296
|
-
`ts="${meta['ts'] ?? new Date().toISOString()}"${optionalAttrs}>${repliedBlock}${params.content ?? ''}</channel>`);
|
|
1467
|
+
`ts="${meta['ts'] ?? new Date().toISOString()}"${optionalAttrs}>${channelNote}${repliedBlock}${params.content ?? ''}</channel>`);
|
|
1297
1468
|
}
|
|
1298
1469
|
/**
|
|
1299
1470
|
* Await a SessionProcess's in-flight crash-triggered auto-restart
|
|
@@ -1552,7 +1723,11 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1552
1723
|
? 'mcp__gateway__line_reply'
|
|
1553
1724
|
: source === 'slack'
|
|
1554
1725
|
? 'mcp__gateway__slack_reply'
|
|
1555
|
-
: '
|
|
1726
|
+
: source === 'whatsapp'
|
|
1727
|
+
? 'mcp__gateway__whatsapp_reply'
|
|
1728
|
+
: source === 'whatsapp_cloud'
|
|
1729
|
+
? 'mcp__gateway__whatsapp_cloud_reply'
|
|
1730
|
+
: 'mcp__gateway__telegram_reply';
|
|
1556
1731
|
proc.on('output', (line) => {
|
|
1557
1732
|
try {
|
|
1558
1733
|
const obj = JSON.parse(line);
|
|
@@ -1865,10 +2040,15 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1865
2040
|
}
|
|
1866
2041
|
else if (channelSrcForResult !== 'discord' &&
|
|
1867
2042
|
channelSrcForResult !== 'slack' &&
|
|
2043
|
+
channelSrcForResult !== 'whatsapp' &&
|
|
2044
|
+
channelSrcForResult !== 'whatsapp_cloud' &&
|
|
1868
2045
|
((0, markdown_1.hasMarkdown)(channelText) || (0, markdown_1.containsTelegramHtml)(channelText))) {
|
|
1869
2046
|
// Telegram HTML entities — Slack has its own mrkdwn format and
|
|
1870
2047
|
// would display these tags literally, so Slack skips this and
|
|
1871
|
-
// falls through to the plain-text branch below.
|
|
2048
|
+
// falls through to the plain-text branch below. WhatsApp (and
|
|
2049
|
+
// WhatsApp Cloud, same lightweight markup) has
|
|
2050
|
+
// its own lightweight markup (*bold*/_italic_/~strike~, not
|
|
2051
|
+
// HTML), same reasoning.
|
|
1872
2052
|
this.writeAutoForward(mapKey, (0, markdown_1.toTelegramHtml)(channelText), 'html', replySendFailed);
|
|
1873
2053
|
}
|
|
1874
2054
|
else {
|
|
@@ -2725,6 +2905,36 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2725
2905
|
}
|
|
2726
2906
|
return;
|
|
2727
2907
|
}
|
|
2908
|
+
// WhatsApp likewise has no .forward consumer, and — unlike Slack/SMS's
|
|
2909
|
+
// outbound REST clients — cannot open a fresh connection per call either
|
|
2910
|
+
// (see WhatsAppManager's doc comment): reach the live socket this
|
|
2911
|
+
// process already holds directly.
|
|
2912
|
+
if (this.channelFor(chatId) === 'whatsapp') {
|
|
2913
|
+
// No account_id to go on here (this is the fallback path, not a tool
|
|
2914
|
+
// call), so whatsAppManagerFor falls back to the account this chat's
|
|
2915
|
+
// last inbound message arrived on — the number the user wrote to.
|
|
2916
|
+
void this.sendWhatsAppMessage(chatId, text).catch((err) => {
|
|
2917
|
+
this.logger.warn('WhatsApp auto-forward failed', {
|
|
2918
|
+
chatId,
|
|
2919
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2920
|
+
});
|
|
2921
|
+
});
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
// WhatsApp Cloud mirrors Slack exactly (no .forward consumer, a real REST
|
|
2925
|
+
// client that can open a fresh connection per call — unlike Baileys'
|
|
2926
|
+
// `whatsapp` above, there's no live socket to reuse or reason to).
|
|
2927
|
+
if (this.channelFor(chatId) === 'whatsapp_cloud') {
|
|
2928
|
+
if (this.whatsAppCloudOutbound) {
|
|
2929
|
+
void this.whatsAppCloudOutbound.sendText(chatId, text).catch((err) => {
|
|
2930
|
+
this.logger.warn('WhatsApp Cloud auto-forward failed', {
|
|
2931
|
+
chatId,
|
|
2932
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2933
|
+
});
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2728
2938
|
const typingDir = this.getTypingDir(chatId);
|
|
2729
2939
|
const forwardPath = path.join(typingDir, `${chatId}.forward`);
|
|
2730
2940
|
try {
|
|
@@ -2824,6 +3034,15 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2824
3034
|
// line_reply tool keeps the plain reply-first → push-fallback path).
|
|
2825
3035
|
this.startLineReply();
|
|
2826
3036
|
this.startSlackOutbound();
|
|
3037
|
+
this.startWhatsAppCloudOutbound();
|
|
3038
|
+
// Unconditional (unlike every credential-gated channel above) — each
|
|
3039
|
+
// WhatsAppManager checks disk for a previously-linked session itself;
|
|
3040
|
+
// there's no config field to gate on, and an agent with no `whatsapp`
|
|
3041
|
+
// block still gets the implicit 'default' account (see
|
|
3042
|
+
// resolveWhatsAppAccounts). A brand-new agent that's never linked just
|
|
3043
|
+
// no-ops here and stays 'unlinked' until the user starts a QR/pairing
|
|
3044
|
+
// flow via the API.
|
|
3045
|
+
this.syncWhatsAppAccounts();
|
|
2827
3046
|
this.startIdleCleaner();
|
|
2828
3047
|
this._startCleanupScheduler();
|
|
2829
3048
|
this.logger.info('AgentRunner started', { agentId: this.agentConfig.id });
|
|
@@ -2877,6 +3096,129 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2877
3096
|
// it up, same reasoning as LineReplyManager above.
|
|
2878
3097
|
this.stopSlackOutbound();
|
|
2879
3098
|
this.startSlackOutbound();
|
|
3099
|
+
// WhatsApp Cloud credentials may have changed (or been cleared) — rebuild
|
|
3100
|
+
// to pick it up, same reasoning as Slack above.
|
|
3101
|
+
this.stopWhatsAppCloudOutbound();
|
|
3102
|
+
this.startWhatsAppCloudOutbound();
|
|
3103
|
+
// WhatsApp has no credential to rebuild on — push the new config
|
|
3104
|
+
// (access-control fields) into every already-running manager, and
|
|
3105
|
+
// add/remove managers only if the set of configured accounts changed.
|
|
3106
|
+
this.syncWhatsAppAccounts();
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* Reconcile `this.whatsappAccounts` with `agentConfig.whatsapp.accounts`.
|
|
3110
|
+
*
|
|
3111
|
+
* Diffing (rather than teardown+recreate) is load-bearing here: a manager
|
|
3112
|
+
* holds a live, authenticated Baileys socket that can take tens of seconds
|
|
3113
|
+
* to re-establish, so an edit to account "sales" must not drop account
|
|
3114
|
+
* "default"'s connection. Accounts that survive the diff only get the new
|
|
3115
|
+
* AgentConfig pushed into them — their socket is untouched.
|
|
3116
|
+
*
|
|
3117
|
+
* Removal uses stop() (socket teardown), never unlink(): dropping an
|
|
3118
|
+
* account from config must not silently wipe its on-disk session. The
|
|
3119
|
+
* DELETE .../whatsapp/accounts/:id route unlinks explicitly before it
|
|
3120
|
+
* rewrites config.
|
|
3121
|
+
*/
|
|
3122
|
+
syncWhatsAppAccounts() {
|
|
3123
|
+
const snapshot = JSON.stringify(this.agentConfig.whatsapp ?? null);
|
|
3124
|
+
const accounts = (0, whatsapp_accounts_1.resolveWhatsAppAccounts)(this.agentConfig.whatsapp);
|
|
3125
|
+
// Always push the fresh config into live managers — access-control fields
|
|
3126
|
+
// are read live off it on every inbound message.
|
|
3127
|
+
for (const manager of this.whatsappAccounts.values()) {
|
|
3128
|
+
manager.updateAgentConfig(this.agentConfig);
|
|
3129
|
+
}
|
|
3130
|
+
// Nothing about the WhatsApp block moved ⇒ the account SET can't have
|
|
3131
|
+
// changed either, so skip the diff entirely.
|
|
3132
|
+
if (this.whatsappConfigSnapshot === snapshot && this.whatsappAccounts.size > 0)
|
|
3133
|
+
return;
|
|
3134
|
+
this.whatsappConfigSnapshot = snapshot;
|
|
3135
|
+
const wanted = new Set(accounts.map((a) => a.id));
|
|
3136
|
+
for (const [accountId, manager] of [...this.whatsappAccounts]) {
|
|
3137
|
+
if (wanted.has(accountId))
|
|
3138
|
+
continue;
|
|
3139
|
+
manager.stop();
|
|
3140
|
+
this.whatsappAccounts.delete(accountId);
|
|
3141
|
+
this.logger.info('WhatsApp account removed', { agentId: this.agentConfig.id, accountId });
|
|
3142
|
+
}
|
|
3143
|
+
for (const account of accounts) {
|
|
3144
|
+
if (this.whatsappAccounts.has(account.id))
|
|
3145
|
+
continue;
|
|
3146
|
+
const manager = new manager_1.WhatsAppManager(this.agentConfig, account.id, this.callbackPort, this.gatewayConfig.gateway.logDir);
|
|
3147
|
+
this.whatsappAccounts.set(account.id, manager);
|
|
3148
|
+
void manager.resumeIfLinked();
|
|
3149
|
+
this.logger.info('WhatsApp account started', { agentId: this.agentConfig.id, accountId: account.id });
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
/** Live managers, in configured order. */
|
|
3153
|
+
whatsAppManagers() {
|
|
3154
|
+
return (0, whatsapp_accounts_1.resolveWhatsAppAccounts)(this.agentConfig.whatsapp)
|
|
3155
|
+
.map((a) => this.whatsappAccounts.get(a.id))
|
|
3156
|
+
.filter((m) => !!m);
|
|
3157
|
+
}
|
|
3158
|
+
/**
|
|
3159
|
+
* Pick the manager a call should use.
|
|
3160
|
+
*
|
|
3161
|
+
* An EXPLICIT accountId must exist — silently falling back would send a
|
|
3162
|
+
* reply from the wrong number, which on WhatsApp is visible to the
|
|
3163
|
+
* recipient and unrecoverable. Without one, prefer the account the chat's
|
|
3164
|
+
* last inbound message arrived on (so a reply leaves on the number the user
|
|
3165
|
+
* actually wrote to), then 'default', then — for an agent whose only
|
|
3166
|
+
* account is named something else — its single account.
|
|
3167
|
+
*/
|
|
3168
|
+
whatsAppManagerFor(chatId, accountId) {
|
|
3169
|
+
if (accountId) {
|
|
3170
|
+
const explicit = this.whatsappAccounts.get(accountId);
|
|
3171
|
+
if (!explicit)
|
|
3172
|
+
throw new Error(`Unknown WhatsApp account '${accountId}'`);
|
|
3173
|
+
return explicit;
|
|
3174
|
+
}
|
|
3175
|
+
const remembered = chatId ? this.whatsappAccountForChat.get(chatId) : undefined;
|
|
3176
|
+
const managers = this.whatsAppManagers();
|
|
3177
|
+
const picked = (remembered ? this.whatsappAccounts.get(remembered) : undefined) ??
|
|
3178
|
+
this.whatsappAccounts.get(whatsapp_accounts_1.DEFAULT_WHATSAPP_ACCOUNT_ID) ??
|
|
3179
|
+
managers[0];
|
|
3180
|
+
if (!picked)
|
|
3181
|
+
throw new Error('Agent not started');
|
|
3182
|
+
return picked;
|
|
3183
|
+
}
|
|
3184
|
+
/**
|
|
3185
|
+
* Which account id a send to `chatId` (with an optional explicit
|
|
3186
|
+
* `accountId`) would actually use — the same resolution
|
|
3187
|
+
* `sendWhatsAppMessage` applies internally, exposed read-only so a caller
|
|
3188
|
+
* (the `/whatsapp/send` route) can run access-control/path-confinement
|
|
3189
|
+
* checks against the REAL target account without duplicating or
|
|
3190
|
+
* second-guessing this resolution logic. Throws the same errors
|
|
3191
|
+
* `whatsAppManagerFor` would (unknown account / agent not started).
|
|
3192
|
+
*/
|
|
3193
|
+
resolveWhatsAppAccountId(chatId, accountId) {
|
|
3194
|
+
return this.whatsAppManagerFor(chatId, accountId).accountId;
|
|
3195
|
+
}
|
|
3196
|
+
/** Live link status for one account (defaults to 'default'). */
|
|
3197
|
+
getWhatsAppStatus(accountId = whatsapp_accounts_1.DEFAULT_WHATSAPP_ACCOUNT_ID) {
|
|
3198
|
+
return this.whatsappAccounts.get(accountId)?.getStatus();
|
|
3199
|
+
}
|
|
3200
|
+
/** Configured account ids that currently have a live manager. */
|
|
3201
|
+
getWhatsAppAccountIds() {
|
|
3202
|
+
return this.whatsAppManagers().map((m) => m.accountId);
|
|
3203
|
+
}
|
|
3204
|
+
async startWhatsAppLinking(accountId) {
|
|
3205
|
+
await this.whatsAppManagerFor(undefined, accountId).startLinking();
|
|
3206
|
+
}
|
|
3207
|
+
async requestWhatsAppPairingCode(phoneNumber, accountId) {
|
|
3208
|
+
return this.whatsAppManagerFor(undefined, accountId).requestPairingCode(phoneNumber);
|
|
3209
|
+
}
|
|
3210
|
+
async unlinkWhatsApp(accountId) {
|
|
3211
|
+
await this.whatsAppManagerFor(undefined, accountId).unlink();
|
|
3212
|
+
}
|
|
3213
|
+
async sendWhatsAppMessage(jid, text, imagePath, accountId,
|
|
3214
|
+
/**
|
|
3215
|
+
* Phase 2 extras, all optional so existing callers (auto-forward, older
|
|
3216
|
+
* MCP builds) are unaffected: quote an inbound message, send the image
|
|
3217
|
+
* uncompressed as a document, and clear the ⏳ ack left on the inbound
|
|
3218
|
+
* message once the send lands.
|
|
3219
|
+
*/
|
|
3220
|
+
opts) {
|
|
3221
|
+
await this.whatsAppManagerFor(jid, accountId).sendMessage(jid, text, imagePath, opts);
|
|
2880
3222
|
}
|
|
2881
3223
|
startSlackOutbound() {
|
|
2882
3224
|
if (!this.agentConfig.slack?.botToken || !this.agentConfig.slack?.signingSecret)
|
|
@@ -2891,6 +3233,21 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2891
3233
|
stopSlackOutbound() {
|
|
2892
3234
|
this.slackOutbound = null;
|
|
2893
3235
|
}
|
|
3236
|
+
startWhatsAppCloudOutbound() {
|
|
3237
|
+
const cfg = this.agentConfig.whatsapp_cloud;
|
|
3238
|
+
if (!cfg?.accessToken || !cfg?.phoneNumberId || !cfg?.appSecret || !cfg?.verifyToken)
|
|
3239
|
+
return;
|
|
3240
|
+
if (this.whatsAppCloudOutbound)
|
|
3241
|
+
return; // already running
|
|
3242
|
+
this.whatsAppCloudOutbound = new whatsapp_cloud_client_1.WhatsAppCloudClient({
|
|
3243
|
+
accessToken: cfg.accessToken,
|
|
3244
|
+
phoneNumberId: cfg.phoneNumberId,
|
|
3245
|
+
logDir: this.gatewayConfig.gateway.logDir,
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
stopWhatsAppCloudOutbound() {
|
|
3249
|
+
this.whatsAppCloudOutbound = null;
|
|
3250
|
+
}
|
|
2894
3251
|
startLineReply() {
|
|
2895
3252
|
const lineThreshold = this.agentConfig.line?.slowResponseThreshold ?? 45;
|
|
2896
3253
|
if (!this.agentConfig.line?.channelSecret || !this.agentConfig.line?.channelAccessToken || lineThreshold <= 0)
|
|
@@ -2987,6 +3344,16 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2987
3344
|
// teardown rather than serially — both are bounded, neither depends on the
|
|
2988
3345
|
// other, and shutdown latency is user-visible.
|
|
2989
3346
|
const receiversStopped = [this.receiver?.stop(), this.discordReceiver?.stop()];
|
|
3347
|
+
// stop(), not unlink() — gateway shutdown should NOT wipe a linked
|
|
3348
|
+
// session; resumeIfLinked() picks each account back up on next boot.
|
|
3349
|
+
// Synchronous (no socket-close promise to await), unlike the receivers above.
|
|
3350
|
+
for (const manager of this.whatsappAccounts.values())
|
|
3351
|
+
manager.stop();
|
|
3352
|
+
this.whatsappAccounts.clear();
|
|
3353
|
+
this.whatsappAccountForChat.clear();
|
|
3354
|
+
// Force syncWhatsAppAccounts() to rebuild from scratch if this runner is
|
|
3355
|
+
// started again, rather than short-circuiting on a stale snapshot.
|
|
3356
|
+
this.whatsappConfigSnapshot = undefined;
|
|
2990
3357
|
this.stopLineReply();
|
|
2991
3358
|
await Promise.all([
|
|
2992
3359
|
...receiversStopped,
|
|
@@ -3090,6 +3457,8 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3090
3457
|
// Refs built from staging paths must follow the files to their promoted
|
|
3091
3458
|
// location — see remapImageParamsRefs (#74).
|
|
3092
3459
|
const imageParams = AgentRunner.remapImageParamsRefs(opts.imageParams, opts.mediaFiles, finalMediaFiles);
|
|
3460
|
+
// The image-to-video source frame follows the same staging→promoted move.
|
|
3461
|
+
const videoParams = AgentRunner.remapVideoParamsRefs(opts.videoParams, opts.mediaFiles, finalMediaFiles);
|
|
3093
3462
|
// Resolve media files to absolute paths for file-path based image passing
|
|
3094
3463
|
// (same pattern as Telegram — Claude Code reads files via Read tool instead of base64 inline)
|
|
3095
3464
|
const imagePaths = finalMediaFiles?.length ? this.resolveMediaPaths(finalMediaFiles) : [];
|
|
@@ -3145,6 +3514,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3145
3514
|
// Build channel XML with image_path attribute (like Telegram) for first image
|
|
3146
3515
|
const imageAttr = effectiveImagePaths.length ? ` image_path="${AgentRunner.escapeXmlAttr(effectiveImagePaths[0])}"` : '';
|
|
3147
3516
|
const imageParamsNote = imageParams ? AgentRunner.buildImageParamsNote(imageParams) : '';
|
|
3517
|
+
const videoParamsNote = videoParams ? AgentRunner.buildVideoParamsNote(videoParams) : '';
|
|
3148
3518
|
// Persist the composer image options to session meta so the web can restore the
|
|
3149
3519
|
// selection on reload (SessionMeta.imageConfig). Only when the send carries them
|
|
3150
3520
|
// (the web sends image_params on first-set/change), so this holds the latest.
|
|
@@ -3156,10 +3526,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3156
3526
|
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { imageConfig: durableImageConfig }, 'api')
|
|
3157
3527
|
.catch(() => { });
|
|
3158
3528
|
}
|
|
3529
|
+
// Persist composer video options the same way (SessionMeta.videoConfig).
|
|
3530
|
+
if (videoParams) {
|
|
3531
|
+
this.sessionStore
|
|
3532
|
+
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { videoConfig: videoParams }, 'api')
|
|
3533
|
+
.catch(() => { });
|
|
3534
|
+
}
|
|
3159
3535
|
const channelXml = `<channel source="api" chat_id="${chatId}" session_id="${sessionId}" ts="${new Date().toISOString()}"${imageAttr}>\n` +
|
|
3160
3536
|
`${message}\n\n` +
|
|
3161
3537
|
`${systemNote}` +
|
|
3162
3538
|
`${imageParamsNote}` +
|
|
3539
|
+
`${videoParamsNote}` +
|
|
3163
3540
|
`</channel>` +
|
|
3164
3541
|
(skillInvocation ? `\n${(0, skills_1.formatSkillContext)(skillInvocation)}` : '');
|
|
3165
3542
|
return new Promise((resolve, reject) => {
|
|
@@ -3362,6 +3739,8 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3362
3739
|
// Refs built from staging paths must follow the files to their promoted
|
|
3363
3740
|
// location — see remapImageParamsRefs (#74).
|
|
3364
3741
|
const imageParamsStream = AgentRunner.remapImageParamsRefs(opts.imageParams, opts.mediaFiles, finalMediaFilesStream);
|
|
3742
|
+
// The image-to-video source frame follows the same staging→promoted move.
|
|
3743
|
+
const videoParamsStream = AgentRunner.remapVideoParamsRefs(opts.videoParams, opts.mediaFiles, finalMediaFilesStream);
|
|
3365
3744
|
// Resolve media files to absolute paths for file-path based image passing
|
|
3366
3745
|
const imagePathsStream = finalMediaFilesStream?.length ? this.resolveMediaPaths(finalMediaFilesStream) : [];
|
|
3367
3746
|
if (!opts.skipUserMessage) {
|
|
@@ -3625,6 +4004,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3625
4004
|
// Build channel XML with image_path attribute (like Telegram) for first image
|
|
3626
4005
|
const imageAttrStream = effectiveImagePathsStream.length ? ` image_path="${AgentRunner.escapeXmlAttr(effectiveImagePathsStream[0])}"` : '';
|
|
3627
4006
|
const imageParamsNoteStream = imageParamsStream ? AgentRunner.buildImageParamsNote(imageParamsStream) : '';
|
|
4007
|
+
const videoParamsNoteStream = videoParamsStream ? AgentRunner.buildVideoParamsNote(videoParamsStream) : '';
|
|
3628
4008
|
// Persist composer image config to session meta (SessionMeta.imageConfig) so the
|
|
3629
4009
|
// web restores the selection on reload. This is the streaming path the web uses.
|
|
3630
4010
|
// image_refs are per-turn and deliberately excluded (#73).
|
|
@@ -3637,10 +4017,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3637
4017
|
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { imageConfig: durableImageConfigStream }, 'api')
|
|
3638
4018
|
.catch(() => { });
|
|
3639
4019
|
}
|
|
4020
|
+
// Persist composer video options the same way (SessionMeta.videoConfig).
|
|
4021
|
+
if (videoParamsStream) {
|
|
4022
|
+
this.sessionStore
|
|
4023
|
+
.updateSessionMeta(this.agentConfig.id, chatId, sessionId, { videoConfig: videoParamsStream }, 'api')
|
|
4024
|
+
.catch(() => { });
|
|
4025
|
+
}
|
|
3640
4026
|
const channelXml = `<channel source="api" chat_id="${chatId}" session_id="${sessionId}" ts="${new Date().toISOString()}"${imageAttrStream}>\n` +
|
|
3641
4027
|
`${message}\n\n` +
|
|
3642
4028
|
systemNote +
|
|
3643
4029
|
imageParamsNoteStream +
|
|
4030
|
+
videoParamsNoteStream +
|
|
3644
4031
|
`</channel>` +
|
|
3645
4032
|
(skillInvocationStream ? `\n${(0, skills_1.formatSkillContext)(skillInvocationStream)}` : '');
|
|
3646
4033
|
session.setProcessing(true);
|