openzca 0.1.50 → 0.1.52

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 CHANGED
@@ -19,7 +19,9 @@ Or run without installing:
19
19
  npx openzca --help
20
20
  ```
21
21
 
22
- Requires Node.js 18+.
22
+ Requires Node.js 22.13+.
23
+
24
+ The built-in DB backend now uses Node's official `node:sqlite` module, so no extra `sqlite3` native addon is installed.
23
25
 
24
26
  ## Quick start
25
27
 
@@ -46,6 +48,9 @@ openzca msg send USER_ID "Reply text" --reply-id MSG_ID
46
48
  # Reply without DB using a listen --raw payload
47
49
  openzca msg send USER_ID "Reply text" --reply-message '{"threadId":"...","msgId":"...","cliMsgId":"...","content":"...","msgType":"webchat","senderId":"...","toId":"...","ts":"..."}'
48
50
 
51
+ # Inspect how a formatted message expands before sending/chunking
52
+ openzca msg analyze-text GROUP_ID "- item one\n- item two" --group --json
53
+
49
54
  # Listen for incoming messages
50
55
  openzca listen
51
56
 
@@ -98,6 +103,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
98
103
  | Command | Description |
99
104
  |---------|-------------|
100
105
  | `openzca msg send <threadId> <message>` | Send text with formatting (`**bold**`, `*italic*`, `~~strike~~`, etc.), group @mention resolution (`--raw` to skip formatting), and quote replies via `--reply-id` or `--reply-message` |
106
+ | `openzca msg analyze-text <threadId> <message>` | Build and inspect the exact text payload `msg send` would hand to `zca-js`, including rendered text length, style count, mention count, `textProperties` size, and request size estimate |
101
107
  | `openzca msg image <threadId> [file]` | Send image(s) from file or URL |
102
108
  | `openzca msg video <threadId> [file]` | Send video(s) from file or URL; single `.mp4` inputs try native video mode |
103
109
  | `openzca msg voice <threadId> [file]` | Send voice message from local file or URL (`.aac`, `.mp3`, `.m4a`, `.wav`, `.ogg`) |
@@ -121,6 +127,7 @@ Media commands accept local files, `file://` paths, and repeatable `--url` optio
121
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.
122
128
  Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
123
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
+ 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.
124
131
  Reply flows:
125
132
 
126
133
  - `--reply-id <id>` resolves a stored message from the local DB by `msgId`, `cliMsgId`, or internal message uid. This requires DB persistence to be enabled for the profile.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { createRequire } from "module";
4
+ import { createRequire as createRequire2 } from "module";
5
5
  import { spawn as spawn2 } from "child_process";
6
6
  import fsSync from "fs";
7
7
  import fs6 from "fs/promises";
@@ -214,13 +214,273 @@ async function clearCache(profileName) {
214
214
  // src/lib/db.ts
215
215
  import crypto from "crypto";
216
216
  import fs2 from "fs/promises";
217
+ import { createRequire } from "module";
217
218
  import path2 from "path";
218
- import { open } from "sqlite";
219
- import sqlite3 from "sqlite3";
219
+ import { Worker } from "worker_threads";
220
+ var require2 = createRequire(import.meta.url);
221
+ function buildDbError(error) {
222
+ const built = new Error(error.message);
223
+ built.name = error.name || "Error";
224
+ built.stack = error.stack ?? built.stack;
225
+ built.code = error.code;
226
+ return built;
227
+ }
228
+ function resolveWorkerSpec() {
229
+ const currentUrl = new URL(import.meta.url);
230
+ if (currentUrl.pathname.endsWith("/src/lib/db.ts")) {
231
+ return {
232
+ url: new URL("./db-worker.ts", currentUrl),
233
+ execArgv: ["--import", require2.resolve("tsx")]
234
+ };
235
+ }
236
+ return {
237
+ url: new URL("./db-worker.js", currentUrl)
238
+ };
239
+ }
240
+ var Database = class _Database {
241
+ #worker;
242
+ #closed = false;
243
+ #closing = false;
244
+ #released = false;
245
+ #nextId = 1;
246
+ #activeRequests = 0;
247
+ #idleTimer;
248
+ #closePromise;
249
+ #pending = /* @__PURE__ */ new Map();
250
+ #ready;
251
+ #exited;
252
+ #releaseConnection;
253
+ constructor(worker, releaseConnection) {
254
+ this.#worker = worker;
255
+ this.#releaseConnection = releaseConnection;
256
+ let resolveReady;
257
+ let rejectReady;
258
+ this.#ready = new Promise((resolve, reject) => {
259
+ resolveReady = resolve;
260
+ rejectReady = reject;
261
+ });
262
+ let resolveExited;
263
+ this.#exited = new Promise((resolve) => {
264
+ resolveExited = resolve;
265
+ });
266
+ const rejectPending = (error) => {
267
+ for (const { reject } of this.#pending.values()) {
268
+ reject(error);
269
+ }
270
+ this.#pending.clear();
271
+ };
272
+ worker.on("message", (message) => {
273
+ if (message.type === "ready") {
274
+ resolveReady();
275
+ return;
276
+ }
277
+ if (message.type === "fatal") {
278
+ const error = buildDbError(message.error);
279
+ rejectReady(error);
280
+ rejectPending(error);
281
+ return;
282
+ }
283
+ const pending = this.#pending.get(message.id);
284
+ if (!pending) {
285
+ return;
286
+ }
287
+ this.#pending.delete(message.id);
288
+ if (message.type === "error") {
289
+ pending.reject(buildDbError(message.error));
290
+ return;
291
+ }
292
+ pending.resolve(message.result);
293
+ });
294
+ worker.once("error", (error) => {
295
+ rejectReady(error);
296
+ rejectPending(error instanceof Error ? error : new Error(String(error)));
297
+ });
298
+ worker.once("exit", (code) => {
299
+ this.#closed = true;
300
+ this.#release();
301
+ resolveExited();
302
+ const error = new Error(
303
+ code === 0 ? "DB worker exited" : `DB worker exited with code ${code}`
304
+ );
305
+ if (code !== 0) {
306
+ rejectReady(error);
307
+ }
308
+ if (this.#pending.size > 0) {
309
+ rejectPending(error);
310
+ }
311
+ });
312
+ }
313
+ static async open(filename, onClosed) {
314
+ const { url, execArgv } = resolveWorkerSpec();
315
+ const worker = new Worker(url, {
316
+ execArgv,
317
+ workerData: { filename }
318
+ });
319
+ worker.unref();
320
+ const db = new _Database(worker, onClosed);
321
+ await db.#ready;
322
+ return db;
323
+ }
324
+ get isClosing() {
325
+ return this.#closing || this.#closed;
326
+ }
327
+ #release() {
328
+ if (this.#released) {
329
+ return;
330
+ }
331
+ this.#released = true;
332
+ this.#releaseConnection();
333
+ }
334
+ #clearIdleClose() {
335
+ if (this.#idleTimer) {
336
+ clearTimeout(this.#idleTimer);
337
+ this.#idleTimer = void 0;
338
+ }
339
+ }
340
+ #scheduleIdleClose() {
341
+ this.#clearIdleClose();
342
+ this.#idleTimer = setTimeout(() => {
343
+ if (this.#activeRequests === 0 && !this.#closed) {
344
+ void this.close().catch(() => {
345
+ });
346
+ }
347
+ }, 100);
348
+ this.#idleTimer.unref();
349
+ }
350
+ async #request(type, payload) {
351
+ if (this.#closed) {
352
+ throw new Error("DB worker is closed");
353
+ }
354
+ if (this.#closing && type !== "close") {
355
+ throw new Error("DB worker is closing");
356
+ }
357
+ this.#clearIdleClose();
358
+ this.#activeRequests += 1;
359
+ await this.#ready;
360
+ const id = this.#nextId;
361
+ this.#nextId += 1;
362
+ const request = payload === void 0 ? { id, type } : { id, type, payload };
363
+ const result = new Promise((resolve, reject) => {
364
+ this.#pending.set(id, { resolve, reject });
365
+ });
366
+ try {
367
+ this.#worker.postMessage(request);
368
+ } catch (error) {
369
+ this.#pending.delete(id);
370
+ throw error;
371
+ }
372
+ try {
373
+ return await result;
374
+ } finally {
375
+ this.#activeRequests -= 1;
376
+ if (this.#activeRequests === 0 && !this.#closed && !this.#closing) {
377
+ this.#scheduleIdleClose();
378
+ }
379
+ }
380
+ }
381
+ async exec(sql) {
382
+ await this.#request("exec", { sql });
383
+ }
384
+ async run(sql, params = []) {
385
+ return await this.#request("run", { sql, params });
386
+ }
387
+ async get(sql, params = []) {
388
+ const result = await this.#request("get", { sql, params });
389
+ return result ?? void 0;
390
+ }
391
+ async all(sql, params = []) {
392
+ return await this.#request("all", { sql, params });
393
+ }
394
+ async batch(commands, transactional = false) {
395
+ await this.#request("batch", { commands, transactional });
396
+ }
397
+ async close() {
398
+ if (this.#closed) {
399
+ return;
400
+ }
401
+ if (this.#closePromise) {
402
+ return this.#closePromise;
403
+ }
404
+ this.#closePromise = (async () => {
405
+ this.#closing = true;
406
+ this.#release();
407
+ this.#clearIdleClose();
408
+ await this.#request("close");
409
+ this.#closed = true;
410
+ await this.#exited;
411
+ })();
412
+ return this.#closePromise;
413
+ }
414
+ };
220
415
  var DB_CONFIG_FILE = "db.json";
