feat: media-send pacing na élke send (niet alleen tussen pagina's)
MEDIA_SEND_DELAY_MS pauzeert nu centraal in sendImageMessage ná elke media-send, zodat ook afbeeldingen naar verschillende ontvangers niet overlappen in de ene chromium-tab (staged-ghost race-risico). Per-pagina delay uit messageHandler verwijderd.
This commit is contained in:
parent
4360a0f46e
commit
7673239933
5 changed files with 83 additions and 20 deletions
|
|
@ -25,7 +25,7 @@ MAX_REQUESTS_PER_DAY=5
|
||||||
ACK_STUCK_TIMEOUT_MS=300000 # 5 min zonder ack 2 = stuck
|
ACK_STUCK_TIMEOUT_MS=300000 # 5 min zonder ack 2 = stuck
|
||||||
ACK_PRUNED_AFTER_MS=1800000 # 30 min: stale trackings opruimen (loop stopt dan)
|
ACK_PRUNED_AFTER_MS=1800000 # 30 min: stale trackings opruimen (loop stopt dan)
|
||||||
ACK_CHECK_INTERVAL_MS=30000 # checkinterval (alleen actief als er iets pending is)
|
ACK_CHECK_INTERVAL_MS=30000 # checkinterval (alleen actief als er iets pending is)
|
||||||
MEDIA_SEND_DELAY_MS=500 # pauze tussen pagina's van een dienst (niet vóór de eerste)
|
MEDIA_SEND_DELAY_MS=1500 # wacht ná élke media-send (media-pipeline is één tab; voorkomt overlappende sends)
|
||||||
|
|
||||||
# Admin notifications
|
# Admin notifications
|
||||||
ADMIN_NUMBER=31612345678@c.us
|
ADMIN_NUMBER=31612345678@c.us
|
||||||
|
|
|
||||||
66
AGENTS.md
66
AGENTS.md
|
|
@ -10,13 +10,18 @@ zag in de chat maar **één** bericht (ghost-sends: id geretourneerd, nooit echt
|
||||||
- **`/chat/fetchMessages`-acks en de lokale message-store van wwebjs-api zijn een lokale
|
- **`/chat/fetchMessages`-acks en de lokale message-store van wwebjs-api zijn een lokale
|
||||||
registratie, géén bezorgbewijs.** Ze kunnen ack 1/2/3 tonen voor berichten die nooit
|
registratie, géén bezorgbewijs.** Ze kunnen ack 1/2/3 tonen voor berichten die nooit
|
||||||
aankwamen.
|
aankwamen.
|
||||||
- **Enige geldige bewijzen van levering:** (1) de **live `message_ack`-progressie** zoals
|
- **Ook de live `message_ack`-progressie is sinds 2026-09-26 géén bezorgbewijs meer:**
|
||||||
die vlak ná verzending via de webhook binnenkomt; (2) de **zichtbare werkelijkheid op
|
de staged-ghost van dienst 5219 blz 2 kreeg via de webhook een `ack=2` afgegeven voor een
|
||||||
het apparaat** van de ontvanger/betrokkene.
|
bericht dat nooit getransmitteerd was (stond niet eens in de WhatsApp-app van de eigenaar).
|
||||||
|
De bridge resolvet media-sends **optimistisch op lokale staging** en fabriceert daarna
|
||||||
|
ack-events + delivery-info. Ack-tracking (stopt bij ack ≥ 2) kan deze klasse per ontwerp
|
||||||
|
niet vangen.
|
||||||
|
- **Enige geldige bewijzen van levering:** (1) de **zichtbare werkelijkheid op het
|
||||||
|
apparaat** van de ontvanger/betrokkene; (2) `getInfo` (`message.getInfo()`): bij een
|
||||||
|
staged-ghost blijft `read` leeg terwijl `delivery` gevuld kan zijn — het enige
|
||||||
|
betrouwbare bridge-signaal is `read`/`readRemaining`.
|
||||||
- **Trek NOOIT een leveringsconclusie uit een later gelezen fetchMessages-ack.** Claim geen
|
- **Trek NOOIT een leveringsconclusie uit een later gelezen fetchMessages-ack.** Claim geen
|
||||||
"bezorgd/gelezen" op basis van de store — dat geeft verkeerde adviezen.
|
"bezorgd/gelezen" op basis van de store — dat geeft verkeerde adviezen.
|
||||||
- Dit is de reden dat ack-tracking bestaat: het hangt af van de live events, niet van de
|
|
||||||
store.
|
|
||||||
|
|
||||||
## Waar draait de bot?
|
## Waar draait de bot?
|
||||||
|
|
||||||
|
|
@ -176,13 +181,47 @@ Sommige mediabestand-verzendingen keren `success` + message-ID terug maar worden
|
||||||
- **Pruning** na `ACK_PRUNED_AFTER_MS` (30 min); de check-loop is **activity-driven** (draait alleen bij openstaande tracking, geen polling in rust).
|
- **Pruning** na `ACK_PRUNED_AFTER_MS` (30 min); de check-loop is **activity-driven** (draait alleen bij openstaande tracking, geen polling in rust).
|
||||||
- Vereiste aan de bridge: `message_ack` staat sinds 2026-09-24 in wwebjs-api `.env` **niet** meer in `DISABLED_CALLBACKS`; container is daarvoor herstart.
|
- Vereiste aan de bridge: `message_ack` staat sinds 2026-09-24 in wwebjs-api `.env` **niet** meer in `DISABLED_CALLBACKS`; container is daarvoor herstart.
|
||||||
- Env-vars: `ACK_STUCK_TIMEOUT_MS` (default 300000), `ACK_PRUNED_AFTER_MS` (default 1800000), `ACK_CHECK_INTERVAL_MS` (default 30000).
|
- Env-vars: `ACK_STUCK_TIMEOUT_MS` (default 300000), `ACK_PRUNED_AFTER_MS` (default 1800000), `ACK_CHECK_INTERVAL_MS` (default 30000).
|
||||||
- **Media-spreiding:** `MEDIA_SEND_DELAY_MS` (default **500**) pauzeert tussen de pagina's van één
|
- **Media-spreiding:** `MEDIA_SEND_DELAY_MS` (default **500**, nu **1500**) pauzeert **na élke
|
||||||
dienst (niet vóór de eerste). Doel: de wwebjs media-pipeline (één chromium-tab) per-verzending
|
media-send** (in `sendImageMessage`), niet alleen tussen pagina's van één dienst. De
|
||||||
lucht geven en gelijktijdige storage/upload-conflicten verminderen. 0 = uitschakelen.
|
wwebjs-media-pipeline is EEN chromium-tab; opeenvolgende media-sends — óók naar
|
||||||
|
verschillende ontvangers — overlappen anders en veroorzaken de staged-ghost-race.
|
||||||
|
0 = uitschakelen. (Tijdelijke mitigatie; permanente fix = fork: send óf écht verzenden
|
||||||
|
óf zichtbaar falen, zie RCA.)
|
||||||
|
|
||||||
Logregels: `[ack] track <id> (<bestand> → <chat>)`, `[ack] delivered ack=<n> <id>`, `[ack] pruned <n> stale entries`.
|
Logregels: `[ack] track <id> (<bestand> → <chat>)`, `[ack] delivered ack=<n> <id>`, `[ack] pruned <n> stale entries`.
|
||||||
|
|
||||||
**Ack-semantiek (wwebjs 1.34.7):** ack 1 = verstuurd naar server (één grijs vinkje), ack 2 = bezorgd op apparaat (twee vinkjes), ack 3 = geopend/gelezen. Let op: eerdere notities die "één grijs vinkje (ack 2)" noemden, zaten er dus naast.
|
**Ack-semantiek (wwebjs 1.34.7):** ack 1 = verstuurd naar server (één grijs vinkje), ack 2 = bezorgd op apparaat (twee vinkjes), ack 3 = geopend/gelezen. Let op: eerdere notities die "één grijs vinkje (ack 2)" noemden, zaten er dus naast. **Let op 2 (2026-09-26):** óók deze acks zijn geen leveringsbewijs — zie RCA hieronder.
|
||||||
|
|
||||||
|
### RCA 2026-09-26: staged-ghost = race-conditie, géén bestandprobleem
|
||||||
|
|
||||||
|
Casus: dienst 5219 (2 pagina's), 09:29 gevraagd in de groep, bot stuurt beide pagina's
|
||||||
|
prive. App toont alleen blz 1; **blz 2 stond in het geheel niet in de WhatsApp-app van de
|
||||||
|
eigenaar** (geen transmissie). Toch: bridge retourneerde success + message-ID (`3EB040AD…`),
|
||||||
|
store toont het bericht, live `message_ack` gaf `ack=2`, `getInfo` toonde `delivery` gevuld
|
||||||
|
maar `read` leeg. Geen enkele fout in de container-logs.
|
||||||
|
|
||||||
|
Mechanisme (bewezen uit fork-broncode + logs):
|
||||||
|
- De send loopt via `WAWebSendMsgChatAction.addAndSendMsgToChat(chat, message)` →
|
||||||
|
`[msgPromise, sendMsgResultPromise]`. De bridge zet `waitUntilMsgSent: true`
|
||||||
|
(stock wwebjs-api, `clientController.js`), maar de promise resolve **ook optimistisch op
|
||||||
|
lokale staging** — een echte serverbevestiging wordt nooit afgewacht. HTTP-success +
|
||||||
|
message-ID betekenen dus **niet** "verzonden".
|
||||||
|
- **Patroon (gebruiker, consistent met logs):** de ghost treedt alleen op bij **2+ media
|
||||||
|
direct achter elkaar**, en dan altijd de **2e** van het paar; enkele sends falen nooit.
|
||||||
|
- Verklaring: de bot wacht per pagina op het HTTP-signaal (staging), niet op echte
|
||||||
|
transmissie → blz 2 wordt gestart terwijl blz 1's **echte** upload/transmissie nog in
|
||||||
|
de lucht is. Twee echte media-transmissies overlappen in de ene chromium-tab → de 2e
|
||||||
|
verliest (intermitterend) → staged-ghost.
|
||||||
|
- Gevolgen: **`MEDIA_SEND_DELAY_MS` kan dit niet oplossen** (wacht op optimistisch
|
||||||
|
signaal); **poppler/re-render maskeert het alleen** (nieuwe poging = nieuwe race-rol,
|
||||||
|
soms gewonnen — wassen neus); **`sendMessageViaUi` lost het óók niet op** (wacht op
|
||||||
|
dezelfde promises, alleen omzeilt het de media-editor); **ack-tracking kan het niet
|
||||||
|
vangen** (stopt bij gefabriceerde ack ≥ 2).
|
||||||
|
|
||||||
|
Enige betrouwbare signalen: apparaat-waarheid; `getInfo` `read`/`readRemaining` (blijft
|
||||||
|
leeg bij staged). Aanbevolen richting (nog niet geïmplementeerd): de fork moet ná
|
||||||
|
`addAndSendMsgToChat` écht wachten op transmissiebevestiging — pas dan serialiseren
|
||||||
|
multi-page sends vanzelf en is de race weg.
|
||||||
|
|
||||||
## Media bezorgtest (afbeeldingen die WhatsApp weigert)
|
## Media bezorgtest (afbeeldingen die WhatsApp weigert)
|
||||||
|
|
||||||
|
|
@ -200,6 +239,15 @@ ack-tracking (zie hierboven), re-render via poppler en een verse bezorgtest.
|
||||||
**Ack-telling:** ack 1 = één grijs vinkje (niet bezorgd), ack 2 = bezorgd op apparaat,
|
**Ack-telling:** ack 1 = één grijs vinkje (niet bezorgd), ack 2 = bezorgd op apparaat,
|
||||||
ack 3 = geopend. (Eerdere notities "één grijs vinkje (ack 2)" zaten er dus naast.)
|
ack 3 = geopend. (Eerdere notities "één grijs vinkje (ack 2)" zaten er dus naast.)
|
||||||
|
|
||||||
|
> **⚠️ 2026-09-26: onderscheid twee foutklassen.** De hierboven beschreven
|
||||||
|
> bestand × ontvanger/offline-klasse (ack 1-hang, ontvanger offline of app weigert
|
||||||
|
> render) is niet hetzelfde als de **staged-ghost-race** (zie RCA hierboven): daar is het
|
||||||
|
> bestand nooit het probleem en **re-render (poppler) is een wassen neus** — het maskert
|
||||||
|
> de race alleen. Check bij een "niet aangekomen bladzijde" eerst: was het de 2e pagina
|
||||||
|
> van een paar binnen ~1s? Dan is het de race, geen bestand. Echte remedie is nog in
|
||||||
|
> ontwerp (fork: wachten op transmissiebevestiging); voor nu: opnieuw versturen (eventuele
|
||||||
|
> nieuwe poging) en apparaat als waarheid.
|
||||||
|
|
||||||
**Na elke omruiling van de dienstenmap: de volledige media-test doorlopen vóór ingebruikname.**
|
**Na elke omruiling van de dienstenmap: de volledige media-test doorlopen vóór ingebruikname.**
|
||||||
|
|
||||||
**Afbeeldingsformaat:** sinds `07b511e` leidt `sendImageMessage` de mimetype af uit de
|
**Afbeeldingsformaat:** sinds `07b511e` leidt `sendImageMessage` de mimetype af uit de
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -145,6 +145,15 @@ Bij elke interactie met de groep controleert de bot of het pinned bericht nog be
|
||||||
|
|
||||||
## Ack-tracking (ghost-send detectie, sinds 2026-09-24)
|
## Ack-tracking (ghost-send detectie, sinds 2026-09-24)
|
||||||
|
|
||||||
|
> **⚠️ 2026-09-26 — ack-tracking is voor media ongeldig gebleken.** Zie de RCA in
|
||||||
|
> AGENTS.md ("staged-ghost = race-conditie"): de bridge resolvet media-sends optimistisch
|
||||||
|
> op lokale staging en fabriceert daarna óók de live `message_ack`-events (bewijs: dienst
|
||||||
|
> 5219 blz 2 kreeg `ack=2` voor een bericht dat nooit getransmitteerd was). Daardoor:
|
||||||
|
> (1) de tracker stopt bij gefabriceerde ack ≥ 2 en mist de ghost; (2) "live ack" is géén
|
||||||
|
> bezorgbewijs meer — enige waarheid is het apparaat en `getInfo` (`read` blijft leeg).
|
||||||
|
> De klasse is een **race bij 2+ media achter elkaar** (altijd de 2e pagina), niet een
|
||||||
|
> bestandprobleem; `MEDIA_SEND_DELAY_MS` en re-render (poppler) lossen het niet op.
|
||||||
|
|
||||||
Sommige mediabestand-verzendingen keren `success` + message-ID terug maar worden nooit
|
Sommige mediabestand-verzendingen keren `success` + message-ID terug maar worden nooit
|
||||||
**bezorgd**: ze blijven op één grijs vinkje hangen (ack 1) en de ontvanger ziet niets
|
**bezorgd**: ze blijven op één grijs vinkje hangen (ack 1) en de ontvanger ziet niets
|
||||||
aankomen. Dit is een bestand×ontvanger-interactie in WhatsApp's mediaverwerking (niet de
|
aankomen. Dit is een bestand×ontvanger-interactie in WhatsApp's mediaverwerking (niet de
|
||||||
|
|
@ -181,15 +190,17 @@ sessie — zie AGENTS.md). De bot detecteert dit via `message_ack`-events en waa
|
||||||
| `ACK_STUCK_TIMEOUT_MS` | Leeftijd zonder ack 2 = stuck | `300000` (5 min) |
|
| `ACK_STUCK_TIMEOUT_MS` | Leeftijd zonder ack 2 = stuck | `300000` (5 min) |
|
||||||
| `ACK_PRUNED_AFTER_MS` | Stale tracking opruimen | `1800000` (30 min) |
|
| `ACK_PRUNED_AFTER_MS` | Stale tracking opruimen | `1800000` (30 min) |
|
||||||
| `ACK_CHECK_INTERVAL_MS` | Checkinterval (alleen actief bij pending) | `30000` |
|
| `ACK_CHECK_INTERVAL_MS` | Checkinterval (alleen actief bij pending) | `30000` |
|
||||||
| `MEDIA_SEND_DELAY_MS` | Pauze tussen pagina's van één dienst (niet vóór de eerste) — geeft de wwebjs media-pipeline lucht | `500` |
|
| `MEDIA_SEND_DELAY_MS` | Wacht ná élke media-send (media-pipeline is één tab; voorkomt overlappende sends naar elk doel) | `1500` |
|
||||||
|
|
||||||
Opvallende logregels: `[ack] track …`, `[ack] delivered ack=…`, `[ack] pruned …`.
|
Opvallende logregels: `[ack] track …`, `[ack] delivered ack=…`, `[ack] pruned …`.
|
||||||
|
|
||||||
> **Let op — bridgestore is geen bezorgbewijs (2026-09-24):** de lokale store van wwebjs-api
|
> **Let op — bridgestore is geen bezorgbewijs (2026-09-24):** de lokale store van wwebjs-api
|
||||||
> (`/chat/fetchMessages`-acks) kan ack 1/2/3 tonen voor berichten die de ontvanger nooit zag
|
> (`/chat/fetchMessages`-acks) kan ack 1/2/3 tonen voor berichten die de ontvanger nooit zag
|
||||||
> (ghost-sends). Trek hier géén leveringsconclusie uit. Ack-tracking gebruikt de **live
|
> (ghost-sends). Trek hier géén leveringsconclusie uit. **En sinds 2026-09-26 geldt hetzelfde
|
||||||
> `message_ack`-progressie**, niet de store; bevestiging komt van het apparaat van de
|
> voor de live `message_ack`-progressie** (zie waarschuwing bovenin deze sectie): ook die
|
||||||
> ontvanger.
|
> kan gefabriceerd zijn bij een staged-ghost. Enig betrouwbaar bridgesignaal is `getInfo`
|
||||||
|
> (`message.getInfo()` → `read`/`readRemaining`); eindoordeel is altijd het apparaat van
|
||||||
|
> de ontvanger.
|
||||||
|
|
||||||
## Spam protectie
|
## Spam protectie
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,20 +68,16 @@ async function handleWebhook(payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pacing gebeurt nu centraal in sendImageMessage (na élke media-send), zodat óók
|
||||||
|
// afbeeldingen naar verschillende ontvangers niet overlappen in de ene chromium-tab.
|
||||||
const pngPaths = getPngFilePaths(dienst)
|
const pngPaths = getPngFilePaths(dienst)
|
||||||
if (!pngPaths || pngPaths.length === 0) {
|
if (!pngPaths || pngPaths.length === 0) {
|
||||||
await sendTextMessage(author, `Geen pagina's gevonden voor dienst ${dienstnummer}.`)
|
await sendTextMessage(author, `Geen pagina's gevonden voor dienst ${dienstnummer}.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defensieve spreiding tussen pagina's (niet vóór de eerste): verlaagt de kans op
|
|
||||||
// gelijktijdige storage/upload-conflicten in de wwebjs-media-pipeline.
|
|
||||||
const mediaSendDelayMs = parseInt(process.env.MEDIA_SEND_DELAY_MS, 10) || 500
|
|
||||||
for (let i = 0; i < pngPaths.length; i++) {
|
for (let i = 0; i < pngPaths.length; i++) {
|
||||||
const pngPath = pngPaths[i]
|
const pngPath = pngPaths[i]
|
||||||
if (i > 0 && mediaSendDelayMs > 0) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, mediaSendDelayMs))
|
|
||||||
}
|
|
||||||
const filename = pngPath.split('/').pop()
|
const filename = pngPath.split('/').pop()
|
||||||
try {
|
try {
|
||||||
await sendImageMessage(author, pngPath, filename)
|
await sendImageMessage(author, pngPath, filename)
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,14 @@ async function sendImageMessage(chatId, filePath, filename) {
|
||||||
})
|
})
|
||||||
const messageId = response.data?.message?._data?.id?.id
|
const messageId = response.data?.message?._data?.id?.id
|
||||||
registerSend(messageId, chatId, filename)
|
registerSend(messageId, chatId, filename)
|
||||||
|
// Pacing na élke media-send: de wwebjs-media-pipeline is EEN chromium-tab. Twee
|
||||||
|
// opeenvolgende media-sends (pagina's van één dienst óf afbeeldingen naar
|
||||||
|
// verschillende ontvangers) mogen niet overlappen — dat veroorzaakt de staged-
|
||||||
|
// ghost-race (2e send verliest). Wacht dus altijd MEDIA_SEND_DELAY_MS ná een send.
|
||||||
|
const mediaSendDelayMs = parseInt(process.env.MEDIA_SEND_DELAY_MS, 10) || 500
|
||||||
|
if (mediaSendDelayMs > 0) {
|
||||||
|
await sleep(mediaSendDelayMs)
|
||||||
|
}
|
||||||
return response.data
|
return response.data
|
||||||
}, `sendImageMessage ${filename} to ${chatId}`)
|
}, `sendImageMessage ${filename} to ${chatId}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue