@rubytech/create-maxy-code 0.1.429 → 0.1.432
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/skills/platform-architecture/SKILL.md +69 -3
- package/payload/platform/plugins/docs/PLUGIN.md +1 -0
- package/payload/platform/plugins/docs/references/internals.md +2 -2
- package/payload/platform/plugins/docs/references/workflows.md +61 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.d.ts +8 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.d.ts.map +1 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.js +8 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.js.map +1 -1
- package/payload/platform/plugins/workflows/PLUGIN.md +29 -9
- package/payload/platform/plugins/workflows/mcp/dist/index.js +34 -0
- package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/step-resolver.d.ts +2 -2
- package/payload/platform/plugins/workflows/mcp/dist/lib/step-resolver.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/step-resolver.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.d.ts +25 -0
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.js +14 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.d.ts +25 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js +79 -15
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.d.ts +2 -0
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.js +4 -0
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.d.ts +3 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.js +8 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.js.map +1 -1
- package/payload/platform/plugins/workflows/skills/workflow-manager/SKILL.md +24 -3
- package/payload/platform/services/telegram-channel/dist/instructions.js +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +10 -5
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.js +9 -6
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +15 -5
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +12 -10
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +12 -11
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.js +12 -1
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +11 -6
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +11 -8
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +57 -14
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +26 -2
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +50 -2
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-C4AZT7cQ.js → AdminLoginScreens-BM45lL5G.js} +1 -1
- package/payload/server/public/assets/{admin-SaF1F4e-.js → admin-B8Eb9Wbj.js} +1 -1
- package/payload/server/public/assets/{audio-attachment-mime-BBW07eyG.js → audio-attachment-mime-Cx5d3Jls.js} +1 -1
- package/payload/server/public/assets/{chat-B9Qjt7C9.js → chat-LEEUAJOG.js} +1 -1
- package/payload/server/public/assets/data-CKojMQSz.js +1 -0
- package/payload/server/public/assets/{operator-CTJjI8te.js → operator-OwY_sWTH.js} +1 -1
- package/payload/server/public/assets/{page-CbI1wtef.js → page-B7nCws9K.js} +1 -1
- package/payload/server/public/assets/{page-JzxyFGle.js → page-C3L6-Ufk.js} +5 -5
- package/payload/server/public/assets/{public-DzeW3_eM.js → public-CSLXSSy5.js} +1 -1
- package/payload/server/public/chat.html +3 -3
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/public/operator.html +4 -4
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +1647 -1470
- package/payload/server/public/assets/data-BKE6Sa-g.js +0 -1
package/payload/server/server.js
CHANGED
|
@@ -1383,9 +1383,9 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
1383
1383
|
};
|
|
1384
1384
|
|
|
1385
1385
|
// server/index.ts
|
|
1386
|
-
import { readFileSync as readFileSync35, existsSync as
|
|
1386
|
+
import { readFileSync as readFileSync35, existsSync as existsSync34, watchFile } from "fs";
|
|
1387
1387
|
import { spawn as spawn3 } from "child_process";
|
|
1388
|
-
import { createHash as
|
|
1388
|
+
import { createHash as createHash7 } from "crypto";
|
|
1389
1389
|
import { resolve as resolve33, join as join35, basename as basename13 } from "path";
|
|
1390
1390
|
import { homedir as homedir3 } from "os";
|
|
1391
1391
|
import { monitorEventLoopDelay } from "perf_hooks";
|
|
@@ -1763,7 +1763,7 @@ async function ensureAuth() {
|
|
|
1763
1763
|
}
|
|
1764
1764
|
|
|
1765
1765
|
// server/routes/health.ts
|
|
1766
|
-
import { existsSync as
|
|
1766
|
+
import { existsSync as existsSync5, readFileSync as readFileSync9 } from "fs";
|
|
1767
1767
|
import { createConnection as createConnection2 } from "net";
|
|
1768
1768
|
|
|
1769
1769
|
// app/lib/network.ts
|
|
@@ -3494,7 +3494,7 @@ function readAllConversations(platformAccountId) {
|
|
|
3494
3494
|
var TAG7 = "[whatsapp-persist]";
|
|
3495
3495
|
var sessionWriteLocks = /* @__PURE__ */ new Map();
|
|
3496
3496
|
async function persistWhatsAppMessage(input) {
|
|
3497
|
-
if (!input.body || !input.msgKeyId) return null;
|
|
3497
|
+
if (!input.body && !input.attachmentId || !input.msgKeyId) return null;
|
|
3498
3498
|
const messageId = `whatsapp-live:${input.accountId}:${input.remoteJid}:${input.msgKeyId}`;
|
|
3499
3499
|
const canonicalRemoteJid = input.canonicalRemoteJid;
|
|
3500
3500
|
const keyResolved = canonicalRemoteJid !== input.remoteJid ? "lid2pn" : "none";
|
|
@@ -3564,6 +3564,7 @@ async function persistWhatsAppMessage(input) {
|
|
|
3564
3564
|
quotedId: input.quoted?.id ?? null,
|
|
3565
3565
|
quotedSender: input.quoted?.sender ?? null,
|
|
3566
3566
|
scope,
|
|
3567
|
+
...input.attachmentId ? { attachmentId: input.attachmentId } : {},
|
|
3567
3568
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3568
3569
|
};
|
|
3569
3570
|
const altDedupeRemoteJids = canonicalRemoteJid !== input.remoteJid ? [input.remoteJid] : [];
|
|
@@ -3781,554 +3782,945 @@ function isCustomerTurnSuppressed(role, managedService) {
|
|
|
3781
3782
|
}
|
|
3782
3783
|
|
|
3783
3784
|
// app/lib/whatsapp/inbound/media.ts
|
|
3784
|
-
import { randomUUID as
|
|
3785
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
3786
|
-
import { join as
|
|
3785
|
+
import { randomUUID as randomUUID4, createHash } from "crypto";
|
|
3786
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
3787
|
+
import { join as join6 } from "path";
|
|
3787
3788
|
import {
|
|
3788
3789
|
downloadMediaMessage,
|
|
3789
3790
|
downloadContentFromMessage,
|
|
3790
3791
|
normalizeMessageContent as normalizeMessageContent2
|
|
3791
3792
|
} from "@whiskeysockets/baileys";
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
const
|
|
3812
|
-
if (
|
|
3813
|
-
|
|
3814
|
-
if (msg.audioMessage) return { downloadable: msg.audioMessage, mediaType: "audio" };
|
|
3815
|
-
if (msg.documentMessage) return { downloadable: msg.documentMessage, mediaType: "document" };
|
|
3816
|
-
if (msg.stickerMessage) return { downloadable: msg.stickerMessage, mediaType: "sticker" };
|
|
3817
|
-
return void 0;
|
|
3793
|
+
|
|
3794
|
+
// app/lib/attachments.ts
|
|
3795
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3796
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
3797
|
+
import { realpathSync as realpathSync2, existsSync as existsSync4 } from "fs";
|
|
3798
|
+
import { resolve as resolve6, extname } from "path";
|
|
3799
|
+
|
|
3800
|
+
// app/lib/data-path.ts
|
|
3801
|
+
import { realpathSync } from "fs";
|
|
3802
|
+
import { resolve as resolve5, normalize, sep as sep2, relative, basename, join as join5 } from "path";
|
|
3803
|
+
var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "../platform");
|
|
3804
|
+
var DATA_ROOT = resolve5(PLATFORM_ROOT3, "..", "data");
|
|
3805
|
+
var CLAUDE_UPLOADS_ROOT = process.env.CLAUDE_CONFIG_DIR ? resolve5(process.env.CLAUDE_CONFIG_DIR, "uploads") : null;
|
|
3806
|
+
var ACCOUNT_PARTITION_DIRS = /* @__PURE__ */ new Set(["accounts"]);
|
|
3807
|
+
var ACCOUNT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3808
|
+
function crossesForeignAccountPartition(relPath, accountId) {
|
|
3809
|
+
const segs = relPath.split("/").filter(Boolean);
|
|
3810
|
+
if (segs.length < 2) return false;
|
|
3811
|
+
if (!ACCOUNT_PARTITION_DIRS.has(segs[0])) return false;
|
|
3812
|
+
const owner = segs[1];
|
|
3813
|
+
if (!ACCOUNT_UUID_RE.test(owner)) return false;
|
|
3814
|
+
return owner !== accountId;
|
|
3818
3815
|
}
|
|
3819
|
-
|
|
3820
|
-
const
|
|
3821
|
-
|
|
3822
|
-
chunks.push(Buffer.from(chunk));
|
|
3823
|
-
}
|
|
3824
|
-
return Buffer.concat(chunks);
|
|
3816
|
+
function isWithinOwnAccountPartition(relPath, accountId) {
|
|
3817
|
+
const segs = relPath.split("/").filter(Boolean);
|
|
3818
|
+
return segs.length >= 2 && segs[0] === "accounts" && segs[1] === accountId;
|
|
3825
3819
|
}
|
|
3826
|
-
|
|
3827
|
-
const
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3820
|
+
function resolveUnderRoot(raw, rootAbs, rootLabel) {
|
|
3821
|
+
const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
3822
|
+
const absolute = resolve5(rootAbs, cleaned);
|
|
3823
|
+
let rootReal;
|
|
3824
|
+
try {
|
|
3825
|
+
rootReal = realpathSync(rootAbs);
|
|
3826
|
+
} catch (err) {
|
|
3827
|
+
return {
|
|
3828
|
+
ok: false,
|
|
3829
|
+
status: 500,
|
|
3830
|
+
error: `${rootLabel} not accessible: ${err instanceof Error ? err.message : String(err)}`
|
|
3831
|
+
};
|
|
3835
3832
|
}
|
|
3836
|
-
|
|
3833
|
+
let resolvedReal;
|
|
3837
3834
|
try {
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
{
|
|
3843
|
-
|
|
3844
|
-
logger: sock.logger
|
|
3845
|
-
}
|
|
3846
|
-
);
|
|
3847
|
-
if (!buffer || buffer.length === 0) {
|
|
3848
|
-
console.error(`${TAG9} primary download returned empty, trying direct fallback`);
|
|
3849
|
-
const downloadable = getDownloadableContent(content);
|
|
3850
|
-
if (downloadable) {
|
|
3851
|
-
try {
|
|
3852
|
-
const stream2 = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
|
|
3853
|
-
buffer = await streamToBuffer(stream2);
|
|
3854
|
-
} catch (fallbackErr) {
|
|
3855
|
-
console.error(`${TAG9} direct download fallback failed: ${String(fallbackErr)}`);
|
|
3856
|
-
}
|
|
3835
|
+
resolvedReal = realpathSync(absolute);
|
|
3836
|
+
} catch (err) {
|
|
3837
|
+
const code = err.code;
|
|
3838
|
+
if (code === "ENOENT") {
|
|
3839
|
+
if (absolute !== rootReal && !absolute.startsWith(rootReal + sep2)) {
|
|
3840
|
+
return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: absolute };
|
|
3857
3841
|
}
|
|
3842
|
+
return { ok: false, status: 404, error: "Not found" };
|
|
3858
3843
|
}
|
|
3859
|
-
if (!buffer || buffer.length === 0) {
|
|
3860
|
-
console.error(`${TAG9} download failed: empty buffer for ${mimetype ?? "unknown"}`);
|
|
3861
|
-
return void 0;
|
|
3862
|
-
}
|
|
3863
|
-
if (buffer.length > maxBytes) {
|
|
3864
|
-
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
|
|
3865
|
-
const limitMB = (maxBytes / (1024 * 1024)).toFixed(0);
|
|
3866
|
-
console.error(`${TAG9} media too large type=${mimetype ?? "unknown"} size=${sizeMB}MB limit=${limitMB}MB`);
|
|
3867
|
-
return void 0;
|
|
3868
|
-
}
|
|
3869
|
-
await mkdir(MEDIA_DIR, { recursive: true });
|
|
3870
|
-
const ext = mimeToExt(mimetype ?? "application/octet-stream");
|
|
3871
|
-
const filename = `${randomUUID3()}.${ext}`;
|
|
3872
|
-
const filePath = join5(MEDIA_DIR, filename);
|
|
3873
|
-
await writeFile(filePath, buffer);
|
|
3874
|
-
const sizeKB = (buffer.length / 1024).toFixed(0);
|
|
3875
|
-
console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
|
|
3876
3844
|
return {
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3845
|
+
ok: false,
|
|
3846
|
+
status: 500,
|
|
3847
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3880
3848
|
};
|
|
3881
|
-
} catch (err) {
|
|
3882
|
-
console.error(`${TAG9} media download failed type=${mimetype ?? "unknown"} error=${String(err)}`);
|
|
3883
|
-
return void 0;
|
|
3884
3849
|
}
|
|
3850
|
+
if (resolvedReal !== rootReal && !resolvedReal.startsWith(rootReal + sep2)) {
|
|
3851
|
+
return { ok: false, status: 403, error: `Path escapes ${rootLabel}`, resolved: resolvedReal };
|
|
3852
|
+
}
|
|
3853
|
+
const relPath = relative(rootReal, resolvedReal) || ".";
|
|
3854
|
+
return { ok: true, absolute: resolvedReal, dataRootReal: rootReal, relative: relPath };
|
|
3855
|
+
}
|
|
3856
|
+
function resolveDataPath(raw) {
|
|
3857
|
+
return resolveUnderRoot(raw, DATA_ROOT, "DATA_ROOT");
|
|
3858
|
+
}
|
|
3859
|
+
function resolveClaudeUploadsPath(raw) {
|
|
3860
|
+
if (!CLAUDE_UPLOADS_ROOT) {
|
|
3861
|
+
return { ok: false, status: 404, error: "Not found" };
|
|
3862
|
+
}
|
|
3863
|
+
return resolveUnderRoot(raw, CLAUDE_UPLOADS_ROOT, "CLAUDE_UPLOADS_ROOT");
|
|
3864
|
+
}
|
|
3865
|
+
function scratchpadTmpBase() {
|
|
3866
|
+
return process.env.MAXY_SCRATCHPAD_BASE ?? "/tmp";
|
|
3867
|
+
}
|
|
3868
|
+
function sessionScratchpadRoot(projectDir, sessionId) {
|
|
3869
|
+
const uid = typeof process.geteuid === "function" ? process.geteuid() : 0;
|
|
3870
|
+
return join5(scratchpadTmpBase(), `claude-${uid}`, basename(projectDir), sessionId, "scratchpad");
|
|
3871
|
+
}
|
|
3872
|
+
function resolveScratchpadPath(raw, scratchpadRootAbs) {
|
|
3873
|
+
return resolveUnderRoot(raw, scratchpadRootAbs, "scratchpad");
|
|
3885
3874
|
}
|
|
3886
3875
|
|
|
3887
|
-
// app/lib/
|
|
3888
|
-
|
|
3889
|
-
var
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
const
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
const batchSize = batch.entries.length;
|
|
3918
|
-
try {
|
|
3919
|
-
console.error(`${TAG10} debounce flush key=${key} batchSize=${batchSize}`);
|
|
3920
|
-
const result = onFlush(batch.entries);
|
|
3921
|
-
if (result && typeof result.catch === "function") {
|
|
3922
|
-
result.catch(onError);
|
|
3923
|
-
}
|
|
3924
|
-
} catch (err) {
|
|
3925
|
-
onError(err);
|
|
3876
|
+
// app/lib/doc-to-text.ts
|
|
3877
|
+
import { unzipSync, strFromU8 } from "fflate";
|
|
3878
|
+
var DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
3879
|
+
var ODT = "application/vnd.oasis.opendocument.text";
|
|
3880
|
+
var CONVERTIBLE_DOC_MIME = /* @__PURE__ */ new Set([
|
|
3881
|
+
DOCX,
|
|
3882
|
+
ODT,
|
|
3883
|
+
"text/rtf",
|
|
3884
|
+
"application/rtf"
|
|
3885
|
+
]);
|
|
3886
|
+
function bareMime(mimeType) {
|
|
3887
|
+
return mimeType.split(";")[0].trim().toLowerCase();
|
|
3888
|
+
}
|
|
3889
|
+
function unescapeXml(s) {
|
|
3890
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
3891
|
+
}
|
|
3892
|
+
function unzipEntry(buffer, entry) {
|
|
3893
|
+
const files = unzipSync(new Uint8Array(buffer));
|
|
3894
|
+
const bytes = files[entry];
|
|
3895
|
+
if (!bytes) throw new Error(`zip entry not found: ${entry}`);
|
|
3896
|
+
return strFromU8(bytes);
|
|
3897
|
+
}
|
|
3898
|
+
function docxToText(buffer) {
|
|
3899
|
+
const xml = unzipEntry(buffer, "word/document.xml");
|
|
3900
|
+
const paras = [];
|
|
3901
|
+
for (const p of xml.split("</w:p>")) {
|
|
3902
|
+
const withBreaks = p.replace(/<w:tab\b[^>]*\/?>/g, " ").replace(/<w:br\b[^>]*\/?>/g, "\n");
|
|
3903
|
+
let line = "";
|
|
3904
|
+
for (const m of withBreaks.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)) {
|
|
3905
|
+
line += unescapeXml(m[1]);
|
|
3926
3906
|
}
|
|
3907
|
+
if (line.length > 0) paras.push(line);
|
|
3927
3908
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3909
|
+
return paras.join("\n");
|
|
3910
|
+
}
|
|
3911
|
+
function odtToText(buffer) {
|
|
3912
|
+
const xml = unzipEntry(buffer, "content.xml");
|
|
3913
|
+
const body = xml.replace(/^[\s\S]*?<office:text\b[^>]*>/, "").replace(/<\/office:text>[\s\S]*$/, "");
|
|
3914
|
+
const withBreaks = body.replace(/<text:line-break\b[^>]*\/?>/g, "\n").replace(/<text:tab\b[^>]*\/?>/g, " ").replace(/<\/text:(?:p|h)>/g, "\n");
|
|
3915
|
+
const stripped = withBreaks.replace(/<[^>]+>/g, "");
|
|
3916
|
+
return unescapeXml(stripped).replace(/\n{3,}/g, "\n\n").trim();
|
|
3917
|
+
}
|
|
3918
|
+
var RTF_DROP_DEST = /^\\(?:\*|fonttbl|colortbl|stylesheet|listtable|info|pict)\b/;
|
|
3919
|
+
function stripRtfDestinations(s) {
|
|
3920
|
+
let out = "";
|
|
3921
|
+
for (let i = 0; i < s.length; i++) {
|
|
3922
|
+
const ch = s[i];
|
|
3923
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
3924
|
+
out += ch + s[i + 1];
|
|
3925
|
+
i++;
|
|
3926
|
+
continue;
|
|
3927
|
+
}
|
|
3928
|
+
if (ch === "{" && RTF_DROP_DEST.test(s.slice(i + 1))) {
|
|
3929
|
+
let depth = 1;
|
|
3930
|
+
let j = i + 1;
|
|
3931
|
+
for (; j < s.length && depth > 0; j++) {
|
|
3932
|
+
if (s[j] === "\\") {
|
|
3933
|
+
j++;
|
|
3934
|
+
continue;
|
|
3939
3935
|
}
|
|
3940
|
-
|
|
3941
|
-
|
|
3936
|
+
if (s[j] === "{") depth++;
|
|
3937
|
+
else if (s[j] === "}") depth--;
|
|
3942
3938
|
}
|
|
3943
|
-
|
|
3939
|
+
i = j - 1;
|
|
3940
|
+
continue;
|
|
3944
3941
|
}
|
|
3945
|
-
|
|
3946
|
-
if (!key) {
|
|
3947
|
-
try {
|
|
3948
|
-
const result = onFlush([msg]);
|
|
3949
|
-
if (result && typeof result.catch === "function") {
|
|
3950
|
-
result.catch(onError);
|
|
3951
|
-
}
|
|
3952
|
-
} catch (err) {
|
|
3953
|
-
onError(err);
|
|
3954
|
-
}
|
|
3955
|
-
return;
|
|
3956
|
-
}
|
|
3957
|
-
const existing = pending.get(key);
|
|
3958
|
-
if (existing) {
|
|
3959
|
-
existing.entries.push(msg);
|
|
3960
|
-
clearTimeout(existing.timer);
|
|
3961
|
-
existing.timer = setTimeout(() => {
|
|
3962
|
-
flushKey(key).catch(onError);
|
|
3963
|
-
}, debounceMs);
|
|
3964
|
-
} else {
|
|
3965
|
-
const timer2 = setTimeout(() => {
|
|
3966
|
-
flushKey(key).catch(onError);
|
|
3967
|
-
}, debounceMs);
|
|
3968
|
-
pending.set(key, { entries: [msg], timer: timer2 });
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
function registerPending(key, promise) {
|
|
3972
|
-
if (destroyed) return;
|
|
3973
|
-
const existing = pendingAwaits.get(key);
|
|
3974
|
-
if (existing) {
|
|
3975
|
-
pendingAwaits.set(key, Promise.all([existing, promise]).then(() => {
|
|
3976
|
-
}));
|
|
3977
|
-
} else {
|
|
3978
|
-
pendingAwaits.set(key, promise);
|
|
3979
|
-
}
|
|
3980
|
-
console.error(`${STT_TAG} pending key=${key}`);
|
|
3981
|
-
}
|
|
3982
|
-
async function destroy() {
|
|
3983
|
-
if (destroyed) return;
|
|
3984
|
-
destroyed = true;
|
|
3985
|
-
await flush();
|
|
3942
|
+
out += ch;
|
|
3986
3943
|
}
|
|
3987
|
-
return
|
|
3944
|
+
return out;
|
|
3988
3945
|
}
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
return {
|
|
3946
|
+
function rtfToText(rtf) {
|
|
3947
|
+
let s = stripRtfDestinations(rtf);
|
|
3948
|
+
s = s.replace(/\\par[d]?\b/g, "\n").replace(/\\line\b/g, "\n").replace(/\\tab\b/g, " ");
|
|
3949
|
+
s = s.replace(/\\'([0-9a-fA-F]{2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)));
|
|
3950
|
+
s = s.replace(/\\[a-zA-Z]+-?\d* ?/g, "");
|
|
3951
|
+
s = s.replace(/\\([{}\\])/g, "$1").replace(/[{}]/g, "");
|
|
3952
|
+
return s.replace(/[ \t]{2,}/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
3953
|
+
}
|
|
3954
|
+
function docToText(buffer, mimeType) {
|
|
3955
|
+
const mime = bareMime(mimeType);
|
|
3956
|
+
if (mime === DOCX) return docxToText(buffer);
|
|
3957
|
+
if (mime === ODT) return odtToText(buffer);
|
|
3958
|
+
if (mime === "text/rtf" || mime === "application/rtf") return rtfToText(buffer.toString("latin1"));
|
|
3959
|
+
return null;
|
|
3996
3960
|
}
|
|
3997
3961
|
|
|
3998
|
-
// app/lib/
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
};
|
|
4045
|
-
}
|
|
4046
|
-
} else if (isTimeInRange(currentTime, spec.opens, spec.closes)) {
|
|
4047
|
-
const hoursStr = `${spec.opens}-${spec.closes}`;
|
|
4048
|
-
console.error(
|
|
4049
|
-
`${TAG11} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
|
|
4050
|
-
);
|
|
4051
|
-
return {
|
|
4052
|
-
open: true,
|
|
4053
|
-
reason: "within business hours",
|
|
4054
|
-
dayOfWeek,
|
|
4055
|
-
currentTime,
|
|
4056
|
-
matchedHours: hoursStr
|
|
4057
|
-
};
|
|
4058
|
-
}
|
|
4059
|
-
}
|
|
4060
|
-
const todaySpecs = specs.filter((s) => s.day === dayOfWeek);
|
|
4061
|
-
const allHours = todaySpecs.map((s) => `${s.opens}-${s.closes}`).join(", ") || "none today";
|
|
4062
|
-
console.error(
|
|
4063
|
-
`${TAG11} [${accountId}] business hours check: closed (day=${dayOfWeek}, time=${currentTime}, hours=${allHours})`
|
|
4064
|
-
);
|
|
4065
|
-
return {
|
|
4066
|
-
open: false,
|
|
4067
|
-
reason: "outside business hours",
|
|
4068
|
-
dayOfWeek,
|
|
4069
|
-
currentTime,
|
|
4070
|
-
matchedHours: allHours
|
|
4071
|
-
};
|
|
4072
|
-
} finally {
|
|
4073
|
-
await session.close();
|
|
4074
|
-
}
|
|
4075
|
-
} catch (err) {
|
|
4076
|
-
console.error(
|
|
4077
|
-
`${TAG11} [${accountId}] business hours check failed, treating as open: ${err instanceof Error ? err.message : String(err)}`
|
|
3962
|
+
// app/lib/attachments.ts
|
|
3963
|
+
function uploadsDirFor(accountId, subPath, attachmentId) {
|
|
3964
|
+
return subPath ? resolve6(DATA_ROOT, "accounts", accountId, "uploads", subPath, attachmentId) : resolve6(DATA_ROOT, "accounts", accountId, "uploads", attachmentId);
|
|
3965
|
+
}
|
|
3966
|
+
var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
3967
|
+
"image/jpeg",
|
|
3968
|
+
"image/png",
|
|
3969
|
+
"image/gif",
|
|
3970
|
+
"image/webp",
|
|
3971
|
+
"application/pdf",
|
|
3972
|
+
"text/plain",
|
|
3973
|
+
"text/markdown",
|
|
3974
|
+
"text/csv",
|
|
3975
|
+
"text/html",
|
|
3976
|
+
"text/calendar",
|
|
3977
|
+
"application/zip",
|
|
3978
|
+
"application/x-zip-compressed",
|
|
3979
|
+
// Task 1347 — Word/OpenDocument/RTF text documents. The bytes are converted to
|
|
3980
|
+
// a readable-text sibling at ingestion (writeAttachment → doc-to-text.ts)
|
|
3981
|
+
// because the on-device Read tool cannot parse the binary containers. rtf is
|
|
3982
|
+
// reported by browsers as either text/rtf or application/rtf.
|
|
3983
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3984
|
+
"application/vnd.oasis.opendocument.text",
|
|
3985
|
+
"text/rtf",
|
|
3986
|
+
"application/rtf",
|
|
3987
|
+
// Task 1240 — voice recordings persist as playable clips in the admin bubble.
|
|
3988
|
+
// These match AUDIO_MIME_TYPES in app/lib/stt/audio-mime.ts exactly.
|
|
3989
|
+
"audio/ogg",
|
|
3990
|
+
"audio/opus",
|
|
3991
|
+
"audio/webm",
|
|
3992
|
+
"audio/mp4",
|
|
3993
|
+
"audio/x-m4a",
|
|
3994
|
+
"audio/mpeg",
|
|
3995
|
+
"audio/wav",
|
|
3996
|
+
"video/mp4"
|
|
3997
|
+
]);
|
|
3998
|
+
var MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024;
|
|
3999
|
+
var MAX_FILES_PER_MESSAGE = 5;
|
|
4000
|
+
var MAX_DATA_UPLOAD_BYTES = 2 * 1024 * 1024 * 1024;
|
|
4001
|
+
var MAX_ZIP_UNCOMPRESSED_BYTES = 100 * 1024 * 1024;
|
|
4002
|
+
var EXTRACTED_TEXT_SUFFIX = ".extracted.txt";
|
|
4003
|
+
function assertSupportedMime(mimeType) {
|
|
4004
|
+
const bare = mimeType.split(";")[0].trim().toLowerCase();
|
|
4005
|
+
if (!SUPPORTED_MIME_TYPES.has(bare)) {
|
|
4006
|
+
throw new Error(
|
|
4007
|
+
`Unsupported file type: "${mimeType}". Supported types: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
|
|
4078
4008
|
);
|
|
4079
|
-
return { open: true, reason: "hours check failed (treating as open)" };
|
|
4080
4009
|
}
|
|
4081
4010
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4011
|
+
function isSupportedMime(mimeType) {
|
|
4012
|
+
return SUPPORTED_MIME_TYPES.has(mimeType.split(";")[0].trim().toLowerCase());
|
|
4013
|
+
}
|
|
4014
|
+
async function storeMediaBuffer(accountId, attachmentId, filename, mimeType, buffer) {
|
|
4015
|
+
assertSupportedMime(mimeType);
|
|
4016
|
+
if (buffer.byteLength > MAX_FILE_SIZE_BYTES) {
|
|
4017
|
+
throw new Error(
|
|
4018
|
+
`Media exceeds the 50 MB limit (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB).`
|
|
4019
|
+
);
|
|
4090
4020
|
}
|
|
4091
|
-
return
|
|
4021
|
+
return writeAttachment(accountId, "", filename, mimeType, buffer.byteLength, buffer, attachmentId, true);
|
|
4092
4022
|
}
|
|
4093
|
-
function
|
|
4094
|
-
return
|
|
4023
|
+
function attachmentExists(accountId, attachmentId) {
|
|
4024
|
+
return existsSync4(resolve6(uploadsDirFor(accountId, "", attachmentId), `${attachmentId}.meta.json`));
|
|
4095
4025
|
}
|
|
4096
|
-
function
|
|
4097
|
-
const
|
|
4098
|
-
|
|
4026
|
+
async function writeAttachment(accountId, subPath, filename, mimeType, sizeBytes, buffer, explicitId, skipDocToText = false) {
|
|
4027
|
+
const attachmentId = explicitId ?? randomUUID3();
|
|
4028
|
+
const dir = uploadsDirFor(accountId, subPath, attachmentId);
|
|
4029
|
+
await mkdir(dir, { recursive: true });
|
|
4030
|
+
const ext = extname(filename) || "";
|
|
4031
|
+
const storagePath = resolve6(dir, `${attachmentId}${ext}`);
|
|
4032
|
+
const metaPath = resolve6(dir, `${attachmentId}.meta.json`);
|
|
4033
|
+
const meta = {
|
|
4034
|
+
attachmentId,
|
|
4035
|
+
scope: subPath || accountId,
|
|
4036
|
+
filename,
|
|
4037
|
+
mimeType,
|
|
4038
|
+
sizeBytes,
|
|
4039
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4040
|
+
};
|
|
4041
|
+
await writeFile(storagePath, buffer);
|
|
4042
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
4043
|
+
let readableTextPath;
|
|
4044
|
+
if (!skipDocToText && CONVERTIBLE_DOC_MIME.has(mimeType.split(";")[0].trim().toLowerCase())) {
|
|
4045
|
+
const text = docToText(buffer, mimeType);
|
|
4046
|
+
if (text) {
|
|
4047
|
+
readableTextPath = resolve6(dir, `${attachmentId}${EXTRACTED_TEXT_SUFFIX}`);
|
|
4048
|
+
await writeFile(readableTextPath, text);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
return { attachmentId, filename, storagePath, metaPath, mimeType, sizeBytes, readableTextPath };
|
|
4099
4052
|
}
|
|
4100
|
-
function
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
}
|
|
4107
|
-
const
|
|
4108
|
-
|
|
4109
|
-
return `${hour}:${minute}`;
|
|
4053
|
+
async function storeAttachment(accountId, file) {
|
|
4054
|
+
assertSupportedMime(file.type);
|
|
4055
|
+
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
4056
|
+
throw new Error(
|
|
4057
|
+
`File "${file.name}" exceeds the 50 MB limit (${(file.size / 1024 / 1024).toFixed(1)} MB).`
|
|
4058
|
+
);
|
|
4059
|
+
}
|
|
4060
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
4061
|
+
return writeAttachment(accountId, "", file.name, file.type, file.size, buffer);
|
|
4110
4062
|
}
|
|
4111
|
-
function
|
|
4112
|
-
|
|
4113
|
-
|
|
4063
|
+
async function storePublicAttachment(accountId, sessionId, file, buffer) {
|
|
4064
|
+
assertSupportedMime(file.type);
|
|
4065
|
+
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
4066
|
+
throw new Error(
|
|
4067
|
+
`File "${file.name}" exceeds the 50 MB limit (${(file.size / 1024 / 1024).toFixed(1)} MB).`
|
|
4068
|
+
);
|
|
4114
4069
|
}
|
|
4115
|
-
return
|
|
4070
|
+
return writeAttachment(accountId, `public/${sessionId}`, file.name, file.type, file.size, buffer);
|
|
4116
4071
|
}
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4072
|
+
var MIME_BY_EXT = {
|
|
4073
|
+
".pdf": "application/pdf",
|
|
4074
|
+
".png": "image/png",
|
|
4075
|
+
".jpg": "image/jpeg",
|
|
4076
|
+
".jpeg": "image/jpeg",
|
|
4077
|
+
".gif": "image/gif",
|
|
4078
|
+
".webp": "image/webp",
|
|
4079
|
+
".svg": "image/svg+xml",
|
|
4080
|
+
".html": "text/html",
|
|
4081
|
+
".htm": "text/html",
|
|
4082
|
+
".css": "text/css",
|
|
4083
|
+
".js": "application/javascript",
|
|
4084
|
+
".json": "application/json",
|
|
4085
|
+
".csv": "text/csv",
|
|
4086
|
+
".txt": "text/plain",
|
|
4087
|
+
".md": "text/markdown",
|
|
4088
|
+
".xml": "text/xml",
|
|
4089
|
+
".zip": "application/zip",
|
|
4090
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
4091
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
4092
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
4093
|
+
".rtf": "text/rtf",
|
|
4094
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
4095
|
+
".ics": "text/calendar"
|
|
4096
|
+
};
|
|
4097
|
+
function detectMimeType(filePath) {
|
|
4098
|
+
const ext = extname(filePath).toLowerCase();
|
|
4099
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
4134
4100
|
}
|
|
4135
|
-
|
|
4136
|
-
const
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
audioBytes = s.size;
|
|
4141
|
-
} catch {
|
|
4142
|
-
console.error(`${TAG12} failed: file not readable path=${audioPath}`);
|
|
4143
|
-
return void 0;
|
|
4101
|
+
function validateFilePathInAccount(filePath, accountDir) {
|
|
4102
|
+
const resolved = realpathSync2(filePath);
|
|
4103
|
+
const accountResolved = realpathSync2(accountDir);
|
|
4104
|
+
if (!resolved.startsWith(accountResolved + "/")) {
|
|
4105
|
+
throw new Error(`File path is outside the account directory`);
|
|
4144
4106
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4107
|
+
return resolved;
|
|
4108
|
+
}
|
|
4109
|
+
async function storeComponentArtefact(accountId, attachmentId, mimeType, content, filename) {
|
|
4110
|
+
const buffer = Buffer.from(content, "utf-8");
|
|
4111
|
+
if (buffer.byteLength > MAX_FILE_SIZE_BYTES) {
|
|
4112
|
+
throw new Error(
|
|
4113
|
+
`Component artefact exceeds the 50 MB limit (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB).`
|
|
4114
|
+
);
|
|
4115
|
+
}
|
|
4116
|
+
const dir = uploadsDirFor(accountId, "", attachmentId);
|
|
4117
|
+
await mkdir(dir, { recursive: true });
|
|
4118
|
+
const ext = extname(filename) || (mimeType === "text/html" ? ".html" : ".md");
|
|
4119
|
+
const storagePath = resolve6(dir, `${attachmentId}${ext}`);
|
|
4120
|
+
const metaPath = resolve6(dir, `${attachmentId}.meta.json`);
|
|
4121
|
+
const meta = {
|
|
4122
|
+
attachmentId,
|
|
4123
|
+
scope: accountId,
|
|
4124
|
+
filename,
|
|
4125
|
+
mimeType,
|
|
4126
|
+
sizeBytes: buffer.byteLength,
|
|
4127
|
+
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4128
|
+
};
|
|
4129
|
+
await writeFile(storagePath, buffer);
|
|
4130
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
4131
|
+
return { attachmentId, filename, storagePath, metaPath, mimeType, sizeBytes: buffer.byteLength };
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
// app/lib/whatsapp/inbound/media.ts
|
|
4135
|
+
var TAG9 = "[whatsapp:media]";
|
|
4136
|
+
var MEDIA_DIR = "/tmp/maxy-media";
|
|
4137
|
+
function deterministicAttachmentId(seed) {
|
|
4138
|
+
const h = createHash("sha256").update(seed).digest("hex");
|
|
4139
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
4140
|
+
}
|
|
4141
|
+
var CACHE_RETAIN_MS = 5e3;
|
|
4142
|
+
var inflightDownloads = /* @__PURE__ */ new Map();
|
|
4143
|
+
function downloadInboundMediaBufferCached(msg, sock, opts) {
|
|
4144
|
+
const key = msg.key.id ? `${msg.key.remoteJid ?? ""}:${msg.key.id}` : null;
|
|
4145
|
+
if (!key) return downloadInboundMediaBuffer(msg, sock, opts);
|
|
4146
|
+
const existing = inflightDownloads.get(key);
|
|
4147
|
+
if (existing) return existing;
|
|
4148
|
+
const p = downloadInboundMediaBuffer(msg, sock, opts);
|
|
4149
|
+
inflightDownloads.set(key, p);
|
|
4150
|
+
void p.then(
|
|
4151
|
+
(res) => {
|
|
4152
|
+
if (res === void 0) {
|
|
4153
|
+
inflightDownloads.delete(key);
|
|
4154
|
+
} else {
|
|
4155
|
+
const t = setTimeout(() => inflightDownloads.delete(key), CACHE_RETAIN_MS);
|
|
4156
|
+
t.unref?.();
|
|
4157
|
+
}
|
|
4158
|
+
},
|
|
4159
|
+
() => inflightDownloads.delete(key)
|
|
4160
|
+
// downloadInboundMediaBuffer never rejects, but be safe
|
|
4147
4161
|
);
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
}
|
|
4164
|
-
|
|
4165
|
-
|
|
4162
|
+
return p;
|
|
4163
|
+
}
|
|
4164
|
+
function mimeToExt(mimetype) {
|
|
4165
|
+
const map = {
|
|
4166
|
+
"image/jpeg": "jpg",
|
|
4167
|
+
"image/png": "png",
|
|
4168
|
+
"image/webp": "webp",
|
|
4169
|
+
"image/gif": "gif",
|
|
4170
|
+
"audio/ogg; codecs=opus": "ogg",
|
|
4171
|
+
"audio/mpeg": "mp3",
|
|
4172
|
+
"audio/mp4": "m4a",
|
|
4173
|
+
"video/mp4": "mp4",
|
|
4174
|
+
"application/pdf": "pdf",
|
|
4175
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
4176
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx"
|
|
4177
|
+
};
|
|
4178
|
+
return map[mimetype] ?? mimetype.split("/").pop()?.replace(/[^a-z0-9]/gi, "") ?? "bin";
|
|
4179
|
+
}
|
|
4180
|
+
function getDownloadableContent(message) {
|
|
4181
|
+
const msg = message;
|
|
4182
|
+
if (msg.imageMessage) return { downloadable: msg.imageMessage, mediaType: "image" };
|
|
4183
|
+
if (msg.videoMessage) return { downloadable: msg.videoMessage, mediaType: "video" };
|
|
4184
|
+
if (msg.audioMessage) return { downloadable: msg.audioMessage, mediaType: "audio" };
|
|
4185
|
+
if (msg.documentMessage) return { downloadable: msg.documentMessage, mediaType: "document" };
|
|
4186
|
+
if (msg.stickerMessage) return { downloadable: msg.stickerMessage, mediaType: "sticker" };
|
|
4187
|
+
return void 0;
|
|
4188
|
+
}
|
|
4189
|
+
async function streamToBuffer(stream2) {
|
|
4190
|
+
const chunks = [];
|
|
4191
|
+
for await (const chunk of stream2) {
|
|
4192
|
+
chunks.push(Buffer.from(chunk));
|
|
4193
|
+
}
|
|
4194
|
+
return Buffer.concat(chunks);
|
|
4195
|
+
}
|
|
4196
|
+
async function downloadInboundMediaBuffer(msg, sock, opts) {
|
|
4197
|
+
const rawMessage = msg.message;
|
|
4198
|
+
if (!rawMessage) return void 0;
|
|
4199
|
+
const content = normalizeMessageContent2(rawMessage);
|
|
4200
|
+
if (!content) return void 0;
|
|
4201
|
+
const inner = content;
|
|
4202
|
+
const mimetype = inner.imageMessage?.mimetype ?? inner.videoMessage?.mimetype ?? inner.audioMessage?.mimetype ?? inner.documentMessage?.mimetype ?? inner.stickerMessage?.mimetype ?? void 0;
|
|
4203
|
+
if (!inner.imageMessage && !inner.videoMessage && !inner.audioMessage && !inner.documentMessage && !inner.stickerMessage) {
|
|
4166
4204
|
return void 0;
|
|
4167
4205
|
}
|
|
4206
|
+
const maxBytes = opts?.maxBytes ?? 50 * 1024 * 1024;
|
|
4168
4207
|
try {
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4208
|
+
let buffer = await downloadMediaMessage(
|
|
4209
|
+
msg,
|
|
4210
|
+
"buffer",
|
|
4211
|
+
{},
|
|
4212
|
+
{
|
|
4213
|
+
reuploadRequest: sock.updateMediaMessage,
|
|
4214
|
+
logger: sock.logger
|
|
4215
|
+
}
|
|
4216
|
+
);
|
|
4217
|
+
if (!buffer || buffer.length === 0) {
|
|
4218
|
+
console.error(`${TAG9} primary download returned empty, trying direct fallback`);
|
|
4219
|
+
const downloadable = getDownloadableContent(content);
|
|
4220
|
+
if (downloadable) {
|
|
4221
|
+
try {
|
|
4222
|
+
const stream2 = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
|
|
4223
|
+
buffer = await streamToBuffer(stream2);
|
|
4224
|
+
} catch (fallbackErr) {
|
|
4225
|
+
console.error(`${TAG9} direct download fallback failed: ${String(fallbackErr)}`);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
if (!buffer || buffer.length === 0) {
|
|
4230
|
+
console.error(`${TAG9} download failed: empty buffer for ${mimetype ?? "unknown"}`);
|
|
4182
4231
|
return void 0;
|
|
4183
4232
|
}
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
return {
|
|
4233
|
+
if (buffer.length > maxBytes) {
|
|
4234
|
+
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
|
|
4235
|
+
const limitMB = (maxBytes / (1024 * 1024)).toFixed(0);
|
|
4236
|
+
console.error(`${TAG9} media too large type=${mimetype ?? "unknown"} size=${sizeMB}MB limit=${limitMB}MB`);
|
|
4237
|
+
return void 0;
|
|
4238
|
+
}
|
|
4239
|
+
return {
|
|
4240
|
+
buffer,
|
|
4241
|
+
mimetype: mimetype ?? "application/octet-stream",
|
|
4242
|
+
filename: inner.documentMessage?.fileName ?? void 0
|
|
4243
|
+
};
|
|
4191
4244
|
} catch (err) {
|
|
4192
|
-
|
|
4193
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
4194
|
-
console.error(`${TAG12} failed provider=whisper-local duration_ms=${durationMs} error=${reason}`);
|
|
4245
|
+
console.error(`${TAG9} media download failed type=${mimetype ?? "unknown"} error=${String(err)}`);
|
|
4195
4246
|
return void 0;
|
|
4196
|
-
} finally {
|
|
4197
|
-
unlink(wavPath).catch(() => {
|
|
4198
|
-
});
|
|
4199
|
-
}
|
|
4200
|
-
}
|
|
4201
|
-
|
|
4202
|
-
// app/lib/whatsapp/reply-error.ts
|
|
4203
|
-
var WaReplyError = class extends Error {
|
|
4204
|
-
terminal;
|
|
4205
|
-
constructor(message, terminal) {
|
|
4206
|
-
super(message);
|
|
4207
|
-
this.name = "WaReplyError";
|
|
4208
|
-
this.terminal = terminal;
|
|
4209
|
-
}
|
|
4210
|
-
};
|
|
4211
|
-
|
|
4212
|
-
// app/lib/whatsapp/manager.ts
|
|
4213
|
-
var TAG13 = "[whatsapp:manager]";
|
|
4214
|
-
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
4215
|
-
var MESSAGE_STORE_MAX = 500;
|
|
4216
|
-
var connections = /* @__PURE__ */ new Map();
|
|
4217
|
-
var configDir = null;
|
|
4218
|
-
var whatsAppConfig = {};
|
|
4219
|
-
var onInboundMessage = null;
|
|
4220
|
-
var initialized = false;
|
|
4221
|
-
var messageStore = /* @__PURE__ */ new Map();
|
|
4222
|
-
function messageStoreKey(accountId, remoteJid) {
|
|
4223
|
-
return `${accountId}:${remoteJid}`;
|
|
4224
|
-
}
|
|
4225
|
-
function storeMessage(storeKey, entry) {
|
|
4226
|
-
let entries = messageStore.get(storeKey);
|
|
4227
|
-
if (!entries) {
|
|
4228
|
-
entries = [];
|
|
4229
|
-
messageStore.set(storeKey, entries);
|
|
4230
|
-
}
|
|
4231
|
-
entries.push(entry);
|
|
4232
|
-
if (entries.length > MESSAGE_STORE_MAX) {
|
|
4233
|
-
const trimmed = entries.length - MESSAGE_STORE_MAX;
|
|
4234
|
-
entries.splice(0, trimmed);
|
|
4235
|
-
console.error(`${TAG13} message store trimmed for ${storeKey}: ${trimmed} oldest messages removed`);
|
|
4236
4247
|
}
|
|
4237
4248
|
}
|
|
4238
|
-
function
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
}
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4249
|
+
async function downloadInboundMedia(msg, sock, opts) {
|
|
4250
|
+
const res = await downloadInboundMediaBufferCached(msg, sock, opts);
|
|
4251
|
+
if (!res) return void 0;
|
|
4252
|
+
await mkdir2(MEDIA_DIR, { recursive: true });
|
|
4253
|
+
const ext = mimeToExt(res.mimetype);
|
|
4254
|
+
const filePath = join6(MEDIA_DIR, `${randomUUID4()}.${ext}`);
|
|
4255
|
+
await writeFile2(filePath, res.buffer);
|
|
4256
|
+
const sizeKB = (res.buffer.length / 1024).toFixed(0);
|
|
4257
|
+
console.error(`${TAG9} media downloaded type=${res.mimetype} size=${sizeKB}KB path=${filePath}`);
|
|
4258
|
+
return { path: filePath, mimetype: res.mimetype, size: res.buffer.length };
|
|
4259
|
+
}
|
|
4260
|
+
async function storeInboundMediaServable(msg, sock, accountId, seed, opts) {
|
|
4261
|
+
const attachmentId = deterministicAttachmentId(seed);
|
|
4262
|
+
try {
|
|
4263
|
+
if (attachmentExists(accountId, attachmentId)) return attachmentId;
|
|
4264
|
+
const res = await downloadInboundMediaBufferCached(msg, sock, opts);
|
|
4265
|
+
if (!res) {
|
|
4266
|
+
console.error(`${TAG9} servable-store skip reason=no-buffer id=${attachmentId}`);
|
|
4267
|
+
return null;
|
|
4247
4268
|
}
|
|
4248
|
-
|
|
4269
|
+
const filename = res.filename ?? `${attachmentId}.${mimeToExt(res.mimetype)}`;
|
|
4270
|
+
await storeMediaBuffer(accountId, attachmentId, filename, res.mimetype, res.buffer);
|
|
4271
|
+
console.error(`${TAG9} servable-store ok id=${attachmentId} mime=${res.mimetype} bytes=${res.buffer.length}`);
|
|
4272
|
+
return attachmentId;
|
|
4273
|
+
} catch (err) {
|
|
4274
|
+
console.error(`${TAG9} servable-store FAIL id=${attachmentId} reason=${String(err).slice(0, 120)}`);
|
|
4275
|
+
return null;
|
|
4249
4276
|
}
|
|
4250
|
-
return `whatsapp:${input.accountId}:${input.senderPhone}`;
|
|
4251
|
-
}
|
|
4252
|
-
function buildSelfChatDispatch(input) {
|
|
4253
|
-
const senderPhone = input.selfPhone ?? "owner";
|
|
4254
|
-
return {
|
|
4255
|
-
accountId: input.accountId,
|
|
4256
|
-
agentType: "admin",
|
|
4257
|
-
senderPhone,
|
|
4258
|
-
text: input.text,
|
|
4259
|
-
isGroup: false,
|
|
4260
|
-
reply: input.reply,
|
|
4261
|
-
composing: input.composing,
|
|
4262
|
-
cacheKey: deriveCacheKey({
|
|
4263
|
-
agentType: "admin",
|
|
4264
|
-
accountId: input.accountId,
|
|
4265
|
-
senderPhone,
|
|
4266
|
-
isGroup: false
|
|
4267
|
-
}),
|
|
4268
|
-
media: [],
|
|
4269
|
-
// Self-chat is the owner: the session scopes to the house account (Task 1383).
|
|
4270
|
-
effectiveAccountId: input.accountId
|
|
4271
|
-
};
|
|
4272
4277
|
}
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4278
|
+
|
|
4279
|
+
// app/lib/whatsapp/inbound/debounce.ts
|
|
4280
|
+
var TAG10 = "[whatsapp:debounce]";
|
|
4281
|
+
var STT_TAG = "[whatsapp:stt-await]";
|
|
4282
|
+
function createInboundDebouncer(opts) {
|
|
4283
|
+
const { debounceMs, buildKey, onFlush, onError } = opts;
|
|
4284
|
+
const awaitTimeoutMs = opts.awaitTimeoutMs ?? 3e4;
|
|
4285
|
+
const pending = /* @__PURE__ */ new Map();
|
|
4286
|
+
const pendingAwaits = /* @__PURE__ */ new Map();
|
|
4287
|
+
let destroyed = false;
|
|
4288
|
+
async function flushKey(key) {
|
|
4289
|
+
const awaiting = pendingAwaits.get(key);
|
|
4290
|
+
if (awaiting) {
|
|
4291
|
+
const start = Date.now();
|
|
4292
|
+
try {
|
|
4293
|
+
await Promise.race([
|
|
4294
|
+
awaiting,
|
|
4295
|
+
new Promise(
|
|
4296
|
+
(_, reject) => setTimeout(() => reject(new Error("stt-await-timeout")), awaitTimeoutMs)
|
|
4297
|
+
)
|
|
4298
|
+
]);
|
|
4299
|
+
console.error(`${STT_TAG} resolved key=${key} waited_ms=${Date.now() - start}`);
|
|
4300
|
+
} catch {
|
|
4301
|
+
console.error(`${STT_TAG} timeout key=${key} waited_ms=${Date.now() - start}`);
|
|
4302
|
+
}
|
|
4303
|
+
pendingAwaits.delete(key);
|
|
4304
|
+
}
|
|
4305
|
+
const batch = pending.get(key);
|
|
4306
|
+
if (!batch) return;
|
|
4307
|
+
clearTimeout(batch.timer);
|
|
4308
|
+
pending.delete(key);
|
|
4309
|
+
const batchSize = batch.entries.length;
|
|
4310
|
+
try {
|
|
4311
|
+
console.error(`${TAG10} debounce flush key=${key} batchSize=${batchSize}`);
|
|
4312
|
+
const result = onFlush(batch.entries);
|
|
4313
|
+
if (result && typeof result.catch === "function") {
|
|
4314
|
+
result.catch(onError);
|
|
4315
|
+
}
|
|
4316
|
+
} catch (err) {
|
|
4317
|
+
onError(err);
|
|
4318
|
+
}
|
|
4277
4319
|
}
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
const accountIds = listCredentialAccountIds(configDir);
|
|
4282
|
-
if (accountIds.length === 0) {
|
|
4283
|
-
console.error(`${TAG13} init: no stored WhatsApp credentials found`);
|
|
4284
|
-
initialized = true;
|
|
4285
|
-
return;
|
|
4320
|
+
async function flush() {
|
|
4321
|
+
const keys = Array.from(pending.keys());
|
|
4322
|
+
await Promise.all(keys.map((key) => flushKey(key)));
|
|
4286
4323
|
}
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4324
|
+
function enqueue(msg) {
|
|
4325
|
+
if (destroyed) return;
|
|
4326
|
+
if (debounceMs <= 0) {
|
|
4327
|
+
try {
|
|
4328
|
+
const result = onFlush([msg]);
|
|
4329
|
+
if (result && typeof result.catch === "function") {
|
|
4330
|
+
result.catch(onError);
|
|
4331
|
+
}
|
|
4332
|
+
} catch (err) {
|
|
4333
|
+
onError(err);
|
|
4334
|
+
}
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
const key = buildKey(msg);
|
|
4338
|
+
if (!key) {
|
|
4339
|
+
try {
|
|
4340
|
+
const result = onFlush([msg]);
|
|
4341
|
+
if (result && typeof result.catch === "function") {
|
|
4342
|
+
result.catch(onError);
|
|
4343
|
+
}
|
|
4344
|
+
} catch (err) {
|
|
4345
|
+
onError(err);
|
|
4346
|
+
}
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
const existing = pending.get(key);
|
|
4350
|
+
if (existing) {
|
|
4351
|
+
existing.entries.push(msg);
|
|
4352
|
+
clearTimeout(existing.timer);
|
|
4353
|
+
existing.timer = setTimeout(() => {
|
|
4354
|
+
flushKey(key).catch(onError);
|
|
4355
|
+
}, debounceMs);
|
|
4356
|
+
} else {
|
|
4357
|
+
const timer2 = setTimeout(() => {
|
|
4358
|
+
flushKey(key).catch(onError);
|
|
4359
|
+
}, debounceMs);
|
|
4360
|
+
pending.set(key, { entries: [msg], timer: timer2 });
|
|
4294
4361
|
}
|
|
4295
|
-
startConnection(accountId).catch((err) => {
|
|
4296
|
-
console.error(`${TAG13} failed to auto-start account=${accountId}: ${formatError(err)}`);
|
|
4297
|
-
});
|
|
4298
4362
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4363
|
+
function registerPending(key, promise) {
|
|
4364
|
+
if (destroyed) return;
|
|
4365
|
+
const existing = pendingAwaits.get(key);
|
|
4366
|
+
if (existing) {
|
|
4367
|
+
pendingAwaits.set(key, Promise.all([existing, promise]).then(() => {
|
|
4368
|
+
}));
|
|
4369
|
+
} else {
|
|
4370
|
+
pendingAwaits.set(key, promise);
|
|
4371
|
+
}
|
|
4372
|
+
console.error(`${STT_TAG} pending key=${key}`);
|
|
4307
4373
|
}
|
|
4308
|
-
|
|
4374
|
+
async function destroy() {
|
|
4375
|
+
if (destroyed) return;
|
|
4376
|
+
destroyed = true;
|
|
4377
|
+
await flush();
|
|
4378
|
+
}
|
|
4379
|
+
return { enqueue, flush, destroy, registerPending };
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
// app/lib/whatsapp/inbound/combine-batch.ts
|
|
4383
|
+
function combineInboundBatch(entries) {
|
|
4384
|
+
const last = entries[entries.length - 1];
|
|
4385
|
+
const combinedText = entries.map((e) => e.text).filter(Boolean).join("\n");
|
|
4386
|
+
const media = entries.flatMap((e) => e.media);
|
|
4387
|
+
return { ...last, text: combinedText, media };
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
// app/lib/whatsapp/opening-hours.ts
|
|
4391
|
+
var TAG11 = "[whatsapp:hours]";
|
|
4392
|
+
async function isBusinessOpen(accountId) {
|
|
4309
4393
|
try {
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
);
|
|
4394
|
+
const timezone = await resolveTimezone(accountId);
|
|
4395
|
+
const now = /* @__PURE__ */ new Date();
|
|
4396
|
+
const dayOfWeek = getDayOfWeek(now, timezone);
|
|
4397
|
+
const previousDayOfWeek = getPreviousDayOfWeek(now, timezone);
|
|
4398
|
+
const currentTime = getCurrentTime(now, timezone);
|
|
4399
|
+
const session = getSession();
|
|
4400
|
+
try {
|
|
4401
|
+
const result = await session.run(
|
|
4402
|
+
`MATCH (b:LocalBusiness {accountId: $accountId})
|
|
4403
|
+
OPTIONAL MATCH (b)-[:HAS_HOURS]->(hours:OpeningHoursSpecification)
|
|
4404
|
+
WHERE hours.dayOfWeek IN [$dayOfWeek, $previousDayOfWeek]
|
|
4405
|
+
RETURN hours.dayOfWeek AS dayOfWeek, hours.opens AS opens, hours.closes AS closes`,
|
|
4406
|
+
{ accountId, dayOfWeek, previousDayOfWeek }
|
|
4407
|
+
);
|
|
4408
|
+
if (result.records.length === 0) {
|
|
4409
|
+
console.error(`${TAG11} [${accountId}] business hours check: no opening hours configured, treating as open`);
|
|
4410
|
+
return { open: true, reason: "no opening hours configured" };
|
|
4411
|
+
}
|
|
4412
|
+
const specs = result.records.filter((r) => r.get("opens") != null && r.get("closes") != null).map((r) => ({
|
|
4413
|
+
day: String(r.get("dayOfWeek")).trim(),
|
|
4414
|
+
opens: String(r.get("opens")).trim(),
|
|
4415
|
+
closes: String(r.get("closes")).trim()
|
|
4416
|
+
}));
|
|
4417
|
+
if (specs.length === 0) {
|
|
4418
|
+
console.error(`${TAG11} [${accountId}] business hours check: no opening hours configured, treating as open`);
|
|
4419
|
+
return { open: true, reason: "no opening hours configured" };
|
|
4420
|
+
}
|
|
4421
|
+
for (const spec of specs) {
|
|
4422
|
+
if (!spec.opens || !spec.closes || spec.opens === spec.closes) continue;
|
|
4423
|
+
const isYesterday = spec.day === previousDayOfWeek;
|
|
4424
|
+
if (isYesterday) {
|
|
4425
|
+
if (spec.opens > spec.closes && currentTime < spec.closes) {
|
|
4426
|
+
const hoursStr = `${spec.opens}-${spec.closes} (${spec.day})`;
|
|
4427
|
+
console.error(
|
|
4428
|
+
`${TAG11} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
|
|
4429
|
+
);
|
|
4430
|
+
return {
|
|
4431
|
+
open: true,
|
|
4432
|
+
reason: "within business hours",
|
|
4433
|
+
dayOfWeek,
|
|
4434
|
+
currentTime,
|
|
4435
|
+
matchedHours: hoursStr
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
} else if (isTimeInRange(currentTime, spec.opens, spec.closes)) {
|
|
4439
|
+
const hoursStr = `${spec.opens}-${spec.closes}`;
|
|
4440
|
+
console.error(
|
|
4441
|
+
`${TAG11} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
|
|
4442
|
+
);
|
|
4443
|
+
return {
|
|
4444
|
+
open: true,
|
|
4445
|
+
reason: "within business hours",
|
|
4446
|
+
dayOfWeek,
|
|
4447
|
+
currentTime,
|
|
4448
|
+
matchedHours: hoursStr
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
const todaySpecs = specs.filter((s) => s.day === dayOfWeek);
|
|
4453
|
+
const allHours = todaySpecs.map((s) => `${s.opens}-${s.closes}`).join(", ") || "none today";
|
|
4454
|
+
console.error(
|
|
4455
|
+
`${TAG11} [${accountId}] business hours check: closed (day=${dayOfWeek}, time=${currentTime}, hours=${allHours})`
|
|
4456
|
+
);
|
|
4457
|
+
return {
|
|
4458
|
+
open: false,
|
|
4459
|
+
reason: "outside business hours",
|
|
4460
|
+
dayOfWeek,
|
|
4461
|
+
currentTime,
|
|
4462
|
+
matchedHours: allHours
|
|
4463
|
+
};
|
|
4464
|
+
} finally {
|
|
4465
|
+
await session.close();
|
|
4466
|
+
}
|
|
4314
4467
|
} catch (err) {
|
|
4315
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
4316
4468
|
console.error(
|
|
4317
|
-
|
|
4469
|
+
`${TAG11} [${accountId}] business hours check failed, treating as open: ${err instanceof Error ? err.message : String(err)}`
|
|
4318
4470
|
);
|
|
4319
|
-
|
|
4471
|
+
return { open: true, reason: "hours check failed (treating as open)" };
|
|
4320
4472
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4473
|
+
}
|
|
4474
|
+
async function resolveTimezone(accountId) {
|
|
4475
|
+
try {
|
|
4476
|
+
const tz = await getUserTimezone(accountId);
|
|
4477
|
+
if (tz) {
|
|
4478
|
+
Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
4479
|
+
return tz;
|
|
4480
|
+
}
|
|
4481
|
+
} catch {
|
|
4482
|
+
}
|
|
4483
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
4484
|
+
}
|
|
4485
|
+
function getDayOfWeek(date, timezone) {
|
|
4486
|
+
return new Intl.DateTimeFormat("en-US", { weekday: "long", timeZone: timezone }).format(date);
|
|
4487
|
+
}
|
|
4488
|
+
function getPreviousDayOfWeek(date, timezone) {
|
|
4489
|
+
const yesterday = new Date(date.getTime() - 24 * 60 * 60 * 1e3);
|
|
4490
|
+
return getDayOfWeek(yesterday, timezone);
|
|
4491
|
+
}
|
|
4492
|
+
function getCurrentTime(date, timezone) {
|
|
4493
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
4494
|
+
hour: "2-digit",
|
|
4495
|
+
minute: "2-digit",
|
|
4496
|
+
hour12: false,
|
|
4497
|
+
timeZone: timezone
|
|
4498
|
+
}).formatToParts(date);
|
|
4499
|
+
const hour = parts.find((p) => p.type === "hour")?.value ?? "00";
|
|
4500
|
+
const minute = parts.find((p) => p.type === "minute")?.value ?? "00";
|
|
4501
|
+
return `${hour}:${minute}`;
|
|
4502
|
+
}
|
|
4503
|
+
function isTimeInRange(time, opens, closes) {
|
|
4504
|
+
if (opens <= closes) {
|
|
4505
|
+
return time >= opens && time < closes;
|
|
4506
|
+
}
|
|
4507
|
+
return time >= opens || time < closes;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
// app/lib/stt/index.ts
|
|
4511
|
+
import { execFile } from "child_process";
|
|
4512
|
+
import { unlink, stat } from "fs/promises";
|
|
4513
|
+
import { promisify } from "util";
|
|
4514
|
+
var execFileAsync = promisify(execFile);
|
|
4515
|
+
var TAG12 = "[stt]";
|
|
4516
|
+
var WHISPER_BINARY = process.env.WHISPER_BINARY ?? "/opt/whisper.cpp/build/bin/whisper-cli";
|
|
4517
|
+
var WHISPER_MODEL = process.env.WHISPER_MODEL ?? "/opt/whisper.cpp/models/ggml-base.bin";
|
|
4518
|
+
var WHISPER_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
4519
|
+
var SPECIAL_TOKEN_RE = /\[_[A-Z]+(?:_\d+)?_?\]/g;
|
|
4520
|
+
function stripSpecialTokens(input) {
|
|
4521
|
+
const matches = input.match(SPECIAL_TOKEN_RE);
|
|
4522
|
+
const count = matches ? matches.length : 0;
|
|
4523
|
+
if (!count) return { text: input, count: 0 };
|
|
4524
|
+
const text = input.replace(SPECIAL_TOKEN_RE, "").replace(/[ \t]{2,}/g, " ").trim();
|
|
4525
|
+
return { text, count };
|
|
4526
|
+
}
|
|
4527
|
+
async function transcribe(audioPath, mimetype) {
|
|
4528
|
+
const startMs = Date.now();
|
|
4529
|
+
let audioBytes = 0;
|
|
4530
|
+
try {
|
|
4531
|
+
const s = await stat(audioPath);
|
|
4532
|
+
audioBytes = s.size;
|
|
4533
|
+
} catch {
|
|
4534
|
+
console.error(`${TAG12} failed: file not readable path=${audioPath}`);
|
|
4535
|
+
return void 0;
|
|
4536
|
+
}
|
|
4537
|
+
console.error(
|
|
4538
|
+
`${TAG12} start provider=whisper-local audio_bytes=${audioBytes} mimetype=${mimetype} path=${audioPath}`
|
|
4539
|
+
);
|
|
4540
|
+
const wavPath = audioPath.replace(/\.[^.]+$/, "") + ".wav";
|
|
4541
|
+
try {
|
|
4542
|
+
await execFileAsync("ffmpeg", [
|
|
4543
|
+
"-i",
|
|
4544
|
+
audioPath,
|
|
4545
|
+
"-ar",
|
|
4546
|
+
"16000",
|
|
4547
|
+
"-ac",
|
|
4548
|
+
"1",
|
|
4549
|
+
"-c:a",
|
|
4550
|
+
"pcm_s16le",
|
|
4551
|
+
"-y",
|
|
4552
|
+
// overwrite without prompting
|
|
4553
|
+
wavPath
|
|
4554
|
+
], { timeout: 3e4 });
|
|
4555
|
+
} catch (err) {
|
|
4556
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4557
|
+
console.error(`${TAG12} failed: ffmpeg conversion error=${reason}`);
|
|
4558
|
+
return void 0;
|
|
4559
|
+
}
|
|
4560
|
+
try {
|
|
4561
|
+
const { stdout, stderr } = await execFileAsync(WHISPER_BINARY, [
|
|
4562
|
+
"-m",
|
|
4563
|
+
WHISPER_MODEL,
|
|
4564
|
+
"-f",
|
|
4565
|
+
wavPath,
|
|
4566
|
+
"--no-timestamps",
|
|
4567
|
+
"-l",
|
|
4568
|
+
"auto"
|
|
4569
|
+
], { timeout: WHISPER_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 });
|
|
4570
|
+
const durationMs = Date.now() - startMs;
|
|
4571
|
+
const { text, count: stripped } = stripSpecialTokens(stdout.trim());
|
|
4572
|
+
if (!text) {
|
|
4573
|
+
console.error(`${TAG12} failed: whisper returned empty output duration_ms=${durationMs}`);
|
|
4574
|
+
return void 0;
|
|
4575
|
+
}
|
|
4576
|
+
const langMatch = stderr.match(/auto-detected language:\s*(\w+)/);
|
|
4577
|
+
const language = langMatch?.[1] ?? "unknown";
|
|
4578
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
4579
|
+
console.error(
|
|
4580
|
+
`${TAG12} done provider=whisper-local duration_ms=${durationMs} words=${words} stripped=${stripped} lang=${language}`
|
|
4581
|
+
);
|
|
4582
|
+
return { text, language, durationMs };
|
|
4583
|
+
} catch (err) {
|
|
4584
|
+
const durationMs = Date.now() - startMs;
|
|
4585
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4586
|
+
console.error(`${TAG12} failed provider=whisper-local duration_ms=${durationMs} error=${reason}`);
|
|
4587
|
+
return void 0;
|
|
4588
|
+
} finally {
|
|
4589
|
+
unlink(wavPath).catch(() => {
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
// app/lib/whatsapp/reply-error.ts
|
|
4595
|
+
var WaReplyError = class extends Error {
|
|
4596
|
+
terminal;
|
|
4597
|
+
constructor(message, terminal) {
|
|
4598
|
+
super(message);
|
|
4599
|
+
this.name = "WaReplyError";
|
|
4600
|
+
this.terminal = terminal;
|
|
4601
|
+
}
|
|
4602
|
+
};
|
|
4603
|
+
|
|
4604
|
+
// app/lib/whatsapp/manager.ts
|
|
4605
|
+
var TAG13 = "[whatsapp:manager]";
|
|
4606
|
+
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
4607
|
+
var MESSAGE_STORE_MAX = 500;
|
|
4608
|
+
var connections = /* @__PURE__ */ new Map();
|
|
4609
|
+
var configDir = null;
|
|
4610
|
+
var whatsAppConfig = {};
|
|
4611
|
+
var onInboundMessage = null;
|
|
4612
|
+
var initialized = false;
|
|
4613
|
+
var messageStore = /* @__PURE__ */ new Map();
|
|
4614
|
+
function messageStoreKey(accountId, remoteJid) {
|
|
4615
|
+
return `${accountId}:${remoteJid}`;
|
|
4616
|
+
}
|
|
4617
|
+
function storeMessage(storeKey, entry) {
|
|
4618
|
+
let entries = messageStore.get(storeKey);
|
|
4619
|
+
if (!entries) {
|
|
4620
|
+
entries = [];
|
|
4621
|
+
messageStore.set(storeKey, entries);
|
|
4622
|
+
}
|
|
4623
|
+
entries.push(entry);
|
|
4624
|
+
if (entries.length > MESSAGE_STORE_MAX) {
|
|
4625
|
+
const trimmed = entries.length - MESSAGE_STORE_MAX;
|
|
4626
|
+
entries.splice(0, trimmed);
|
|
4627
|
+
console.error(`${TAG13} message store trimmed for ${storeKey}: ${trimmed} oldest messages removed`);
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
function deriveCacheKey(input) {
|
|
4631
|
+
if (input.isOwnerMirror || input.agentType === "admin") {
|
|
4632
|
+
return `whatsapp:${input.accountId}`;
|
|
4633
|
+
}
|
|
4634
|
+
if (input.isGroup) {
|
|
4635
|
+
if (!input.groupJid) {
|
|
4636
|
+
throw new Error(
|
|
4637
|
+
`deriveCacheKey: isGroup=true requires groupJid (accountId=${input.accountId}, senderPhone=${input.senderPhone})`
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4640
|
+
return `whatsapp:${input.accountId}:group:${input.groupJid}`;
|
|
4641
|
+
}
|
|
4642
|
+
return `whatsapp:${input.accountId}:${input.senderPhone}`;
|
|
4643
|
+
}
|
|
4644
|
+
function buildSelfChatDispatch(input) {
|
|
4645
|
+
const senderPhone = input.selfPhone ?? "owner";
|
|
4646
|
+
return {
|
|
4647
|
+
accountId: input.accountId,
|
|
4648
|
+
agentType: "admin",
|
|
4649
|
+
senderPhone,
|
|
4650
|
+
text: input.text,
|
|
4651
|
+
isGroup: false,
|
|
4652
|
+
reply: input.reply,
|
|
4653
|
+
composing: input.composing,
|
|
4654
|
+
cacheKey: deriveCacheKey({
|
|
4655
|
+
agentType: "admin",
|
|
4656
|
+
accountId: input.accountId,
|
|
4657
|
+
senderPhone,
|
|
4658
|
+
isGroup: false
|
|
4659
|
+
}),
|
|
4660
|
+
media: [],
|
|
4661
|
+
// Self-chat is the owner: the session scopes to the house account (Task 1383).
|
|
4662
|
+
effectiveAccountId: input.accountId
|
|
4663
|
+
};
|
|
4664
|
+
}
|
|
4665
|
+
async function init(opts) {
|
|
4666
|
+
if (initialized) {
|
|
4667
|
+
console.error(`${TAG13} already initialized`);
|
|
4668
|
+
return;
|
|
4669
|
+
}
|
|
4670
|
+
configDir = opts.configDir;
|
|
4671
|
+
onInboundMessage = opts.onMessage;
|
|
4672
|
+
loadConfig(opts.accountConfig);
|
|
4673
|
+
const accountIds = listCredentialAccountIds(configDir);
|
|
4674
|
+
if (accountIds.length === 0) {
|
|
4675
|
+
console.error(`${TAG13} init: no stored WhatsApp credentials found`);
|
|
4676
|
+
initialized = true;
|
|
4677
|
+
return;
|
|
4678
|
+
}
|
|
4679
|
+
console.error(`${TAG13} init: found ${accountIds.length} credentialed account(s): ${accountIds.join(", ")}`);
|
|
4680
|
+
initialized = true;
|
|
4681
|
+
for (const accountId of accountIds) {
|
|
4682
|
+
const accountCfg = whatsAppConfig.accounts?.[accountId];
|
|
4683
|
+
if (accountCfg?.enabled === false) {
|
|
4684
|
+
console.error(`${TAG13} skipping disabled account=${accountId}`);
|
|
4685
|
+
continue;
|
|
4686
|
+
}
|
|
4687
|
+
startConnection(accountId).catch((err) => {
|
|
4688
|
+
console.error(`${TAG13} failed to auto-start account=${accountId}: ${formatError(err)}`);
|
|
4689
|
+
});
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
async function startConnection(accountId) {
|
|
4693
|
+
if (!configDir) throw new Error("WhatsApp manager not initialized");
|
|
4694
|
+
const authDir = resolveAuthDir(configDir, accountId);
|
|
4695
|
+
const hasAuth = await authExists(authDir);
|
|
4696
|
+
if (!hasAuth) {
|
|
4697
|
+
console.error(`${TAG13} no credentials for account=${accountId}`);
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
let platformAccountId;
|
|
4701
|
+
try {
|
|
4702
|
+
platformAccountId = resolvePlatformAccountId();
|
|
4703
|
+
console.error(
|
|
4704
|
+
`[whatsapp-persist] resolved-account-id waname=${accountId} uuid=${platformAccountId}`
|
|
4705
|
+
);
|
|
4706
|
+
} catch (err) {
|
|
4707
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4708
|
+
console.error(
|
|
4709
|
+
`[whatsapp-persist] resolved-account-id FAIL waname=${accountId} reason=${reason}`
|
|
4710
|
+
);
|
|
4711
|
+
throw err;
|
|
4712
|
+
}
|
|
4713
|
+
await stopConnection(accountId);
|
|
4714
|
+
const conn = {
|
|
4715
|
+
accountId,
|
|
4716
|
+
platformAccountId,
|
|
4717
|
+
accountName: whatsAppConfig.accounts?.[accountId]?.name,
|
|
4718
|
+
authDir,
|
|
4719
|
+
sock: null,
|
|
4720
|
+
connected: false,
|
|
4721
|
+
selfPhone: null,
|
|
4722
|
+
selfJid: null,
|
|
4723
|
+
selfLid: null,
|
|
4332
4724
|
reconnectAttempts: 0,
|
|
4333
4725
|
abortController: new AbortController(),
|
|
4334
4726
|
debouncer: null,
|
|
@@ -4375,9 +4767,12 @@ function getStatus() {
|
|
|
4375
4767
|
function getSocket(accountId) {
|
|
4376
4768
|
return connections.get(accountId)?.sock ?? null;
|
|
4377
4769
|
}
|
|
4770
|
+
function findConnectionByKey(conns, accountId) {
|
|
4771
|
+
return conns.find((c) => c.accountId === accountId) ?? conns.find((c) => c.platformAccountId === accountId);
|
|
4772
|
+
}
|
|
4378
4773
|
function selectConnection(conns, accountId) {
|
|
4379
4774
|
const presentKeys = conns.map((c) => c.accountId);
|
|
4380
|
-
const conn =
|
|
4775
|
+
const conn = findConnectionByKey(conns, accountId);
|
|
4381
4776
|
if (!conn) return { ok: false, reason: "key-miss", presentKeys };
|
|
4382
4777
|
if (!conn.sock) return { ok: false, reason: "disconnected", presentKeys };
|
|
4383
4778
|
return { ok: true, sock: conn.sock, accountId: conn.accountId };
|
|
@@ -4385,6 +4780,13 @@ function selectConnection(conns, accountId) {
|
|
|
4385
4780
|
function resolveSocket(accountId) {
|
|
4386
4781
|
return selectConnection(Array.from(connections.values()), accountId);
|
|
4387
4782
|
}
|
|
4783
|
+
function selectStoreAccountKey(conns, accountId) {
|
|
4784
|
+
const conn = findConnectionByKey(conns, accountId);
|
|
4785
|
+
return conn ? conn.platformAccountId : accountId;
|
|
4786
|
+
}
|
|
4787
|
+
function resolveReadStoreKey(accountId) {
|
|
4788
|
+
return selectStoreAccountKey(Array.from(connections.values()), accountId);
|
|
4789
|
+
}
|
|
4388
4790
|
function listConnectionPlatformAccountIds() {
|
|
4389
4791
|
return [...connections.values()].map((conn) => conn.platformAccountId);
|
|
4390
4792
|
}
|
|
@@ -4703,21 +5105,24 @@ function monitorInbound(conn) {
|
|
|
4703
5105
|
if (!remoteJid || remoteJid === "status@broadcast") continue;
|
|
4704
5106
|
if (!msg.message) continue;
|
|
4705
5107
|
const extracted = extractMessage(msg);
|
|
4706
|
-
|
|
5108
|
+
const mediaServable = !!(extracted.mediaType && extracted.mimetype && isSupportedMime(extracted.mimetype));
|
|
5109
|
+
if (extracted.text || mediaServable) {
|
|
4707
5110
|
const isGroup = isGroupJid(remoteJid);
|
|
4708
5111
|
const senderJid = isGroup ? msg.key.participant ?? remoteJid : remoteJid;
|
|
4709
5112
|
const senderPhone = await resolveJidToE164(senderJid, conn.lidMapping) ?? senderJid;
|
|
4710
5113
|
const ts = msg.messageTimestamp ? Number(msg.messageTimestamp) : Math.floor(Date.now() / 1e3);
|
|
4711
5114
|
const storeKey = messageStoreKey(conn.platformAccountId, remoteJid);
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
5115
|
+
if (extracted.text) {
|
|
5116
|
+
storeMessage(storeKey, {
|
|
5117
|
+
id: msg.key.id ?? `${remoteJid}-${ts}`,
|
|
5118
|
+
sender: senderPhone,
|
|
5119
|
+
senderName: msg.pushName ?? void 0,
|
|
5120
|
+
body: extracted.text,
|
|
5121
|
+
timestamp: ts,
|
|
5122
|
+
fromMe: Boolean(msg.key.fromMe),
|
|
5123
|
+
quoted: extracted.quotedMessage ? { id: extracted.quotedMessage.id, body: extracted.quotedMessage.text, sender: extracted.quotedMessage.sender } : void 0
|
|
5124
|
+
});
|
|
5125
|
+
}
|
|
4721
5126
|
const entries = messageStore.get(storeKey);
|
|
4722
5127
|
console.error(
|
|
4723
5128
|
`${TAG13} stored message ${msg.key.id ?? "?"} for ${remoteJid} (type: ${upsert.type}, store size: ${entries?.length ?? 0}/${MESSAGE_STORE_MAX}) account=${conn.platformAccountId}`
|
|
@@ -4755,6 +5160,17 @@ function monitorInbound(conn) {
|
|
|
4755
5160
|
remoteJid,
|
|
4756
5161
|
conn.lidMapping
|
|
4757
5162
|
);
|
|
5163
|
+
let mediaAttachmentId;
|
|
5164
|
+
if (mediaServable && !fromMe && conn.sock && upsert.type === "notify") {
|
|
5165
|
+
const maxMb = whatsAppConfig.mediaMaxMb ?? 50;
|
|
5166
|
+
mediaAttachmentId = await storeInboundMediaServable(
|
|
5167
|
+
msg,
|
|
5168
|
+
conn.sock,
|
|
5169
|
+
conn.platformAccountId,
|
|
5170
|
+
`whatsapp-live:${conn.accountId}:${canonicalRemoteJid}:${msg.key.id}`,
|
|
5171
|
+
{ maxBytes: maxMb * 1024 * 1024 }
|
|
5172
|
+
) ?? void 0;
|
|
5173
|
+
}
|
|
4758
5174
|
await persistWhatsAppMessage({
|
|
4759
5175
|
accountId: conn.accountId,
|
|
4760
5176
|
platformAccountId: conn.platformAccountId,
|
|
@@ -4766,7 +5182,13 @@ function monitorInbound(conn) {
|
|
|
4766
5182
|
fromMe,
|
|
4767
5183
|
senderPhone,
|
|
4768
5184
|
selfPhone: conn.selfPhone,
|
|
4769
|
-
|
|
5185
|
+
// A stored media message persists the human caption (or empty) plus
|
|
5186
|
+
// the attachment id — the reader shows the inline media, never the
|
|
5187
|
+
// synthetic `[document: …]` / `[sticker]` line. If the store failed
|
|
5188
|
+
// (no id), fall back to the extracted text so a document/sticker
|
|
5189
|
+
// still shows its placeholder rather than a blank bubble.
|
|
5190
|
+
body: mediaAttachmentId ? extracted.caption ?? "" : extracted.text,
|
|
5191
|
+
attachmentId: mediaAttachmentId,
|
|
4770
5192
|
timestamp: ts,
|
|
4771
5193
|
pushName: msg.pushName ?? void 0,
|
|
4772
5194
|
quoted: extracted.quotedMessage ? {
|
|
@@ -4884,868 +5306,543 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4884
5306
|
agentType: "admin",
|
|
4885
5307
|
accountId: conn.accountId,
|
|
4886
5308
|
senderPhone: senderPhone2,
|
|
4887
|
-
isGroup: isGroup2,
|
|
4888
|
-
groupJid: isGroup2 ? remoteJid : void 0,
|
|
4889
|
-
isOwnerMirror: true
|
|
4890
|
-
}),
|
|
4891
|
-
media: [],
|
|
4892
|
-
isOwnerMirror: true
|
|
4893
|
-
});
|
|
4894
|
-
return;
|
|
4895
|
-
}
|
|
4896
|
-
const extracted = extractMessage(msg);
|
|
4897
|
-
if (!extracted.text && !extracted.mediaType) {
|
|
4898
|
-
console.error(`${TAG13} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
|
|
4899
|
-
return;
|
|
4900
|
-
}
|
|
4901
|
-
let mediaResult;
|
|
4902
|
-
if (extracted.mediaType && conn.sock) {
|
|
4903
|
-
const maxMb = whatsAppConfig.mediaMaxMb ?? 50;
|
|
4904
|
-
mediaResult = await downloadInboundMedia(msg, conn.sock, {
|
|
4905
|
-
maxBytes: maxMb * 1024 * 1024
|
|
4906
|
-
});
|
|
4907
|
-
if (!mediaResult) {
|
|
4908
|
-
console.error(`${TAG13} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
|
|
4909
|
-
}
|
|
4910
|
-
}
|
|
4911
|
-
const isGroup = isGroupJid(remoteJid);
|
|
4912
|
-
const senderJid = isGroup ? msg.key.participant ?? remoteJid : remoteJid;
|
|
4913
|
-
const senderPhone = await resolveJidToE164(senderJid, conn.lidMapping) ?? senderJid;
|
|
4914
|
-
const lidCapture = lidCaptureFor(senderJid, senderPhone);
|
|
4915
|
-
if (lidCapture) recordLidMapping(lidCapture.numericLid, lidCapture.e164);
|
|
4916
|
-
const selfPhone = conn.selfPhone ?? "";
|
|
4917
|
-
if (extracted.mediaType === "audio" && mediaResult?.path) {
|
|
4918
|
-
const conversationKey = isGroup ? remoteJid : senderPhone;
|
|
4919
|
-
const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
|
|
4920
|
-
let resolvePending;
|
|
4921
|
-
const sttPending = new Promise((resolve34) => {
|
|
4922
|
-
resolvePending = resolve34;
|
|
4923
|
-
});
|
|
4924
|
-
if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
|
|
4925
|
-
try {
|
|
4926
|
-
const sttResult = await transcribe(mediaResult.path, mediaResult.mimetype);
|
|
4927
|
-
if (sttResult?.text) {
|
|
4928
|
-
extracted.text = sttResult.text;
|
|
4929
|
-
}
|
|
4930
|
-
} finally {
|
|
4931
|
-
resolvePending();
|
|
4932
|
-
}
|
|
4933
|
-
}
|
|
4934
|
-
let accessResult;
|
|
4935
|
-
if (isGroup) {
|
|
4936
|
-
accessResult = checkGroupAccess({ groupJid: remoteJid });
|
|
4937
|
-
} else {
|
|
4938
|
-
accessResult = await resolveDmAccess({
|
|
4939
|
-
senderPhone,
|
|
4940
|
-
selfPhone,
|
|
4941
|
-
config: whatsAppConfig,
|
|
4942
|
-
accountConfig: whatsAppConfig.accounts?.[conn.accountId],
|
|
4943
|
-
accountId: conn.accountId,
|
|
4944
|
-
isValidAccount: (id) => isValidAccountId(id),
|
|
4945
|
-
resolvePersonByPhone
|
|
4946
|
-
});
|
|
4947
|
-
}
|
|
4948
|
-
console.error(
|
|
4949
|
-
`${TAG13} 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}` : "")
|
|
4950
|
-
);
|
|
4951
|
-
if (!accessResult.allowed && accessResult.reason === "account-manager-unresolved") {
|
|
4952
|
-
console.error(
|
|
4953
|
-
`[whatsapp-native] op=account-manager-reject senderId=${senderPhone} reason=unresolved-effective-account`
|
|
4954
|
-
);
|
|
4955
|
-
}
|
|
4956
|
-
if (accessResult.allowed && accessResult.reason === "account-manager" && accessResult.effectiveAccountId === conn.accountId) {
|
|
4957
|
-
console.error(
|
|
4958
|
-
`[whatsapp-native] op=escalation-tripwire senderId=${senderPhone} reason=account-manager-routed-to-house effectiveAccount=${accessResult.effectiveAccountId}`
|
|
4959
|
-
);
|
|
4960
|
-
return;
|
|
4961
|
-
}
|
|
4962
|
-
if (!accessResult.allowed) return;
|
|
4963
|
-
if (isCustomerTurnSuppressed(accessResult.agentType, isHouseManagedService())) {
|
|
4964
|
-
console.error(
|
|
4965
|
-
`[channel-house] op=inbound from=${senderPhone} accountId=${conn.platformAccountId} autoReply=suppressed`
|
|
4966
|
-
);
|
|
4967
|
-
return;
|
|
4968
|
-
}
|
|
4969
|
-
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
4970
|
-
if (sendReceipts && msg.key.id) {
|
|
4971
|
-
sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0);
|
|
4972
|
-
}
|
|
4973
|
-
const reply = async (text) => {
|
|
4974
|
-
if (isGroupJid(remoteJid)) {
|
|
4975
|
-
console.error(`${TAG13} op=group-reply-blocked jid=${remoteJid}`);
|
|
4976
|
-
throw new WaReplyError("group reply blocked \u2014 observe-only", false);
|
|
4977
|
-
}
|
|
4978
|
-
const currentSock = conn.sock;
|
|
4979
|
-
if (!currentSock) {
|
|
4980
|
-
const reason = conn.terminalReason;
|
|
4981
|
-
throw new WaReplyError(reason ?? "WhatsApp disconnected \u2014 cannot reply", Boolean(reason));
|
|
4982
|
-
}
|
|
4983
|
-
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
4984
|
-
};
|
|
4985
|
-
const composing = async () => {
|
|
4986
|
-
const currentSock = conn.sock;
|
|
4987
|
-
if (currentSock) await sendComposing(currentSock, remoteJid);
|
|
4988
|
-
};
|
|
4989
|
-
const cacheKey = deriveCacheKey({
|
|
4990
|
-
agentType: accessResult.agentType,
|
|
4991
|
-
accountId: conn.accountId,
|
|
4992
|
-
senderPhone,
|
|
4993
|
-
isGroup,
|
|
4994
|
-
groupJid: isGroup ? remoteJid : void 0
|
|
4995
|
-
});
|
|
4996
|
-
conn.lastMessageAt = Date.now();
|
|
4997
|
-
const payload = {
|
|
4998
|
-
accountId: conn.accountId,
|
|
4999
|
-
agentType: accessResult.agentType,
|
|
5000
|
-
passive: accessResult.passive === true,
|
|
5001
|
-
personId: accessResult.personId ?? null,
|
|
5002
|
-
senderPhone,
|
|
5003
|
-
text: extracted.text,
|
|
5004
|
-
isGroup,
|
|
5005
|
-
groupJid: isGroup ? remoteJid : void 0,
|
|
5006
|
-
reply,
|
|
5007
|
-
composing,
|
|
5008
|
-
cacheKey,
|
|
5009
|
-
media: mediaResult?.path ? [{ path: mediaResult.path, type: extracted.mediaType, mimetype: mediaResult.mimetype }] : [],
|
|
5010
|
-
replyContext: extracted.quotedMessage,
|
|
5011
|
-
// Task 1383 — the gate's single resolution feeds the spawn (house or the
|
|
5012
|
-
// bound sub-account); the router never re-resolves.
|
|
5013
|
-
effectiveAccountId: accessResult.effectiveAccountId
|
|
5014
|
-
};
|
|
5015
|
-
if (accessResult.agentType === "public") {
|
|
5016
|
-
const hoursResult = await isBusinessOpen(conn.accountId);
|
|
5017
|
-
if (!hoursResult.open) {
|
|
5018
|
-
console.error(`${TAG13} [${conn.accountId}] dispatch skipped: business closed`);
|
|
5019
|
-
const afterHoursMessage = whatsAppConfig.accounts?.[conn.accountId]?.afterHoursMessage ?? whatsAppConfig.afterHoursMessage;
|
|
5020
|
-
if (afterHoursMessage) {
|
|
5021
|
-
try {
|
|
5022
|
-
await reply(afterHoursMessage);
|
|
5023
|
-
console.error(`${TAG13} [${conn.accountId}] after-hours auto-reply sent to ${remoteJid}`);
|
|
5024
|
-
} catch (err) {
|
|
5025
|
-
console.error(
|
|
5026
|
-
`${TAG13} [${conn.accountId}] after-hours auto-reply failed: ${err instanceof Error ? err.message : String(err)}`
|
|
5027
|
-
);
|
|
5028
|
-
}
|
|
5029
|
-
}
|
|
5030
|
-
return;
|
|
5031
|
-
}
|
|
5032
|
-
}
|
|
5033
|
-
if (conn.debouncer) {
|
|
5034
|
-
conn.debouncer.enqueue(payload);
|
|
5035
|
-
} else {
|
|
5036
|
-
onInboundMessage(payload);
|
|
5037
|
-
}
|
|
5038
|
-
}
|
|
5039
|
-
|
|
5040
|
-
// app/lib/vnc.ts
|
|
5041
|
-
import { spawnSync, execFileSync } from "child_process";
|
|
5042
|
-
import { createConnection } from "net";
|
|
5043
|
-
import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
5044
|
-
import { resolve as resolve5 } from "path";
|
|
5045
|
-
var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
|
|
5046
|
-
var VNC_SCRIPT = resolve5(PLATFORM_ROOT3, "scripts/vnc.sh");
|
|
5047
|
-
var displayMode = process.env.DISPLAY_MODE ?? "virtual";
|
|
5048
|
-
if (displayMode === "native") {
|
|
5049
|
-
console.log(`[vnc] DISPLAY_MODE=native \u2014 local requests use desktop display, remote requests use VNC`);
|
|
5050
|
-
}
|
|
5051
|
-
function resolveBrowserTransport(req, remoteAddress) {
|
|
5052
|
-
if (displayMode !== "native") return "vnc";
|
|
5053
|
-
const xff = req.headers.get("x-forwarded-for") ?? void 0;
|
|
5054
|
-
const client = resolveClientIp(remoteAddress ?? "127.0.0.1", xff);
|
|
5055
|
-
const transport = client.kind !== "external" && !xff ? "native" : "vnc";
|
|
5056
|
-
if (remoteAddress && remoteAddress !== "127.0.0.1" && remoteAddress !== "::1") {
|
|
5057
|
-
const oldClient = resolveClientIp("127.0.0.1", xff);
|
|
5058
|
-
const oldTransport = oldClient.kind !== "external" && !xff ? "native" : "vnc";
|
|
5059
|
-
if (transport !== oldTransport) {
|
|
5060
|
-
console.log(`[resolve-transport] remoteAddress="${remoteAddress}" transport="${transport}" (was: "${oldTransport}" with hardcoded loopback)`);
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
return transport;
|
|
5064
|
-
}
|
|
5065
|
-
var currentCdpDisplay = null;
|
|
5066
|
-
function discoverNativeDisplay() {
|
|
5067
|
-
const fallback = { sessionType: "x11", display: ":0", waylandDisplay: "" };
|
|
5068
|
-
try {
|
|
5069
|
-
const listResult = spawnSync("loginctl", ["list-sessions", "--no-legend"], {
|
|
5070
|
-
timeout: 5e3,
|
|
5071
|
-
encoding: "utf8",
|
|
5072
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5073
|
-
});
|
|
5074
|
-
if (listResult.status !== 0 || !listResult.stdout.trim()) {
|
|
5075
|
-
vncLog("native-display-discovery", { action: "fallback", reason: "no-sessions" });
|
|
5076
|
-
return fallback;
|
|
5077
|
-
}
|
|
5078
|
-
const sessionId = listResult.stdout.trim().split("\n")[0].trim().split(/\s+/)[0];
|
|
5079
|
-
const typeResult = spawnSync("loginctl", ["show-session", sessionId, "-p", "Type", "--value"], {
|
|
5080
|
-
timeout: 5e3,
|
|
5081
|
-
encoding: "utf8",
|
|
5082
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5083
|
-
});
|
|
5084
|
-
const displayResult = spawnSync("loginctl", ["show-session", sessionId, "-p", "Display", "--value"], {
|
|
5085
|
-
timeout: 5e3,
|
|
5086
|
-
encoding: "utf8",
|
|
5087
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5088
|
-
});
|
|
5089
|
-
const sessionType = typeResult.stdout?.trim() || "";
|
|
5090
|
-
const sessionDisplay = displayResult.stdout?.trim() || ":0";
|
|
5091
|
-
if (sessionType === "wayland") {
|
|
5092
|
-
let waylandDisplay = "wayland-0";
|
|
5093
|
-
const leaderResult = spawnSync("loginctl", ["show-session", sessionId, "-p", "Leader", "--value"], {
|
|
5094
|
-
timeout: 5e3,
|
|
5095
|
-
encoding: "utf8",
|
|
5096
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5097
|
-
});
|
|
5098
|
-
const leaderPid = leaderResult.stdout?.trim();
|
|
5099
|
-
if (leaderPid) {
|
|
5100
|
-
try {
|
|
5101
|
-
const environ = readFileSync8(`/proc/${leaderPid}/environ`, "utf8");
|
|
5102
|
-
const match = environ.split("\0").find((e) => e.startsWith("WAYLAND_DISPLAY="));
|
|
5103
|
-
if (match) waylandDisplay = match.split("=")[1];
|
|
5104
|
-
} catch {
|
|
5105
|
-
}
|
|
5106
|
-
}
|
|
5107
|
-
const info2 = { sessionType: "wayland", display: sessionDisplay, waylandDisplay };
|
|
5108
|
-
vncLog("native-display-discovery", {
|
|
5109
|
-
action: "discovered",
|
|
5110
|
-
session_type: "wayland",
|
|
5111
|
-
display: sessionDisplay,
|
|
5112
|
-
wayland_display: waylandDisplay
|
|
5113
|
-
});
|
|
5114
|
-
return info2;
|
|
5115
|
-
}
|
|
5116
|
-
const info = { sessionType: "x11", display: sessionDisplay, waylandDisplay: "" };
|
|
5117
|
-
vncLog("native-display-discovery", {
|
|
5118
|
-
action: "discovered",
|
|
5119
|
-
session_type: sessionType || "unknown (defaulting to x11)",
|
|
5120
|
-
display: sessionDisplay
|
|
5121
|
-
});
|
|
5122
|
-
return info;
|
|
5123
|
-
} catch (err) {
|
|
5124
|
-
vncLog("native-display-discovery", { action: "fallback", reason: "error", err: err.message });
|
|
5125
|
-
return fallback;
|
|
5126
|
-
}
|
|
5127
|
-
}
|
|
5128
|
-
function sleep(ms) {
|
|
5129
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
5130
|
-
}
|
|
5131
|
-
async function waitForPort(port2, timeoutMs = 12e3) {
|
|
5132
|
-
const deadline = Date.now() + timeoutMs;
|
|
5133
|
-
while (Date.now() < deadline) {
|
|
5134
|
-
const ready = await new Promise((res) => {
|
|
5135
|
-
const socket = createConnection(port2, "127.0.0.1");
|
|
5136
|
-
socket.setTimeout(500);
|
|
5137
|
-
socket.once("connect", () => {
|
|
5138
|
-
socket.destroy();
|
|
5139
|
-
res(true);
|
|
5140
|
-
});
|
|
5141
|
-
socket.once("error", () => {
|
|
5142
|
-
socket.destroy();
|
|
5143
|
-
res(false);
|
|
5144
|
-
});
|
|
5145
|
-
socket.once("timeout", () => {
|
|
5146
|
-
socket.destroy();
|
|
5147
|
-
res(false);
|
|
5148
|
-
});
|
|
5149
|
-
});
|
|
5150
|
-
if (ready) return true;
|
|
5151
|
-
await sleep(300);
|
|
5152
|
-
}
|
|
5153
|
-
return false;
|
|
5154
|
-
}
|
|
5155
|
-
function ensureLogDir() {
|
|
5156
|
-
mkdirSync2(LOG_DIR, { recursive: true });
|
|
5157
|
-
}
|
|
5158
|
-
function logPath(name) {
|
|
5159
|
-
return resolve5(LOG_DIR, `${name}.log`);
|
|
5160
|
-
}
|
|
5161
|
-
async function ensureVnc() {
|
|
5162
|
-
if (process.platform !== "linux") {
|
|
5163
|
-
vncLog("ensure-vnc", { action: "skipped", reason: "platform-not-linux", platform: process.platform });
|
|
5164
|
-
return false;
|
|
5165
|
-
}
|
|
5166
|
-
const up = await waitForPort(RFB_PORT, 1e3);
|
|
5167
|
-
if (up) return true;
|
|
5168
|
-
vncLog("ensure-vnc", { action: "restart", reason: "x-down", rfb_port: RFB_PORT });
|
|
5169
|
-
try {
|
|
5170
|
-
execFileSync("bash", [VNC_SCRIPT, "start"], { timeout: 2e4 });
|
|
5171
|
-
} catch (err) {
|
|
5172
|
-
vncLog("ensure-vnc", { action: "restart-failed", err: err.message });
|
|
5173
|
-
return false;
|
|
5309
|
+
isGroup: isGroup2,
|
|
5310
|
+
groupJid: isGroup2 ? remoteJid : void 0,
|
|
5311
|
+
isOwnerMirror: true
|
|
5312
|
+
}),
|
|
5313
|
+
media: [],
|
|
5314
|
+
isOwnerMirror: true
|
|
5315
|
+
});
|
|
5316
|
+
return;
|
|
5174
5317
|
}
|
|
5175
|
-
const
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
async function ensureCdp(transport = "vnc") {
|
|
5180
|
-
if (process.platform !== "linux") {
|
|
5181
|
-
vncLog("ensure-cdp", { action: "skipped", reason: "platform-not-linux", platform: process.platform });
|
|
5182
|
-
return false;
|
|
5318
|
+
const extracted = extractMessage(msg);
|
|
5319
|
+
if (!extracted.text && !extracted.mediaType) {
|
|
5320
|
+
console.error(`${TAG13} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
|
|
5321
|
+
return;
|
|
5183
5322
|
}
|
|
5184
|
-
|
|
5185
|
-
if (
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
to_display: targetSentinel
|
|
5193
|
-
});
|
|
5194
|
-
killChromium();
|
|
5195
|
-
await sleep(500);
|
|
5196
|
-
} else {
|
|
5197
|
-
return true;
|
|
5323
|
+
let mediaResult;
|
|
5324
|
+
if (extracted.mediaType && conn.sock) {
|
|
5325
|
+
const maxMb = whatsAppConfig.mediaMaxMb ?? 50;
|
|
5326
|
+
mediaResult = await downloadInboundMedia(msg, conn.sock, {
|
|
5327
|
+
maxBytes: maxMb * 1024 * 1024
|
|
5328
|
+
});
|
|
5329
|
+
if (!mediaResult) {
|
|
5330
|
+
console.error(`${TAG13} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
|
|
5198
5331
|
}
|
|
5199
5332
|
}
|
|
5200
|
-
const
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5333
|
+
const isGroup = isGroupJid(remoteJid);
|
|
5334
|
+
const senderJid = isGroup ? msg.key.participant ?? remoteJid : remoteJid;
|
|
5335
|
+
const senderPhone = await resolveJidToE164(senderJid, conn.lidMapping) ?? senderJid;
|
|
5336
|
+
const lidCapture = lidCaptureFor(senderJid, senderPhone);
|
|
5337
|
+
if (lidCapture) recordLidMapping(lidCapture.numericLid, lidCapture.e164);
|
|
5338
|
+
const selfPhone = conn.selfPhone ?? "";
|
|
5339
|
+
if (extracted.mediaType === "audio" && mediaResult?.path) {
|
|
5340
|
+
const conversationKey = isGroup ? remoteJid : senderPhone;
|
|
5341
|
+
const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
|
|
5342
|
+
let resolvePending;
|
|
5343
|
+
const sttPending = new Promise((resolve34) => {
|
|
5344
|
+
resolvePending = resolve34;
|
|
5345
|
+
});
|
|
5346
|
+
if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
|
|
5347
|
+
try {
|
|
5348
|
+
const sttResult = await transcribe(mediaResult.path, mediaResult.mimetype);
|
|
5349
|
+
if (sttResult?.text) {
|
|
5350
|
+
extracted.text = sttResult.text;
|
|
5211
5351
|
}
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
vncLog("ensure-cdp", { action: "post-vnc-restart", transport, cdp_port: CDP_PORT, ok: cdpOk2 });
|
|
5215
|
-
return cdpOk2;
|
|
5352
|
+
} finally {
|
|
5353
|
+
resolvePending();
|
|
5216
5354
|
}
|
|
5217
5355
|
}
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5356
|
+
let accessResult;
|
|
5357
|
+
if (isGroup) {
|
|
5358
|
+
accessResult = checkGroupAccess({ groupJid: remoteJid });
|
|
5359
|
+
} else {
|
|
5360
|
+
accessResult = await resolveDmAccess({
|
|
5361
|
+
senderPhone,
|
|
5362
|
+
selfPhone,
|
|
5363
|
+
config: whatsAppConfig,
|
|
5364
|
+
accountConfig: whatsAppConfig.accounts?.[conn.accountId],
|
|
5365
|
+
accountId: conn.accountId,
|
|
5366
|
+
isValidAccount: (id) => isValidAccountId(id),
|
|
5367
|
+
resolvePersonByPhone
|
|
5226
5368
|
});
|
|
5227
5369
|
}
|
|
5228
|
-
console.error(
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5370
|
+
console.error(
|
|
5371
|
+
`${TAG13} 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}` : "")
|
|
5372
|
+
);
|
|
5373
|
+
if (!accessResult.allowed && accessResult.reason === "account-manager-unresolved") {
|
|
5374
|
+
console.error(
|
|
5375
|
+
`[whatsapp-native] op=account-manager-reject senderId=${senderPhone} reason=unresolved-effective-account`
|
|
5376
|
+
);
|
|
5235
5377
|
}
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
function killChromium() {
|
|
5242
|
-
spawnSync("pkill", ["-f", "chromium"], { stdio: "pipe" });
|
|
5243
|
-
}
|
|
5244
|
-
function writeChromiumWrapper() {
|
|
5245
|
-
mkdirSync2(BIN_DIR, { recursive: true });
|
|
5246
|
-
const wrapperPath = resolve5(BIN_DIR, "chromium");
|
|
5247
|
-
writeFileSync3(wrapperPath, `#!/bin/bash
|
|
5248
|
-
LOG="${LOG_DIR}/chromium.log"
|
|
5249
|
-
echo "==== [$(date)] chromium wrapper ====" >> "$LOG"
|
|
5250
|
-
echo " DISPLAY=$DISPLAY WAYLAND=$WAYLAND_DISPLAY XDG_SESSION_TYPE=$XDG_SESSION_TYPE" >> "$LOG"
|
|
5251
|
-
echo " args: $*" >> "$LOG"
|
|
5252
|
-
|
|
5253
|
-
# Check the brand's virtual display is up (Tasks 553 + 924 \u2014 per-brand
|
|
5254
|
-
# display + per-brand CDP port).
|
|
5255
|
-
if DISPLAY=${VNC_DISPLAY} xdpyinfo >/dev/null 2>&1; then
|
|
5256
|
-
echo " xdpyinfo ${VNC_DISPLAY} OK" >> "$LOG"
|
|
5257
|
-
else
|
|
5258
|
-
echo " xdpyinfo ${VNC_DISPLAY} FAILED \u2014 display not ready" >> "$LOG"
|
|
5259
|
-
fi
|
|
5260
|
-
|
|
5261
|
-
# read the install-time-resolved non-snap Chromium binary path.
|
|
5262
|
-
# The installer (ensureNonSnapChromium + writeChromiumBinaryPathFile) writes
|
|
5263
|
-
# this file with the absolute path to either /usr/bin/chromium (Pi Bookworm:
|
|
5264
|
-
# real .deb) or /usr/bin/google-chrome-stable (Ubuntu Noble laptop: snap
|
|
5265
|
-
# replaced). Refusing the legacy probe loop closes the silent-fallback path
|
|
5266
|
-
# where /snap/bin/chromium would re-introduce the AppArmor SingletonLock
|
|
5267
|
-
# denial that triggered.
|
|
5268
|
-
PATH_FILE="${PLATFORM_ROOT3}/config/chromium-binary.path"
|
|
5269
|
-
if [ ! -r "$PATH_FILE" ]; then
|
|
5270
|
-
echo " ERROR: $PATH_FILE missing \u2014 re-run installer" >> "$LOG"
|
|
5271
|
-
exit 1
|
|
5272
|
-
fi
|
|
5273
|
-
BIN="$(head -n1 "$PATH_FILE" | tr -d '[:space:]')"
|
|
5274
|
-
if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then
|
|
5275
|
-
echo " ERROR: $PATH_FILE empty or non-executable: $BIN" >> "$LOG"
|
|
5276
|
-
exit 1
|
|
5277
|
-
fi
|
|
5278
|
-
RESOLVED="$(readlink -f "$BIN" 2>/dev/null || echo "$BIN")"
|
|
5279
|
-
case ":$(echo "$RESOLVED" | tr '/' ':'):" in
|
|
5280
|
-
*:snap:*)
|
|
5281
|
-
echo " ERROR: $BIN resolves to snap-confined $RESOLVED \u2014 re-run installer" >> "$LOG"
|
|
5282
|
-
exit 1
|
|
5283
|
-
;;
|
|
5284
|
-
esac
|
|
5285
|
-
echo " launching $BIN (realpath=$RESOLVED)" >> "$LOG"
|
|
5286
|
-
|
|
5287
|
-
# nohup detaches chromium from this wrapper's process group so it outlives
|
|
5288
|
-
# claude auth login exiting (e.g. on timeout). --user-data-dir is brand-scoped
|
|
5289
|
-
# so a wrapper-spawned chromium attaches to the same per-brand
|
|
5290
|
-
# profile that vnc.sh's start_chrome targets \u2014 no shared cookies across brands.
|
|
5291
|
-
nohup "$BIN" \\
|
|
5292
|
-
--user-data-dir=${CHROMIUM_PROFILE_DIR} \\
|
|
5293
|
-
--no-sandbox \\
|
|
5294
|
-
--test-type \\
|
|
5295
|
-
--disable-dev-shm-usage \\
|
|
5296
|
-
--disable-gpu \\
|
|
5297
|
-
--password-store=basic \\
|
|
5298
|
-
--use-mock-keychain \\
|
|
5299
|
-
--ozone-platform=x11 \\
|
|
5300
|
-
--no-first-run \\
|
|
5301
|
-
--no-default-browser-check \\
|
|
5302
|
-
--disable-session-crashed-bubble \\
|
|
5303
|
-
--hide-crash-restore-bubble \\
|
|
5304
|
-
--noerrdialogs \\
|
|
5305
|
-
--window-size=1280,800 \\
|
|
5306
|
-
--window-position=0,0 \\
|
|
5307
|
-
"$@" >> "$LOG" 2>&1 &
|
|
5308
|
-
echo " chromium PID: $!" >> "$LOG"
|
|
5309
|
-
`, { mode: 493 });
|
|
5310
|
-
return wrapperPath;
|
|
5311
|
-
}
|
|
5312
|
-
function buildX11Env(chromiumWrapperPath, transport = "vnc") {
|
|
5313
|
-
if (transport === "native") {
|
|
5314
|
-
const nativeInfo = discoverNativeDisplay();
|
|
5315
|
-
const env = {
|
|
5316
|
-
...process.env,
|
|
5317
|
-
DISPLAY: nativeInfo.display,
|
|
5318
|
-
BROWSER: chromiumWrapperPath,
|
|
5319
|
-
PATH: `${BIN_DIR}:${process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}`
|
|
5320
|
-
};
|
|
5321
|
-
if (nativeInfo.sessionType === "wayland") {
|
|
5322
|
-
env.WAYLAND_DISPLAY = nativeInfo.waylandDisplay;
|
|
5323
|
-
} else {
|
|
5324
|
-
env.WAYLAND_DISPLAY = "";
|
|
5325
|
-
}
|
|
5326
|
-
vncLog("build-x11-env", {
|
|
5327
|
-
transport,
|
|
5328
|
-
session_type: nativeInfo.sessionType,
|
|
5329
|
-
display: nativeInfo.display,
|
|
5330
|
-
wayland_display: env.WAYLAND_DISPLAY || void 0
|
|
5331
|
-
});
|
|
5332
|
-
return env;
|
|
5378
|
+
if (accessResult.allowed && accessResult.reason === "account-manager" && accessResult.effectiveAccountId === conn.accountId) {
|
|
5379
|
+
console.error(
|
|
5380
|
+
`[whatsapp-native] op=escalation-tripwire senderId=${senderPhone} reason=account-manager-routed-to-house effectiveAccount=${accessResult.effectiveAccountId}`
|
|
5381
|
+
);
|
|
5382
|
+
return;
|
|
5333
5383
|
}
|
|
5334
|
-
return
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5384
|
+
if (!accessResult.allowed) return;
|
|
5385
|
+
if (isCustomerTurnSuppressed(accessResult.agentType, isHouseManagedService())) {
|
|
5386
|
+
console.error(
|
|
5387
|
+
`[channel-house] op=inbound from=${senderPhone} accountId=${conn.platformAccountId} autoReply=suppressed`
|
|
5388
|
+
);
|
|
5389
|
+
return;
|
|
5390
|
+
}
|
|
5391
|
+
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
5392
|
+
if (sendReceipts && msg.key.id) {
|
|
5393
|
+
sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0);
|
|
5394
|
+
}
|
|
5395
|
+
const reply = async (text) => {
|
|
5396
|
+
if (isGroupJid(remoteJid)) {
|
|
5397
|
+
console.error(`${TAG13} op=group-reply-blocked jid=${remoteJid}`);
|
|
5398
|
+
throw new WaReplyError("group reply blocked \u2014 observe-only", false);
|
|
5399
|
+
}
|
|
5400
|
+
const currentSock = conn.sock;
|
|
5401
|
+
if (!currentSock) {
|
|
5402
|
+
const reason = conn.terminalReason;
|
|
5403
|
+
throw new WaReplyError(reason ?? "WhatsApp disconnected \u2014 cannot reply", Boolean(reason));
|
|
5404
|
+
}
|
|
5405
|
+
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
5406
|
+
};
|
|
5407
|
+
const composing = async () => {
|
|
5408
|
+
const currentSock = conn.sock;
|
|
5409
|
+
if (currentSock) await sendComposing(currentSock, remoteJid);
|
|
5410
|
+
};
|
|
5411
|
+
const cacheKey = deriveCacheKey({
|
|
5412
|
+
agentType: accessResult.agentType,
|
|
5413
|
+
accountId: conn.accountId,
|
|
5414
|
+
senderPhone,
|
|
5415
|
+
isGroup,
|
|
5416
|
+
groupJid: isGroup ? remoteJid : void 0
|
|
5361
5417
|
});
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5418
|
+
conn.lastMessageAt = Date.now();
|
|
5419
|
+
const payload = {
|
|
5420
|
+
accountId: conn.accountId,
|
|
5421
|
+
agentType: accessResult.agentType,
|
|
5422
|
+
passive: accessResult.passive === true,
|
|
5423
|
+
personId: accessResult.personId ?? null,
|
|
5424
|
+
senderPhone,
|
|
5425
|
+
text: extracted.text,
|
|
5426
|
+
isGroup,
|
|
5427
|
+
groupJid: isGroup ? remoteJid : void 0,
|
|
5428
|
+
reply,
|
|
5429
|
+
composing,
|
|
5430
|
+
cacheKey,
|
|
5431
|
+
media: mediaResult?.path ? [{ path: mediaResult.path, type: extracted.mediaType, mimetype: mediaResult.mimetype }] : [],
|
|
5432
|
+
replyContext: extracted.quotedMessage,
|
|
5433
|
+
// Task 1383 — the gate's single resolution feeds the spawn (house or the
|
|
5434
|
+
// bound sub-account); the router never re-resolves.
|
|
5435
|
+
effectiveAccountId: accessResult.effectiveAccountId
|
|
5436
|
+
};
|
|
5437
|
+
if (accessResult.agentType === "public") {
|
|
5438
|
+
const hoursResult = await isBusinessOpen(conn.accountId);
|
|
5439
|
+
if (!hoursResult.open) {
|
|
5440
|
+
console.error(`${TAG13} [${conn.accountId}] dispatch skipped: business closed`);
|
|
5441
|
+
const afterHoursMessage = whatsAppConfig.accounts?.[conn.accountId]?.afterHoursMessage ?? whatsAppConfig.afterHoursMessage;
|
|
5442
|
+
if (afterHoursMessage) {
|
|
5443
|
+
try {
|
|
5444
|
+
await reply(afterHoursMessage);
|
|
5445
|
+
console.error(`${TAG13} [${conn.accountId}] after-hours auto-reply sent to ${remoteJid}`);
|
|
5446
|
+
} catch (err) {
|
|
5447
|
+
console.error(
|
|
5448
|
+
`${TAG13} [${conn.accountId}] after-hours auto-reply failed: ${err instanceof Error ? err.message : String(err)}`
|
|
5449
|
+
);
|
|
5450
|
+
}
|
|
5373
5451
|
}
|
|
5452
|
+
return;
|
|
5374
5453
|
}
|
|
5375
|
-
} catch (err) {
|
|
5376
|
-
console.error(`[health] users.json corrupt \u2014 reporting pin_configured=false: ${err instanceof Error ? err.message : String(err)}`);
|
|
5377
|
-
}
|
|
5378
|
-
let authHealth;
|
|
5379
|
-
try {
|
|
5380
|
-
authHealth = await ensureAuth();
|
|
5381
|
-
} catch {
|
|
5382
|
-
authHealth = { status: "missing" };
|
|
5383
5454
|
}
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
whatsappAccounts = getStatus().map((a) => ({
|
|
5389
|
-
accountId: a.accountId,
|
|
5390
|
-
accountName: a.accountName ?? null,
|
|
5391
|
-
connected: a.connected,
|
|
5392
|
-
selfPhone: a.selfPhone ?? null,
|
|
5393
|
-
reconnectAttempts: a.reconnectAttempts,
|
|
5394
|
-
lastError: a.lastError ?? null,
|
|
5395
|
-
sessionStuckReason: a.sessionStuckReason ?? null,
|
|
5396
|
-
terminalReason: a.terminalReason ?? null
|
|
5397
|
-
}));
|
|
5398
|
-
} catch (err) {
|
|
5399
|
-
console.error(`[health] failed to read WhatsApp status: ${err instanceof Error ? err.message : String(err)}`);
|
|
5455
|
+
if (conn.debouncer) {
|
|
5456
|
+
conn.debouncer.enqueue(payload);
|
|
5457
|
+
} else {
|
|
5458
|
+
onInboundMessage(payload);
|
|
5400
5459
|
}
|
|
5401
|
-
const whatsappAnyConnected = whatsappAccounts.some((a) => a.connected);
|
|
5402
|
-
const whatsappAnyStuck = whatsappAccounts.some((a) => Boolean(a.sessionStuckReason));
|
|
5403
|
-
const whatsappAnyTerminal = whatsappAccounts.some((a) => Boolean(a.terminalReason));
|
|
5404
|
-
const account = resolveAccount();
|
|
5405
|
-
const missingPlugins = findMissingPlugins(account?.config.enabledPlugins);
|
|
5406
|
-
return c.json({
|
|
5407
|
-
pin_configured: pinConfigured,
|
|
5408
|
-
claude_authenticated: claudeAuthenticated,
|
|
5409
|
-
vnc_running: vncRunning,
|
|
5410
|
-
browser_transport: browserTransport,
|
|
5411
|
-
auth_status: authHealth.status,
|
|
5412
|
-
auth_expires_at: authHealth.expiresAt ?? null,
|
|
5413
|
-
server_ip: getLanIp(),
|
|
5414
|
-
missingPlugins,
|
|
5415
|
-
whatsapp: {
|
|
5416
|
-
any_connected: whatsappAnyConnected,
|
|
5417
|
-
any_stuck: whatsappAnyStuck,
|
|
5418
|
-
any_terminal: whatsappAnyTerminal,
|
|
5419
|
-
accounts: whatsappAccounts
|
|
5420
|
-
}
|
|
5421
|
-
});
|
|
5422
|
-
});
|
|
5423
|
-
var health_default = app;
|
|
5424
|
-
|
|
5425
|
-
// app/lib/attachments.ts
|
|
5426
|
-
import { randomUUID as randomUUID4 } from "crypto";
|
|
5427
|
-
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
5428
|
-
import { realpathSync as realpathSync2 } from "fs";
|
|
5429
|
-
import { resolve as resolve7, extname } from "path";
|
|
5430
|
-
|
|
5431
|
-
// app/lib/data-path.ts
|
|
5432
|
-
import { realpathSync } from "fs";
|
|
5433
|
-
import { resolve as resolve6, normalize, sep as sep2, relative, basename, join as join6 } from "path";
|
|
5434
|
-
var PLATFORM_ROOT4 = process.env.MAXY_PLATFORM_ROOT ?? resolve6(process.cwd(), "../platform");
|
|
5435
|
-
var DATA_ROOT = resolve6(PLATFORM_ROOT4, "..", "data");
|
|
5436
|
-
var CLAUDE_UPLOADS_ROOT = process.env.CLAUDE_CONFIG_DIR ? resolve6(process.env.CLAUDE_CONFIG_DIR, "uploads") : null;
|
|
5437
|
-
var ACCOUNT_PARTITION_DIRS = /* @__PURE__ */ new Set(["accounts"]);
|
|
5438
|
-
var ACCOUNT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5439
|
-
function crossesForeignAccountPartition(relPath, accountId) {
|
|
5440
|
-
const segs = relPath.split("/").filter(Boolean);
|
|
5441
|
-
if (segs.length < 2) return false;
|
|
5442
|
-
if (!ACCOUNT_PARTITION_DIRS.has(segs[0])) return false;
|
|
5443
|
-
const owner = segs[1];
|
|
5444
|
-
if (!ACCOUNT_UUID_RE.test(owner)) return false;
|
|
5445
|
-
return owner !== accountId;
|
|
5446
5460
|
}
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5461
|
+
|
|
5462
|
+
// app/lib/vnc.ts
|
|
5463
|
+
import { spawnSync, execFileSync } from "child_process";
|
|
5464
|
+
import { createConnection } from "net";
|
|
5465
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
5466
|
+
import { resolve as resolve7 } from "path";
|
|
5467
|
+
var PLATFORM_ROOT4 = process.env.MAXY_PLATFORM_ROOT ?? resolve7(process.cwd(), "..");
|
|
5468
|
+
var VNC_SCRIPT = resolve7(PLATFORM_ROOT4, "scripts/vnc.sh");
|
|
5469
|
+
var displayMode = process.env.DISPLAY_MODE ?? "virtual";
|
|
5470
|
+
if (displayMode === "native") {
|
|
5471
|
+
console.log(`[vnc] DISPLAY_MODE=native \u2014 local requests use desktop display, remote requests use VNC`);
|
|
5450
5472
|
}
|
|
5451
|
-
function
|
|
5452
|
-
|
|
5453
|
-
const
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
};
|
|
5473
|
+
function resolveBrowserTransport(req, remoteAddress) {
|
|
5474
|
+
if (displayMode !== "native") return "vnc";
|
|
5475
|
+
const xff = req.headers.get("x-forwarded-for") ?? void 0;
|
|
5476
|
+
const client = resolveClientIp(remoteAddress ?? "127.0.0.1", xff);
|
|
5477
|
+
const transport = client.kind !== "external" && !xff ? "native" : "vnc";
|
|
5478
|
+
if (remoteAddress && remoteAddress !== "127.0.0.1" && remoteAddress !== "::1") {
|
|
5479
|
+
const oldClient = resolveClientIp("127.0.0.1", xff);
|
|
5480
|
+
const oldTransport = oldClient.kind !== "external" && !xff ? "native" : "vnc";
|
|
5481
|
+
if (transport !== oldTransport) {
|
|
5482
|
+
console.log(`[resolve-transport] remoteAddress="${remoteAddress}" transport="${transport}" (was: "${oldTransport}" with hardcoded loopback)`);
|
|
5483
|
+
}
|
|
5463
5484
|
}
|
|
5464
|
-
|
|
5485
|
+
return transport;
|
|
5486
|
+
}
|
|
5487
|
+
var currentCdpDisplay = null;
|
|
5488
|
+
function discoverNativeDisplay() {
|
|
5489
|
+
const fallback = { sessionType: "x11", display: ":0", waylandDisplay: "" };
|
|
5465
5490
|
try {
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5491
|
+
const listResult = spawnSync("loginctl", ["list-sessions", "--no-legend"], {
|
|
5492
|
+
timeout: 5e3,
|
|
5493
|
+
encoding: "utf8",
|
|
5494
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5495
|
+
});
|
|
5496
|
+
if (listResult.status !== 0 || !listResult.stdout.trim()) {
|
|
5497
|
+
vncLog("native-display-discovery", { action: "fallback", reason: "no-sessions" });
|
|
5498
|
+
return fallback;
|
|
5499
|
+
}
|
|
5500
|
+
const sessionId = listResult.stdout.trim().split("\n")[0].trim().split(/\s+/)[0];
|
|
5501
|
+
const typeResult = spawnSync("loginctl", ["show-session", sessionId, "-p", "Type", "--value"], {
|
|
5502
|
+
timeout: 5e3,
|
|
5503
|
+
encoding: "utf8",
|
|
5504
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5505
|
+
});
|
|
5506
|
+
const displayResult = spawnSync("loginctl", ["show-session", sessionId, "-p", "Display", "--value"], {
|
|
5507
|
+
timeout: 5e3,
|
|
5508
|
+
encoding: "utf8",
|
|
5509
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5510
|
+
});
|
|
5511
|
+
const sessionType = typeResult.stdout?.trim() || "";
|
|
5512
|
+
const sessionDisplay = displayResult.stdout?.trim() || ":0";
|
|
5513
|
+
if (sessionType === "wayland") {
|
|
5514
|
+
let waylandDisplay = "wayland-0";
|
|
5515
|
+
const leaderResult = spawnSync("loginctl", ["show-session", sessionId, "-p", "Leader", "--value"], {
|
|
5516
|
+
timeout: 5e3,
|
|
5517
|
+
encoding: "utf8",
|
|
5518
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5519
|
+
});
|
|
5520
|
+
const leaderPid = leaderResult.stdout?.trim();
|
|
5521
|
+
if (leaderPid) {
|
|
5522
|
+
try {
|
|
5523
|
+
const environ = readFileSync8(`/proc/${leaderPid}/environ`, "utf8");
|
|
5524
|
+
const match = environ.split("\0").find((e) => e.startsWith("WAYLAND_DISPLAY="));
|
|
5525
|
+
if (match) waylandDisplay = match.split("=")[1];
|
|
5526
|
+
} catch {
|
|
5527
|
+
}
|
|
5472
5528
|
}
|
|
5473
|
-
|
|
5529
|
+
const info2 = { sessionType: "wayland", display: sessionDisplay, waylandDisplay };
|
|
5530
|
+
vncLog("native-display-discovery", {
|
|
5531
|
+
action: "discovered",
|
|
5532
|
+
session_type: "wayland",
|
|
5533
|
+
display: sessionDisplay,
|
|
5534
|
+
wayland_display: waylandDisplay
|
|
5535
|
+
});
|
|
5536
|
+
return info2;
|
|
5474
5537
|
}
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5538
|
+
const info = { sessionType: "x11", display: sessionDisplay, waylandDisplay: "" };
|
|
5539
|
+
vncLog("native-display-discovery", {
|
|
5540
|
+
action: "discovered",
|
|
5541
|
+
session_type: sessionType || "unknown (defaulting to x11)",
|
|
5542
|
+
display: sessionDisplay
|
|
5543
|
+
});
|
|
5544
|
+
return info;
|
|
5545
|
+
} catch (err) {
|
|
5546
|
+
vncLog("native-display-discovery", { action: "fallback", reason: "error", err: err.message });
|
|
5547
|
+
return fallback;
|
|
5483
5548
|
}
|
|
5484
|
-
const relPath = relative(rootReal, resolvedReal) || ".";
|
|
5485
|
-
return { ok: true, absolute: resolvedReal, dataRootReal: rootReal, relative: relPath };
|
|
5486
5549
|
}
|
|
5487
|
-
function
|
|
5488
|
-
return
|
|
5550
|
+
function sleep(ms) {
|
|
5551
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
5489
5552
|
}
|
|
5490
|
-
function
|
|
5491
|
-
|
|
5492
|
-
|
|
5553
|
+
async function waitForPort(port2, timeoutMs = 12e3) {
|
|
5554
|
+
const deadline = Date.now() + timeoutMs;
|
|
5555
|
+
while (Date.now() < deadline) {
|
|
5556
|
+
const ready = await new Promise((res) => {
|
|
5557
|
+
const socket = createConnection(port2, "127.0.0.1");
|
|
5558
|
+
socket.setTimeout(500);
|
|
5559
|
+
socket.once("connect", () => {
|
|
5560
|
+
socket.destroy();
|
|
5561
|
+
res(true);
|
|
5562
|
+
});
|
|
5563
|
+
socket.once("error", () => {
|
|
5564
|
+
socket.destroy();
|
|
5565
|
+
res(false);
|
|
5566
|
+
});
|
|
5567
|
+
socket.once("timeout", () => {
|
|
5568
|
+
socket.destroy();
|
|
5569
|
+
res(false);
|
|
5570
|
+
});
|
|
5571
|
+
});
|
|
5572
|
+
if (ready) return true;
|
|
5573
|
+
await sleep(300);
|
|
5493
5574
|
}
|
|
5494
|
-
return
|
|
5495
|
-
}
|
|
5496
|
-
function scratchpadTmpBase() {
|
|
5497
|
-
return process.env.MAXY_SCRATCHPAD_BASE ?? "/tmp";
|
|
5498
|
-
}
|
|
5499
|
-
function sessionScratchpadRoot(projectDir, sessionId) {
|
|
5500
|
-
const uid = typeof process.geteuid === "function" ? process.geteuid() : 0;
|
|
5501
|
-
return join6(scratchpadTmpBase(), `claude-${uid}`, basename(projectDir), sessionId, "scratchpad");
|
|
5502
|
-
}
|
|
5503
|
-
function resolveScratchpadPath(raw, scratchpadRootAbs) {
|
|
5504
|
-
return resolveUnderRoot(raw, scratchpadRootAbs, "scratchpad");
|
|
5505
|
-
}
|
|
5506
|
-
|
|
5507
|
-
// app/lib/doc-to-text.ts
|
|
5508
|
-
import { unzipSync, strFromU8 } from "fflate";
|
|
5509
|
-
var DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
5510
|
-
var ODT = "application/vnd.oasis.opendocument.text";
|
|
5511
|
-
var CONVERTIBLE_DOC_MIME = /* @__PURE__ */ new Set([
|
|
5512
|
-
DOCX,
|
|
5513
|
-
ODT,
|
|
5514
|
-
"text/rtf",
|
|
5515
|
-
"application/rtf"
|
|
5516
|
-
]);
|
|
5517
|
-
function bareMime(mimeType) {
|
|
5518
|
-
return mimeType.split(";")[0].trim().toLowerCase();
|
|
5575
|
+
return false;
|
|
5519
5576
|
}
|
|
5520
|
-
function
|
|
5521
|
-
|
|
5577
|
+
function ensureLogDir() {
|
|
5578
|
+
mkdirSync2(LOG_DIR, { recursive: true });
|
|
5522
5579
|
}
|
|
5523
|
-
function
|
|
5524
|
-
|
|
5525
|
-
const bytes = files[entry];
|
|
5526
|
-
if (!bytes) throw new Error(`zip entry not found: ${entry}`);
|
|
5527
|
-
return strFromU8(bytes);
|
|
5580
|
+
function logPath(name) {
|
|
5581
|
+
return resolve7(LOG_DIR, `${name}.log`);
|
|
5528
5582
|
}
|
|
5529
|
-
function
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
const withBreaks = p.replace(/<w:tab\b[^>]*\/?>/g, " ").replace(/<w:br\b[^>]*\/?>/g, "\n");
|
|
5534
|
-
let line = "";
|
|
5535
|
-
for (const m of withBreaks.matchAll(/<w:t\b[^>]*>([\s\S]*?)<\/w:t>/g)) {
|
|
5536
|
-
line += unescapeXml(m[1]);
|
|
5537
|
-
}
|
|
5538
|
-
if (line.length > 0) paras.push(line);
|
|
5583
|
+
async function ensureVnc() {
|
|
5584
|
+
if (process.platform !== "linux") {
|
|
5585
|
+
vncLog("ensure-vnc", { action: "skipped", reason: "platform-not-linux", platform: process.platform });
|
|
5586
|
+
return false;
|
|
5539
5587
|
}
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5588
|
+
const up = await waitForPort(RFB_PORT, 1e3);
|
|
5589
|
+
if (up) return true;
|
|
5590
|
+
vncLog("ensure-vnc", { action: "restart", reason: "x-down", rfb_port: RFB_PORT });
|
|
5591
|
+
try {
|
|
5592
|
+
execFileSync("bash", [VNC_SCRIPT, "start"], { timeout: 2e4 });
|
|
5593
|
+
} catch (err) {
|
|
5594
|
+
vncLog("ensure-vnc", { action: "restart-failed", err: err.message });
|
|
5595
|
+
return false;
|
|
5596
|
+
}
|
|
5597
|
+
const ok = await waitForPort(RFB_PORT, 12e3);
|
|
5598
|
+
vncLog("ensure-vnc", { action: "restart-complete", rfb_port: RFB_PORT, ok });
|
|
5599
|
+
return ok;
|
|
5548
5600
|
}
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5601
|
+
async function ensureCdp(transport = "vnc") {
|
|
5602
|
+
if (process.platform !== "linux") {
|
|
5603
|
+
vncLog("ensure-cdp", { action: "skipped", reason: "platform-not-linux", platform: process.platform });
|
|
5604
|
+
return false;
|
|
5605
|
+
}
|
|
5606
|
+
const targetSentinel = transport === "native" ? "native" : VNC_DISPLAY;
|
|
5607
|
+
if (await waitForPort(CDP_PORT, 1e3)) {
|
|
5608
|
+
if (currentCdpDisplay !== null && currentCdpDisplay !== targetSentinel) {
|
|
5609
|
+
console.error(`[ensureCdp] Display switch: ${currentCdpDisplay} \u2192 ${targetSentinel}`);
|
|
5610
|
+
vncLog("ensure-cdp", {
|
|
5611
|
+
action: "display-switch",
|
|
5612
|
+
transport,
|
|
5613
|
+
from_display: currentCdpDisplay,
|
|
5614
|
+
to_display: targetSentinel
|
|
5615
|
+
});
|
|
5616
|
+
killChromium();
|
|
5617
|
+
await sleep(500);
|
|
5618
|
+
} else {
|
|
5619
|
+
return true;
|
|
5558
5620
|
}
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5621
|
+
}
|
|
5622
|
+
const vncCommand = transport === "native" ? "start-chrome-native" : "start-chrome";
|
|
5623
|
+
if (transport === "vnc") {
|
|
5624
|
+
const xAlive = await waitForPort(RFB_PORT, 1e3);
|
|
5625
|
+
if (!xAlive) {
|
|
5626
|
+
console.error(`[ensureCdp] X server down on rfb=${RFB_PORT} \u2014 escalating to full VNC restart`);
|
|
5627
|
+
vncLog("ensure-cdp", { action: "escalate-vnc-restart", reason: "x-down", transport, rfb_port: RFB_PORT });
|
|
5628
|
+
const vncOk = await ensureVnc();
|
|
5629
|
+
if (!vncOk) {
|
|
5630
|
+
console.error("[ensureCdp] Full VNC restart failed \u2014 browser-specialist degraded");
|
|
5631
|
+
vncLog("ensure-cdp", { action: "degraded", reason: "vnc-restart-failed", transport });
|
|
5632
|
+
return false;
|
|
5569
5633
|
}
|
|
5570
|
-
|
|
5571
|
-
|
|
5634
|
+
const cdpOk2 = await waitForPort(CDP_PORT, 12e3);
|
|
5635
|
+
if (cdpOk2) currentCdpDisplay = targetSentinel;
|
|
5636
|
+
vncLog("ensure-cdp", { action: "post-vnc-restart", transport, cdp_port: CDP_PORT, ok: cdpOk2 });
|
|
5637
|
+
return cdpOk2;
|
|
5572
5638
|
}
|
|
5573
|
-
out += ch;
|
|
5574
5639
|
}
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5640
|
+
if (transport === "native") {
|
|
5641
|
+
const nativeInfo = discoverNativeDisplay();
|
|
5642
|
+
vncLog("ensure-cdp", {
|
|
5643
|
+
action: "native-display-resolved",
|
|
5644
|
+
transport,
|
|
5645
|
+
session_type: nativeInfo.sessionType,
|
|
5646
|
+
display: nativeInfo.display,
|
|
5647
|
+
wayland_display: nativeInfo.waylandDisplay || void 0
|
|
5648
|
+
});
|
|
5649
|
+
}
|
|
5650
|
+
console.error(`[ensureCdp] Restarting Chrome (${vncCommand}) for transport=${transport}`);
|
|
5651
|
+
vncLog("ensure-cdp", { action: `restart-${vncCommand}`, reason: "cdp-dead", transport });
|
|
5652
|
+
try {
|
|
5653
|
+
execFileSync("bash", [VNC_SCRIPT, vncCommand], { timeout: 2e4 });
|
|
5654
|
+
} catch (err) {
|
|
5655
|
+
vncLog("ensure-cdp", { action: "restart-chrome-failed", transport, err: err.message });
|
|
5656
|
+
return false;
|
|
5657
|
+
}
|
|
5658
|
+
const cdpOk = await waitForPort(CDP_PORT, 12e3);
|
|
5659
|
+
if (cdpOk) currentCdpDisplay = targetSentinel;
|
|
5660
|
+
vncLog("ensure-cdp", { action: "restart-chrome-complete", transport, sentinel: targetSentinel, cdp_port: CDP_PORT, ok: cdpOk });
|
|
5661
|
+
return cdpOk;
|
|
5584
5662
|
}
|
|
5585
|
-
function
|
|
5586
|
-
|
|
5587
|
-
if (mime === DOCX) return docxToText(buffer);
|
|
5588
|
-
if (mime === ODT) return odtToText(buffer);
|
|
5589
|
-
if (mime === "text/rtf" || mime === "application/rtf") return rtfToText(buffer.toString("latin1"));
|
|
5590
|
-
return null;
|
|
5663
|
+
function killChromium() {
|
|
5664
|
+
spawnSync("pkill", ["-f", "chromium"], { stdio: "pipe" });
|
|
5591
5665
|
}
|
|
5666
|
+
function writeChromiumWrapper() {
|
|
5667
|
+
mkdirSync2(BIN_DIR, { recursive: true });
|
|
5668
|
+
const wrapperPath = resolve7(BIN_DIR, "chromium");
|
|
5669
|
+
writeFileSync3(wrapperPath, `#!/bin/bash
|
|
5670
|
+
LOG="${LOG_DIR}/chromium.log"
|
|
5671
|
+
echo "==== [$(date)] chromium wrapper ====" >> "$LOG"
|
|
5672
|
+
echo " DISPLAY=$DISPLAY WAYLAND=$WAYLAND_DISPLAY XDG_SESSION_TYPE=$XDG_SESSION_TYPE" >> "$LOG"
|
|
5673
|
+
echo " args: $*" >> "$LOG"
|
|
5592
5674
|
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
}
|
|
5597
|
-
|
|
5598
|
-
"
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
"
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
"
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
"
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5675
|
+
# Check the brand's virtual display is up (Tasks 553 + 924 \u2014 per-brand
|
|
5676
|
+
# display + per-brand CDP port).
|
|
5677
|
+
if DISPLAY=${VNC_DISPLAY} xdpyinfo >/dev/null 2>&1; then
|
|
5678
|
+
echo " xdpyinfo ${VNC_DISPLAY} OK" >> "$LOG"
|
|
5679
|
+
else
|
|
5680
|
+
echo " xdpyinfo ${VNC_DISPLAY} FAILED \u2014 display not ready" >> "$LOG"
|
|
5681
|
+
fi
|
|
5682
|
+
|
|
5683
|
+
# read the install-time-resolved non-snap Chromium binary path.
|
|
5684
|
+
# The installer (ensureNonSnapChromium + writeChromiumBinaryPathFile) writes
|
|
5685
|
+
# this file with the absolute path to either /usr/bin/chromium (Pi Bookworm:
|
|
5686
|
+
# real .deb) or /usr/bin/google-chrome-stable (Ubuntu Noble laptop: snap
|
|
5687
|
+
# replaced). Refusing the legacy probe loop closes the silent-fallback path
|
|
5688
|
+
# where /snap/bin/chromium would re-introduce the AppArmor SingletonLock
|
|
5689
|
+
# denial that triggered.
|
|
5690
|
+
PATH_FILE="${PLATFORM_ROOT4}/config/chromium-binary.path"
|
|
5691
|
+
if [ ! -r "$PATH_FILE" ]; then
|
|
5692
|
+
echo " ERROR: $PATH_FILE missing \u2014 re-run installer" >> "$LOG"
|
|
5693
|
+
exit 1
|
|
5694
|
+
fi
|
|
5695
|
+
BIN="$(head -n1 "$PATH_FILE" | tr -d '[:space:]')"
|
|
5696
|
+
if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then
|
|
5697
|
+
echo " ERROR: $PATH_FILE empty or non-executable: $BIN" >> "$LOG"
|
|
5698
|
+
exit 1
|
|
5699
|
+
fi
|
|
5700
|
+
RESOLVED="$(readlink -f "$BIN" 2>/dev/null || echo "$BIN")"
|
|
5701
|
+
case ":$(echo "$RESOLVED" | tr '/' ':'):" in
|
|
5702
|
+
*:snap:*)
|
|
5703
|
+
echo " ERROR: $BIN resolves to snap-confined $RESOLVED \u2014 re-run installer" >> "$LOG"
|
|
5704
|
+
exit 1
|
|
5705
|
+
;;
|
|
5706
|
+
esac
|
|
5707
|
+
echo " launching $BIN (realpath=$RESOLVED)" >> "$LOG"
|
|
5708
|
+
|
|
5709
|
+
# nohup detaches chromium from this wrapper's process group so it outlives
|
|
5710
|
+
# claude auth login exiting (e.g. on timeout). --user-data-dir is brand-scoped
|
|
5711
|
+
# so a wrapper-spawned chromium attaches to the same per-brand
|
|
5712
|
+
# profile that vnc.sh's start_chrome targets \u2014 no shared cookies across brands.
|
|
5713
|
+
nohup "$BIN" \\
|
|
5714
|
+
--user-data-dir=${CHROMIUM_PROFILE_DIR} \\
|
|
5715
|
+
--no-sandbox \\
|
|
5716
|
+
--test-type \\
|
|
5717
|
+
--disable-dev-shm-usage \\
|
|
5718
|
+
--disable-gpu \\
|
|
5719
|
+
--password-store=basic \\
|
|
5720
|
+
--use-mock-keychain \\
|
|
5721
|
+
--ozone-platform=x11 \\
|
|
5722
|
+
--no-first-run \\
|
|
5723
|
+
--no-default-browser-check \\
|
|
5724
|
+
--disable-session-crashed-bubble \\
|
|
5725
|
+
--hide-crash-restore-bubble \\
|
|
5726
|
+
--noerrdialogs \\
|
|
5727
|
+
--window-size=1280,800 \\
|
|
5728
|
+
--window-position=0,0 \\
|
|
5729
|
+
"$@" >> "$LOG" 2>&1 &
|
|
5730
|
+
echo " chromium PID: $!" >> "$LOG"
|
|
5731
|
+
`, { mode: 493 });
|
|
5732
|
+
return wrapperPath;
|
|
5641
5733
|
}
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
storedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5656
|
-
};
|
|
5657
|
-
await writeFile2(storagePath, buffer);
|
|
5658
|
-
await writeFile2(metaPath, JSON.stringify(meta, null, 2));
|
|
5659
|
-
let readableTextPath;
|
|
5660
|
-
if (CONVERTIBLE_DOC_MIME.has(mimeType.split(";")[0].trim().toLowerCase())) {
|
|
5661
|
-
const text = docToText(buffer, mimeType);
|
|
5662
|
-
if (text) {
|
|
5663
|
-
readableTextPath = resolve7(dir, `${attachmentId}${EXTRACTED_TEXT_SUFFIX}`);
|
|
5664
|
-
await writeFile2(readableTextPath, text);
|
|
5734
|
+
function buildX11Env(chromiumWrapperPath, transport = "vnc") {
|
|
5735
|
+
if (transport === "native") {
|
|
5736
|
+
const nativeInfo = discoverNativeDisplay();
|
|
5737
|
+
const env = {
|
|
5738
|
+
...process.env,
|
|
5739
|
+
DISPLAY: nativeInfo.display,
|
|
5740
|
+
BROWSER: chromiumWrapperPath,
|
|
5741
|
+
PATH: `${BIN_DIR}:${process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}`
|
|
5742
|
+
};
|
|
5743
|
+
if (nativeInfo.sessionType === "wayland") {
|
|
5744
|
+
env.WAYLAND_DISPLAY = nativeInfo.waylandDisplay;
|
|
5745
|
+
} else {
|
|
5746
|
+
env.WAYLAND_DISPLAY = "";
|
|
5665
5747
|
}
|
|
5748
|
+
vncLog("build-x11-env", {
|
|
5749
|
+
transport,
|
|
5750
|
+
session_type: nativeInfo.sessionType,
|
|
5751
|
+
display: nativeInfo.display,
|
|
5752
|
+
wayland_display: env.WAYLAND_DISPLAY || void 0
|
|
5753
|
+
});
|
|
5754
|
+
return env;
|
|
5666
5755
|
}
|
|
5667
|
-
return {
|
|
5756
|
+
return {
|
|
5757
|
+
...process.env,
|
|
5758
|
+
DISPLAY: VNC_DISPLAY,
|
|
5759
|
+
WAYLAND_DISPLAY: "",
|
|
5760
|
+
XDG_SESSION_TYPE: "x11",
|
|
5761
|
+
BROWSER: chromiumWrapperPath,
|
|
5762
|
+
PATH: `${BIN_DIR}:${process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}`
|
|
5763
|
+
};
|
|
5668
5764
|
}
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
);
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5765
|
+
|
|
5766
|
+
// server/routes/health.ts
|
|
5767
|
+
function checkPort(port2, timeoutMs = 500) {
|
|
5768
|
+
return new Promise((resolve34) => {
|
|
5769
|
+
const socket = createConnection2(port2, "127.0.0.1");
|
|
5770
|
+
socket.setTimeout(timeoutMs);
|
|
5771
|
+
socket.once("connect", () => {
|
|
5772
|
+
socket.destroy();
|
|
5773
|
+
resolve34(true);
|
|
5774
|
+
});
|
|
5775
|
+
socket.once("error", () => {
|
|
5776
|
+
socket.destroy();
|
|
5777
|
+
resolve34(false);
|
|
5778
|
+
});
|
|
5779
|
+
socket.once("timeout", () => {
|
|
5780
|
+
socket.destroy();
|
|
5781
|
+
resolve34(false);
|
|
5782
|
+
});
|
|
5783
|
+
});
|
|
5678
5784
|
}
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
)
|
|
5785
|
+
var app = new Hono();
|
|
5786
|
+
app.get("/", async (c) => {
|
|
5787
|
+
const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
|
|
5788
|
+
let pinConfigured = false;
|
|
5789
|
+
try {
|
|
5790
|
+
if (existsSync5(USERS_FILE)) {
|
|
5791
|
+
const raw = readFileSync9(USERS_FILE, "utf-8").trim();
|
|
5792
|
+
if (raw) {
|
|
5793
|
+
const users = JSON.parse(raw);
|
|
5794
|
+
pinConfigured = Array.isArray(users) && users.length > 0;
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
} catch (err) {
|
|
5798
|
+
console.error(`[health] users.json corrupt \u2014 reporting pin_configured=false: ${err instanceof Error ? err.message : String(err)}`);
|
|
5685
5799
|
}
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
".jpg": "image/jpeg",
|
|
5692
|
-
".jpeg": "image/jpeg",
|
|
5693
|
-
".gif": "image/gif",
|
|
5694
|
-
".webp": "image/webp",
|
|
5695
|
-
".svg": "image/svg+xml",
|
|
5696
|
-
".html": "text/html",
|
|
5697
|
-
".htm": "text/html",
|
|
5698
|
-
".css": "text/css",
|
|
5699
|
-
".js": "application/javascript",
|
|
5700
|
-
".json": "application/json",
|
|
5701
|
-
".csv": "text/csv",
|
|
5702
|
-
".txt": "text/plain",
|
|
5703
|
-
".md": "text/markdown",
|
|
5704
|
-
".xml": "text/xml",
|
|
5705
|
-
".zip": "application/zip",
|
|
5706
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
5707
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
5708
|
-
".odt": "application/vnd.oasis.opendocument.text",
|
|
5709
|
-
".rtf": "text/rtf",
|
|
5710
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
5711
|
-
".ics": "text/calendar"
|
|
5712
|
-
};
|
|
5713
|
-
function detectMimeType(filePath) {
|
|
5714
|
-
const ext = extname(filePath).toLowerCase();
|
|
5715
|
-
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
5716
|
-
}
|
|
5717
|
-
function validateFilePathInAccount(filePath, accountDir) {
|
|
5718
|
-
const resolved = realpathSync2(filePath);
|
|
5719
|
-
const accountResolved = realpathSync2(accountDir);
|
|
5720
|
-
if (!resolved.startsWith(accountResolved + "/")) {
|
|
5721
|
-
throw new Error(`File path is outside the account directory`);
|
|
5800
|
+
let authHealth;
|
|
5801
|
+
try {
|
|
5802
|
+
authHealth = await ensureAuth();
|
|
5803
|
+
} catch {
|
|
5804
|
+
authHealth = { status: "missing" };
|
|
5722
5805
|
}
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5806
|
+
const claudeAuthenticated = authHealth.status === "ok" || authHealth.status === "expiring";
|
|
5807
|
+
const vncRunning = await checkPort(WEBSOCKIFY_PORT);
|
|
5808
|
+
let whatsappAccounts = [];
|
|
5809
|
+
try {
|
|
5810
|
+
whatsappAccounts = getStatus().map((a) => ({
|
|
5811
|
+
accountId: a.accountId,
|
|
5812
|
+
accountName: a.accountName ?? null,
|
|
5813
|
+
connected: a.connected,
|
|
5814
|
+
selfPhone: a.selfPhone ?? null,
|
|
5815
|
+
reconnectAttempts: a.reconnectAttempts,
|
|
5816
|
+
lastError: a.lastError ?? null,
|
|
5817
|
+
sessionStuckReason: a.sessionStuckReason ?? null,
|
|
5818
|
+
terminalReason: a.terminalReason ?? null
|
|
5819
|
+
}));
|
|
5820
|
+
} catch (err) {
|
|
5821
|
+
console.error(`[health] failed to read WhatsApp status: ${err instanceof Error ? err.message : String(err)}`);
|
|
5731
5822
|
}
|
|
5732
|
-
const
|
|
5733
|
-
|
|
5734
|
-
const
|
|
5735
|
-
const
|
|
5736
|
-
const
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5823
|
+
const whatsappAnyConnected = whatsappAccounts.some((a) => a.connected);
|
|
5824
|
+
const whatsappAnyStuck = whatsappAccounts.some((a) => Boolean(a.sessionStuckReason));
|
|
5825
|
+
const whatsappAnyTerminal = whatsappAccounts.some((a) => Boolean(a.terminalReason));
|
|
5826
|
+
const account = resolveAccount();
|
|
5827
|
+
const missingPlugins = findMissingPlugins(account?.config.enabledPlugins);
|
|
5828
|
+
return c.json({
|
|
5829
|
+
pin_configured: pinConfigured,
|
|
5830
|
+
claude_authenticated: claudeAuthenticated,
|
|
5831
|
+
vnc_running: vncRunning,
|
|
5832
|
+
browser_transport: browserTransport,
|
|
5833
|
+
auth_status: authHealth.status,
|
|
5834
|
+
auth_expires_at: authHealth.expiresAt ?? null,
|
|
5835
|
+
server_ip: getLanIp(),
|
|
5836
|
+
missingPlugins,
|
|
5837
|
+
whatsapp: {
|
|
5838
|
+
any_connected: whatsappAnyConnected,
|
|
5839
|
+
any_stuck: whatsappAnyStuck,
|
|
5840
|
+
any_terminal: whatsappAnyTerminal,
|
|
5841
|
+
accounts: whatsappAccounts
|
|
5842
|
+
}
|
|
5843
|
+
});
|
|
5844
|
+
});
|
|
5845
|
+
var health_default = app;
|
|
5749
5846
|
|
|
5750
5847
|
// app/lib/stt/voice-note.ts
|
|
5751
5848
|
import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
|
|
@@ -6204,10 +6301,10 @@ ${result.result.text}` : result.result.text;
|
|
|
6204
6301
|
}
|
|
6205
6302
|
|
|
6206
6303
|
// app/lib/channel-pty-bridge/admin-session-id.ts
|
|
6207
|
-
import { createHash } from "crypto";
|
|
6304
|
+
import { createHash as createHash2 } from "crypto";
|
|
6208
6305
|
function adminSessionIdFor(accountId, senderId, personId) {
|
|
6209
6306
|
const subject = personId && personId.length > 0 ? `person:${personId}` : senderId;
|
|
6210
|
-
const b =
|
|
6307
|
+
const b = createHash2("sha256").update(`${accountId}:admin:${subject}`).digest().subarray(0, 16);
|
|
6211
6308
|
const v = Buffer.from(b);
|
|
6212
6309
|
v[6] = v[6] & 15 | 64;
|
|
6213
6310
|
v[8] = v[8] & 63 | 128;
|
|
@@ -6363,13 +6460,13 @@ async function sendTelegramText(botToken, chatId, text) {
|
|
|
6363
6460
|
|
|
6364
6461
|
// server/routes/whatsapp.ts
|
|
6365
6462
|
import { join as join10, resolve as resolve10 } from "path";
|
|
6366
|
-
import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as
|
|
6463
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync7 } from "fs";
|
|
6367
6464
|
|
|
6368
6465
|
// app/lib/whatsapp/login.ts
|
|
6369
6466
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
6370
6467
|
|
|
6371
6468
|
// app/lib/whatsapp/config-persist.ts
|
|
6372
|
-
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as
|
|
6469
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
6373
6470
|
import { resolve as resolve8, join as join8, dirname as dirname2, basename as basename2 } from "path";
|
|
6374
6471
|
var TAG17 = "[whatsapp:config]";
|
|
6375
6472
|
function configPath(accountDir) {
|
|
@@ -6377,7 +6474,7 @@ function configPath(accountDir) {
|
|
|
6377
6474
|
}
|
|
6378
6475
|
function readConfig(accountDir) {
|
|
6379
6476
|
const path2 = configPath(accountDir);
|
|
6380
|
-
if (!
|
|
6477
|
+
if (!existsSync6(path2)) throw new Error(`account.json not found at ${path2}`);
|
|
6381
6478
|
return JSON.parse(readFileSync10(path2, "utf-8"));
|
|
6382
6479
|
}
|
|
6383
6480
|
function writeConfig(accountDir, config) {
|
|
@@ -6676,7 +6773,7 @@ function setPublicAgent(accountDir, slug) {
|
|
|
6676
6773
|
return { ok: false, error: "Agent slug cannot be empty." };
|
|
6677
6774
|
}
|
|
6678
6775
|
const agentConfigPath = join8(accountDir, "agents", trimmed, "config.json");
|
|
6679
|
-
if (!
|
|
6776
|
+
if (!existsSync6(agentConfigPath)) {
|
|
6680
6777
|
return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
6681
6778
|
}
|
|
6682
6779
|
try {
|
|
@@ -7638,13 +7735,13 @@ app2.post("/config", async (c) => {
|
|
|
7638
7735
|
case "list-public-agents": {
|
|
7639
7736
|
const agentsDir = resolve10(account.accountDir, "agents");
|
|
7640
7737
|
const agents = [];
|
|
7641
|
-
if (
|
|
7738
|
+
if (existsSync7(agentsDir)) {
|
|
7642
7739
|
try {
|
|
7643
7740
|
const entries = readdirSync4(agentsDir, { withFileTypes: true });
|
|
7644
7741
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7645
7742
|
if (!entry.isDirectory() || entry.name === "admin") continue;
|
|
7646
7743
|
const configPath2 = resolve10(agentsDir, entry.name, "config.json");
|
|
7647
|
-
if (!
|
|
7744
|
+
if (!existsSync7(configPath2)) continue;
|
|
7648
7745
|
try {
|
|
7649
7746
|
const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
|
|
7650
7747
|
agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
|
|
@@ -7788,9 +7885,10 @@ app2.get("/activity", (c) => {
|
|
|
7788
7885
|
});
|
|
7789
7886
|
app2.get("/conversations", (c) => {
|
|
7790
7887
|
try {
|
|
7791
|
-
const
|
|
7792
|
-
const denial = callerScopeDenial(c,
|
|
7888
|
+
const requestedAccountId = validateAccountId(c.req.query("accountId") ?? null);
|
|
7889
|
+
const denial = callerScopeDenial(c, requestedAccountId);
|
|
7793
7890
|
if (denial) return denial;
|
|
7891
|
+
const accountId = resolveReadStoreKey(requestedAccountId);
|
|
7794
7892
|
const jids = listConversationJids(accountId);
|
|
7795
7893
|
const conversations = jids.map((jid) => {
|
|
7796
7894
|
const messages = getMessages(accountId, jid);
|
|
@@ -7803,7 +7901,7 @@ app2.get("/conversations", (c) => {
|
|
|
7803
7901
|
};
|
|
7804
7902
|
});
|
|
7805
7903
|
conversations.sort((a, b) => (b.lastMessageTimestamp ?? 0) - (a.lastMessageTimestamp ?? 0));
|
|
7806
|
-
console.error(`${TAG21} conversations account=${accountId} storeKeyFound=${jids.length > 0} count=${conversations.length}`);
|
|
7904
|
+
console.error(`${TAG21} conversations requested=${requestedAccountId} account=${accountId} storeKeyFound=${jids.length > 0} count=${conversations.length}`);
|
|
7807
7905
|
return c.json({ conversations });
|
|
7808
7906
|
} catch (err) {
|
|
7809
7907
|
console.error(`${TAG21} conversations error: ${String(err)}`);
|
|
@@ -7812,9 +7910,10 @@ app2.get("/conversations", (c) => {
|
|
|
7812
7910
|
});
|
|
7813
7911
|
app2.get("/messages", (c) => {
|
|
7814
7912
|
try {
|
|
7815
|
-
const
|
|
7816
|
-
const denial = callerScopeDenial(c,
|
|
7913
|
+
const requestedAccountId = validateAccountId(c.req.query("accountId") ?? null);
|
|
7914
|
+
const denial = callerScopeDenial(c, requestedAccountId);
|
|
7817
7915
|
if (denial) return denial;
|
|
7916
|
+
const accountId = resolveReadStoreKey(requestedAccountId);
|
|
7818
7917
|
const jid = c.req.query("jid");
|
|
7819
7918
|
if (!jid) {
|
|
7820
7919
|
return c.json({ error: "Missing required parameter: jid" }, 400);
|
|
@@ -7824,7 +7923,7 @@ app2.get("/messages", (c) => {
|
|
|
7824
7923
|
const effectiveLimit = limit && !Number.isNaN(limit) && limit > 0 ? limit : void 0;
|
|
7825
7924
|
const messages = getMessages(accountId, jid, effectiveLimit);
|
|
7826
7925
|
const storeKeyFound = listConversationJids(accountId).length > 0;
|
|
7827
|
-
console.error(`${TAG21} messages account=${accountId} jid=${jid} storeKeyFound=${storeKeyFound} limit=${effectiveLimit ?? "all"} returned=${messages.length}`);
|
|
7926
|
+
console.error(`${TAG21} messages requested=${requestedAccountId} account=${accountId} jid=${jid} storeKeyFound=${storeKeyFound} limit=${effectiveLimit ?? "all"} returned=${messages.length}`);
|
|
7828
7927
|
return c.json({ messages });
|
|
7829
7928
|
} catch (err) {
|
|
7830
7929
|
console.error(`${TAG21} messages error: ${String(err)}`);
|
|
@@ -7959,7 +8058,7 @@ app2.get("/group-info", async (c) => {
|
|
|
7959
8058
|
var whatsapp_default = app2;
|
|
7960
8059
|
|
|
7961
8060
|
// server/routes/whatsapp-reader.ts
|
|
7962
|
-
import { readFileSync as readFileSync15, watch, statSync as statSync5, openSync, readSync, closeSync, existsSync as
|
|
8061
|
+
import { readFileSync as readFileSync15, watch, statSync as statSync5, openSync, readSync, closeSync, existsSync as existsSync9, readdirSync as readdirSync8, realpathSync as realpathSync4 } from "fs";
|
|
7963
8062
|
import { basename as basename5, dirname as dirname4, isAbsolute, join as join14, relative as relative2, resolve as resolve12, sep as sep3 } from "path";
|
|
7964
8063
|
|
|
7965
8064
|
// server/routes/admin/sidebar-sessions.ts
|
|
@@ -8319,14 +8418,14 @@ app3.get("/", requireAdminSession, async (c) => {
|
|
|
8319
8418
|
var sidebar_sessions_default = app3;
|
|
8320
8419
|
|
|
8321
8420
|
// app/lib/admin-identity/pin-validator.ts
|
|
8322
|
-
import { createHash as
|
|
8323
|
-
import { existsSync as
|
|
8421
|
+
import { createHash as createHash3 } from "crypto";
|
|
8422
|
+
import { existsSync as existsSync8, readFileSync as readFileSync13, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
|
|
8324
8423
|
import { join as join12 } from "path";
|
|
8325
8424
|
function hashPin(pin) {
|
|
8326
|
-
return
|
|
8425
|
+
return createHash3("sha256").update(pin).digest("hex");
|
|
8327
8426
|
}
|
|
8328
8427
|
function readUsersFile(usersFilePath) {
|
|
8329
|
-
if (!
|
|
8428
|
+
if (!existsSync8(usersFilePath)) return null;
|
|
8330
8429
|
const raw = readFileSync13(usersFilePath, "utf-8").trim();
|
|
8331
8430
|
if (!raw) return [];
|
|
8332
8431
|
return JSON.parse(raw);
|
|
@@ -8481,14 +8580,14 @@ function joinTextBlocks(content) {
|
|
|
8481
8580
|
}
|
|
8482
8581
|
return parts.join("");
|
|
8483
8582
|
}
|
|
8484
|
-
function assistantTurns(content, ts) {
|
|
8583
|
+
function assistantTurns(content, ts, promoteFinal = false) {
|
|
8485
8584
|
if (!Array.isArray(content)) return [];
|
|
8486
8585
|
const out = [];
|
|
8487
8586
|
for (const block of content) {
|
|
8488
8587
|
if (!block || typeof block !== "object") continue;
|
|
8489
8588
|
const b = block;
|
|
8490
8589
|
if (b.type === "text" && typeof b.text === "string") {
|
|
8491
|
-
out.push({ kind: "agent-text", text: b.text, ts });
|
|
8590
|
+
out.push(promoteFinal && b.text.trim().length > 0 ? { kind: "agent-webchat-final", text: b.text, ts } : { kind: "agent-text", text: b.text, ts });
|
|
8492
8591
|
} else if (b.type === "tool_use" && (b.name === WA_REPLY_TOOL || b.name === WEBCHAT_REPLY_TOOL || b.name === CHANNEL_REPLY_TOOL)) {
|
|
8493
8592
|
const text = b.input?.text;
|
|
8494
8593
|
out.push({ kind: "agent-reply", text: typeof text === "string" ? text : "", ts });
|
|
@@ -8539,7 +8638,7 @@ function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress) {
|
|
|
8539
8638
|
out.push(operatorInboundTurn(u, ts));
|
|
8540
8639
|
}
|
|
8541
8640
|
}
|
|
8542
|
-
function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map()) {
|
|
8641
|
+
function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(), turnCtx = {}) {
|
|
8543
8642
|
const out = [];
|
|
8544
8643
|
for (const line of lines) {
|
|
8545
8644
|
if (!line || line[0] !== "{") continue;
|
|
@@ -8562,23 +8661,31 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
|
|
|
8562
8661
|
out.push({ kind: "agent-error", code, raw, ts });
|
|
8563
8662
|
continue;
|
|
8564
8663
|
}
|
|
8565
|
-
|
|
8664
|
+
const isEnd = msg?.stop_reason === "end_turn";
|
|
8665
|
+
out.push(...assistantTurns(msg?.content, ts, isEnd && turnCtx.lastChannelSource === "webchat"));
|
|
8566
8666
|
continue;
|
|
8567
8667
|
}
|
|
8568
8668
|
if (row.type === "queue-operation" && row.operation === "enqueue") {
|
|
8569
8669
|
const content = asString(row.content);
|
|
8570
|
-
if (content !== null && CHANNEL_WRAPPER2.test(content)
|
|
8670
|
+
if (content !== null && CHANNEL_WRAPPER2.test(content)) {
|
|
8571
8671
|
const u = unwrapChannel(content);
|
|
8572
|
-
|
|
8573
|
-
|
|
8672
|
+
turnCtx.lastChannelSource = u.source;
|
|
8673
|
+
if (!isScheduleSource(content)) {
|
|
8674
|
+
out.push(operatorInboundTurn(u, ts));
|
|
8675
|
+
queuedPendingSuppress.set(u.text, (queuedPendingSuppress.get(u.text) ?? 0) + 1);
|
|
8676
|
+
}
|
|
8574
8677
|
}
|
|
8575
8678
|
continue;
|
|
8576
8679
|
}
|
|
8577
8680
|
if (row.type === "attachment") {
|
|
8578
8681
|
const att = row.attachment;
|
|
8579
8682
|
const isChannelQueued = att?.type === "queued_command" && att.isMeta === true && typeof att.origin === "object" && att.origin !== null && att.origin.kind === "channel" && typeof att.prompt === "string";
|
|
8580
|
-
if (isChannelQueued
|
|
8581
|
-
|
|
8683
|
+
if (isChannelQueued) {
|
|
8684
|
+
const u = unwrapChannel(att.prompt);
|
|
8685
|
+
turnCtx.lastChannelSource = u.source;
|
|
8686
|
+
if (!isScheduleSource(att.prompt)) {
|
|
8687
|
+
renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress);
|
|
8688
|
+
}
|
|
8582
8689
|
}
|
|
8583
8690
|
continue;
|
|
8584
8691
|
}
|
|
@@ -8603,8 +8710,10 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
|
|
|
8603
8710
|
// enqueue row AND this row both render and every channel message doubles.
|
|
8604
8711
|
CHANNEL_WRAPPER2.test(text);
|
|
8605
8712
|
if (isChannel) {
|
|
8713
|
+
const u = unwrapChannel(text);
|
|
8714
|
+
turnCtx.lastChannelSource = u.source;
|
|
8606
8715
|
if (!isScheduleSource(text)) {
|
|
8607
|
-
renderOrSuppressChannelInbound(
|
|
8716
|
+
renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress);
|
|
8608
8717
|
}
|
|
8609
8718
|
} else if (row.isMeta === true) {
|
|
8610
8719
|
out.push({ kind: "harness-internal", text, ts });
|
|
@@ -9027,7 +9136,7 @@ data: ${JSON.stringify(turn)}
|
|
|
9027
9136
|
const sessionKey = c.req.query("session_key") ?? "";
|
|
9028
9137
|
const subagentsDir = resolve12(join14(projectDir, sessionId, "subagents"));
|
|
9029
9138
|
console.log(
|
|
9030
|
-
`[webchat-activity] op=watch-subagents key=${sessionKey} dir=${subagentsDir} exists=${
|
|
9139
|
+
`[webchat-activity] op=watch-subagents key=${sessionKey} dir=${subagentsDir} exists=${existsSync9(subagentsDir)}`
|
|
9031
9140
|
);
|
|
9032
9141
|
const actx = makeActivityCtx({
|
|
9033
9142
|
subagentsDir,
|
|
@@ -9044,6 +9153,7 @@ data: ${JSON.stringify(obj)}
|
|
|
9044
9153
|
log: (line) => console.log(line)
|
|
9045
9154
|
});
|
|
9046
9155
|
const queuedPendingSuppress = /* @__PURE__ */ new Map();
|
|
9156
|
+
const turnCtx = {};
|
|
9047
9157
|
const drain = () => {
|
|
9048
9158
|
try {
|
|
9049
9159
|
const { buf, end } = readFrom(jsonlPath, offset);
|
|
@@ -9051,7 +9161,7 @@ data: ${JSON.stringify(obj)}
|
|
|
9051
9161
|
const { lines, nextOffset } = splitNewLines(buf, offset);
|
|
9052
9162
|
offset = nextOffset;
|
|
9053
9163
|
let n = 0;
|
|
9054
|
-
for (const turn of parseTranscript(lines, queuedPendingSuppress)) {
|
|
9164
|
+
for (const turn of parseTranscript(lines, queuedPendingSuppress, turnCtx)) {
|
|
9055
9165
|
send(controller, enrichAdminAttachments(enrichDownloads(turn, scratchpadRoot)), offset);
|
|
9056
9166
|
actx.lastParentTurns.push(turn);
|
|
9057
9167
|
if (turn.kind === "agent-error") {
|
|
@@ -9071,7 +9181,7 @@ data: ${JSON.stringify(obj)}
|
|
|
9071
9181
|
const { buf, end } = readFrom(jsonlPath, 0);
|
|
9072
9182
|
offset = end;
|
|
9073
9183
|
let n = 0;
|
|
9074
|
-
for (const turn of parseTranscript(buf.toString("utf8").split("\n"), queuedPendingSuppress)) {
|
|
9184
|
+
for (const turn of parseTranscript(buf.toString("utf8").split("\n"), queuedPendingSuppress, turnCtx)) {
|
|
9075
9185
|
send(controller, enrichAdminAttachments(enrichDownloads(turn, scratchpadRoot)), offset);
|
|
9076
9186
|
actx.lastParentTurns.push(turn);
|
|
9077
9187
|
if (turn.kind === "agent-error") {
|
|
@@ -9133,7 +9243,7 @@ app4.get("/directives", requireAdminSession, (c) => {
|
|
|
9133
9243
|
const sessionId = c.req.query("sessionId") ?? "";
|
|
9134
9244
|
if (!SESSION_ID_RE2.test(sessionId)) return c.json({ error: "bad session reference" }, 400);
|
|
9135
9245
|
const dir = directiveStoreDir(sessionId);
|
|
9136
|
-
if (!dir || !
|
|
9246
|
+
if (!dir || !existsSync9(dir)) return c.json({ entries: [] });
|
|
9137
9247
|
let names;
|
|
9138
9248
|
try {
|
|
9139
9249
|
names = readdirSync8(dir).filter((n) => DIRECTIVE_NAME_RE.test(n));
|
|
@@ -9191,14 +9301,22 @@ app4.get("/store-stream", requireAdminSession, (c) => {
|
|
|
9191
9301
|
const connId = `${remoteJid.slice(0, 12)}-${process.hrtime.bigint().toString(36).slice(-6)}`;
|
|
9192
9302
|
const startedAt = Date.now();
|
|
9193
9303
|
const lastEventId = c.req.header("Last-Event-ID");
|
|
9194
|
-
const
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9304
|
+
const uploadsBase = resolve12(DATA_ROOT, "accounts", accountId, "uploads");
|
|
9305
|
+
const toTurn = (r) => {
|
|
9306
|
+
const turn = {
|
|
9307
|
+
kind: "whatsapp-persisted",
|
|
9308
|
+
text: r.body,
|
|
9309
|
+
ts: r.dateSent,
|
|
9310
|
+
fromMe: r.fromMe,
|
|
9311
|
+
senderName: r.senderName,
|
|
9312
|
+
origin: r.origin
|
|
9313
|
+
};
|
|
9314
|
+
if (r.attachmentId) {
|
|
9315
|
+
const meta = readAttachmentMeta(join14(uploadsBase, r.attachmentId));
|
|
9316
|
+
if (meta) return { ...turn, attachments: [meta] };
|
|
9317
|
+
}
|
|
9318
|
+
return turn;
|
|
9319
|
+
};
|
|
9202
9320
|
const readable = new ReadableStream({
|
|
9203
9321
|
start(controller) {
|
|
9204
9322
|
const send = (turn, id) => {
|
|
@@ -9382,7 +9500,7 @@ app4.post("/reply", requireAdminSession, async (c) => {
|
|
|
9382
9500
|
return c.json({ error: `WhatsApp account "${accountId}" is registered but its socket is disconnected.` }, 503);
|
|
9383
9501
|
}
|
|
9384
9502
|
console.log(`${MANUAL_TAG} op=socket sendId=${sendId} accountId=${accountId} result=ok socketAccountId=${res.accountId}`);
|
|
9385
|
-
const record = (waMessageId, body) => {
|
|
9503
|
+
const record = (waMessageId, body, attachmentId) => {
|
|
9386
9504
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
9387
9505
|
const rec = {
|
|
9388
9506
|
messageId: `whatsapp-live:${res.accountId}:${remoteJid}:${waMessageId}`,
|
|
@@ -9398,11 +9516,15 @@ app4.post("/reply", requireAdminSession, async (c) => {
|
|
|
9398
9516
|
quotedSender: null,
|
|
9399
9517
|
scope: template.scope,
|
|
9400
9518
|
origin: "operator-manual",
|
|
9401
|
-
createdAt: nowIso
|
|
9519
|
+
createdAt: nowIso,
|
|
9520
|
+
// Task 1536 — a media send carries the servable attachment reference (the
|
|
9521
|
+
// file already lives in the account's uploads store via storeAttachment),
|
|
9522
|
+
// so the store reader renders it inline instead of a `[sent …]` text line.
|
|
9523
|
+
...attachmentId ? { attachmentId } : {}
|
|
9402
9524
|
};
|
|
9403
9525
|
appendMessage(accountId, rec);
|
|
9404
9526
|
const present = readConversation(accountId, remoteJid).some((r) => r.messageId === rec.messageId);
|
|
9405
|
-
console.log(`${MANUAL_TAG} op=recorded sendId=${sendId} waMessageId=${waMessageId} present=${present}`);
|
|
9527
|
+
console.log(`${MANUAL_TAG} op=recorded sendId=${sendId} waMessageId=${waMessageId} present=${present} attachment=${attachmentId ? "yes" : "no"}`);
|
|
9406
9528
|
};
|
|
9407
9529
|
let sent = 0;
|
|
9408
9530
|
if (text) {
|
|
@@ -9440,7 +9562,7 @@ app4.post("/reply", requireAdminSession, async (c) => {
|
|
|
9440
9562
|
mid = r.messageId;
|
|
9441
9563
|
}
|
|
9442
9564
|
console.log(`${MANUAL_TAG} op=sent sendId=${sendId} kind=${kind ?? "document"} idx=${i} waMessageId=${mid ?? ""}`);
|
|
9443
|
-
if (mid) record(mid,
|
|
9565
|
+
if (mid) record(mid, "", stored.attachmentId);
|
|
9444
9566
|
sent++;
|
|
9445
9567
|
}
|
|
9446
9568
|
exit("delivered");
|
|
@@ -9751,7 +9873,7 @@ var public_reader_default = app5;
|
|
|
9751
9873
|
// server/routes/webchat.ts
|
|
9752
9874
|
import { basename as basename7, dirname as dirname6 } from "path";
|
|
9753
9875
|
import { join as join18 } from "path";
|
|
9754
|
-
import { existsSync as
|
|
9876
|
+
import { existsSync as existsSync11, readdirSync as readdirSync11, readFileSync as readFileSync19, renameSync as renameSync4, statSync as statSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
9755
9877
|
|
|
9756
9878
|
// server/canonical-webchat-override.ts
|
|
9757
9879
|
import { readFileSync as readFileSync17, writeFileSync as writeFileSync5, renameSync as renameSync2 } from "fs";
|
|
@@ -9838,7 +9960,7 @@ function isNewSessionEffortLevel(value) {
|
|
|
9838
9960
|
}
|
|
9839
9961
|
|
|
9840
9962
|
// ../services/claude-session-manager/src/sidecar-store.ts
|
|
9841
|
-
import { existsSync as
|
|
9963
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync10, readFileSync as readFileSync18, renameSync as renameSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
9842
9964
|
import { join as join17 } from "path";
|
|
9843
9965
|
function escapeRegExp(s) {
|
|
9844
9966
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -9859,7 +9981,7 @@ function createSidecarStore(config) {
|
|
|
9859
9981
|
return { ok: true };
|
|
9860
9982
|
} catch (error) {
|
|
9861
9983
|
try {
|
|
9862
|
-
if (
|
|
9984
|
+
if (existsSync10(tmp)) unlinkSync2(tmp);
|
|
9863
9985
|
} catch {
|
|
9864
9986
|
}
|
|
9865
9987
|
return { ok: false, error };
|
|
@@ -9915,7 +10037,7 @@ function createSidecarStore(config) {
|
|
|
9915
10037
|
function clear(sessionsDir, id) {
|
|
9916
10038
|
const path2 = pathFor(sessionsDir, id);
|
|
9917
10039
|
try {
|
|
9918
|
-
if (
|
|
10040
|
+
if (existsSync10(path2)) {
|
|
9919
10041
|
unlinkSync2(path2);
|
|
9920
10042
|
return { ok: true, removed: true };
|
|
9921
10043
|
}
|
|
@@ -9983,7 +10105,7 @@ function locateSidecar(sessionId) {
|
|
|
9983
10105
|
}
|
|
9984
10106
|
for (const slug of slugs) {
|
|
9985
10107
|
const metaPath = join18(projectsRoot, slug, `${sessionId}.meta.json`);
|
|
9986
|
-
if (
|
|
10108
|
+
if (existsSync11(metaPath)) {
|
|
9987
10109
|
const meta = readSidecarMeta(metaPath);
|
|
9988
10110
|
return { channel: meta.channel, adminUserId: meta.adminUserId, accountId: meta.accountId };
|
|
9989
10111
|
}
|
|
@@ -10578,7 +10700,7 @@ ${note}` : note;
|
|
|
10578
10700
|
import { resolve as resolve14 } from "path";
|
|
10579
10701
|
|
|
10580
10702
|
// app/lib/claude-agent/specialist-roster.ts
|
|
10581
|
-
import { existsSync as
|
|
10703
|
+
import { existsSync as existsSync12, readFileSync as readFileSync20, readdirSync as readdirSync12 } from "fs";
|
|
10582
10704
|
import { join as join19 } from "path";
|
|
10583
10705
|
function field(fm, name) {
|
|
10584
10706
|
const m = fm.match(new RegExp(`^${name}:\\s*(.*?)\\r?$`, "m"));
|
|
@@ -10590,7 +10712,7 @@ function field(fm, name) {
|
|
|
10590
10712
|
return value || null;
|
|
10591
10713
|
}
|
|
10592
10714
|
function readSpecialistRoster(specialistsDir) {
|
|
10593
|
-
if (!
|
|
10715
|
+
if (!existsSync12(specialistsDir)) return { specialists: [], skipped: [] };
|
|
10594
10716
|
const entries = readdirSync12(specialistsDir);
|
|
10595
10717
|
const specialists = [];
|
|
10596
10718
|
const skipped = [];
|
|
@@ -10716,7 +10838,7 @@ var webchat_greeting_default = app6;
|
|
|
10716
10838
|
// server/routes/telegram.ts
|
|
10717
10839
|
import { homedir } from "os";
|
|
10718
10840
|
import { resolve as resolve15, join as join20 } from "path";
|
|
10719
|
-
import { existsSync as
|
|
10841
|
+
import { existsSync as existsSync13, readFileSync as readFileSync21 } from "fs";
|
|
10720
10842
|
|
|
10721
10843
|
// app/lib/telegram/gateway/instance.ts
|
|
10722
10844
|
var instance = null;
|
|
@@ -10776,7 +10898,7 @@ var TAG23 = "[telegram-inbound]";
|
|
|
10776
10898
|
function configDirName() {
|
|
10777
10899
|
const platformRoot3 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "..");
|
|
10778
10900
|
const brandPath = join20(platformRoot3, "config", "brand.json");
|
|
10779
|
-
if (
|
|
10901
|
+
if (existsSync13(brandPath)) {
|
|
10780
10902
|
try {
|
|
10781
10903
|
return JSON.parse(readFileSync21(brandPath, "utf-8")).configDir ?? ".maxy";
|
|
10782
10904
|
} catch {
|
|
@@ -10797,7 +10919,7 @@ app7.post("/", async (c) => {
|
|
|
10797
10919
|
return c.json({ ok: false }, 400);
|
|
10798
10920
|
}
|
|
10799
10921
|
const sp = secretPath(botType);
|
|
10800
|
-
if (!
|
|
10922
|
+
if (!existsSync13(sp)) {
|
|
10801
10923
|
console.error(`${TAG23} op=reject reason=no-secret-file botType=${botType}`);
|
|
10802
10924
|
return c.json({ ok: false }, 401);
|
|
10803
10925
|
}
|
|
@@ -10865,7 +10987,7 @@ var telegram_default = app7;
|
|
|
10865
10987
|
|
|
10866
10988
|
// server/routes/quickbooks.ts
|
|
10867
10989
|
import { join as join21 } from "path";
|
|
10868
|
-
import { existsSync as
|
|
10990
|
+
import { existsSync as existsSync14, readFileSync as readFileSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync4, rmSync, renameSync as renameSync5 } from "fs";
|
|
10869
10991
|
var TAG24 = "[quickbooks]";
|
|
10870
10992
|
var INTUIT_TOKEN_URL = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer";
|
|
10871
10993
|
var STATE_RE = /^[A-Za-z0-9_-]+$/;
|
|
@@ -10878,7 +11000,7 @@ function storePath(accountDir) {
|
|
|
10878
11000
|
async function completeConsent(args) {
|
|
10879
11001
|
const { accountDir, code, realmId } = args;
|
|
10880
11002
|
const state = args.state;
|
|
10881
|
-
if (!state || !STATE_RE.test(state) || !
|
|
11003
|
+
if (!state || !STATE_RE.test(state) || !existsSync14(pendingPath(accountDir, state))) {
|
|
10882
11004
|
console.error(`${TAG24} op=callback state=${state ?? ""} stateValid=false realmId=${realmId ?? ""}`);
|
|
10883
11005
|
return { ok: false, status: 400, message: "Invalid or unknown authorization state.", stateValid: false };
|
|
10884
11006
|
}
|
|
@@ -10905,7 +11027,7 @@ async function completeConsent(args) {
|
|
|
10905
11027
|
if (!code || !realmId) {
|
|
10906
11028
|
return { ok: false, status: 400, message: "Missing code or realmId in the callback.", stateValid: true };
|
|
10907
11029
|
}
|
|
10908
|
-
if (!
|
|
11030
|
+
if (!existsSync14(storePath(accountDir))) {
|
|
10909
11031
|
return { ok: false, status: 500, message: "No QuickBooks credentials are stored for this account.", stateValid: true };
|
|
10910
11032
|
}
|
|
10911
11033
|
const store2 = JSON.parse(readFileSync22(storePath(accountDir), "utf-8"));
|
|
@@ -10973,8 +11095,8 @@ var quickbooks_default = app8;
|
|
|
10973
11095
|
|
|
10974
11096
|
// server/routes/onboarding.ts
|
|
10975
11097
|
import { spawn, spawnSync as spawnSync2, execFileSync as execFileSync3 } from "child_process";
|
|
10976
|
-
import { openSync as openSync3, closeSync as closeSync3, writeFileSync as writeFileSync10, writeSync, existsSync as
|
|
10977
|
-
import { createHash as
|
|
11098
|
+
import { openSync as openSync3, closeSync as closeSync3, writeFileSync as writeFileSync10, writeSync, existsSync as existsSync16, readFileSync as readFileSync24, unlinkSync as unlinkSync3, renameSync as renameSync7 } from "fs";
|
|
11099
|
+
import { createHash as createHash4, randomUUID as randomUUID7 } from "crypto";
|
|
10978
11100
|
|
|
10979
11101
|
// app/lib/claude-spawn-env.ts
|
|
10980
11102
|
function claudeBin() {
|
|
@@ -11111,7 +11233,7 @@ function readHostCredsBlob(service) {
|
|
|
11111
11233
|
}
|
|
11112
11234
|
|
|
11113
11235
|
// ../lib/admins-write/src/index.ts
|
|
11114
|
-
import { existsSync as
|
|
11236
|
+
import { existsSync as existsSync15, readFileSync as readFileSync23, writeFileSync as writeFileSync9, renameSync as renameSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync13, statSync as statSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
11115
11237
|
import { dirname as dirname7, join as join22 } from "path";
|
|
11116
11238
|
function id8(value) {
|
|
11117
11239
|
return value.slice(0, 8);
|
|
@@ -11152,7 +11274,7 @@ function writeAdminEntry(input) {
|
|
|
11152
11274
|
const result = { usersJsonResult: "noop", accountJsonResult: "noop" };
|
|
11153
11275
|
try {
|
|
11154
11276
|
let users = [];
|
|
11155
|
-
if (
|
|
11277
|
+
if (existsSync15(input.usersFile)) {
|
|
11156
11278
|
const raw = readFileSync23(input.usersFile, "utf-8").trim();
|
|
11157
11279
|
if (raw) {
|
|
11158
11280
|
const parsed = JSON.parse(raw);
|
|
@@ -11185,7 +11307,7 @@ function writeAdminEntry(input) {
|
|
|
11185
11307
|
}
|
|
11186
11308
|
try {
|
|
11187
11309
|
const accountJsonPath = join22(input.accountDir, "account.json");
|
|
11188
|
-
if (!
|
|
11310
|
+
if (!existsSync15(accountJsonPath)) {
|
|
11189
11311
|
throw new Error(`account.json not found at ${accountJsonPath}`);
|
|
11190
11312
|
}
|
|
11191
11313
|
const config = JSON.parse(readFileSync23(accountJsonPath, "utf-8"));
|
|
@@ -11208,7 +11330,7 @@ function writeAdminEntry(input) {
|
|
|
11208
11330
|
function computeAdminStoreDivergence(input) {
|
|
11209
11331
|
const result = { divergences: 0, accountWithoutUsers: [], usersWithoutAccount: [], errors: [] };
|
|
11210
11332
|
const usersUserIds = /* @__PURE__ */ new Set();
|
|
11211
|
-
if (
|
|
11333
|
+
if (existsSync15(input.usersFile)) {
|
|
11212
11334
|
try {
|
|
11213
11335
|
const raw = readFileSync23(input.usersFile, "utf-8").trim();
|
|
11214
11336
|
if (raw) {
|
|
@@ -11222,7 +11344,7 @@ function computeAdminStoreDivergence(input) {
|
|
|
11222
11344
|
}
|
|
11223
11345
|
}
|
|
11224
11346
|
const accountUserIds = /* @__PURE__ */ new Set();
|
|
11225
|
-
if (
|
|
11347
|
+
if (existsSync15(input.accountsDir)) {
|
|
11226
11348
|
let entries;
|
|
11227
11349
|
try {
|
|
11228
11350
|
entries = readdirSync13(input.accountsDir);
|
|
@@ -11239,7 +11361,7 @@ function computeAdminStoreDivergence(input) {
|
|
|
11239
11361
|
continue;
|
|
11240
11362
|
}
|
|
11241
11363
|
const accountJsonPath = join22(accountDir, "account.json");
|
|
11242
|
-
if (!
|
|
11364
|
+
if (!existsSync15(accountJsonPath)) continue;
|
|
11243
11365
|
let admins = [];
|
|
11244
11366
|
try {
|
|
11245
11367
|
const config = JSON.parse(readFileSync23(accountJsonPath, "utf-8"));
|
|
@@ -11287,10 +11409,10 @@ function checkAdminAuthInvariant(input) {
|
|
|
11287
11409
|
|
|
11288
11410
|
// server/routes/onboarding.ts
|
|
11289
11411
|
function hashPin2(pin) {
|
|
11290
|
-
return
|
|
11412
|
+
return createHash4("sha256").update(pin).digest("hex");
|
|
11291
11413
|
}
|
|
11292
11414
|
function readUsersFile2() {
|
|
11293
|
-
if (!
|
|
11415
|
+
if (!existsSync16(USERS_FILE)) return null;
|
|
11294
11416
|
const raw = readFileSync24(USERS_FILE, "utf-8").trim();
|
|
11295
11417
|
if (!raw) return [];
|
|
11296
11418
|
return JSON.parse(raw);
|
|
@@ -11350,7 +11472,7 @@ app9.post("/claude-auth", async (c) => {
|
|
|
11350
11472
|
} catch (err) {
|
|
11351
11473
|
logoutError = err instanceof Error ? err.message : String(err);
|
|
11352
11474
|
}
|
|
11353
|
-
const credentialsPresent =
|
|
11475
|
+
const credentialsPresent = existsSync16(CLAUDE_CREDENTIALS_FILE);
|
|
11354
11476
|
const ranMs = Date.now() - start;
|
|
11355
11477
|
console.log(`[onboarding] op=claude-logout credentialsPresent=${credentialsPresent} ranMs=${ranMs} logoutError=${logoutError ? JSON.stringify(logoutError.slice(0, 120)) : "none"}`);
|
|
11356
11478
|
return c.json({ logged_out: !credentialsPresent });
|
|
@@ -11499,7 +11621,7 @@ app9.post("/set-pin", async (c) => {
|
|
|
11499
11621
|
console.log(`[set-pin] wrote users.json + account.json admins: userId=${userId.slice(0, 8)}\u2026 role=owner`);
|
|
11500
11622
|
if (process.platform === "linux") {
|
|
11501
11623
|
let installOwner = null;
|
|
11502
|
-
if (
|
|
11624
|
+
if (existsSync16(INSTALL_OWNER_FILE)) {
|
|
11503
11625
|
try {
|
|
11504
11626
|
const raw = readFileSync24(INSTALL_OWNER_FILE, "utf-8").trim();
|
|
11505
11627
|
if (raw.length > 0) installOwner = raw;
|
|
@@ -11647,7 +11769,7 @@ app9.put("/set-pin", requireAdminSession, async (c) => {
|
|
|
11647
11769
|
console.log(`[set-pin] changed PIN in place: userId=${sessionUserId.slice(0, 8)}\u2026`);
|
|
11648
11770
|
if (process.platform === "linux") {
|
|
11649
11771
|
let installOwner = null;
|
|
11650
|
-
if (
|
|
11772
|
+
if (existsSync16(INSTALL_OWNER_FILE)) {
|
|
11651
11773
|
try {
|
|
11652
11774
|
const raw = readFileSync24(INSTALL_OWNER_FILE, "utf-8").trim();
|
|
11653
11775
|
if (raw.length > 0) installOwner = raw;
|
|
@@ -11681,7 +11803,7 @@ ${body.newPin}
|
|
|
11681
11803
|
var onboarding_default = app9;
|
|
11682
11804
|
|
|
11683
11805
|
// server/routes/client-error.ts
|
|
11684
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
11806
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync17, renameSync as renameSync8, statSync as statSync9 } from "fs";
|
|
11685
11807
|
import { join as join24 } from "path";
|
|
11686
11808
|
var CLIENT_ERRORS_LOG = join24(LOG_DIR, "client-errors.log");
|
|
11687
11809
|
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
@@ -11731,7 +11853,7 @@ function stackHead(stack) {
|
|
|
11731
11853
|
}
|
|
11732
11854
|
function rotateIfNeeded() {
|
|
11733
11855
|
try {
|
|
11734
|
-
if (!
|
|
11856
|
+
if (!existsSync17(CLIENT_ERRORS_LOG)) return;
|
|
11735
11857
|
const stats = statSync9(CLIENT_ERRORS_LOG);
|
|
11736
11858
|
if (stats.size < MAX_LOG_SIZE) return;
|
|
11737
11859
|
renameSync8(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
|
|
@@ -12051,18 +12173,18 @@ app12.get("/", requireAdminSession, async (c) => {
|
|
|
12051
12173
|
var accounts_default = app12;
|
|
12052
12174
|
|
|
12053
12175
|
// server/routes/admin/logs.ts
|
|
12054
|
-
import { existsSync as
|
|
12176
|
+
import { existsSync as existsSync19, readdirSync as readdirSync14, readFileSync as readFileSync25, statSync as statSync10 } from "fs";
|
|
12055
12177
|
import { resolve as resolve16, basename as basename8 } from "path";
|
|
12056
12178
|
|
|
12057
12179
|
// app/lib/logs-read-resolve.ts
|
|
12058
|
-
import { existsSync as
|
|
12180
|
+
import { existsSync as existsSync18 } from "fs";
|
|
12059
12181
|
import { join as join25 } from "path";
|
|
12060
12182
|
function resolveSessionLogPaths(filename, logDirs) {
|
|
12061
12183
|
const tried = [filename];
|
|
12062
12184
|
const hits = [];
|
|
12063
12185
|
for (const dir of logDirs) {
|
|
12064
12186
|
const fullPath = join25(dir, filename);
|
|
12065
|
-
if (
|
|
12187
|
+
if (existsSync18(fullPath)) {
|
|
12066
12188
|
hits.push({ path: fullPath, dir });
|
|
12067
12189
|
}
|
|
12068
12190
|
}
|
|
@@ -12190,7 +12312,7 @@ app13.get("/", async (c) => {
|
|
|
12190
12312
|
const seen = /* @__PURE__ */ new Set();
|
|
12191
12313
|
const logs = {};
|
|
12192
12314
|
for (const dir of logDirs) {
|
|
12193
|
-
if (!
|
|
12315
|
+
if (!existsSync19(dir)) continue;
|
|
12194
12316
|
let files;
|
|
12195
12317
|
try {
|
|
12196
12318
|
files = readdirSync14(dir).filter((f) => f.endsWith(".log"));
|
|
@@ -12242,7 +12364,7 @@ var claude_info_default = app14;
|
|
|
12242
12364
|
|
|
12243
12365
|
// server/routes/admin/attachment.ts
|
|
12244
12366
|
import { readFile as readFile2, readdir } from "fs/promises";
|
|
12245
|
-
import { existsSync as
|
|
12367
|
+
import { existsSync as existsSync20 } from "fs";
|
|
12246
12368
|
import { resolve as resolve17 } from "path";
|
|
12247
12369
|
var app15 = new Hono();
|
|
12248
12370
|
var ATTACHMENT_ID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
@@ -12265,12 +12387,12 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
12265
12387
|
}
|
|
12266
12388
|
let accountId = pinnedAccountId;
|
|
12267
12389
|
let dir = uploadsDirFor2(accountId, attachmentId);
|
|
12268
|
-
let found =
|
|
12390
|
+
let found = existsSync20(dir);
|
|
12269
12391
|
if (!found) {
|
|
12270
12392
|
for (const candidate of listValidAccounts()) {
|
|
12271
12393
|
if (candidate.accountId === pinnedAccountId) continue;
|
|
12272
12394
|
const candidateDir = uploadsDirFor2(candidate.accountId, attachmentId);
|
|
12273
|
-
if (
|
|
12395
|
+
if (existsSync20(candidateDir)) {
|
|
12274
12396
|
accountId = candidate.accountId;
|
|
12275
12397
|
dir = candidateDir;
|
|
12276
12398
|
found = true;
|
|
@@ -12285,7 +12407,7 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
12285
12407
|
return new Response("Not found", { status: 404 });
|
|
12286
12408
|
}
|
|
12287
12409
|
const metaPath = resolve17(dir, `${attachmentId}.meta.json`);
|
|
12288
|
-
if (!
|
|
12410
|
+
if (!existsSync20(metaPath)) {
|
|
12289
12411
|
return new Response("Not found", { status: 404 });
|
|
12290
12412
|
}
|
|
12291
12413
|
let meta;
|
|
@@ -12313,13 +12435,13 @@ var attachment_default = app15;
|
|
|
12313
12435
|
|
|
12314
12436
|
// server/routes/admin/agents.ts
|
|
12315
12437
|
import { resolve as resolve18 } from "path";
|
|
12316
|
-
import { readdirSync as readdirSync15, readFileSync as readFileSync26, existsSync as
|
|
12438
|
+
import { readdirSync as readdirSync15, readFileSync as readFileSync26, existsSync as existsSync21, rmSync as rmSync2 } from "fs";
|
|
12317
12439
|
var app16 = new Hono();
|
|
12318
12440
|
app16.get("/", (c) => {
|
|
12319
12441
|
const account = resolveAccount();
|
|
12320
12442
|
if (!account) return c.json({ agents: [] });
|
|
12321
12443
|
const agentsDir = resolve18(account.accountDir, "agents");
|
|
12322
|
-
if (!
|
|
12444
|
+
if (!existsSync21(agentsDir)) return c.json({ agents: [] });
|
|
12323
12445
|
const agents = [];
|
|
12324
12446
|
try {
|
|
12325
12447
|
const entries = readdirSync15(agentsDir, { withFileTypes: true });
|
|
@@ -12327,7 +12449,7 @@ app16.get("/", (c) => {
|
|
|
12327
12449
|
if (!entry.isDirectory()) continue;
|
|
12328
12450
|
if (entry.name === "admin") continue;
|
|
12329
12451
|
const configPath2 = resolve18(agentsDir, entry.name, "config.json");
|
|
12330
|
-
if (!
|
|
12452
|
+
if (!existsSync21(configPath2)) continue;
|
|
12331
12453
|
try {
|
|
12332
12454
|
const config = JSON.parse(readFileSync26(configPath2, "utf-8"));
|
|
12333
12455
|
agents.push({
|
|
@@ -12356,7 +12478,7 @@ app16.delete("/:slug", async (c) => {
|
|
|
12356
12478
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
12357
12479
|
}
|
|
12358
12480
|
const agentDir = resolve18(account.accountDir, "agents", slug);
|
|
12359
|
-
if (!
|
|
12481
|
+
if (!existsSync21(agentDir)) {
|
|
12360
12482
|
return c.json({ error: "Agent not found" }, 404);
|
|
12361
12483
|
}
|
|
12362
12484
|
try {
|
|
@@ -12386,7 +12508,7 @@ app16.post("/:slug/project", async (c) => {
|
|
|
12386
12508
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
12387
12509
|
}
|
|
12388
12510
|
const agentDir = resolve18(account.accountDir, "agents", slug);
|
|
12389
|
-
if (!
|
|
12511
|
+
if (!existsSync21(agentDir)) {
|
|
12390
12512
|
return c.json({ error: "Agent not found on disk" }, 404);
|
|
12391
12513
|
}
|
|
12392
12514
|
try {
|
|
@@ -12402,7 +12524,7 @@ var agents_default = app16;
|
|
|
12402
12524
|
// server/routes/admin/sessions.ts
|
|
12403
12525
|
import crypto2 from "crypto";
|
|
12404
12526
|
import { resolve as resolvePath } from "path";
|
|
12405
|
-
import { existsSync as
|
|
12527
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync6, createWriteStream } from "fs";
|
|
12406
12528
|
|
|
12407
12529
|
// ../lib/admin-conversation-purge/src/index.ts
|
|
12408
12530
|
async function purgeAdminConversationJsonls(args) {
|
|
@@ -12562,7 +12684,7 @@ function validateAndShapeAttachments(raws, conversationAccountId, sessionId, mes
|
|
|
12562
12684
|
let reason = null;
|
|
12563
12685
|
if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
|
|
12564
12686
|
else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
|
|
12565
|
-
else if (!
|
|
12687
|
+
else if (!existsSync22(a.storagePath)) reason = "missing-file";
|
|
12566
12688
|
if (reason) {
|
|
12567
12689
|
invalid++;
|
|
12568
12690
|
try {
|
|
@@ -13088,7 +13210,7 @@ app17.put("/:id/label", requireAdminSession, async (c) => {
|
|
|
13088
13210
|
var sessions_default = app17;
|
|
13089
13211
|
|
|
13090
13212
|
// app/lib/claude-agent/spawn-context.ts
|
|
13091
|
-
import { existsSync as
|
|
13213
|
+
import { existsSync as existsSync23, readFileSync as readFileSync27, readdirSync as readdirSync16, statSync as statSync11 } from "fs";
|
|
13092
13214
|
import { dirname as dirname9, resolve as resolve19, join as join26 } from "path";
|
|
13093
13215
|
async function resolveOwnerProfileBlock(accountId, userId) {
|
|
13094
13216
|
if (!userId) return { ok: false, reason: "missing-user-id" };
|
|
@@ -13135,7 +13257,7 @@ function extractPluginToolDescriptions(pluginDir) {
|
|
|
13135
13257
|
];
|
|
13136
13258
|
let raw = null;
|
|
13137
13259
|
for (const path2 of candidates) {
|
|
13138
|
-
if (!
|
|
13260
|
+
if (!existsSync23(path2)) continue;
|
|
13139
13261
|
try {
|
|
13140
13262
|
raw = readFileSync27(path2, "utf-8");
|
|
13141
13263
|
break;
|
|
@@ -13187,7 +13309,7 @@ function parseSkillPathsFromFrontmatter(fm) {
|
|
|
13187
13309
|
function listPluginDirs() {
|
|
13188
13310
|
const out = /* @__PURE__ */ new Map();
|
|
13189
13311
|
const platformPlugins = resolve19(PLATFORM_ROOT, "plugins");
|
|
13190
|
-
if (
|
|
13312
|
+
if (existsSync23(platformPlugins)) {
|
|
13191
13313
|
for (const name of readdirSync16(platformPlugins)) {
|
|
13192
13314
|
const dir = join26(platformPlugins, name);
|
|
13193
13315
|
try {
|
|
@@ -13197,10 +13319,10 @@ function listPluginDirs() {
|
|
|
13197
13319
|
}
|
|
13198
13320
|
}
|
|
13199
13321
|
const premiumRoot = resolve19(dirname9(PLATFORM_ROOT), "premium-plugins");
|
|
13200
|
-
if (
|
|
13322
|
+
if (existsSync23(premiumRoot)) {
|
|
13201
13323
|
for (const bundle of readdirSync16(premiumRoot)) {
|
|
13202
13324
|
const bundlePlugins = join26(premiumRoot, bundle, "plugins");
|
|
13203
|
-
if (!
|
|
13325
|
+
if (!existsSync23(bundlePlugins)) continue;
|
|
13204
13326
|
try {
|
|
13205
13327
|
for (const name of readdirSync16(bundlePlugins)) {
|
|
13206
13328
|
const dir = join26(bundlePlugins, name);
|
|
@@ -13217,7 +13339,7 @@ function listPluginDirs() {
|
|
|
13217
13339
|
}
|
|
13218
13340
|
function readEnabledPlugins(accountId) {
|
|
13219
13341
|
const accountFile = resolve19(PLATFORM_ROOT, "..", "data/accounts", accountId, "account.json");
|
|
13220
|
-
if (!
|
|
13342
|
+
if (!existsSync23(accountFile)) return /* @__PURE__ */ new Set();
|
|
13221
13343
|
try {
|
|
13222
13344
|
const parsed = JSON.parse(readFileSync27(accountFile, "utf-8"));
|
|
13223
13345
|
return new Set(
|
|
@@ -13228,7 +13350,7 @@ function readEnabledPlugins(accountId) {
|
|
|
13228
13350
|
}
|
|
13229
13351
|
}
|
|
13230
13352
|
function readFrontmatter(path2) {
|
|
13231
|
-
if (!
|
|
13353
|
+
if (!existsSync23(path2)) return null;
|
|
13232
13354
|
let raw;
|
|
13233
13355
|
try {
|
|
13234
13356
|
raw = readFileSync27(path2, "utf-8");
|
|
@@ -13263,7 +13385,7 @@ ${fm}
|
|
|
13263
13385
|
`plugin-manifest-tool-coverage plugin=${name} tools=${tools.length} with-desc=${withDesc}`
|
|
13264
13386
|
);
|
|
13265
13387
|
if (toolNames.length > 0 && descMap.size === 0) {
|
|
13266
|
-
const hasSource =
|
|
13388
|
+
const hasSource = existsSync23(join26(dir, "mcp/dist/index.js")) || existsSync23(join26(dir, "mcp/src/index.ts"));
|
|
13267
13389
|
if (hasSource) {
|
|
13268
13390
|
console.warn(
|
|
13269
13391
|
`[spawn-context] WARN plugin=${name} mcp source present but tool-description regex matched zero entries \u2014 SDK upgrade may have changed the registration shape`
|
|
@@ -13290,7 +13412,7 @@ ${skillFm}
|
|
|
13290
13412
|
}
|
|
13291
13413
|
function computeSpecialistDomains(accountId) {
|
|
13292
13414
|
const specialistsDir = resolve19(PLATFORM_ROOT, "..", "data/accounts", accountId, "specialists", "agents");
|
|
13293
|
-
if (!
|
|
13415
|
+
if (!existsSync23(specialistsDir)) return [];
|
|
13294
13416
|
let entries;
|
|
13295
13417
|
try {
|
|
13296
13418
|
entries = readdirSync16(specialistsDir);
|
|
@@ -13336,7 +13458,7 @@ ${fm}
|
|
|
13336
13458
|
}
|
|
13337
13459
|
function computeDormantPlugins(accountId) {
|
|
13338
13460
|
const accountFile = resolve19(PLATFORM_ROOT, "..", "data/accounts", accountId, "account.json");
|
|
13339
|
-
if (!
|
|
13461
|
+
if (!existsSync23(accountFile)) return [];
|
|
13340
13462
|
let enabled;
|
|
13341
13463
|
try {
|
|
13342
13464
|
const raw = readFileSync27(accountFile, "utf-8");
|
|
@@ -13351,7 +13473,7 @@ function computeDormantPlugins(accountId) {
|
|
|
13351
13473
|
return [];
|
|
13352
13474
|
}
|
|
13353
13475
|
const pluginsDir = resolve19(PLATFORM_ROOT, "plugins");
|
|
13354
|
-
if (!
|
|
13476
|
+
if (!existsSync23(pluginsDir)) return [];
|
|
13355
13477
|
let entries;
|
|
13356
13478
|
try {
|
|
13357
13479
|
entries = readdirSync16(pluginsDir);
|
|
@@ -13362,7 +13484,7 @@ function computeDormantPlugins(accountId) {
|
|
|
13362
13484
|
for (const name of entries) {
|
|
13363
13485
|
if (enabled.has(name)) continue;
|
|
13364
13486
|
const manifestPath = resolve19(pluginsDir, name, "PLUGIN.md");
|
|
13365
|
-
if (!
|
|
13487
|
+
if (!existsSync23(manifestPath)) continue;
|
|
13366
13488
|
let manifest;
|
|
13367
13489
|
try {
|
|
13368
13490
|
manifest = readFileSync27(manifestPath, "utf-8");
|
|
@@ -13379,11 +13501,11 @@ function computeDormantPlugins(accountId) {
|
|
|
13379
13501
|
}
|
|
13380
13502
|
|
|
13381
13503
|
// server/routes/admin/claude-sessions.ts
|
|
13382
|
-
import { existsSync as
|
|
13504
|
+
import { existsSync as existsSync24, readFileSync as readFileSync28 } from "fs";
|
|
13383
13505
|
import { resolve as resolve20 } from "path";
|
|
13384
13506
|
function readTunnelState(brandConfigDir) {
|
|
13385
13507
|
const statePath = `${process.env.HOME ?? ""}/${brandConfigDir}/cloudflared/tunnel.state`;
|
|
13386
|
-
if (!
|
|
13508
|
+
if (!existsSync24(statePath)) return null;
|
|
13387
13509
|
try {
|
|
13388
13510
|
const parsed = JSON.parse(readFileSync28(statePath, "utf-8"));
|
|
13389
13511
|
const tunnelId = typeof parsed.tunnelId === "string" ? parsed.tunnelId : null;
|
|
@@ -13641,7 +13763,7 @@ app20.post("/", async (c) => {
|
|
|
13641
13763
|
var events_default = app20;
|
|
13642
13764
|
|
|
13643
13765
|
// server/routes/admin/files.ts
|
|
13644
|
-
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2, existsSync as
|
|
13766
|
+
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2, existsSync as existsSync25 } from "fs";
|
|
13645
13767
|
import { readdir as readdir3, readFile as readFile4, stat as stat4, mkdir as mkdir3, unlink as unlink2, rename } from "fs/promises";
|
|
13646
13768
|
import { realpathSync as realpathSync5 } from "fs";
|
|
13647
13769
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
@@ -15004,7 +15126,7 @@ app21.post("/upload", requireAdminSession, async (c) => {
|
|
|
15004
15126
|
});
|
|
15005
15127
|
await unlink2(tmpPath).catch(() => {
|
|
15006
15128
|
});
|
|
15007
|
-
const tempRemoved = !
|
|
15129
|
+
const tempRemoved = !existsSync25(tmpPath);
|
|
15008
15130
|
console.error(`[data-upload] op=cleanup uid=${uid} tempRemoved=${tempRemoved}`);
|
|
15009
15131
|
};
|
|
15010
15132
|
const reader = body.getReader();
|
|
@@ -15078,7 +15200,7 @@ async function discardChunkUpload(uploadId, state) {
|
|
|
15078
15200
|
});
|
|
15079
15201
|
await unlink2(state.tmpPath).catch(() => {
|
|
15080
15202
|
});
|
|
15081
|
-
const tempRemoved = !
|
|
15203
|
+
const tempRemoved = !existsSync25(state.tmpPath);
|
|
15082
15204
|
console.error(`[data-upload] op=cleanup uid=${uploadId} tempRemoved=${tempRemoved}`);
|
|
15083
15205
|
}
|
|
15084
15206
|
async function writeChunkBody(body, out, alreadyReceived, ceiling) {
|
|
@@ -15488,7 +15610,7 @@ function applyBacklinkBoost(hits) {
|
|
|
15488
15610
|
}
|
|
15489
15611
|
|
|
15490
15612
|
// ../lib/graph-search/src/dedup.ts
|
|
15491
|
-
import { createHash as
|
|
15613
|
+
import { createHash as createHash5 } from "crypto";
|
|
15492
15614
|
var DEFAULT_LAYERS = [
|
|
15493
15615
|
"nodeId",
|
|
15494
15616
|
"slug",
|
|
@@ -15515,7 +15637,7 @@ function readContentHash(props) {
|
|
|
15515
15637
|
const content = props["content"];
|
|
15516
15638
|
const source = typeof compiled === "string" && compiled.length > 0 ? compiled : typeof content === "string" && content.length > 0 ? content : null;
|
|
15517
15639
|
if (source === null) return null;
|
|
15518
|
-
return
|
|
15640
|
+
return createHash5("sha256").update(source).digest("hex").slice(0, 16);
|
|
15519
15641
|
}
|
|
15520
15642
|
function dedup(hits, layers = DEFAULT_LAYERS) {
|
|
15521
15643
|
const sorted = [...hits].sort((a, b) => b.score - a.score);
|
|
@@ -17535,7 +17657,7 @@ var graph_default_view_default = app27;
|
|
|
17535
17657
|
// server/routes/admin/sidebar-artefacts.ts
|
|
17536
17658
|
import { readdir as readdir4, stat as stat5 } from "fs/promises";
|
|
17537
17659
|
import { resolve as resolve23, relative as relative5, isAbsolute as isAbsolute2, sep as sep7, basename as basename11 } from "path";
|
|
17538
|
-
import { existsSync as
|
|
17660
|
+
import { existsSync as existsSync26 } from "fs";
|
|
17539
17661
|
var LIMIT = 50;
|
|
17540
17662
|
var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
|
|
17541
17663
|
function toDataRootRelPath(absPath) {
|
|
@@ -17642,7 +17764,7 @@ async function fetchAgentTemplateRows(accountDir) {
|
|
|
17642
17764
|
async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
17643
17765
|
const names = /* @__PURE__ */ new Set();
|
|
17644
17766
|
for (const dir of [overrideDir, bundledDir]) {
|
|
17645
|
-
if (!
|
|
17767
|
+
if (!existsSync26(dir)) continue;
|
|
17646
17768
|
try {
|
|
17647
17769
|
const entries = await readdir4(dir);
|
|
17648
17770
|
for (const entry of entries) {
|
|
@@ -17657,7 +17779,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
|
17657
17779
|
}
|
|
17658
17780
|
async function readAgentTemplateRow(inp) {
|
|
17659
17781
|
let chosenPath = null;
|
|
17660
|
-
if (
|
|
17782
|
+
if (existsSync26(inp.overridePath)) {
|
|
17661
17783
|
try {
|
|
17662
17784
|
validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
|
|
17663
17785
|
chosenPath = inp.overridePath;
|
|
@@ -17668,7 +17790,7 @@ async function readAgentTemplateRow(inp) {
|
|
|
17668
17790
|
);
|
|
17669
17791
|
return null;
|
|
17670
17792
|
}
|
|
17671
|
-
} else if (
|
|
17793
|
+
} else if (existsSync26(inp.bundledPath)) {
|
|
17672
17794
|
if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
|
|
17673
17795
|
console.error(
|
|
17674
17796
|
`[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
|
|
@@ -18271,12 +18393,12 @@ app36.get("/", async (c) => {
|
|
|
18271
18393
|
var system_stats_default = app36;
|
|
18272
18394
|
|
|
18273
18395
|
// server/routes/admin/health.ts
|
|
18274
|
-
import { existsSync as
|
|
18396
|
+
import { existsSync as existsSync27, readFileSync as readFileSync29 } from "fs";
|
|
18275
18397
|
import { resolve as resolve24, join as join29 } from "path";
|
|
18276
18398
|
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve24(process.cwd(), "..");
|
|
18277
18399
|
var brandHostname = "maxy";
|
|
18278
18400
|
var brandJsonPath = join29(PLATFORM_ROOT6, "config", "brand.json");
|
|
18279
|
-
if (
|
|
18401
|
+
if (existsSync27(brandJsonPath)) {
|
|
18280
18402
|
try {
|
|
18281
18403
|
const brand = JSON.parse(readFileSync29(brandJsonPath, "utf-8"));
|
|
18282
18404
|
if (brand.hostname) brandHostname = brand.hostname;
|
|
@@ -18287,7 +18409,7 @@ var VERSION_FILE = resolve24(PLATFORM_ROOT6, `config/.${brandHostname}-version`)
|
|
|
18287
18409
|
var PROCESS_STARTED_AT = (/* @__PURE__ */ new Date()).toISOString();
|
|
18288
18410
|
var PROBE_TIMEOUT_MS = 1e3;
|
|
18289
18411
|
function readVersion() {
|
|
18290
|
-
if (!
|
|
18412
|
+
if (!existsSync27(VERSION_FILE)) return "unknown";
|
|
18291
18413
|
return readFileSync29(VERSION_FILE, "utf-8").trim() || "unknown";
|
|
18292
18414
|
}
|
|
18293
18415
|
async function probeConversationDb() {
|
|
@@ -19064,7 +19186,7 @@ app44.post("/launch", requireAdminSession, async (c) => {
|
|
|
19064
19186
|
var browser_default = app44;
|
|
19065
19187
|
|
|
19066
19188
|
// server/routes/admin/calendar.ts
|
|
19067
|
-
import { existsSync as
|
|
19189
|
+
import { existsSync as existsSync28 } from "fs";
|
|
19068
19190
|
import { join as join31 } from "path";
|
|
19069
19191
|
|
|
19070
19192
|
// server/lib/calendar-ics.ts
|
|
@@ -19536,7 +19658,7 @@ app45.get("/booking-link", requireAdminSession, (c) => {
|
|
|
19536
19658
|
console.error('[calendar] op=booking-link-fail err="no account configured"');
|
|
19537
19659
|
return c.json({ bookingDomain: null });
|
|
19538
19660
|
}
|
|
19539
|
-
if (!
|
|
19661
|
+
if (!existsSync28(join31(account.accountDir, AVAILABILITY_FILENAME))) {
|
|
19540
19662
|
console.log("[calendar] op=booking-link bookingDomain=none source=availability-config");
|
|
19541
19663
|
return c.json({ bookingDomain: null });
|
|
19542
19664
|
}
|
|
@@ -20084,7 +20206,7 @@ app53.route("/request-magic-link", request_magic_link_default);
|
|
|
20084
20206
|
var access_default = app53;
|
|
20085
20207
|
|
|
20086
20208
|
// server/routes/sites.ts
|
|
20087
|
-
import { existsSync as
|
|
20209
|
+
import { existsSync as existsSync29, readFileSync as readFileSync31, realpathSync as realpathSync6, statSync as statSync12 } from "fs";
|
|
20088
20210
|
import { resolve as resolve26 } from "path";
|
|
20089
20211
|
var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|
|
20090
20212
|
var MIME = {
|
|
@@ -20150,7 +20272,7 @@ app54.get("/:rel{.*}", (c) => {
|
|
|
20150
20272
|
}
|
|
20151
20273
|
let stat8;
|
|
20152
20274
|
try {
|
|
20153
|
-
stat8 =
|
|
20275
|
+
stat8 = existsSync29(filePath) ? statSync12(filePath) : null;
|
|
20154
20276
|
} catch {
|
|
20155
20277
|
stat8 = null;
|
|
20156
20278
|
}
|
|
@@ -20169,7 +20291,7 @@ app54.get("/:rel{.*}", (c) => {
|
|
|
20169
20291
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
20170
20292
|
return c.text("Forbidden", 403);
|
|
20171
20293
|
}
|
|
20172
|
-
if (!
|
|
20294
|
+
if (!existsSync29(filePath)) {
|
|
20173
20295
|
console.error(`[sites] not-found path=${reqPath} status=404`);
|
|
20174
20296
|
return c.text("Not found", 404);
|
|
20175
20297
|
}
|
|
@@ -20300,7 +20422,7 @@ var VISITOR_COOKIE_NAME = "mxy_v";
|
|
|
20300
20422
|
var VISITOR_COOKIE_MAX_AGE_SECONDS = Math.floor(DEFAULT_TTL_MS / 1e3);
|
|
20301
20423
|
|
|
20302
20424
|
// app/lib/brand-config.ts
|
|
20303
|
-
import { existsSync as
|
|
20425
|
+
import { existsSync as existsSync30, readFileSync as readFileSync33 } from "fs";
|
|
20304
20426
|
import { join as join32 } from "path";
|
|
20305
20427
|
var cached = null;
|
|
20306
20428
|
var cachedAttempted = false;
|
|
@@ -20310,7 +20432,7 @@ function readBrandConfig() {
|
|
|
20310
20432
|
const platformRoot3 = process.env.MAXY_PLATFORM_ROOT;
|
|
20311
20433
|
if (!platformRoot3) return null;
|
|
20312
20434
|
const brandPath = join32(platformRoot3, "config", "brand.json");
|
|
20313
|
-
if (!
|
|
20435
|
+
if (!existsSync30(brandPath)) return null;
|
|
20314
20436
|
try {
|
|
20315
20437
|
cached = JSON.parse(readFileSync33(brandPath, "utf-8"));
|
|
20316
20438
|
return cached;
|
|
@@ -20801,7 +20923,7 @@ var visitor_event_default = app57;
|
|
|
20801
20923
|
|
|
20802
20924
|
// server/routes/session.ts
|
|
20803
20925
|
import { resolve as resolve27 } from "path";
|
|
20804
|
-
import { existsSync as
|
|
20926
|
+
import { existsSync as existsSync31, writeFileSync as writeFileSync12, mkdirSync as mkdirSync8 } from "fs";
|
|
20805
20927
|
var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20806
20928
|
function writeBrandingCache(accountId, agentSlug, branding) {
|
|
20807
20929
|
try {
|
|
@@ -20885,7 +21007,7 @@ app58.post("/", async (c) => {
|
|
|
20885
21007
|
let agentConfig = null;
|
|
20886
21008
|
if (account) {
|
|
20887
21009
|
const agentDir = resolve27(account.accountDir, "agents", agentSlug);
|
|
20888
|
-
if (!
|
|
21010
|
+
if (!existsSync31(agentDir) || !existsSync31(resolve27(agentDir, "config.json"))) {
|
|
20889
21011
|
return c.json({ error: "Agent not found" }, 404);
|
|
20890
21012
|
}
|
|
20891
21013
|
agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
|
|
@@ -21180,9 +21302,17 @@ function startTimeEntryCensus(openSession, opts = {}) {
|
|
|
21180
21302
|
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
21181
21303
|
const horizonMs = opts.horizonMs ?? DEFAULT_HORIZON_MS;
|
|
21182
21304
|
const tick = async () => {
|
|
21183
|
-
|
|
21305
|
+
let session;
|
|
21306
|
+
try {
|
|
21307
|
+
session = openSession();
|
|
21308
|
+
} catch (err) {
|
|
21309
|
+
console.error(`${TAG39} open-session-failed error="${err instanceof Error ? err.message : String(err)}"`);
|
|
21310
|
+
return;
|
|
21311
|
+
}
|
|
21184
21312
|
try {
|
|
21185
21313
|
await runTimeEntryCensusOnce(session, Date.now(), horizonMs);
|
|
21314
|
+
} catch (err) {
|
|
21315
|
+
console.error(`${TAG39} tick-failed error="${err instanceof Error ? err.message : String(err)}"`);
|
|
21186
21316
|
} finally {
|
|
21187
21317
|
try {
|
|
21188
21318
|
await session.close();
|
|
@@ -21545,7 +21675,7 @@ async function migrateUploads(opts = {}) {
|
|
|
21545
21675
|
|
|
21546
21676
|
// app/lib/migrate-admin-webchat-sidecars.ts
|
|
21547
21677
|
import { readdir as readdir6, readFile as readFile6, rename as rename3, writeFile as writeFile4, unlink as unlink3 } from "fs/promises";
|
|
21548
|
-
import { existsSync as
|
|
21678
|
+
import { existsSync as existsSync32 } from "fs";
|
|
21549
21679
|
import { join as join33 } from "path";
|
|
21550
21680
|
var ADMIN_ROLE2 = "admin";
|
|
21551
21681
|
var WEBCHAT_CHANNEL2 = "webchat";
|
|
@@ -21575,7 +21705,7 @@ async function writeRaw(path2, obj) {
|
|
|
21575
21705
|
await rename3(tmp, path2);
|
|
21576
21706
|
} catch (err) {
|
|
21577
21707
|
try {
|
|
21578
|
-
if (
|
|
21708
|
+
if (existsSync32(tmp)) await unlink3(tmp);
|
|
21579
21709
|
} catch {
|
|
21580
21710
|
}
|
|
21581
21711
|
throw err;
|
|
@@ -22073,7 +22203,24 @@ function createWaChannelRoutes(deps) {
|
|
|
22073
22203
|
if (typeof senderId !== "string" || typeof sessionId !== "string" || typeof replied !== "boolean") {
|
|
22074
22204
|
return c.json({ error: "senderId, sessionId and replied required" }, 400);
|
|
22075
22205
|
}
|
|
22206
|
+
const interactive = body?.interactive === true;
|
|
22207
|
+
const finalText = typeof body?.finalText === "string" ? body.finalText : "";
|
|
22076
22208
|
const sid = sessionId.slice(0, 8);
|
|
22209
|
+
const deliver = interactive && finalText.trim().length > 0;
|
|
22210
|
+
if (deliver) {
|
|
22211
|
+
const bytes = Buffer.byteLength(finalText, "utf8");
|
|
22212
|
+
try {
|
|
22213
|
+
await deps.sendOutbound(senderId, finalText);
|
|
22214
|
+
} catch (err) {
|
|
22215
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22216
|
+
console.error(
|
|
22217
|
+
`[whatsapp-native] op=turn-strand-anomaly senderId=${senderId} sessionId=${sid} interactive=yes bytes=${bytes} delivered=undelivered error=${message}`
|
|
22218
|
+
);
|
|
22219
|
+
return c.json({ ok: true, delivered: "undelivered" });
|
|
22220
|
+
}
|
|
22221
|
+
console.error(`[whatsapp-native] op=turn-end sessionId=${sid} replied=${replied ? "yes" : "no"} delivered=fallback bytes=${bytes}`);
|
|
22222
|
+
return c.json({ ok: true, delivered: "fallback" });
|
|
22223
|
+
}
|
|
22077
22224
|
if (replied) {
|
|
22078
22225
|
console.error(`[whatsapp-native] op=turn-end sessionId=${sid} replied=yes delivered=reply`);
|
|
22079
22226
|
return c.json({ ok: true, delivered: "reply" });
|
|
@@ -22194,6 +22341,11 @@ var WaGateway = class {
|
|
|
22194
22341
|
console.error(
|
|
22195
22342
|
`[whatsapp-native] op=inbound senderId=${input.senderId} accountId=${input.accountId} bytes=${bytes} ${mediaField} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"}`
|
|
22196
22343
|
);
|
|
22344
|
+
if (source === "schedule" && (hadSubscriber || this.spawning.has(input.senderId))) {
|
|
22345
|
+
console.error(
|
|
22346
|
+
`[whatsapp-native] op=coalesced-onto-live-session eventId=${input.scheduleProvenance?.eventId ?? "-"} senderId=${input.senderId} droppedSeat=${input.scheduledSessionId ?? "-"} reason=${hadSubscriber ? "live-subscriber" : "spawn-in-flight"}`
|
|
22347
|
+
);
|
|
22348
|
+
}
|
|
22197
22349
|
if (!hadSubscriber && !this.spawning.has(input.senderId)) {
|
|
22198
22350
|
this.spawning.add(input.senderId);
|
|
22199
22351
|
console.error(`[whatsapp-native] op=spawn-trigger senderId=${input.senderId}`);
|
|
@@ -22500,17 +22652,20 @@ function createWebchatChannelRoutes(deps) {
|
|
|
22500
22652
|
if (typeof key !== "string" || typeof sessionId !== "string" || typeof replied !== "boolean") {
|
|
22501
22653
|
return c.json({ error: "key, sessionId and replied required" }, 400);
|
|
22502
22654
|
}
|
|
22655
|
+
const interactive = body?.interactive === true;
|
|
22656
|
+
const finalBytes = typeof body?.finalBytes === "number" && Number.isFinite(body.finalBytes) ? body.finalBytes : 0;
|
|
22503
22657
|
const k = key.slice(0, 8);
|
|
22504
22658
|
const sid = sessionId.slice(0, 8);
|
|
22659
|
+
if (interactive && finalBytes > 0) {
|
|
22660
|
+
console.error(`[webchat-native] op=turn-end channel=webchat key=${k} sessionId=${sid} replied=${replied ? "yes" : "no"} delivered=fallback bytes=${finalBytes}`);
|
|
22661
|
+
return c.json({ ok: true, delivered: "fallback" });
|
|
22662
|
+
}
|
|
22505
22663
|
if (replied) {
|
|
22664
|
+
console.error(`[webchat-native] op=turn-end channel=webchat key=${k} sessionId=${sid} delivered=reply`);
|
|
22506
22665
|
return c.json({ ok: true, delivered: "reply" });
|
|
22507
22666
|
}
|
|
22508
|
-
|
|
22509
|
-
|
|
22510
|
-
return c.json({ ok: true, delivered: "none" });
|
|
22511
|
-
}
|
|
22512
|
-
console.error(`[webchat-native] op=turn-undelivered channel=webchat key=${k} sessionId=${sid} bytes=${finalBytes}`);
|
|
22513
|
-
return c.json({ ok: true, delivered: "undelivered" });
|
|
22667
|
+
console.error(`[webchat-native] op=turn-end channel=webchat key=${k} sessionId=${sid} delivered=none`);
|
|
22668
|
+
return c.json({ ok: true, delivered: "none" });
|
|
22514
22669
|
});
|
|
22515
22670
|
return app61;
|
|
22516
22671
|
}
|
|
@@ -23443,7 +23598,24 @@ function createTelegramChannelRoutes(deps) {
|
|
|
23443
23598
|
if (typeof key !== "string" || typeof sessionId !== "string" || typeof replied !== "boolean") {
|
|
23444
23599
|
return c.json({ error: "key, sessionId and replied required" }, 400);
|
|
23445
23600
|
}
|
|
23601
|
+
const interactive = body?.interactive === true;
|
|
23602
|
+
const finalText = typeof body?.finalText === "string" ? body.finalText : "";
|
|
23446
23603
|
const sid = sessionId.slice(0, 8);
|
|
23604
|
+
const deliver = interactive && finalText.trim().length > 0;
|
|
23605
|
+
if (deliver) {
|
|
23606
|
+
const bytes = Buffer.byteLength(finalText, "utf8");
|
|
23607
|
+
try {
|
|
23608
|
+
await deps.sendOutbound(key, finalText);
|
|
23609
|
+
} catch (err) {
|
|
23610
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
23611
|
+
console.error(
|
|
23612
|
+
`[telegram-native] op=turn-strand-anomaly key=${key} sessionId=${sid} interactive=yes bytes=${bytes} delivered=undelivered error=${message}`
|
|
23613
|
+
);
|
|
23614
|
+
return c.json({ ok: true, delivered: "undelivered" });
|
|
23615
|
+
}
|
|
23616
|
+
console.error(`[telegram-native] op=turn-end key=${key} sessionId=${sid} replied=${replied ? "yes" : "no"} delivered=fallback bytes=${bytes}`);
|
|
23617
|
+
return c.json({ ok: true, delivered: "fallback" });
|
|
23618
|
+
}
|
|
23447
23619
|
if (replied) {
|
|
23448
23620
|
console.error(`[telegram-native] op=turn-end key=${key} sessionId=${sid} replied=yes delivered=reply`);
|
|
23449
23621
|
return c.json({ ok: true, delivered: "reply" });
|
|
@@ -23524,6 +23696,11 @@ var TelegramGateway = class {
|
|
|
23524
23696
|
console.error(
|
|
23525
23697
|
`[telegram-native] op=inbound key=${input.senderId} accountId=${input.accountId} botType=${input.role} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"}`
|
|
23526
23698
|
);
|
|
23699
|
+
if (source === "schedule" && (hadSubscriber || this.spawning.has(input.senderId))) {
|
|
23700
|
+
console.error(
|
|
23701
|
+
`[telegram-native] op=coalesced-onto-live-session eventId=${input.scheduleProvenance?.eventId ?? "-"} senderId=${input.senderId} droppedSeat=${input.scheduledSessionId ?? "-"} reason=${hadSubscriber ? "live-subscriber" : "spawn-in-flight"}`
|
|
23702
|
+
);
|
|
23703
|
+
}
|
|
23527
23704
|
if (!hadSubscriber && !this.spawning.has(input.senderId)) {
|
|
23528
23705
|
this.spawning.add(input.senderId);
|
|
23529
23706
|
console.error(`[telegram-native] op=spawn-trigger key=${input.senderId} role=${input.role}`);
|
|
@@ -23977,8 +24154,8 @@ function broadcastAdminShutdown(reason) {
|
|
|
23977
24154
|
}
|
|
23978
24155
|
|
|
23979
24156
|
// ../lib/entitlement/src/index.ts
|
|
23980
|
-
import { createPublicKey, createHash as
|
|
23981
|
-
import { existsSync as
|
|
24157
|
+
import { createPublicKey, createHash as createHash6, verify as cryptoVerify } from "crypto";
|
|
24158
|
+
import { existsSync as existsSync33, readFileSync as readFileSync34, statSync as statSync13 } from "fs";
|
|
23982
24159
|
import { resolve as resolve32 } from "path";
|
|
23983
24160
|
|
|
23984
24161
|
// ../lib/entitlement/src/canonicalize.ts
|
|
@@ -24027,7 +24204,7 @@ function resolveEntitlement(brand, account) {
|
|
|
24027
24204
|
return logResolved(implicitTrust(account), null);
|
|
24028
24205
|
}
|
|
24029
24206
|
const entitlementPath = resolve32(brand.configDir, "entitlement.json");
|
|
24030
|
-
if (!
|
|
24207
|
+
if (!existsSync33(entitlementPath)) {
|
|
24031
24208
|
return logResolved(anonymousFallback("missing"), { reason: "missing" });
|
|
24032
24209
|
}
|
|
24033
24210
|
const stat8 = statSync13(entitlementPath);
|
|
@@ -24048,7 +24225,7 @@ function verifyAndResolve(brand, entitlementPath, account) {
|
|
|
24048
24225
|
reason: "pubkey-missing"
|
|
24049
24226
|
});
|
|
24050
24227
|
}
|
|
24051
|
-
const pubkeyHash =
|
|
24228
|
+
const pubkeyHash = createHash6("sha256").update(pubkeyPem).digest("hex");
|
|
24052
24229
|
if (pubkeyHash !== PUBKEY_SHA256) {
|
|
24053
24230
|
return logResolved(anonymousFallback("pubkey-tamper"), {
|
|
24054
24231
|
reason: "pubkey-tamper"
|
|
@@ -24219,10 +24396,10 @@ function clientFrom(c) {
|
|
|
24219
24396
|
var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
24220
24397
|
var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join35(PLATFORM_ROOT8, "config", "brand.json") : "";
|
|
24221
24398
|
var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
|
|
24222
|
-
if (BRAND_JSON_PATH && !
|
|
24399
|
+
if (BRAND_JSON_PATH && !existsSync34(BRAND_JSON_PATH)) {
|
|
24223
24400
|
console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
|
|
24224
24401
|
}
|
|
24225
|
-
if (BRAND_JSON_PATH &&
|
|
24402
|
+
if (BRAND_JSON_PATH && existsSync34(BRAND_JSON_PATH)) {
|
|
24226
24403
|
try {
|
|
24227
24404
|
const parsed = JSON.parse(readFileSync35(BRAND_JSON_PATH, "utf-8"));
|
|
24228
24405
|
BRAND = { ...BRAND, ...parsed };
|
|
@@ -24259,7 +24436,7 @@ var brandLoginOpts = {
|
|
|
24259
24436
|
var ALIAS_DOMAINS_PATH = join35(homedir3(), BRAND.configDir, "alias-domains.json");
|
|
24260
24437
|
function loadAliasDomains() {
|
|
24261
24438
|
try {
|
|
24262
|
-
if (!
|
|
24439
|
+
if (!existsSync34(ALIAS_DOMAINS_PATH)) return null;
|
|
24263
24440
|
const parsed = JSON.parse(readFileSync35(ALIAS_DOMAINS_PATH, "utf-8"));
|
|
24264
24441
|
if (!Array.isArray(parsed)) {
|
|
24265
24442
|
console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
|
|
@@ -24344,7 +24521,7 @@ var waGateway = new WaGateway({
|
|
|
24344
24521
|
},
|
|
24345
24522
|
ensureChannelSession: async ({ accountId, effectiveAccountId, senderId, role, personId, gatewayUrl, serverPath, passive, text, msgId, sessionIdOverride }) => {
|
|
24346
24523
|
if (passive) {
|
|
24347
|
-
const phoneHash =
|
|
24524
|
+
const phoneHash = createHash7("sha256").update(senderId).digest("hex").slice(0, 12);
|
|
24348
24525
|
const prompt = (text ?? "").trim();
|
|
24349
24526
|
if (prompt.length === 0) {
|
|
24350
24527
|
console.error(`[wa-passive] op=dropped account=${effectiveAccountId} phoneHash=${phoneHash} msgId=${msgId ?? "none"} reason=empty-text`);
|
|
@@ -24941,7 +25118,7 @@ app60.get("/agent-assets/:slug/:filename", (c) => {
|
|
|
24941
25118
|
console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
|
|
24942
25119
|
return c.text("Forbidden", 403);
|
|
24943
25120
|
}
|
|
24944
|
-
if (!
|
|
25121
|
+
if (!existsSync34(filePath)) {
|
|
24945
25122
|
console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
|
|
24946
25123
|
return c.text("Not found", 404);
|
|
24947
25124
|
}
|
|
@@ -24971,7 +25148,7 @@ app60.get("/generated/:filename", (c) => {
|
|
|
24971
25148
|
console.error(`[generated] serve file=${filename} status=403`);
|
|
24972
25149
|
return c.text("Forbidden", 403);
|
|
24973
25150
|
}
|
|
24974
|
-
if (!
|
|
25151
|
+
if (!existsSync34(filePath)) {
|
|
24975
25152
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
24976
25153
|
return c.text("Not found", 404);
|
|
24977
25154
|
}
|
|
@@ -24991,7 +25168,7 @@ app60.route("/v", visitor_consent_default);
|
|
|
24991
25168
|
var htmlCache = /* @__PURE__ */ new Map();
|
|
24992
25169
|
var brandLogoPath = "/brand/maxy-monochrome.png";
|
|
24993
25170
|
var brandIconPath = "/brand/maxy-monochrome.png";
|
|
24994
|
-
if (BRAND_JSON_PATH &&
|
|
25171
|
+
if (BRAND_JSON_PATH && existsSync34(BRAND_JSON_PATH)) {
|
|
24995
25172
|
try {
|
|
24996
25173
|
const fullBrand = JSON.parse(readFileSync35(BRAND_JSON_PATH, "utf-8"));
|
|
24997
25174
|
if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
|
|
@@ -25020,7 +25197,7 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
|
|
|
25020
25197
|
var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
|
|
25021
25198
|
var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
|
|
25022
25199
|
var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskableIconPath].every(
|
|
25023
|
-
(p) =>
|
|
25200
|
+
(p) => existsSync34(resolve33(process.cwd(), "public", p.replace(/^\//, "")))
|
|
25024
25201
|
);
|
|
25025
25202
|
var SW_SOURCE = (() => {
|
|
25026
25203
|
try {
|
|
@@ -25033,7 +25210,7 @@ function readInstalledVersion() {
|
|
|
25033
25210
|
try {
|
|
25034
25211
|
if (!PLATFORM_ROOT8) return "unknown";
|
|
25035
25212
|
const versionFile = join35(PLATFORM_ROOT8, "config", `.${BRAND.hostname}-version`);
|
|
25036
|
-
if (!
|
|
25213
|
+
if (!existsSync34(versionFile)) return "unknown";
|
|
25037
25214
|
const content = readFileSync35(versionFile, "utf-8").trim();
|
|
25038
25215
|
return content || "unknown";
|
|
25039
25216
|
} catch {
|
|
@@ -25097,7 +25274,7 @@ function loadBrandingCache(agentSlug) {
|
|
|
25097
25274
|
const accountId = getDefaultAccountId();
|
|
25098
25275
|
if (!accountId) return null;
|
|
25099
25276
|
const cachePath = join35(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
|
|
25100
|
-
if (!
|
|
25277
|
+
if (!existsSync34(cachePath)) return null;
|
|
25101
25278
|
return JSON.parse(readFileSync35(cachePath, "utf-8"));
|
|
25102
25279
|
} catch {
|
|
25103
25280
|
return null;
|
|
@@ -25324,7 +25501,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
|
|
|
25324
25501
|
const reconcileScript = resolve33(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
|
|
25325
25502
|
const RECONCILE_INTERVAL_MS = 12e4;
|
|
25326
25503
|
const runReconcile = () => {
|
|
25327
|
-
if (!
|
|
25504
|
+
if (!existsSync34(reconcileScript)) return;
|
|
25328
25505
|
try {
|
|
25329
25506
|
const child = spawn3(process.execPath, [reconcileScript], {
|
|
25330
25507
|
env: { ...process.env, PLATFORM_ROOT: reconcilePlatformRoot },
|
|
@@ -25348,7 +25525,7 @@ startTimeEntryCensus(() => getSession());
|
|
|
25348
25525
|
const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
|
|
25349
25526
|
const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
|
|
25350
25527
|
const runConnectorAudit = () => {
|
|
25351
|
-
if (!
|
|
25528
|
+
if (!existsSync34(auditScript)) return;
|
|
25352
25529
|
try {
|
|
25353
25530
|
const child = spawn3(process.execPath, [auditScript], {
|
|
25354
25531
|
env: { ...process.env, PLATFORM_ROOT: auditPlatformRoot, LOG_DIR: auditLogDir },
|
|
@@ -25458,7 +25635,7 @@ try {
|
|
|
25458
25635
|
var ADMINUSER_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
25459
25636
|
async function runAdminUserReconcileTick() {
|
|
25460
25637
|
try {
|
|
25461
|
-
if (!
|
|
25638
|
+
if (!existsSync34(USERS_FILE)) {
|
|
25462
25639
|
console.error("[adminuser-self-heal] skip reason=no-users-file");
|
|
25463
25640
|
return;
|
|
25464
25641
|
}
|
|
@@ -25491,7 +25668,7 @@ var adminUserReconcileTimer = setInterval(() => {
|
|
|
25491
25668
|
if (typeof adminUserReconcileTimer.unref === "function") adminUserReconcileTimer.unref();
|
|
25492
25669
|
var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
25493
25670
|
function countUsersRows() {
|
|
25494
|
-
if (!
|
|
25671
|
+
if (!existsSync34(USERS_FILE)) return 0;
|
|
25495
25672
|
const raw = readFileSync35(USERS_FILE, "utf-8").trim();
|
|
25496
25673
|
if (!raw) return 0;
|
|
25497
25674
|
const users = JSON.parse(raw);
|