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 +8 -1
- package/dist/cli.js +482 -327
- package/dist/db-worker.js +292 -0
- package/package.json +6 -5
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
|
|
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 {
|
|
219
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
799
|
-
`DELETE FROM message_media WHERE profile = ? AND message_uid = ?`,
|
|
800
|
-
[record.profile, messageUid]
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
`DELETE FROM message_mentions WHERE profile = ? AND message_uid = ?`,
|
|
804
|
-
[record.profile, messageUid]
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
|
2724
|
-
var { version: PKG_VERSION } =
|
|
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.
|
|
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": "
|
|
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": ">=
|
|
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": {
|