221
416
  var DB_FILENAME = "messages.sqlite";
222
417
  var connections = /* @__PURE__ */ new Map();
223
418
  var writeQueues = /* @__PURE__ */ new Map();
419
+ var UPSERT_THREAD_SQL = `
420
+ INSERT INTO threads (
421
+ profile, scope_thread_id, raw_thread_id, thread_type, peer_id, title,
422
+ is_pinned, is_hidden, is_archived, raw_json, created_at, updated_at
423
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
424
+ ON CONFLICT(profile, scope_thread_id) DO UPDATE SET
425
+ raw_thread_id = excluded.raw_thread_id,
426
+ thread_type = excluded.thread_type,
427
+ peer_id = COALESCE(excluded.peer_id, threads.peer_id),
428
+ title = COALESCE(excluded.title, threads.title),
429
+ is_pinned = excluded.is_pinned,
430
+ is_hidden = excluded.is_hidden,
431
+ is_archived = excluded.is_archived,
432
+ raw_json = COALESCE(excluded.raw_json, threads.raw_json),
433
+ updated_at = excluded.updated_at
434
+ `;
435
+ var INSERT_THREAD_MEMBER_SQL = `
436
+ INSERT INTO thread_members (
437
+ profile, scope_thread_id, user_id, display_name, zalo_name, avatar,
438
+ account_status, member_type, raw_json, snapshot_at_ms, created_at, updated_at
439
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
440
+ `;
441
+ var UPSERT_MESSAGE_SQL = `
442
+ INSERT INTO messages (
443
+ profile, message_uid, scope_thread_id, raw_thread_id, thread_type,
444
+ msg_id, cli_msg_id, action_id, sender_id, sender_name, to_id,
445
+ timestamp_ms, msg_type, content_text, content_json,
446
+ quote_msg_id, quote_cli_msg_id, quote_owner_id, quote_text,
447
+ source, raw_message_json, raw_payload_json, created_at, updated_at
448
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
449
+ ON CONFLICT(profile, message_uid) DO UPDATE SET
450
+ scope_thread_id = excluded.scope_thread_id,
451
+ raw_thread_id = excluded.raw_thread_id,
452
+ thread_type = excluded.thread_type,
453
+ msg_id = COALESCE(excluded.msg_id, messages.msg_id),
454
+ cli_msg_id = COALESCE(excluded.cli_msg_id, messages.cli_msg_id),
455
+ action_id = COALESCE(excluded.action_id, messages.action_id),
456
+ sender_id = COALESCE(excluded.sender_id, messages.sender_id),
457
+ sender_name = COALESCE(excluded.sender_name, messages.sender_name),
458
+ to_id = COALESCE(excluded.to_id, messages.to_id),
459
+ timestamp_ms = excluded.timestamp_ms,
460
+ msg_type = COALESCE(excluded.msg_type, messages.msg_type),
461
+ content_text = COALESCE(excluded.content_text, messages.content_text),
462
+ content_json = COALESCE(excluded.content_json, messages.content_json),
463
+ quote_msg_id = COALESCE(excluded.quote_msg_id, messages.quote_msg_id),
464
+ quote_cli_msg_id = COALESCE(excluded.quote_cli_msg_id, messages.quote_cli_msg_id),
465
+ quote_owner_id = COALESCE(excluded.quote_owner_id, messages.quote_owner_id),
466
+ quote_text = COALESCE(excluded.quote_text, messages.quote_text),
467
+ source = excluded.source,
468
+ raw_message_json = COALESCE(excluded.raw_message_json, messages.raw_message_json),
469
+ raw_payload_json = COALESCE(excluded.raw_payload_json, messages.raw_payload_json),
470
+ updated_at = excluded.updated_at
471
+ `;
472
+ var INSERT_MESSAGE_MEDIA_SQL = `
473
+ INSERT INTO message_media (
474
+ profile, message_uid, item_index, media_kind, media_url,
475
+ media_path, media_type, raw_json, created_at, updated_at
476
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
477
+ `;
478
+ var INSERT_MESSAGE_MENTION_SQL = `
479
+ INSERT INTO message_mentions (
480
+ profile, message_uid, item_index, target_user_id, pos, len,
481
+ mention_type, raw_json, created_at, updated_at
482
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
483
+ `;
224
484
  function nowIso2() {
225
485
  return (/* @__PURE__ */ new Date()).toISOString();
226
486
  }
