openzca 0.1.52 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli.js +304 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,7 @@ Media commands accept local files, `file://` paths, and repeatable `--url` optio
|
|
|
127
127
|
`openzca msg video` attempts native video send for a single `.mp4` input by uploading the video and thumbnail to Zalo first. If `ffmpeg` is unavailable, the input is not a single `.mp4`, or native send fails, it falls back to the normal attachment send path. Use `--thumbnail <path-or-url>` to supply the preview image explicitly.
|
|
128
128
|
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
129
129
|
Group text sends via `openzca msg send --group` resolve unique `@Name` or `@userId` mentions against the current group member list using member ids, display names, and usernames. Mention offsets are computed after formatting markers are parsed, so messages like `**@Alice Nguyen** hello` work. If multiple members share the same label, the command fails instead of guessing.
|
|
130
|
+
When formatted text would produce an oversized outbound payload, `openzca msg send` automatically splits it into multiple sequential text messages using the final outbound text and rebased style/mention offsets. The split happens after formatting is parsed, using both rendered text length and estimated request payload size rather than the raw input string.
|
|
130
131
|
Use `openzca msg analyze-text ... --json` when you need to predict whether a formatted reply will expand into a large `textProperties` payload before attempting delivery.
|
|
131
132
|
Reply flows:
|
|
132
133
|
|
|
@@ -391,6 +392,10 @@ Listener resilience override:
|
|
|
391
392
|
- `OPENZCA_RECENT_GROUP_MAX_PAGES`: max websocket history pages to scan for `msg recent -g` when direct group-history path fails.
|
|
392
393
|
- Default: `20`.
|
|
393
394
|
- Increase if a group thread is old and not found quickly.
|
|
395
|
+
- `OPENZCA_TEXT_MESSAGE_MAX_LENGTH`: max rendered characters allowed per outbound text chunk before `msg send` splits it.
|
|
396
|
+
- Default: `2000`.
|
|
397
|
+
- `OPENZCA_TEXT_REQUEST_PARAMS_MAX_ESTIMATE`: max estimated serialized request size allowed per outbound text chunk before `msg send` splits it.
|
|
398
|
+
- Default: `4000`.
|
|
394
399
|
- `OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER`: enforce one `listen` owner process per profile.
|
|
395
400
|
- Default: enabled.
|
|
396
401
|
- Set to `0` to allow multiple listeners on the same profile (not recommended).
|
package/dist/cli.js
CHANGED
|
@@ -2333,6 +2333,8 @@ function normalizeCodeBlockLeadingWhitespace(line) {
|
|
|
2333
2333
|
}
|
|
2334
2334
|
|
|
2335
2335
|
// src/lib/text-send.ts
|
|
2336
|
+
var ZALO_TEXT_MESSAGE_MAX_LENGTH = 2e3;
|
|
2337
|
+
var ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE = 4e3;
|
|
2336
2338
|
async function buildTextSendPayload(params) {
|
|
2337
2339
|
if (params.raw) {
|
|
2338
2340
|
const mentions2 = await resolveGroupMentionsIfNeeded(params, params.message);
|
|
@@ -2368,15 +2370,103 @@ async function analyzeTextSendPayload(params) {
|
|
|
2368
2370
|
});
|
|
2369
2371
|
return {
|
|
2370
2372
|
payload,
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2373
|
+
...buildTextSendPayloadAnalysis({
|
|
2374
|
+
payloadObject,
|
|
2375
|
+
rawInputLength: params.message.length,
|
|
2376
|
+
textProperties,
|
|
2377
|
+
mentionInfo,
|
|
2378
|
+
requestParamsLengthEstimate: JSON.stringify(requestParams).length,
|
|
2379
|
+
threadType: params.threadType
|
|
2380
|
+
})
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
function planTextSendPayloadsForDelivery(params) {
|
|
2384
|
+
const maxMessageLength = resolvePositiveLimit(
|
|
2385
|
+
params.maxMessageLength,
|
|
2386
|
+
ZALO_TEXT_MESSAGE_MAX_LENGTH
|
|
2387
|
+
);
|
|
2388
|
+
const maxRequestParamsLengthEstimate = resolvePositiveLimit(
|
|
2389
|
+
params.maxRequestParamsLengthEstimate,
|
|
2390
|
+
ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
|
|
2391
|
+
);
|
|
2392
|
+
const chunks = [];
|
|
2393
|
+
const analyses = [];
|
|
2394
|
+
const pending = [params.payload];
|
|
2395
|
+
while (pending.length > 0) {
|
|
2396
|
+
const currentPayload = pending.shift();
|
|
2397
|
+
const analysis = analyzePreparedTextSendPayload({
|
|
2398
|
+
payload: currentPayload,
|
|
2399
|
+
threadType: params.threadType,
|
|
2400
|
+
threadId: params.threadId
|
|
2401
|
+
});
|
|
2402
|
+
if (isTextSendPayloadWithinDeliveryLimits(analysis, {
|
|
2403
|
+
maxMessageLength,
|
|
2404
|
+
maxRequestParamsLengthEstimate
|
|
2405
|
+
})) {
|
|
2406
|
+
chunks.push(currentPayload);
|
|
2407
|
+
analyses.push(analysis);
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
const targetLength = computeNextChunkLength(analysis, {
|
|
2411
|
+
maxMessageLength,
|
|
2412
|
+
maxRequestParamsLengthEstimate
|
|
2413
|
+
});
|
|
2414
|
+
const splitChunks = splitTextSendPayload(currentPayload, targetLength);
|
|
2415
|
+
if (splitChunks.length <= 1) {
|
|
2416
|
+
throw new Error(
|
|
2417
|
+
`Unable to split formatted text payload into deliverable chunks within ${targetLength} characters.`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
pending.unshift(...splitChunks);
|
|
2421
|
+
}
|
|
2422
|
+
return { chunks, analyses };
|
|
2423
|
+
}
|
|
2424
|
+
function splitTextSendPayload(payload, maxLength = ZALO_TEXT_MESSAGE_MAX_LENGTH) {
|
|
2425
|
+
if (!Number.isInteger(maxLength) || maxLength <= 0) {
|
|
2426
|
+
throw new Error("Text chunk size must be a positive integer");
|
|
2427
|
+
}
|
|
2428
|
+
const payloadObject = normalizeTextSendPayload(payload);
|
|
2429
|
+
if (payloadObject.msg.length <= maxLength) {
|
|
2430
|
+
return [payload];
|
|
2431
|
+
}
|
|
2432
|
+
const chunks = [];
|
|
2433
|
+
let start = 0;
|
|
2434
|
+
while (start < payloadObject.msg.length) {
|
|
2435
|
+
const end = findChunkEnd(payloadObject, start, maxLength);
|
|
2436
|
+
chunks.push(sliceTextSendPayload(payloadObject, start, end));
|
|
2437
|
+
start = end;
|
|
2438
|
+
}
|
|
2439
|
+
return chunks;
|
|
2440
|
+
}
|
|
2441
|
+
function analyzePreparedTextSendPayload(params) {
|
|
2442
|
+
const payloadObject = normalizeTextSendPayload(params.payload);
|
|
2443
|
+
const textProperties = buildTextProperties(payloadObject.styles);
|
|
2444
|
+
const mentionInfo = buildMentionInfo(
|
|
2445
|
+
params.threadType,
|
|
2446
|
+
payloadObject.msg,
|
|
2447
|
+
payloadObject.mentions
|
|
2448
|
+
);
|
|
2449
|
+
const requestParams = omitUndefined({
|
|
2450
|
+
message: payloadObject.msg,
|
|
2451
|
+
clientId: 17e11,
|
|
2452
|
+
mentionInfo,
|
|
2453
|
+
imei: params.threadType === ThreadType.Group ? void 0 : "000000000000000",
|
|
2454
|
+
ttl: 0,
|
|
2455
|
+
visibility: params.threadType === ThreadType.Group ? 0 : void 0,
|
|
2456
|
+
toid: params.threadType === ThreadType.Group ? void 0 : params.threadId,
|
|
2457
|
+
grid: params.threadType === ThreadType.Group ? params.threadId : void 0,
|
|
2458
|
+
textProperties
|
|
2459
|
+
});
|
|
2460
|
+
return {
|
|
2461
|
+
payload: params.payload,
|
|
2462
|
+
...buildTextSendPayloadAnalysis({
|
|
2463
|
+
payloadObject,
|
|
2464
|
+
rawInputLength: payloadObject.msg.length,
|
|
2465
|
+
textProperties,
|
|
2466
|
+
mentionInfo,
|
|
2467
|
+
requestParamsLengthEstimate: JSON.stringify(requestParams).length,
|
|
2468
|
+
threadType: params.threadType
|
|
2469
|
+
})
|
|
2380
2470
|
};
|
|
2381
2471
|
}
|
|
2382
2472
|
async function resolveGroupMentionsIfNeeded(params, text) {
|
|
@@ -2399,6 +2489,153 @@ function normalizeTextSendPayload(payload) {
|
|
|
2399
2489
|
}
|
|
2400
2490
|
return payload;
|
|
2401
2491
|
}
|
|
2492
|
+
function buildTextSendPayloadAnalysis(params) {
|
|
2493
|
+
return {
|
|
2494
|
+
payloadObject: params.payloadObject,
|
|
2495
|
+
rawInputLength: params.rawInputLength,
|
|
2496
|
+
renderedTextLength: params.payloadObject.msg.length,
|
|
2497
|
+
styleCount: params.payloadObject.styles?.length ?? 0,
|
|
2498
|
+
mentionCount: params.payloadObject.mentions?.length ?? 0,
|
|
2499
|
+
textPropertiesLength: params.textProperties?.length ?? 0,
|
|
2500
|
+
mentionInfoLength: params.mentionInfo?.length ?? 0,
|
|
2501
|
+
requestParamsLengthEstimate: params.requestParamsLengthEstimate,
|
|
2502
|
+
sendPath: params.threadType === ThreadType.Group ? params.mentionInfo ? "mention" : "sendmsg" : "sms"
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function isTextSendPayloadWithinDeliveryLimits(analysis, limits) {
|
|
2506
|
+
return analysis.renderedTextLength <= limits.maxMessageLength && analysis.requestParamsLengthEstimate <= limits.maxRequestParamsLengthEstimate;
|
|
2507
|
+
}
|
|
2508
|
+
function computeNextChunkLength(analysis, limits) {
|
|
2509
|
+
const currentLength = analysis.renderedTextLength;
|
|
2510
|
+
const targetLengths = [limits.maxMessageLength, currentLength - 1].filter((value) => value > 0);
|
|
2511
|
+
if (analysis.requestParamsLengthEstimate > limits.maxRequestParamsLengthEstimate) {
|
|
2512
|
+
targetLengths.push(
|
|
2513
|
+
Math.floor(
|
|
2514
|
+
currentLength * limits.maxRequestParamsLengthEstimate / analysis.requestParamsLengthEstimate
|
|
2515
|
+
)
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
const targetLength = Math.max(
|
|
2519
|
+
1,
|
|
2520
|
+
Math.min(...targetLengths.filter((value) => Number.isFinite(value) && value > 0))
|
|
2521
|
+
);
|
|
2522
|
+
return Math.min(targetLength, currentLength - 1);
|
|
2523
|
+
}
|
|
2524
|
+
function resolvePositiveLimit(value, fallback) {
|
|
2525
|
+
if (!value || !Number.isInteger(value) || value <= 0) {
|
|
2526
|
+
return fallback;
|
|
2527
|
+
}
|
|
2528
|
+
return value;
|
|
2529
|
+
}
|
|
2530
|
+
function sliceTextSendPayload(payloadObject, start, end) {
|
|
2531
|
+
const msg2 = payloadObject.msg.slice(start, end);
|
|
2532
|
+
const styles = sliceStyles(payloadObject.styles, start, end);
|
|
2533
|
+
const mentions = sliceMentions(payloadObject.mentions, start, end);
|
|
2534
|
+
if (!styles && !mentions) {
|
|
2535
|
+
return msg2;
|
|
2536
|
+
}
|
|
2537
|
+
return omitUndefined({
|
|
2538
|
+
msg: msg2,
|
|
2539
|
+
styles,
|
|
2540
|
+
mentions
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
function sliceStyles(styles, start, end) {
|
|
2544
|
+
if (!styles || styles.length === 0) {
|
|
2545
|
+
return void 0;
|
|
2546
|
+
}
|
|
2547
|
+
const sliced = [];
|
|
2548
|
+
for (const style of styles) {
|
|
2549
|
+
const styleStart = style.start;
|
|
2550
|
+
const styleEnd = style.start + style.len;
|
|
2551
|
+
const overlapStart = Math.max(styleStart, start);
|
|
2552
|
+
const overlapEnd = Math.min(styleEnd, end);
|
|
2553
|
+
if (overlapStart >= overlapEnd) {
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
if (style.st === "ind_$") {
|
|
2557
|
+
sliced.push({
|
|
2558
|
+
start: overlapStart - start,
|
|
2559
|
+
len: overlapEnd - overlapStart,
|
|
2560
|
+
st: style.st,
|
|
2561
|
+
indentSize: style.indentSize
|
|
2562
|
+
});
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
sliced.push({
|
|
2566
|
+
start: overlapStart - start,
|
|
2567
|
+
len: overlapEnd - overlapStart,
|
|
2568
|
+
st: style.st
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
return sliced.length > 0 ? sliced : void 0;
|
|
2572
|
+
}
|
|
2573
|
+
function sliceMentions(mentions, start, end) {
|
|
2574
|
+
if (!mentions || mentions.length === 0) {
|
|
2575
|
+
return void 0;
|
|
2576
|
+
}
|
|
2577
|
+
const sliced = mentions.filter((mention) => mention.pos >= start && mention.pos + mention.len <= end).map((mention) => ({
|
|
2578
|
+
pos: mention.pos - start,
|
|
2579
|
+
uid: mention.uid,
|
|
2580
|
+
len: mention.len
|
|
2581
|
+
}));
|
|
2582
|
+
return sliced.length > 0 ? sliced : void 0;
|
|
2583
|
+
}
|
|
2584
|
+
function findChunkEnd(payloadObject, start, maxLength) {
|
|
2585
|
+
const remaining = payloadObject.msg.length - start;
|
|
2586
|
+
if (remaining <= maxLength) {
|
|
2587
|
+
return payloadObject.msg.length;
|
|
2588
|
+
}
|
|
2589
|
+
const maxEnd = start + maxLength;
|
|
2590
|
+
const newlineBreak = findPreferredBreak(payloadObject, start, maxEnd, "\n");
|
|
2591
|
+
if (newlineBreak > start) {
|
|
2592
|
+
return newlineBreak;
|
|
2593
|
+
}
|
|
2594
|
+
const whitespaceBreak = findWhitespaceBreak(payloadObject, start, maxEnd);
|
|
2595
|
+
if (whitespaceBreak > start) {
|
|
2596
|
+
return whitespaceBreak;
|
|
2597
|
+
}
|
|
2598
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2599
|
+
if (isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2600
|
+
return cursor;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
throw new Error(
|
|
2604
|
+
`Unable to split text payload safely within ${maxLength} characters.`
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
function findPreferredBreak(payloadObject, start, maxEnd, marker) {
|
|
2608
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2609
|
+
if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
if (payloadObject.msg[cursor - 1] === marker) {
|
|
2613
|
+
return cursor;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return start;
|
|
2617
|
+
}
|
|
2618
|
+
function findWhitespaceBreak(payloadObject, start, maxEnd) {
|
|
2619
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2620
|
+
if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
const previousChar = payloadObject.msg[cursor - 1];
|
|
2624
|
+
if (previousChar === " " || previousChar === " ") {
|
|
2625
|
+
return cursor;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return start;
|
|
2629
|
+
}
|
|
2630
|
+
function isSafeSplitPosition(mentions, position) {
|
|
2631
|
+
if (!mentions || mentions.length === 0) {
|
|
2632
|
+
return true;
|
|
2633
|
+
}
|
|
2634
|
+
return mentions.every((mention) => {
|
|
2635
|
+
const mentionEnd = mention.pos + mention.len;
|
|
2636
|
+
return position <= mention.pos || position >= mentionEnd;
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2402
2639
|
function buildTextProperties(styles) {
|
|
2403
2640
|
if (!styles || styles.length === 0) {
|
|
2404
2641
|
return void 0;
|
|
@@ -6523,20 +6760,67 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
|
|
|
6523
6760
|
...typeof textPayload === "string" ? { msg: textPayload } : textPayload,
|
|
6524
6761
|
...quote ? { quote } : {}
|
|
6525
6762
|
} : textPayload;
|
|
6526
|
-
const
|
|
6763
|
+
const deliveryPlan = planTextSendPayloadsForDelivery({
|
|
6764
|
+
payload: textPayload,
|
|
6765
|
+
threadType,
|
|
6766
|
+
threadId,
|
|
6767
|
+
maxMessageLength: parsePositiveIntFromEnv(
|
|
6768
|
+
"OPENZCA_TEXT_MESSAGE_MAX_LENGTH",
|
|
6769
|
+
ZALO_TEXT_MESSAGE_MAX_LENGTH
|
|
6770
|
+
),
|
|
6771
|
+
maxRequestParamsLengthEstimate: parsePositiveIntFromEnv(
|
|
6772
|
+
"OPENZCA_TEXT_REQUEST_PARAMS_MAX_ESTIMATE",
|
|
6773
|
+
ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
|
|
6774
|
+
)
|
|
6775
|
+
});
|
|
6776
|
+
const payloadChunks = deliveryPlan.chunks;
|
|
6777
|
+
const responses = [];
|
|
6778
|
+
const sentPayloads = [];
|
|
6779
|
+
for (let index = 0; index < payloadChunks.length; index += 1) {
|
|
6780
|
+
const chunk = payloadChunks[index];
|
|
6781
|
+
const chunkPayload = quote && index === 0 ? {
|
|
6782
|
+
...typeof chunk === "string" ? { msg: chunk } : chunk,
|
|
6783
|
+
quote
|
|
6784
|
+
} : chunk;
|
|
6785
|
+
sentPayloads.push(chunkPayload);
|
|
6786
|
+
responses.push(await api.sendMessage(chunkPayload, threadId, threadType));
|
|
6787
|
+
}
|
|
6788
|
+
const response = responses.length === 1 ? responses[0] : {
|
|
6789
|
+
chunked: true,
|
|
6790
|
+
chunkCount: responses.length,
|
|
6791
|
+
msgId: responses.at(-1)?.message?.msgId?.toString(),
|
|
6792
|
+
response: responses
|
|
6793
|
+
};
|
|
6527
6794
|
output(response, false);
|
|
6528
6795
|
if (await shouldWriteToDb(profile)) {
|
|
6529
6796
|
scheduleDbWrite(profile, command, "msg.send.db.persist_error", async () => {
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6797
|
+
if (payloadChunks.length === 1) {
|
|
6798
|
+
await persistOutgoingMessageBestEffort({
|
|
6799
|
+
profile,
|
|
6800
|
+
api,
|
|
6801
|
+
threadId,
|
|
6802
|
+
group: opts.group,
|
|
6803
|
+
text: message,
|
|
6804
|
+
msgType: "text",
|
|
6805
|
+
response,
|
|
6806
|
+
rawPayload: payload
|
|
6807
|
+
});
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
for (let index = 0; index < payloadChunks.length; index += 1) {
|
|
6811
|
+
const chunk = sentPayloads[index];
|
|
6812
|
+
const chunkText = typeof chunk === "string" ? chunk : chunk.msg;
|
|
6813
|
+
await persistOutgoingMessageBestEffort({
|
|
6814
|
+
profile,
|
|
6815
|
+
api,
|
|
6816
|
+
threadId,
|
|
6817
|
+
group: opts.group,
|
|
6818
|
+
text: chunkText,
|
|
6819
|
+
msgType: "text",
|
|
6820
|
+
response: responses[index],
|
|
6821
|
+
rawPayload: chunk
|
|
6822
|
+
});
|
|
6823
|
+
}
|
|
6540
6824
|
});
|
|
6541
6825
|
}
|
|
6542
6826
|
})
|