fix(whatsapp): unify contact identity and reduce realtime latency

This commit is contained in:
wmq
2026-08-03 00:54:59 +08:00
parent bb696c7de8
commit 7e739ad7b8
11 changed files with 280 additions and 75 deletions
+44 -16
View File
@@ -125,7 +125,7 @@ async function saveWebhookDeliveries() {
const sorted = [...webhookDeliveries.values()]
.sort((a, b) => Date.parse(b.createdAt || '') - Date.parse(a.createdAt || ''))
const active = sorted.filter((item) => !['delivered', 'dead'].includes(item.status))
const terminal = sorted.filter((item) => ['delivered', 'dead'].includes(item.status)).slice(0, 10_000)
const terminal = sorted.filter((item) => ['delivered', 'dead'].includes(item.status)).slice(0, 2_000)
const values = [...active, ...terminal]
const keep = new Set(values.map((item) => item.id))
for (const id of webhookDeliveries.keys()) {
@@ -134,11 +134,20 @@ async function saveWebhookDeliveries() {
await atomicJson(webhookDeliveriesFile, values)
}
let webhookDeliverySaveChain = Promise.resolve()
function queueWebhookDeliveriesSave() {
webhookDeliverySaveChain = webhookDeliverySaveChain
.catch(() => undefined)
.then(() => saveWebhookDeliveries())
.catch((error) => log.error({ err: error }, 'webhook delivery persistence failed'))
return webhookDeliverySaveChain
}
async function saveOutboundJobs() {
const sorted = [...outboundJobs.values()]
.sort((a, b) => Date.parse(b.createdAt || '') - Date.parse(a.createdAt || ''))
const active = sorted.filter((item) => !['sent', 'dead'].includes(item.status))
const terminal = sorted.filter((item) => ['sent', 'dead'].includes(item.status)).slice(0, 10_000)
const terminal = sorted.filter((item) => ['sent', 'dead'].includes(item.status)).slice(0, 2_000)
const values = [...active, ...terminal]
const keep = new Set(values.map((item) => item.id))
for (const id of outboundJobs.keys()) {
@@ -147,6 +156,15 @@ async function saveOutboundJobs() {
await atomicJson(outboundJobsFile, values)
}
let outboundJobsSaveChain = Promise.resolve()
function queueOutboundJobsSave() {
outboundJobsSaveChain = outboundJobsSaveChain
.catch(() => undefined)
.then(() => saveOutboundJobs())
.catch((error) => log.error({ err: error }, 'outbound job persistence failed'))
return outboundJobsSaveChain
}
async function saveAccounts() {
const values = [...accounts.values()].map(({ qrDataUrl, pairingCode, ...item }) => item)
await atomicJson(accountsFile, values)
@@ -484,7 +502,7 @@ async function processWebhookDelivery(deliveryId) {
delivery.status = 'delivering'
delivery.attempts = Number(delivery.attempts || 0) + 1
delivery.updatedAt = new Date().toISOString()
await saveWebhookDeliveries()
void queueWebhookDeliveriesSave()
try {
const signature = crypto.createHmac('sha256', webhookSecret)
.update(delivery.payload)
@@ -523,13 +541,19 @@ async function processWebhookDelivery(deliveryId) {
} finally {
delivery.updatedAt = new Date().toISOString()
webhookDeliveriesProcessing.delete(deliveryId)
await saveWebhookDeliveries()
await queueWebhookDeliveriesSave()
}
}
async function deliverWebhook(account, event, data) {
if (!account.webhookUrl || !account.webhookActive || !accountWebhookSecret(account)) return
const id = crypto.randomUUID()
const messageId = ['message.received', 'message.sent', 'message.edited'].includes(event)
? String(data?.message?.id || '')
: ''
const id = messageId
? `wa-${crypto.createHash('sha256').update(`${account.id}:${event}:${messageId}`).digest('hex').slice(0, 32)}`
: crypto.randomUUID()
if (webhookDeliveries.has(id)) return
const payload = JSON.stringify({
id,
event,
@@ -547,8 +571,8 @@ async function deliverWebhook(account, event, data) {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
await saveWebhookDeliveries()
scheduleWebhookDelivery(id)
await queueWebhookDeliveriesSave()
}
function queuedOutboundMessage(job) {
@@ -611,7 +635,7 @@ async function processOutboundJob(jobId) {
job.status = 'queued'
job.nextAttemptAt = new Date(Date.now() + 5_000).toISOString()
job.updatedAt = new Date().toISOString()
await saveOutboundJobs()
await queueOutboundJobsSave()
scheduleOutboundJob(job.id, 5_000)
return queuedOutboundMessage(job)
}
@@ -621,7 +645,7 @@ async function processOutboundJob(jobId) {
job.throttled = true
job.nextAttemptAt = new Date(Date.now() + throttleDelay).toISOString()
job.updatedAt = new Date().toISOString()
await saveOutboundJobs()
await queueOutboundJobsSave()
scheduleOutboundJob(job.id, throttleDelay)
return queuedOutboundMessage(job)
}
@@ -629,7 +653,7 @@ async function processOutboundJob(jobId) {
job.status = 'sending'
job.attempts = Number(job.attempts || 0) + 1
job.updatedAt = new Date().toISOString()
await saveOutboundJobs()
void queueOutboundJobsSave()
try {
const store = await loadStore(job.accountId)
const quoted = job.quotedMessageId
@@ -713,7 +737,7 @@ async function processOutboundJob(jobId) {
} finally {
job.updatedAt = new Date().toISOString()
outboundJobsProcessing.delete(job.id)
await saveOutboundJobs()
await queueOutboundJobsSave()
}
}
@@ -1415,7 +1439,7 @@ app.post('/accounts/:id/backup/restore', async (req, res) => {
if (delivery.accountId === account.id) webhookDeliveries.delete(id)
}
for (const delivery of snapshot.webhookDeliveries || []) webhookDeliveries.set(delivery.id, delivery)
await Promise.all([saveAccounts(), saveOutboundJobs(), saveWebhookDeliveries()])
await Promise.all([saveAccounts(), queueOutboundJobsSave(), queueWebhookDeliveriesSave()])
await connectAccount(account.id)
res.json({ code: 0, data: { restored: true, account: accountPublic(account) } })
})
@@ -1575,12 +1599,16 @@ app.get('/accounts/:id/chats/:jid/identity', async (req, res) => {
const group = jid.endsWith('@g.us')
? await sock.groupMetadata(jid).catch(() => store.groups.get(jid))
: null
const avatarUrl = await sock.profilePictureUrl(jid, 'image')
.catch(() => phoneJid !== jid ? sock.profilePictureUrl(phoneJid, 'image').catch(() => '') : '')
const includeProfile = req.query.includeProfile !== 'false'
const avatarUrl = includeProfile
? await sock.profilePictureUrl(jid, 'image')
.catch(() => phoneJid !== jid ? sock.profilePictureUrl(phoneJid, 'image').catch(() => '') : '')
: ''
res.json({
code: 0,
data: {
id: jid,
canonicalJid: phoneJid.endsWith('@s.whatsapp.net') ? phoneJid : jid,
name: group?.subject || contact.name || contact.notify || contact.verifiedName || jid.split('@')[0],
phone: phoneJid.endsWith('@s.whatsapp.net') ? phoneJid.split('@')[0] : '',
avatarUrl,
@@ -1663,7 +1691,7 @@ app.post('/accounts/:id/messages/send', async (req, res) => {
updatedAt: new Date().toISOString(),
}
outboundJobs.set(job.id, job)
await saveOutboundJobs()
await queueOutboundJobsSave()
const result = await processOutboundJob(job.id)
res.json({ code: 0, data: result || queuedOutboundMessage(job) })
})
@@ -1776,7 +1804,7 @@ app.post('/accounts/:id/outbound-jobs/:jobId/retry', async (req, res) => {
delete job.deadAt
delete job.lastError
delete job.nextAttemptAt
await saveOutboundJobs()
await queueOutboundJobsSave()
const result = await processOutboundJob(job.id)
res.json({ code: 0, data: result || queuedOutboundMessage(job) })
})
@@ -1835,7 +1863,7 @@ app.post('/accounts/:id/messages/media', upload.single('file'), async (req, res)
job.filePath = path.join(outboundMediaDir, `${job.id}.bin`)
await fs.writeFile(job.filePath, req.file.buffer, { mode: 0o600 })
outboundJobs.set(job.id, job)
await saveOutboundJobs()
await queueOutboundJobsSave()
const result = await processOutboundJob(job.id)
res.json({ code: 0, data: result || queuedOutboundMessage(job) })
})