@@ -326,165 +586,21 @@ async function resolveDbPath(profile) {
326
586
  }
327
587
  return path2.isAbsolute(configured) ? configured : path2.resolve(getProfileDir(profile), configured);
328
588
  }
329
- async function migrateDb(db) {
330
- await db.exec(`
331
- PRAGMA journal_mode = WAL;
332
- PRAGMA foreign_keys = ON;
333
-
334
- CREATE TABLE IF NOT EXISTS threads (
335
- profile TEXT NOT NULL,
336
- scope_thread_id TEXT NOT NULL,
337
- raw_thread_id TEXT NOT NULL,
338
- thread_type TEXT NOT NULL,
339
- peer_id TEXT,
340
- title TEXT,
341
- is_pinned INTEGER NOT NULL DEFAULT 0,
342
- is_hidden INTEGER NOT NULL DEFAULT 0,
343
- is_archived INTEGER NOT NULL DEFAULT 0,
344
- raw_json TEXT,
345
- created_at TEXT NOT NULL,
346
- updated_at TEXT NOT NULL,
347
- PRIMARY KEY (profile, scope_thread_id)
348
- );
349
-
350
- CREATE TABLE IF NOT EXISTS thread_members (
351
- profile TEXT NOT NULL,
352
- scope_thread_id TEXT NOT NULL,
353
- user_id TEXT NOT NULL,
354
- display_name TEXT,
355
- zalo_name TEXT,
356
- avatar TEXT,
357
- account_status INTEGER,
358
- member_type INTEGER,
359
- raw_json TEXT,
360
- snapshot_at_ms INTEGER NOT NULL,
361
- created_at TEXT NOT NULL,
362
- updated_at TEXT NOT NULL,
363
- PRIMARY KEY (profile, scope_thread_id, user_id)
364
- );
365
-
366
- CREATE TABLE IF NOT EXISTS friends (
367
- profile TEXT NOT NULL,
368
- user_id TEXT NOT NULL,
369
- display_name TEXT,
370
- zalo_name TEXT,
371
- avatar TEXT,
372
- account_status INTEGER,
373
- raw_json TEXT,
374
- created_at TEXT NOT NULL,
375
- updated_at TEXT NOT NULL,
376
- PRIMARY KEY (profile, user_id)
377
- );
378
-
379
- CREATE TABLE IF NOT EXISTS self_profiles (
380
- profile TEXT NOT NULL,
381
- user_id TEXT NOT NULL,
382
- display_name TEXT,
383
- info_json TEXT,
384
- created_at TEXT NOT NULL,
385
- updated_at TEXT NOT NULL,
386
- PRIMARY KEY (profile)
387
- );
388
-
389
- CREATE TABLE IF NOT EXISTS messages (
390
- profile TEXT NOT NULL,
391
- message_uid TEXT NOT NULL,
392
- scope_thread_id TEXT NOT NULL,
393
- raw_thread_id TEXT NOT NULL,
394
- thread_type TEXT NOT NULL,
395
- msg_id TEXT,
396
- cli_msg_id TEXT,
397
- action_id TEXT,
398
- sender_id TEXT,
399
- sender_name TEXT,
400
- to_id TEXT,
401
- timestamp_ms INTEGER NOT NULL,
402
- msg_type TEXT,
403
- content_text TEXT,
404
- content_json TEXT,
405
- quote_msg_id TEXT,
406
- quote_cli_msg_id TEXT,
407
- quote_owner_id TEXT,
408
- quote_text TEXT,
409
- source TEXT NOT NULL,
410
- raw_message_json TEXT,
411
- raw_payload_json TEXT,
412
- created_at TEXT NOT NULL,
413
- updated_at TEXT NOT NULL,
414
- PRIMARY KEY (profile, message_uid)
415
- );
416
-
417
- CREATE TABLE IF NOT EXISTS message_media (
418
- profile TEXT NOT NULL,
419
- message_uid TEXT NOT NULL,
420
- item_index INTEGER NOT NULL,
421
- media_kind TEXT,
422
- media_url TEXT,
423
- media_path TEXT,
424
- media_type TEXT,
425
- raw_json TEXT,
426
- created_at TEXT NOT NULL,
427
- updated_at TEXT NOT NULL,
428
- PRIMARY KEY (profile, message_uid, item_index)
429
- );
430
-
431
- CREATE TABLE IF NOT EXISTS message_mentions (
432
- profile TEXT NOT NULL,
433
- message_uid TEXT NOT NULL,
434
- item_index INTEGER NOT NULL,
435
- target_user_id TEXT NOT NULL,
436
- pos INTEGER,
437
- len INTEGER,
438
- mention_type INTEGER,
439
- raw_json TEXT,
440
- created_at TEXT NOT NULL,
441
- updated_at TEXT NOT NULL,
442
- PRIMARY KEY (profile, message_uid, item_index)
443
- );
444
-
445
- CREATE TABLE IF NOT EXISTS sync_state (
446
- profile TEXT NOT NULL,
447
- scope TEXT NOT NULL,
448
- scope_thread_id TEXT NOT NULL,
449
- thread_type TEXT NOT NULL,
450
- status TEXT NOT NULL,
451
- completeness TEXT,
452
- cursor TEXT,
453
- last_sync_at TEXT,
454
- error TEXT,
455
- created_at TEXT NOT NULL,
456
- updated_at TEXT NOT NULL,
457
- PRIMARY KEY (profile, scope)
458
- );
459
-
460
- CREATE INDEX IF NOT EXISTS idx_messages_thread_time
461
- ON messages (profile, scope_thread_id, timestamp_ms DESC);
462
- CREATE INDEX IF NOT EXISTS idx_messages_msg_id
463
- ON messages (profile, msg_id);
464
- CREATE INDEX IF NOT EXISTS idx_messages_cli_msg_id
465
- ON messages (profile, cli_msg_id);
466
- CREATE INDEX IF NOT EXISTS idx_threads_type
467
- ON threads (profile, thread_type, updated_at DESC);
468
- CREATE INDEX IF NOT EXISTS idx_members_thread
469
- ON thread_members (profile, scope_thread_id);
470
- CREATE INDEX IF NOT EXISTS idx_friends_name
471
- ON friends (profile, display_name, zalo_name, user_id);
472
- `);
473
- }
474
589
  async function openDb(profile) {
475
590
  const filename = await resolveDbPath(profile);
476
591
  await fs2.mkdir(path2.dirname(filename), { recursive: true });
477
- const db = await open({
478
- filename,
479
- driver: sqlite3.Database
592
+ return Database.open(filename, () => {
593
+ connections.delete(profile);
480
594
  });
481
- await migrateDb(db);
482
- return db;
483
595
  }
484
596
  async function getDb(profile) {
485
597
  const existing = connections.get(profile);
486
598
  if (existing) {
487
- return existing;
599
+ const db = await existing;
600
+ if (!db.isClosing) {
601
+ return db;
602
+ }
603
+ connections.delete(profile);
488
604
  }
489
605
  const created = openDb(profile).catch((error) => {
490
606
  connections.delete(profile);
@@ -596,77 +712,48 @@ function enqueueDbWrite(profile, task) {
596
712
  async function persistThread(record) {
597
713
  const db = await getDb(record.profile);
598
714
  const now = nowIso2();
599
- await db.run(
600
- `
601
- INSERT INTO threads (
602
- profile, scope_thread_id, raw_thread_id, thread_type, peer_id, title,
603
- is_pinned, is_hidden, is_archived, raw_json, created_at, updated_at
604
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
605
- ON CONFLICT(profile, scope_thread_id) DO UPDATE SET
606
- raw_thread_id = excluded.raw_thread_id,
607
- thread_type = excluded.thread_type,
608
- peer_id = COALESCE(excluded.peer_id, threads.peer_id),
609
- title = COALESCE(excluded.title, threads.title),
610
- is_pinned = excluded.is_pinned,
611
- is_hidden = excluded.is_hidden,
612
- is_archived = excluded.is_archived,
613
- raw_json = COALESCE(excluded.raw_json, threads.raw_json),
614
- updated_at = excluded.updated_at
615
- `,
616
- [
617
- record.profile,
618
- record.scopeThreadId,
619
- record.rawThreadId,
620
- record.threadType,
621
- record.peerId ?? null,
622
- record.title ?? null,
623
- record.isPinned ? 1 : 0,
624
- record.isHidden ? 1 : 0,
625
- record.isArchived ? 1 : 0,
626
- record.rawJson ?? null,
627
- now,
628
- now
629
- ]
630
- );
715
+ await db.run(UPSERT_THREAD_SQL, [
716
+ record.profile,
717
+ record.scopeThreadId,
718
+ record.rawThreadId,
719
+ record.threadType,
720
+ record.peerId ?? null,
721
+ record.title ?? null,
722
+ record.isPinned ? 1 : 0,
723
+ record.isHidden ? 1 : 0,
724
+ record.isArchived ? 1 : 0,
725
+ record.rawJson ?? null,
726
+ now,
727
+ now
728
+ ]);
631
729
  }
632
730
  async function replaceThreadMembers(profile, scopeThreadId, members) {
633
731
  const db = await getDb(profile);
634
732
  const now = nowIso2();
635
- await db.exec("BEGIN");
636
- try {
637
- await db.run(
638
- `DELETE FROM thread_members WHERE profile = ? AND scope_thread_id = ?`,
639
- [profile, scopeThreadId]
640
- );
641
- for (const member of members) {
642
- await db.run(
643
- `
644
- INSERT INTO thread_members (
645
- profile, scope_thread_id, user_id, display_name, zalo_name, avatar,
646
- account_status, member_type, raw_json, snapshot_at_ms, created_at, updated_at
647
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
648
- `,
649
- [
650
- member.profile,
651
- member.scopeThreadId,
652
- member.userId,
653
- member.displayName ?? null,
654
- member.zaloName ?? null,
655
- member.avatar ?? null,
656
- member.accountStatus ?? null,
657
- member.memberType ?? null,
658
- member.rawJson ?? null,
659
- member.snapshotAtMs,
660
- now,
661
- now
662
- ]
663
- );
664
- }
665
- await db.exec("COMMIT");
666
- } catch (error) {
667
- await db.exec("ROLLBACK");
668
- throw error;
669
- }
733
+ const commands = [
734
+ {
735
+ sql: `DELETE FROM thread_members WHERE profile = ? AND scope_thread_id = ?`,
736
+ params: [profile, scopeThreadId]
737
+ },
738
+ ...members.map((member) => ({
739
+ sql: INSERT_THREAD_MEMBER_SQL,
740
+ params: [
741
+ member.profile,
742
+ member.scopeThreadId,
743
+ member.userId,
744
+ member.displayName ?? null,
745
+ member.zaloName ?? null,
746
+ member.avatar ?? null,
747
+ member.accountStatus ?? null,
748
+ member.memberType ?? null,
749
+ member.rawJson ?? null,
750
+ member.snapshotAtMs,
751
+ now,
752
+ now
753
+ ]
754
+ }))
755
+ ];
756
+ await db.batch(commands, true);
670
757
  }
671
758
  async function persistFriend(record) {
672
759
  const db = await getDb(record.profile);
@@ -726,49 +813,27 @@ async function persistMessage(record) {
726
813
  const db = await getDb(record.profile);
727
814
  const now = nowIso2();
728
815
  const messageUid = toMessageUid(record);
729
- await db.exec("BEGIN");
730
- try {
731
- await persistThread({
732
- profile: record.profile,
733
- scopeThreadId: record.scopeThreadId,
734
- rawThreadId: record.rawThreadId,
735
- threadType: record.threadType,
736
- peerId: record.peerId,
737
- title: record.title
738
- });
739
- await db.run(
740
- `
741
- INSERT INTO messages (
742
- profile, message_uid, scope_thread_id, raw_thread_id, thread_type,
743
- msg_id, cli_msg_id, action_id, sender_id, sender_name, to_id,
744
- timestamp_ms, msg_type, content_text, content_json,
745
- quote_msg_id, quote_cli_msg_id, quote_owner_id, quote_text,
746
- source, raw_message_json, raw_payload_json, created_at, updated_at
747
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
748
- ON CONFLICT(profile, message_uid) DO UPDATE SET
749
- scope_thread_id = excluded.scope_thread_id,
750
- raw_thread_id = excluded.raw_thread_id,
751
- thread_type = excluded.thread_type,
752
- msg_id = COALESCE(excluded.msg_id, messages.msg_id),
753
- cli_msg_id = COALESCE(excluded.cli_msg_id, messages.cli_msg_id),
754
- action_id = COALESCE(excluded.action_id, messages.action_id),
755
- sender_id = COALESCE(excluded.sender_id, messages.sender_id),
756
- sender_name = COALESCE(excluded.sender_name, messages.sender_name),
757
- to_id = COALESCE(excluded.to_id, messages.to_id),
758
- timestamp_ms = excluded.timestamp_ms,
759
- msg_type = COALESCE(excluded.msg_type, messages.msg_type),
760
- content_text = COALESCE(excluded.content_text, messages.content_text),
761
- content_json = COALESCE(excluded.content_json, messages.content_json),
762
- quote_msg_id = COALESCE(excluded.quote_msg_id, messages.quote_msg_id),
763
- quote_cli_msg_id = COALESCE(excluded.quote_cli_msg_id, messages.quote_cli_msg_id),
764
- quote_owner_id = COALESCE(excluded.quote_owner_id, messages.quote_owner_id),
765
- quote_text = COALESCE(excluded.quote_text, messages.quote_text),
766
- source = excluded.source,
767
- raw_message_json = COALESCE(excluded.raw_message_json, messages.raw_message_json),
768
- raw_payload_json = COALESCE(excluded.raw_payload_json, messages.raw_payload_json),
769
- updated_at = excluded.updated_at
770
- `,
771
- [
816
+ const commands = [
817
+ {
818
+ sql: UPSERT_THREAD_SQL,
819
+ params: [
820
+ record.profile,
821
+ record.scopeThreadId,
822
+ record.rawThreadId,
823
+ record.threadType,
824
+ record.peerId ?? null,
825
+ record.title ?? null,
826
+ 0,
827
+ 0,
828
+ 0,
829
+ null,
830
+ now,
831
+ now
832
+ ]
833
+ },
834
+ {
835
+ sql: UPSERT_MESSAGE_SQL,
836
+ params: [
772
837
  record.profile,
773
838
  messageUid,
774
839
  record.scopeThreadId,
@@ -794,64 +859,47 @@ async function persistMessage(record) {
794
859
  now,
795
860
  now
796
861
  ]
797
- );
798
- await db.run(
799
- `DELETE FROM message_media WHERE profile = ? AND message_uid = ?`,
800
- [record.profile, messageUid]
801
- );
802
- await db.run(
803
- `DELETE FROM message_mentions WHERE profile = ? AND message_uid = ?`,
804
- [record.profile, messageUid]
805
- );
806
- for (const [index, media] of (record.media ?? []).entries()) {
807
- await db.run(
808
- `
809
- INSERT INTO message_media (
810
- profile, message_uid, item_index, media_kind, media_url,
811
- media_path, media_type, raw_json, created_at, updated_at
812
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
813
- `,
814
- [
815
- record.profile,
816
- messageUid,
817
- index,
818
- media.mediaKind ?? null,
819
- media.mediaUrl ?? null,
820
- media.mediaPath ?? null,
821
- media.mediaType ?? null,
822
- media.rawJson ?? null,
823
- now,
824
- now
825
- ]
826
- );
827
- }
828
- for (const [index, mention] of (record.mentions ?? []).entries()) {
829
- await db.run(
830
- `
831
- INSERT INTO message_mentions (
832
- profile, message_uid, item_index, target_user_id, pos, len,
833
- mention_type, raw_json, created_at, updated_at
834
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
835
- `,
836
- [
837
- record.profile,
838
- messageUid,
839
- index,
840
- mention.uid,
841
- mention.pos ?? null,
842
- mention.len ?? null,
843
- mention.type ?? null,
844
- mention.rawJson ?? null,
845
- now,
846
- now
847
- ]
848
- );
849
- }
850
- await db.exec("COMMIT");
851
- } catch (error) {
852
- await db.exec("ROLLBACK");
853
- throw error;
854
- }
862
+ },
863
+ {
864
+ sql: `DELETE FROM message_media WHERE profile = ? AND message_uid = ?`,
865
+ params: [record.profile, messageUid]
866
+ },
867
+ {
868
+ sql: `DELETE FROM message_mentions WHERE profile = ? AND message_uid = ?`,
869
+ params: [record.profile, messageUid]
870
+ },
871
+ ...(record.media ?? []).map((media, index) => ({
872
+ sql: INSERT_MESSAGE_MEDIA_SQL,
873
+ params: [
874
+ record.profile,
875
+ messageUid,
876
+ index,
877
+ media.mediaKind ?? null,
878
+ media.mediaUrl ?? null,
879
+ media.mediaPath ?? null,
880
+ media.mediaType ?? null,
881
+ media.rawJson ?? null,
882
+ now,
883
+ now
884
+ ]
885
+ })),
886
+ ...(record.mentions ?? []).map((mention, index) => ({
887
+ sql: INSERT_MESSAGE_MENTION_SQL,
888
+ params: [
889
+ record.profile,
890
+ messageUid,
891
+ index,
892
+ mention.uid,
893
+ mention.pos ?? null,
894
+ mention.len ?? null,
895
+ mention.type ?? null,
896
+ mention.rawJson ?? null,
897
+ now,
898
+ now
899
+ ]
900
+ }))
901
+ ];
902
+ await db.batch(commands, true);
855
903
  }
856
904
  async function setSyncState(params) {
857
905
  const db = await getDb(params.profile);
@@ -1931,9 +1979,6 @@ function parseTimeBoundaryInput(value, _nowMs = Date.now()) {
1931
1979
  return void 0;
1932
1980
  }
1933
1981
 
1934
- // src/lib/text-send.ts
1935
- import { ThreadType } from "zca-js";
1936
-
1937
1982
  // src/lib/group-mentions.ts
1938
1983
  var ALLOWED_START_BOUNDARY_CHARS = /* @__PURE__ */ new Set(["(", "[", "{", "<", '"', ",", ";", ":"]);
1939
1984
  var ALLOWED_END_BOUNDARY_CHARS = /* @__PURE__ */ new Set([",", ";", ":", "!", "?", ")", "]", "}", ">", '"']);
@@ -2038,6 +2083,9 @@ function isMentionStartBoundary(text, atIndex) {
2038
2083
  return /\s/u.test(previous) || ALLOWED_START_BOUNDARY_CHARS.has(previous);
2039
2084
  }
2040
2085
 
2086
+ // src/lib/text-send.ts
2087
+ import { ThreadType } from "zca-js";
2088
+
2041
2089
  // src/lib/text-styles.ts
2042
2090
  import { TextStyle } from "zca-js";
2043
2091
  var TAG_STYLE_MAP = {
@@ -2298,6 +2346,39 @@ async function buildTextSendPayload(params) {
2298
2346
  mentions
2299
2347
  };
2300
2348
  }
2349
+ async function analyzeTextSendPayload(params) {
2350
+ const payload = await buildTextSendPayload(params);
2351
+ const payloadObject = normalizeTextSendPayload(payload);
2352
+ const textProperties = buildTextProperties(payloadObject.styles);
2353
+ const mentionInfo = buildMentionInfo(
2354
+ params.threadType,
2355
+ payloadObject.msg,
2356
+ payloadObject.mentions
2357
+ );
2358
+ const requestParams = omitUndefined({
2359
+ message: payloadObject.msg,
2360
+ clientId: 17e11,
2361
+ mentionInfo,
2362
+ imei: params.threadType === ThreadType.Group ? void 0 : "000000000000000",
2363
+ ttl: 0,
2364
+ visibility: params.threadType === ThreadType.Group ? 0 : void 0,
2365
+ toid: params.threadType === ThreadType.Group ? void 0 : params.threadId,
2366
+ grid: params.threadType === ThreadType.Group ? params.threadId : void 0,
2367
+ textProperties
2368
+ });
2369
+ return {
2370
+ payload,
2371
+ payloadObject,
2372
+ rawInputLength: params.message.length,
2373
+ renderedTextLength: payloadObject.msg.length,
2374
+ styleCount: payloadObject.styles?.length ?? 0,
2375
+ mentionCount: payloadObject.mentions?.length ?? 0,
2376
+ textPropertiesLength: textProperties?.length ?? 0,
2377
+ mentionInfoLength: mentionInfo?.length ?? 0,
2378
+ requestParamsLengthEstimate: JSON.stringify(requestParams).length,
2379
+ sendPath: params.threadType === ThreadType.Group ? mentionInfo ? "mention" : "sendmsg" : "sms"
2380
+ };
2381
+ }
2301
2382
  async function resolveGroupMentionsIfNeeded(params, text) {
2302
2383
  if (params.threadType !== ThreadType.Group) {
2303
2384
  return void 0;
@@ -2312,6 +2393,61 @@ async function resolveGroupMentionsIfNeeded(params, text) {
2312
2393
  const mentions = resolveOutboundGroupMentions(text, members);
2313
2394
  return mentions.length > 0 ? mentions : void 0;
2314
2395
  }
2396
+ function normalizeTextSendPayload(payload) {
2397
+ if (typeof payload === "string") {
2398
+ return { msg: payload };
2399
+ }
2400
+ return payload;
2401
+ }
2402
+ function buildTextProperties(styles) {
2403
+ if (!styles || styles.length === 0) {
2404
+ return void 0;
2405
+ }
2406
+ return JSON.stringify({
2407
+ styles: styles.map((style) => {
2408
+ if (style.st === "ind_$") {
2409
+ return omitUndefined({
2410
+ start: style.start,
2411
+ len: style.len,
2412
+ st: `ind_${style.indentSize ?? 1}0`
2413
+ });
2414
+ }
2415
+ return {
2416
+ start: style.start,
2417
+ len: style.len,
2418
+ st: style.st
2419
+ };
2420
+ }),
2421
+ ver: 0
2422
+ });
2423
+ }
2424
+ function buildMentionInfo(threadType, msg2, mentions) {
2425
+ if (threadType !== ThreadType.Group || !mentions || mentions.length === 0) {
2426
+ return void 0;
2427
+ }
2428
+ let totalMentionLen = 0;
2429
+ const mentionsFinal = mentions.filter((mention) => mention.pos >= 0 && Boolean(mention.uid) && mention.len > 0).map((mention) => {
2430
+ totalMentionLen += mention.len;
2431
+ return {
2432
+ pos: mention.pos,
2433
+ uid: mention.uid,
2434
+ len: mention.len,
2435
+ type: mention.uid === "-1" ? 1 : 0
2436
+ };
2437
+ });
2438
+ if (totalMentionLen > msg2.length) {
2439
+ throw new Error("Invalid mentions: total mention characters exceed message length");
2440
+ }
2441
+ if (mentionsFinal.length === 0) {
2442
+ return void 0;
2443
+ }
2444
+ return JSON.stringify(mentionsFinal);
2445
+ }
2446
+ function omitUndefined(value) {
2447
+ return Object.fromEntries(
2448
+ Object.entries(value).filter(([, entry]) => entry !== void 0)
2449
+ );
2450
+ }
2315
2451
 
2316
2452
  // src/lib/video-send.ts
2317
2453
  import { execFile } from "child_process";
@@ -2720,8 +2856,8 @@ function inferReplyMessageThreadId(params) {
2720
2856
  }
2721
2857
 
2722
2858
  // src/cli.ts
2723
- var require2 = createRequire(import.meta.url);
2724
- var { version: PKG_VERSION } = require2("../package.json");
2859
+ var require3 = createRequire2(import.meta.url);
2860
+ var { version: PKG_VERSION } = require3("../package.json");
2725
2861
  var program = new Command();
2726
2862
  var EMOJI_REACTION_MAP = {
2727
2863
  "\u2764\uFE0F": Reactions.HEART,
@@ -6405,6 +6541,25 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
6405
6541
  }
6406
6542
  })
6407
6543
  );
6544
+ msg.command("analyze-text <threadId> <message>").option("-g, --group", "Analyze as group text").option("--raw", "Analyze raw text without parsing formatting markers").option("-j, --json", "JSON output").description("Build and analyze the exact text payload that msg send would hand to zca-js. Useful for pre-send chunking/debugging.").action(
6545
+ wrapAction(async (threadId, message, opts, command) => {
6546
+ const threadType = asThreadType(opts.group);
6547
+ const mentionProbeText = opts.raw ? message : parseTextStyles(message).text;
6548
+ let listGroupMembers;
6549
+ if (threadType === ThreadType3.Group && hasPotentialOutboundGroupMention(mentionProbeText)) {
6550
+ const { api } = await requireApi(command);
6551
+ listGroupMembers = (groupId) => listGroupMentionMembers(api, groupId);
6552
+ }
6553
+ const analysis = await analyzeTextSendPayload({
6554
+ message,
6555
+ raw: opts.raw,
6556
+ threadType,
6557
+ threadId,
6558
+ listGroupMembers
6559
+ });
6560
+ output(analysis, shouldOutputJson(opts));
6561
+ })
6562
+ );
6408
6563
  msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (repeatable)", collectValues, []).option("-m, --message <message>", "Caption").option("-g, --group", "Send to group").description("Send image(s) from file or URL").action(
6409
6564
  wrapAction(
6410
6565
  async (threadId, file, opts, command) => {
@@ -0,0 +1,292 @@
1
+ // src/lib/db-worker.ts
2
+ import { parentPort, workerData } from "worker_threads";
3
+ var INIT_SQL = `
4
+ PRAGMA journal_mode = WAL;
5
+ PRAGMA synchronous = FULL;
6
+ PRAGMA busy_timeout = 5000;
7
+ PRAGMA foreign_keys = ON;
8
+
9
+ CREATE TABLE IF NOT EXISTS threads (
10
+ profile TEXT NOT NULL,
11
+ scope_thread_id TEXT NOT NULL,
12
+ raw_thread_id TEXT NOT NULL,
13
+ thread_type TEXT NOT NULL,
14
+ peer_id TEXT,
15
+ title TEXT,
16
+ is_pinned INTEGER NOT NULL DEFAULT 0,
17
+ is_hidden INTEGER NOT NULL DEFAULT 0,
18
+ is_archived INTEGER NOT NULL DEFAULT 0,
19
+ raw_json TEXT,
20
+ created_at TEXT NOT NULL,
21
+ updated_at TEXT NOT NULL,
22
+ PRIMARY KEY (profile, scope_thread_id)
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS thread_members (
26
+ profile TEXT NOT NULL,
27
+ scope_thread_id TEXT NOT NULL,
28
+ user_id TEXT NOT NULL,
29
+ display_name TEXT,
30
+ zalo_name TEXT,
31
+ avatar TEXT,
32
+ account_status INTEGER,
33
+ member_type INTEGER,
34
+ raw_json TEXT,
35
+ snapshot_at_ms INTEGER NOT NULL,
36
+ created_at TEXT NOT NULL,
37
+ updated_at TEXT NOT NULL,
38
+ PRIMARY KEY (profile, scope_thread_id, user_id)
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS friends (
42
+ profile TEXT NOT NULL,
43
+ user_id TEXT NOT NULL,
44
+ display_name TEXT,
45
+ zalo_name TEXT,
46
+ avatar TEXT,
47
+ account_status INTEGER,
48
+ raw_json TEXT,
49
+ created_at TEXT NOT NULL,
50
+ updated_at TEXT NOT NULL,
51
+ PRIMARY KEY (profile, user_id)
52
+ );
53
+
54
+ CREATE TABLE IF NOT EXISTS self_profiles (
55
+ profile TEXT NOT NULL,
56
+ user_id TEXT NOT NULL,
57
+ display_name TEXT,
58
+ info_json TEXT,
59
+ created_at TEXT NOT NULL,
60
+ updated_at TEXT NOT NULL,
61
+ PRIMARY KEY (profile)
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS messages (
65
+ profile TEXT NOT NULL,
66
+ message_uid TEXT NOT NULL,
67
+ scope_thread_id TEXT NOT NULL,
68
+ raw_thread_id TEXT NOT NULL,
69
+ thread_type TEXT NOT NULL,
70
+ msg_id TEXT,
71
+ cli_msg_id TEXT,
72
+ action_id TEXT,
73
+ sender_id TEXT,
74
+ sender_name TEXT,
75
+ to_id TEXT,
76
+ timestamp_ms INTEGER NOT NULL,
77
+ msg_type TEXT,
78
+ content_text TEXT,
79
+ content_json TEXT,
80
+ quote_msg_id TEXT,
81
+ quote_cli_msg_id TEXT,
82
+ quote_owner_id TEXT,
83
+ quote_text TEXT,
84
+ source TEXT NOT NULL,
85
+ raw_message_json TEXT,
86
+ raw_payload_json TEXT,
87
+ created_at TEXT NOT NULL,
88
+ updated_at TEXT NOT NULL,
89
+ PRIMARY KEY (profile, message_uid)
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS message_media (
93
+ profile TEXT NOT NULL,
94
+ message_uid TEXT NOT NULL,
95
+ item_index INTEGER NOT NULL,
96
+ media_kind TEXT,
97
+ media_url TEXT,
98
+ media_path TEXT,
99
+ media_type TEXT,
100
+ raw_json TEXT,
101
+ created_at TEXT NOT NULL,
102
+ updated_at TEXT NOT NULL,
103
+ PRIMARY KEY (profile, message_uid, item_index)
104
+ );
105
+
106
+ CREATE TABLE IF NOT EXISTS message_mentions (
107
+ profile TEXT NOT NULL,
108
+ message_uid TEXT NOT NULL,
109
+ item_index INTEGER NOT NULL,
110
+ target_user_id TEXT NOT NULL,
111
+ pos INTEGER,
112
+ len INTEGER,
113
+ mention_type INTEGER,
114
+ raw_json TEXT,
115
+ created_at TEXT NOT NULL,
116
+ updated_at TEXT NOT NULL,
117
+ PRIMARY KEY (profile, message_uid, item_index)
118
+ );
119
+
120
+ CREATE TABLE IF NOT EXISTS sync_state (
121
+ profile TEXT NOT NULL,
122
+ scope TEXT NOT NULL,
123
+ scope_thread_id TEXT NOT NULL,
124
+ thread_type TEXT NOT NULL,
125
+ status TEXT NOT NULL,
126
+ completeness TEXT,
127
+ cursor TEXT,
128
+ last_sync_at TEXT,
129
+ error TEXT,
130
+ created_at TEXT NOT NULL,
131
+ updated_at TEXT NOT NULL,
132
+ PRIMARY KEY (profile, scope)
133
+ );
134
+
135
+ CREATE INDEX IF NOT EXISTS idx_messages_thread_time
136
+ ON messages (profile, scope_thread_id, timestamp_ms DESC);
137
+ CREATE INDEX IF NOT EXISTS idx_messages_msg_id
138
+ ON messages (profile, msg_id);
139
+ CREATE INDEX IF NOT EXISTS idx_messages_cli_msg_id
140
+ ON messages (profile, cli_msg_id);
141
+ CREATE INDEX IF NOT EXISTS idx_threads_type
142
+ ON threads (profile, thread_type, updated_at DESC);
143
+ CREATE INDEX IF NOT EXISTS idx_members_thread
144
+ ON thread_members (profile, scope_thread_id);
145
+ CREATE INDEX IF NOT EXISTS idx_friends_name
146
+ ON friends (profile, display_name, zalo_name, user_id);
147
+ `;
148
+ if (!parentPort) {
149
+ throw new Error("DB worker requires parentPort");
150
+ }
151
+ var port = parentPort;
152
+ function serializeError(error) {
153
+ if (error instanceof Error) {
154
+ return {
155
+ name: error.name,
156
+ message: error.message,
157
+ stack: error.stack,
158
+ code: typeof error.code === "string" ? error.code : void 0
159
+ };
160
+ }
161
+ return {
162
+ name: "Error",
163
+ message: String(error)
164
+ };
165
+ }
166
+ async function loadSqliteModule() {
167
+ const originalEmitWarning = process.emitWarning;
168
+ const importDynamic = new Function("specifier", "return import(specifier);");
169
+ process.emitWarning = ((warning, options, ...args) => {
170
+ const type = typeof options === "string" ? options : void 0;
171
+ const message = warning instanceof Error ? warning.message : String(warning);
172
+ if (type === "ExperimentalWarning" && message.includes("SQLite")) {
173
+ return;
174
+ }
175
+ Reflect.apply(originalEmitWarning, process, [warning, options, ...args]);
176
+ });
177
+ try {
178
+ return await importDynamic("node:sqlite");
179
+ } finally {
180
+ process.emitWarning = originalEmitWarning;
181
+ }
182
+ }
183
+ function setDefensiveMode(db) {
184
+ const maybeDb = db;
185
+ if (typeof maybeDb.enableDefensive === "function") {
186
+ maybeDb.enableDefensive(true);
187
+ }
188
+ }
189
+ function runStatement(db, statement) {
190
+ return db.prepare(statement.sql).run(...statement.params ?? []);
191
+ }
192
+ function getStatement(db, statement) {
193
+ return db.prepare(statement.sql).get(...statement.params ?? []);
194
+ }
195
+ function allStatement(db, statement) {
196
+ return db.prepare(statement.sql).all(...statement.params ?? []);
197
+ }
198
+ async function main() {
199
+ const { DatabaseSync } = await loadSqliteModule();
200
+ const { filename } = workerData;
201
+ const db = new DatabaseSync(filename);
202
+ db.exec(INIT_SQL);
203
+ setDefensiveMode(db);
204
+ port.postMessage({ type: "ready" });
205
+ port.on("message", (message) => {
206
+ try {
207
+ switch (message.type) {
208
+ case "exec":
209
+ db.exec(message.payload.sql);
210
+ port.postMessage({
211
+ type: "result",
212
+ id: message.id,
213
+ result: null
214
+ });
215
+ return;
216
+ case "run":
217
+ port.postMessage({
218
+ type: "result",
219
+ id: message.id,
220
+ result: runStatement(db, message.payload)
221
+ });
222
+ return;
223
+ case "get":
224
+ port.postMessage({
225
+ type: "result",
226
+ id: message.id,
227
+ result: getStatement(db, message.payload) ?? null
228
+ });
229
+ return;
230
+ case "all":
231
+ port.postMessage({
232
+ type: "result",
233
+ id: message.id,
234
+ result: allStatement(db, message.payload)
235
+ });
236
+ return;
237
+ case "batch":
238
+ if (message.payload.transactional) {
239
+ db.exec("BEGIN");
240
+ }
241
+ try {
242
+ for (const command of message.payload.commands) {
243
+ if ((command.params ?? []).length === 0) {
244
+ db.exec(command.sql);
245
+ } else {
246
+ runStatement(db, command);
247
+ }
248
+ }
249
+ if (message.payload.transactional) {
250
+ db.exec("COMMIT");
251
+ }
252
+ } catch (error) {
253
+ if (message.payload.transactional) {
254
+ try {
255
+ db.exec("ROLLBACK");
256
+ } catch {
257
+ }
258
+ }
259
+ throw error;
260
+ }
261
+ port.postMessage({
262
+ type: "result",
263
+ id: message.id,
264
+ result: null
265
+ });
266
+ return;
267
+ case "close":
268
+ db.close();
269
+ port.postMessage({
270
+ type: "result",
271
+ id: message.id,
272
+ result: null
273
+ });
274
+ setImmediate(() => process.exit(0));
275
+ return;
276
+ }
277
+ } catch (error) {
278
+ port.postMessage({
279
+ type: "error",
280
+ id: message.id,
281
+ error: serializeError(error)
282
+ });
283
+ }
284
+ });
285
+ }
286
+ void main().catch((error) => {
287
+ port.postMessage({
288
+ type: "fatal",
289
+ error: serializeError(error)
290
+ });
291
+ process.exit(1);
292
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,11 +14,14 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsup src/cli.ts --format esm --target node18 --out-dir dist --clean",
17
+ "build": "npm run build:cli && npm run build:worker",
18
+ "build:cli": "tsup src/cli.ts --format esm --target node22 --out-dir dist --clean",
19
+ "build:worker": "tsup src/lib/db-worker.ts --format esm --target node22 --out-dir dist",
18
20
  "dev": "tsx src/cli.ts",
19
21
  "test": "tsx --test tests/*.test.ts",
20
22
  "typecheck": "tsc -p tsconfig.json",
21
23
  "lint": "tsc -p tsconfig.json --noEmit",
24
+ "prepare": "npm run build",
22
25
  "prepublishOnly": "npm run build"
23
26
  },
24
27
  "keywords": [
@@ -39,15 +42,13 @@
39
42
  "url": "https://github.com/darkamenosa/openzca/issues"
40
43
  },
41
44
  "engines": {
42
- "node": ">=18"
45
+ "node": ">=22.13.0"
43
46
  },
44
47
  "dependencies": {
45
48
  "@types/qrcode-terminal": "^0.12.2",
46
49
  "commander": "^14.0.3",
47
50
  "image-size": "^2.0.2",
48
51
  "qrcode-terminal": "^0.12.0",
49
- "sqlite": "^5.1.1",
50
- "sqlite3": "^6.0.1",
51
52
  "zca-js": "^2.1.2"
52
53
  },
53
54
  "devDependencies": {