@rubytech/create-maxy-code 0.1.281 → 0.1.282
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/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +2 -1
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +154 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +24 -0
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +174 -0
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +22 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +378 -56
- package/payload/platform/plugins/cloudflare/references/api.md +35 -1
- package/payload/platform/plugins/cloudflare/references/d1-data-capture.md +2 -0
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js +25 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts +7 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js +9 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +6 -2
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
- package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -0
- package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/whatsapp/PLUGIN.md +7 -11
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +10 -56
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +26 -2
- package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +9 -14
- package/payload/platform/scripts/setup-account.sh +7 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -2
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +63 -3
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +14 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +5 -3
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/server.js +55 -236
package/payload/server/server.js
CHANGED
|
@@ -1513,24 +1513,14 @@ function getLanIp() {
|
|
|
1513
1513
|
// app/lib/whatsapp/schema.ts
|
|
1514
1514
|
import { z } from "zod";
|
|
1515
1515
|
var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
1516
|
-
var GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
1517
1516
|
var WhatsAppAccountSchema = z.object({
|
|
1518
1517
|
name: z.string().optional().describe("Display name for this WhatsApp account (e.g. 'Main', 'Sales')."),
|
|
1519
1518
|
enabled: z.boolean().optional().describe("If disabled, this account won't connect on startup."),
|
|
1520
1519
|
dmPolicy: DmPolicySchema.optional().describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
|
|
1521
|
-
groupPolicy: GroupPolicySchema.optional().describe("Whether your agent responds in WhatsApp group chats. 'Open' enables group responses (subject to per-group activation). 'Allowlist' restricts to approved senders. 'Disabled' ignores all group messages."),
|
|
1522
1520
|
selfChatMode: z.boolean().optional().describe("Enable if the bot uses the owner's personal WhatsApp number (same phone for admin and bot). Changes how self-messages are routed."),
|
|
1523
1521
|
adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
|
|
1524
1522
|
allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
|
|
1525
|
-
groupAllowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to trigger the agent in groups when group policy is 'allowlist'."),
|
|
1526
1523
|
sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
|
|
1527
|
-
groups: z.record(
|
|
1528
|
-
z.string(),
|
|
1529
|
-
z.object({
|
|
1530
|
-
activation: z.enum(["always", "mention", "off"]).optional(),
|
|
1531
|
-
publicAgent: z.string().optional().describe("Per-group public-agent slug override. When set, inbound messages in this group route to this agent instead of the per-account or top-level publicAgent.")
|
|
1532
|
-
}).strict().optional()
|
|
1533
|
-
).optional().describe("Per-group settings: 'activation' controls when the agent responds ('always', 'mention', 'off'); 'publicAgent' optionally overrides the public-agent slug for this group (precedence: group \u2192 account \u2192 top-level)."),
|
|
1534
1524
|
ackReaction: z.object({
|
|
1535
1525
|
emoji: z.string().optional(),
|
|
1536
1526
|
direct: z.boolean().optional().default(true),
|
|
@@ -1552,10 +1542,8 @@ var WhatsAppAccountSchema = z.object({
|
|
|
1552
1542
|
var WhatsAppConfigSchema = z.object({
|
|
1553
1543
|
accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional().describe("Per-account config keyed by account ID."),
|
|
1554
1544
|
dmPolicy: DmPolicySchema.optional().default("disabled").describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
|
|
1555
|
-
groupPolicy: GroupPolicySchema.optional().default("disabled").describe("Whether your agent responds in WhatsApp group chats. 'Open' enables group responses (subject to per-group activation). 'Allowlist' restricts to approved senders. 'Disabled' ignores all group messages."),
|
|
1556
1545
|
adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
|
|
1557
1546
|
allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
|
|
1558
|
-
groupAllowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to trigger the agent in groups when group policy is 'allowlist'."),
|
|
1559
1547
|
sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
|
|
1560
1548
|
mediaMaxMb: z.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
|
|
1561
1549
|
textChunkLimit: z.number().int().positive().optional().default(4e3).describe("Maximum characters per outbound message. Longer replies are split into multiple messages."),
|
|
@@ -2321,10 +2309,8 @@ function extractMessage(msg) {
|
|
|
2321
2309
|
function resolveAccountConfig(config, accountConfig) {
|
|
2322
2310
|
return {
|
|
2323
2311
|
dmPolicy: accountConfig?.dmPolicy ?? config.dmPolicy ?? "disabled",
|
|
2324
|
-
groupPolicy: accountConfig?.groupPolicy ?? config.groupPolicy ?? "disabled",
|
|
2325
2312
|
adminPhones: accountConfig?.adminPhones ?? config.adminPhones ?? [],
|
|
2326
2313
|
allowFrom: accountConfig?.allowFrom ?? config.allowFrom ?? [],
|
|
2327
|
-
groupAllowFrom: accountConfig?.groupAllowFrom ?? config.groupAllowFrom ?? [],
|
|
2328
2314
|
selfChatMode: accountConfig?.selfChatMode ?? false
|
|
2329
2315
|
};
|
|
2330
2316
|
}
|
|
@@ -2361,36 +2347,8 @@ function checkDmAccess(params) {
|
|
|
2361
2347
|
return { allowed: false, reason: "unknown-dm-policy", agentType: "public" };
|
|
2362
2348
|
}
|
|
2363
2349
|
}
|
|
2364
|
-
function checkGroupAccess(
|
|
2365
|
-
|
|
2366
|
-
const cfg = resolveAccountConfig(params.config, params.accountConfig);
|
|
2367
|
-
if (phonesMatch(senderPhone, selfPhone)) {
|
|
2368
|
-
return { allowed: true, reason: "self-phone-group", agentType: "admin" };
|
|
2369
|
-
}
|
|
2370
|
-
switch (cfg.groupPolicy) {
|
|
2371
|
-
case "open": {
|
|
2372
|
-
const groupConfig = params.accountConfig?.groups?.[groupJid];
|
|
2373
|
-
const activation = groupConfig?.activation ?? "mention";
|
|
2374
|
-
if (activation === "off") {
|
|
2375
|
-
return { allowed: false, reason: "group-activation-off", agentType: "public" };
|
|
2376
|
-
}
|
|
2377
|
-
if (activation === "mention" && !isMentioned) {
|
|
2378
|
-
return { allowed: false, reason: "mention-gating", agentType: "public" };
|
|
2379
|
-
}
|
|
2380
|
-
return { allowed: true, reason: "group-policy-open", agentType: "public" };
|
|
2381
|
-
}
|
|
2382
|
-
case "allowlist": {
|
|
2383
|
-
const inList = cfg.groupAllowFrom.some((entry) => phonesMatch(entry, senderPhone)) || cfg.adminPhones.some((entry) => phonesMatch(entry, senderPhone));
|
|
2384
|
-
if (!inList) {
|
|
2385
|
-
return { allowed: false, reason: "not-in-group-allowlist", agentType: "public" };
|
|
2386
|
-
}
|
|
2387
|
-
return { allowed: true, reason: "group-allowlist-match", agentType: "public" };
|
|
2388
|
-
}
|
|
2389
|
-
case "disabled":
|
|
2390
|
-
return { allowed: false, reason: "group-policy-disabled", agentType: "public" };
|
|
2391
|
-
default:
|
|
2392
|
-
return { allowed: false, reason: "unknown-group-policy", agentType: "public" };
|
|
2393
|
-
}
|
|
2350
|
+
function checkGroupAccess(_params) {
|
|
2351
|
+
return { allowed: false, reason: "group-observe-only", agentType: "public" };
|
|
2394
2352
|
}
|
|
2395
2353
|
|
|
2396
2354
|
// app/lib/whatsapp/inbound/dedupe.ts
|
|
@@ -3275,11 +3233,20 @@ async function transcribe(audioPath, mimetype) {
|
|
|
3275
3233
|
}
|
|
3276
3234
|
}
|
|
3277
3235
|
|
|
3236
|
+
// app/lib/whatsapp/reply-error.ts
|
|
3237
|
+
var WaReplyError = class extends Error {
|
|
3238
|
+
terminal;
|
|
3239
|
+
constructor(message, terminal) {
|
|
3240
|
+
super(message);
|
|
3241
|
+
this.name = "WaReplyError";
|
|
3242
|
+
this.terminal = terminal;
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
|
|
3278
3246
|
// app/lib/whatsapp/manager.ts
|
|
3279
3247
|
var TAG12 = "[whatsapp:manager]";
|
|
3280
3248
|
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
3281
3249
|
var MESSAGE_STORE_MAX = 500;
|
|
3282
|
-
var GROUP_META_TTL = 5 * 60 * 1e3;
|
|
3283
3250
|
var connections = /* @__PURE__ */ new Map();
|
|
3284
3251
|
var configDir = null;
|
|
3285
3252
|
var whatsAppConfig = {};
|
|
@@ -3375,8 +3342,7 @@ async function startConnection(accountId) {
|
|
|
3375
3342
|
reconnectAttempts: 0,
|
|
3376
3343
|
abortController: new AbortController(),
|
|
3377
3344
|
debouncer: null,
|
|
3378
|
-
lidMapping: null
|
|
3379
|
-
groupMetaCache: /* @__PURE__ */ new Map()
|
|
3345
|
+
lidMapping: null
|
|
3380
3346
|
};
|
|
3381
3347
|
connections.set(accountId, conn);
|
|
3382
3348
|
await connectWithReconnect(conn);
|
|
@@ -3412,7 +3378,8 @@ function getStatus() {
|
|
|
3412
3378
|
reconnectAttempts: conn.reconnectAttempts,
|
|
3413
3379
|
lastConnectedAt: conn.lastConnectedAt,
|
|
3414
3380
|
lastError: conn.lastError,
|
|
3415
|
-
sessionStuckReason: conn.sessionStuckReason
|
|
3381
|
+
sessionStuckReason: conn.sessionStuckReason,
|
|
3382
|
+
terminalReason: conn.terminalReason
|
|
3416
3383
|
}));
|
|
3417
3384
|
}
|
|
3418
3385
|
function getSocket(accountId) {
|
|
@@ -3456,8 +3423,7 @@ async function registerLoginSocket(accountId, sock, authDir) {
|
|
|
3456
3423
|
lastConnectedAt: Date.now(),
|
|
3457
3424
|
abortController: new AbortController(),
|
|
3458
3425
|
debouncer: null,
|
|
3459
|
-
lidMapping
|
|
3460
|
-
groupMetaCache: /* @__PURE__ */ new Map()
|
|
3426
|
+
lidMapping
|
|
3461
3427
|
};
|
|
3462
3428
|
connections.set(accountId, conn);
|
|
3463
3429
|
try {
|
|
@@ -3570,6 +3536,7 @@ async function connectWithReconnect(conn) {
|
|
|
3570
3536
|
conn.selfLid = selfId.lid;
|
|
3571
3537
|
conn.lastConnectedAt = connectedAt;
|
|
3572
3538
|
conn.lastError = void 0;
|
|
3539
|
+
conn.terminalReason = void 0;
|
|
3573
3540
|
const rawLidMapping = sock.signalRepository?.lidMapping ?? null;
|
|
3574
3541
|
conn.lidMapping = withSelfLidShortcut(rawLidMapping, selfId.lid, selfId.jid);
|
|
3575
3542
|
if (selfId.lid) {
|
|
@@ -3612,6 +3579,7 @@ async function connectWithReconnect(conn) {
|
|
|
3612
3579
|
conn.lastError = classification.message;
|
|
3613
3580
|
console.error(`${TAG12} disconnect account=${conn.accountId}: ${classification.kind} \u2014 ${classification.message}`);
|
|
3614
3581
|
if (!classification.shouldRetry) {
|
|
3582
|
+
conn.terminalReason = classification.message;
|
|
3615
3583
|
console.error(`${TAG12} terminal disconnect for account=${conn.accountId}, stopping reconnection`);
|
|
3616
3584
|
return;
|
|
3617
3585
|
}
|
|
@@ -3687,37 +3655,6 @@ function watchForDisconnect(conn) {
|
|
|
3687
3655
|
}
|
|
3688
3656
|
});
|
|
3689
3657
|
}
|
|
3690
|
-
async function getGroupMeta(conn, jid) {
|
|
3691
|
-
const cached3 = conn.groupMetaCache.get(jid);
|
|
3692
|
-
if (cached3 && cached3.expires > Date.now()) return cached3;
|
|
3693
|
-
if (!conn.sock) return null;
|
|
3694
|
-
console.error(`${TAG12} group metadata cache miss for ${jid}, fetching from Baileys account=${conn.accountId}`);
|
|
3695
|
-
try {
|
|
3696
|
-
const meta = await conn.sock.groupMetadata(jid);
|
|
3697
|
-
const participants = await Promise.all(
|
|
3698
|
-
(meta.participants ?? []).map(async (p) => {
|
|
3699
|
-
return await resolveJidToE164(p.id, conn.lidMapping) ?? p.id;
|
|
3700
|
-
})
|
|
3701
|
-
);
|
|
3702
|
-
const entry = {
|
|
3703
|
-
subject: meta.subject,
|
|
3704
|
-
participants,
|
|
3705
|
-
expires: Date.now() + GROUP_META_TTL
|
|
3706
|
-
};
|
|
3707
|
-
conn.groupMetaCache.set(jid, entry);
|
|
3708
|
-
console.error(
|
|
3709
|
-
`${TAG12} group metadata cached for ${jid}: "${meta.subject}", ${participants.length} participants, expires in ${GROUP_META_TTL}ms account=${conn.accountId}`
|
|
3710
|
-
);
|
|
3711
|
-
return entry;
|
|
3712
|
-
} catch (err) {
|
|
3713
|
-
console.error(
|
|
3714
|
-
`${TAG12} group metadata fetch failed for ${jid}: ${err instanceof Error ? err.message : String(err)}, caching empty entry for ${GROUP_META_TTL}ms account=${conn.accountId}`
|
|
3715
|
-
);
|
|
3716
|
-
const emptyEntry = { expires: Date.now() + GROUP_META_TTL };
|
|
3717
|
-
conn.groupMetaCache.set(jid, emptyEntry);
|
|
3718
|
-
return null;
|
|
3719
|
-
}
|
|
3720
|
-
}
|
|
3721
3658
|
function monitorInbound(conn) {
|
|
3722
3659
|
if (!conn.sock || !onInboundMessage) return;
|
|
3723
3660
|
const sock = conn.sock;
|
|
@@ -3941,24 +3878,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
3941
3878
|
}
|
|
3942
3879
|
let accessResult;
|
|
3943
3880
|
if (isGroup) {
|
|
3944
|
-
accessResult = checkGroupAccess({
|
|
3945
|
-
senderPhone,
|
|
3946
|
-
selfPhone,
|
|
3947
|
-
groupJid: remoteJid,
|
|
3948
|
-
// Three-way mention recognition (Sub-fix 4): legacy phone-JID equality,
|
|
3949
|
-
// legacy E.164-via-jidToE164 equality, plus the new self-LID equality so
|
|
3950
|
-
// mentions in LID-addressed groups (deliver as `<self-lid>@lid`) match.
|
|
3951
|
-
// Pre-LID groups continue matching the first two — additive, never blocks.
|
|
3952
|
-
// Each clause guards against null on its right operand; a creds-parse
|
|
3953
|
-
// failure leaves all three of selfJid/selfLid/selfPhone null and
|
|
3954
|
-
// jidToE164 of a malformed mention also returns null — without the
|
|
3955
|
-
// guards, `null === null` would spuriously match every nullified field.
|
|
3956
|
-
isMentioned: extracted.mentionedJids.some(
|
|
3957
|
-
(jid) => conn.selfJid !== null && jid === conn.selfJid || conn.selfLid !== null && jid === conn.selfLid || conn.selfPhone !== null && jidToE164(jid) === conn.selfPhone
|
|
3958
|
-
),
|
|
3959
|
-
config: whatsAppConfig,
|
|
3960
|
-
accountConfig: whatsAppConfig.accounts?.[conn.accountId]
|
|
3961
|
-
});
|
|
3881
|
+
accessResult = checkGroupAccess({ groupJid: remoteJid });
|
|
3962
3882
|
} else {
|
|
3963
3883
|
accessResult = checkDmAccess({
|
|
3964
3884
|
senderPhone,
|
|
@@ -3971,18 +3891,20 @@ async function handleInboundMessage(conn, msg) {
|
|
|
3971
3891
|
`${TAG12} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
|
|
3972
3892
|
);
|
|
3973
3893
|
if (!accessResult.allowed) return;
|
|
3974
|
-
let groupSubject;
|
|
3975
|
-
if (isGroup) {
|
|
3976
|
-
const groupMeta = await getGroupMeta(conn, remoteJid);
|
|
3977
|
-
groupSubject = groupMeta?.subject;
|
|
3978
|
-
}
|
|
3979
3894
|
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
3980
3895
|
if (sendReceipts && msg.key.id) {
|
|
3981
3896
|
sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0);
|
|
3982
3897
|
}
|
|
3983
3898
|
const reply = async (text) => {
|
|
3899
|
+
if (isGroupJid(remoteJid)) {
|
|
3900
|
+
console.error(`${TAG12} op=group-reply-blocked jid=${remoteJid}`);
|
|
3901
|
+
throw new WaReplyError("group reply blocked \u2014 observe-only", false);
|
|
3902
|
+
}
|
|
3984
3903
|
const currentSock = conn.sock;
|
|
3985
|
-
if (!currentSock)
|
|
3904
|
+
if (!currentSock) {
|
|
3905
|
+
const reason = conn.terminalReason;
|
|
3906
|
+
throw new WaReplyError(reason ?? "WhatsApp disconnected \u2014 cannot reply", Boolean(reason));
|
|
3907
|
+
}
|
|
3986
3908
|
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
3987
3909
|
};
|
|
3988
3910
|
const composing = async () => {
|
|
@@ -4004,7 +3926,6 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4004
3926
|
text: extracted.text,
|
|
4005
3927
|
isGroup,
|
|
4006
3928
|
groupJid: isGroup ? remoteJid : void 0,
|
|
4007
|
-
groupSubject,
|
|
4008
3929
|
reply,
|
|
4009
3930
|
composing,
|
|
4010
3931
|
cacheKey,
|
|
@@ -4383,13 +4304,15 @@ app.get("/", async (c) => {
|
|
|
4383
4304
|
selfPhone: a.selfPhone ?? null,
|
|
4384
4305
|
reconnectAttempts: a.reconnectAttempts,
|
|
4385
4306
|
lastError: a.lastError ?? null,
|
|
4386
|
-
sessionStuckReason: a.sessionStuckReason ?? null
|
|
4307
|
+
sessionStuckReason: a.sessionStuckReason ?? null,
|
|
4308
|
+
terminalReason: a.terminalReason ?? null
|
|
4387
4309
|
}));
|
|
4388
4310
|
} catch (err) {
|
|
4389
4311
|
console.error(`[health] failed to read WhatsApp status: ${err instanceof Error ? err.message : String(err)}`);
|
|
4390
4312
|
}
|
|
4391
4313
|
const whatsappAnyConnected = whatsappAccounts.some((a) => a.connected);
|
|
4392
4314
|
const whatsappAnyStuck = whatsappAccounts.some((a) => Boolean(a.sessionStuckReason));
|
|
4315
|
+
const whatsappAnyTerminal = whatsappAccounts.some((a) => Boolean(a.terminalReason));
|
|
4393
4316
|
const account = resolveAccount();
|
|
4394
4317
|
const missingPlugins = findMissingPlugins(account?.config.enabledPlugins);
|
|
4395
4318
|
return c.json({
|
|
@@ -4404,6 +4327,7 @@ app.get("/", async (c) => {
|
|
|
4404
4327
|
whatsapp: {
|
|
4405
4328
|
any_connected: whatsappAnyConnected,
|
|
4406
4329
|
any_stuck: whatsappAnyStuck,
|
|
4330
|
+
any_terminal: whatsappAnyTerminal,
|
|
4407
4331
|
accounts: whatsappAccounts
|
|
4408
4332
|
}
|
|
4409
4333
|
});
|
|
@@ -4677,7 +4601,10 @@ async function managerRcSpawn(opts) {
|
|
|
4677
4601
|
sliceToken: opts.sliceToken,
|
|
4678
4602
|
personId: opts.personId,
|
|
4679
4603
|
userId: opts.userId,
|
|
4680
|
-
waChannel: opts.waChannel
|
|
4604
|
+
waChannel: opts.waChannel,
|
|
4605
|
+
role: opts.role,
|
|
4606
|
+
channel: opts.channel,
|
|
4607
|
+
senderId: opts.senderId
|
|
4681
4608
|
})
|
|
4682
4609
|
}).catch((err) => ({ __throw: err instanceof Error ? err.message : String(err) }));
|
|
4683
4610
|
if ("__throw" in res) {
|
|
@@ -5286,6 +5213,13 @@ async function ensureEntry(input) {
|
|
|
5286
5213
|
spawned = await managerRcSpawn({
|
|
5287
5214
|
sessionId: adminSessionId,
|
|
5288
5215
|
name: nameValue ?? void 0,
|
|
5216
|
+
// Task 724 — pass the real channel + role + senderId so the manager writes
|
|
5217
|
+
// the classification sidecar for this admin channel (webchat, email, …),
|
|
5218
|
+
// not just WhatsApp. Without these the live session was left sidecar-less
|
|
5219
|
+
// and the sidebar channel badge / archive classification mis-read it.
|
|
5220
|
+
role: "admin",
|
|
5221
|
+
channel: input.channel,
|
|
5222
|
+
senderId: input.senderId,
|
|
5289
5223
|
logContext: `channel=${input.channel} senderId=${input.senderId}`
|
|
5290
5224
|
});
|
|
5291
5225
|
} else {
|
|
@@ -5987,16 +5921,6 @@ function resolvePublicAgent(accountDir, opts) {
|
|
|
5987
5921
|
const config = readConfig(accountDir);
|
|
5988
5922
|
const wa = config.whatsapp;
|
|
5989
5923
|
if (!wa) return null;
|
|
5990
|
-
if (opts.groupJid) {
|
|
5991
|
-
const accounts2 = wa.accounts;
|
|
5992
|
-
const account2 = accounts2?.[opts.accountId];
|
|
5993
|
-
const groups = account2?.groups;
|
|
5994
|
-
const group = groups?.[opts.groupJid];
|
|
5995
|
-
const groupSlug = group?.publicAgent;
|
|
5996
|
-
if (typeof groupSlug === "string" && groupSlug.trim()) {
|
|
5997
|
-
return { slug: groupSlug.trim(), source: "group" };
|
|
5998
|
-
}
|
|
5999
|
-
}
|
|
6000
5924
|
const accounts = wa.accounts;
|
|
6001
5925
|
const account = accounts?.[opts.accountId];
|
|
6002
5926
|
const accountSlug = account?.publicAgent;
|
|
@@ -6012,77 +5936,6 @@ function resolvePublicAgent(accountDir, opts) {
|
|
|
6012
5936
|
return null;
|
|
6013
5937
|
}
|
|
6014
5938
|
}
|
|
6015
|
-
function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
|
|
6016
|
-
const trimmedSlug = slug.trim();
|
|
6017
|
-
const trimmedGroup = groupJid.trim();
|
|
6018
|
-
const trimmedAccount = accountId.trim();
|
|
6019
|
-
if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
|
|
6020
|
-
if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
|
|
6021
|
-
if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
|
|
6022
|
-
const agentConfigPath = join4(accountDir, "agents", trimmedSlug, "config.json");
|
|
6023
|
-
if (!existsSync4(agentConfigPath)) {
|
|
6024
|
-
return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
6025
|
-
}
|
|
6026
|
-
try {
|
|
6027
|
-
const config = readConfig(accountDir);
|
|
6028
|
-
if (!config.whatsapp || typeof config.whatsapp !== "object") config.whatsapp = {};
|
|
6029
|
-
const wa = config.whatsapp;
|
|
6030
|
-
if (!wa.accounts || typeof wa.accounts !== "object") wa.accounts = {};
|
|
6031
|
-
const accounts = wa.accounts;
|
|
6032
|
-
if (!accounts[trimmedAccount] || typeof accounts[trimmedAccount] !== "object") accounts[trimmedAccount] = {};
|
|
6033
|
-
const account = accounts[trimmedAccount];
|
|
6034
|
-
if (!account.groups || typeof account.groups !== "object") account.groups = {};
|
|
6035
|
-
const groups = account.groups;
|
|
6036
|
-
const existing = groups[trimmedGroup] && typeof groups[trimmedGroup] === "object" ? groups[trimmedGroup] : {};
|
|
6037
|
-
groups[trimmedGroup] = { ...existing, publicAgent: trimmedSlug };
|
|
6038
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
6039
|
-
if (!parsed.success) {
|
|
6040
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
6041
|
-
return { ok: false, error: `Validation failed: ${msg}` };
|
|
6042
|
-
}
|
|
6043
|
-
config.whatsapp = parsed.data;
|
|
6044
|
-
writeConfig(accountDir, config);
|
|
6045
|
-
console.error(`${TAG17} setGroupPublicAgent account=${trimmedAccount} groupJid=${trimmedGroup} slug=${trimmedSlug}`);
|
|
6046
|
-
reloadManagerConfig(accountDir);
|
|
6047
|
-
return { ok: true, message: `Per-group public agent set to "${trimmedSlug}" for group ${trimmedGroup}.` };
|
|
6048
|
-
} catch (err) {
|
|
6049
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6050
|
-
console.error(`${TAG17} setGroupPublicAgent failed: ${msg}`);
|
|
6051
|
-
return { ok: false, error: msg };
|
|
6052
|
-
}
|
|
6053
|
-
}
|
|
6054
|
-
function unsetGroupPublicAgent(accountDir, accountId, groupJid) {
|
|
6055
|
-
const trimmedGroup = groupJid.trim();
|
|
6056
|
-
const trimmedAccount = accountId.trim();
|
|
6057
|
-
if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
|
|
6058
|
-
if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
|
|
6059
|
-
try {
|
|
6060
|
-
const config = readConfig(accountDir);
|
|
6061
|
-
const wa = config.whatsapp ?? {};
|
|
6062
|
-
const accounts = wa.accounts;
|
|
6063
|
-
const account = accounts?.[trimmedAccount];
|
|
6064
|
-
const groups = account?.groups;
|
|
6065
|
-
const group = groups?.[trimmedGroup];
|
|
6066
|
-
if (!group || typeof group.publicAgent !== "string") {
|
|
6067
|
-
return { ok: true, message: `No per-group publicAgent override for group ${trimmedGroup}; nothing to remove.` };
|
|
6068
|
-
}
|
|
6069
|
-
delete group.publicAgent;
|
|
6070
|
-
const parsed = WhatsAppConfigSchema.safeParse(wa);
|
|
6071
|
-
if (!parsed.success) {
|
|
6072
|
-
const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
6073
|
-
return { ok: false, error: `Validation failed: ${msg}` };
|
|
6074
|
-
}
|
|
6075
|
-
config.whatsapp = parsed.data;
|
|
6076
|
-
writeConfig(accountDir, config);
|
|
6077
|
-
console.error(`${TAG17} unsetGroupPublicAgent account=${trimmedAccount} groupJid=${trimmedGroup}`);
|
|
6078
|
-
reloadManagerConfig(accountDir);
|
|
6079
|
-
return { ok: true, message: `Per-group public agent override removed for group ${trimmedGroup}.` };
|
|
6080
|
-
} catch (err) {
|
|
6081
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
6082
|
-
console.error(`${TAG17} unsetGroupPublicAgent failed: ${msg}`);
|
|
6083
|
-
return { ok: false, error: msg };
|
|
6084
|
-
}
|
|
6085
|
-
}
|
|
6086
5939
|
function updateConfig(accountDir, fields) {
|
|
6087
5940
|
const fieldNames = Object.keys(fields);
|
|
6088
5941
|
if (fieldNames.length === 0) {
|
|
@@ -6615,29 +6468,10 @@ app3.post("/reconnect", async (c) => {
|
|
|
6615
6468
|
return c.json({ error: String(err) }, 500);
|
|
6616
6469
|
}
|
|
6617
6470
|
});
|
|
6618
|
-
app3.post("/send", async (c) => {
|
|
6619
|
-
try {
|
|
6620
|
-
const body = await c.req.json();
|
|
6621
|
-
const { to, text } = body;
|
|
6622
|
-
const accountId = validateAccountId(body.accountId);
|
|
6623
|
-
if (!to || !text) {
|
|
6624
|
-
return c.json({ error: "Missing required fields: to, text" }, 400);
|
|
6625
|
-
}
|
|
6626
|
-
const sock = getSocket(accountId);
|
|
6627
|
-
if (!sock) {
|
|
6628
|
-
return c.json({ error: `WhatsApp account "${accountId}" is not connected` }, 503);
|
|
6629
|
-
}
|
|
6630
|
-
const result = await sendTextMessage(sock, to, text, { accountId });
|
|
6631
|
-
return c.json(result);
|
|
6632
|
-
} catch (err) {
|
|
6633
|
-
console.error(`${TAG20} send error: ${String(err)}`);
|
|
6634
|
-
return c.json({ error: String(err) }, 500);
|
|
6635
|
-
}
|
|
6636
|
-
});
|
|
6637
6471
|
app3.post("/config", async (c) => {
|
|
6638
6472
|
try {
|
|
6639
6473
|
const body = await c.req.json().catch(() => ({}));
|
|
6640
|
-
const { action, phone, slug,
|
|
6474
|
+
const { action, phone, slug, fields, accountId } = body;
|
|
6641
6475
|
if (!action || typeof action !== "string") {
|
|
6642
6476
|
return c.json({ ok: false, error: 'Missing required field "action".' }, 400);
|
|
6643
6477
|
}
|
|
@@ -6677,32 +6511,10 @@ app3.post("/config", async (c) => {
|
|
|
6677
6511
|
}
|
|
6678
6512
|
case "get-public-agent": {
|
|
6679
6513
|
const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
|
|
6680
|
-
const
|
|
6681
|
-
|
|
6682
|
-
console.error(`${TAG20} config action=get-public-agent accountId=${targetAccount} groupJid=${targetGroup ?? "none"} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
|
|
6514
|
+
const resolved = resolvePublicAgent(account.accountDir, { accountId: targetAccount });
|
|
6515
|
+
console.error(`${TAG20} config action=get-public-agent accountId=${targetAccount} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
|
|
6683
6516
|
return c.json({ ok: true, slug: resolved?.slug ?? null, source: resolved?.source ?? null });
|
|
6684
6517
|
}
|
|
6685
|
-
case "set-group-public-agent": {
|
|
6686
|
-
if (!slug || typeof slug !== "string") {
|
|
6687
|
-
return c.json({ ok: false, error: 'Missing required field "slug" (the public-agent directory name, e.g. "my-agent").' }, 400);
|
|
6688
|
-
}
|
|
6689
|
-
if (!groupJid || typeof groupJid !== "string") {
|
|
6690
|
-
return c.json({ ok: false, error: 'Missing required field "groupJid" (group JID ending in @g.us).' }, 400);
|
|
6691
|
-
}
|
|
6692
|
-
const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
|
|
6693
|
-
const result = setGroupPublicAgent(account.accountDir, targetAccount, groupJid, slug);
|
|
6694
|
-
console.error(`${TAG20} config action=set-group-public-agent accountId=${targetAccount} groupJid=${groupJid} slug=${slug} ok=${result.ok}`);
|
|
6695
|
-
return c.json(result, result.ok ? 200 : 400);
|
|
6696
|
-
}
|
|
6697
|
-
case "unset-group-public-agent": {
|
|
6698
|
-
if (!groupJid || typeof groupJid !== "string") {
|
|
6699
|
-
return c.json({ ok: false, error: 'Missing required field "groupJid" (group JID ending in @g.us).' }, 400);
|
|
6700
|
-
}
|
|
6701
|
-
const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
|
|
6702
|
-
const result = unsetGroupPublicAgent(account.accountDir, targetAccount, groupJid);
|
|
6703
|
-
console.error(`${TAG20} config action=unset-group-public-agent accountId=${targetAccount} groupJid=${groupJid} ok=${result.ok}`);
|
|
6704
|
-
return c.json(result, result.ok ? 200 : 400);
|
|
6705
|
-
}
|
|
6706
6518
|
case "list-public-agents": {
|
|
6707
6519
|
const agentsDir = resolve7(account.accountDir, "agents");
|
|
6708
6520
|
const agents = [];
|
|
@@ -6769,7 +6581,7 @@ app3.post("/config", async (c) => {
|
|
|
6769
6581
|
}
|
|
6770
6582
|
default:
|
|
6771
6583
|
return c.json(
|
|
6772
|
-
{ ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, set-public-agent, get-public-agent,
|
|
6584
|
+
{ ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, set-public-agent, get-public-agent, list-public-agents, update-config, get-config, schema, list-groups.` },
|
|
6773
6585
|
400
|
|
6774
6586
|
);
|
|
6775
6587
|
}
|
|
@@ -15830,10 +15642,12 @@ function createWaChannelRoutes(deps) {
|
|
|
15830
15642
|
try {
|
|
15831
15643
|
await deps.sendOutbound(senderId, text);
|
|
15832
15644
|
} catch (err) {
|
|
15645
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15646
|
+
const terminal = err instanceof WaReplyError ? err.terminal : false;
|
|
15833
15647
|
console.error(
|
|
15834
|
-
`[whatsapp-native] op=reply-failed senderId=${senderId} bytes=${bytes} error=${
|
|
15648
|
+
`[whatsapp-native] op=reply-failed senderId=${senderId} bytes=${bytes} terminal=${terminal} error=${message}`
|
|
15835
15649
|
);
|
|
15836
|
-
return c.json({ error:
|
|
15650
|
+
return c.json({ error: message, terminal }, 500);
|
|
15837
15651
|
}
|
|
15838
15652
|
console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} bytes=${bytes}`);
|
|
15839
15653
|
return c.json({ ok: true });
|
|
@@ -16353,6 +16167,11 @@ var waGateway = new WaGateway({
|
|
|
16353
16167
|
const result = await managerRcSpawn({
|
|
16354
16168
|
sessionId: adminSessionIdFor(accountId, senderId),
|
|
16355
16169
|
userId,
|
|
16170
|
+
// Task 724 — supply the channel so /rc-spawn writes the classification
|
|
16171
|
+
// sidecar from the body, the same path every other admin channel uses.
|
|
16172
|
+
// senderId for the sidecar continues to come from the waChannel binding.
|
|
16173
|
+
role: "admin",
|
|
16174
|
+
channel: "whatsapp",
|
|
16356
16175
|
waChannel: { senderId, gatewayUrl, serverPath },
|
|
16357
16176
|
logContext: `channel=whatsapp-native senderId=${senderId}`
|
|
16358
16177
|
});
|