@ardennguyen/zalo-agent-cli 2.2.2 → 2.5.0

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.
@@ -33,6 +33,7 @@ import {
33
33
  replaceLiveGroupEventPlaceholder,
34
34
  getThreadType,
35
35
  getRecentThreads,
36
+ findMessageByClientId,
36
37
  } from "../db.js";
37
38
  import { ensureAssets, loadCodecs } from "./assets.js";
38
39
  import { classifySyncMessage } from "./message-types.js";
@@ -403,12 +404,18 @@ export function isSessionComplete(state) {
403
404
  * where they overwrote one another through insertMessage's upsert.
404
405
  *
405
406
  * @param {object} msg - a decoded Sync2.Message.Message
407
+ * @param {string|null} [threadId=null] - thread id to match an already-stored live row by cliMsgId
406
408
  * @returns {string}
407
409
  */
408
- function syncMsgId(msg) {
410
+ function syncMsgId(msg, threadId = null) {
409
411
  const gid = msg?.globalId;
410
412
  if (gid !== undefined && gid !== null && String(gid) !== "0" && String(gid) !== "") return String(gid);
411
- return String(msg?.clientId ?? "");
413
+ const cid = String(msg?.clientId ?? "");
414
+ if (cid && cid !== "0" && threadId) {
415
+ const existing = findMessageByClientId(cid, threadId);
416
+ if (existing?.msgId) return String(existing.msgId);
417
+ }
418
+ return cid;
412
419
  }
413
420
 
414
421
  export class SyncV2 {
@@ -491,6 +498,22 @@ export class SyncV2 {
491
498
  }
492
499
  }
493
500
 
501
+ /** Refresh the active account's own display name via onSelfName if provided. */
502
+ async _refreshSelfName(onSelfName) {
503
+ if (typeof onSelfName !== "function") return;
504
+ const ownId = String(this.accountName || "");
505
+ if (!/^[1-9][0-9]*$/.test(ownId)) return;
506
+ try {
507
+ const names = await resolveCurrentUserNames(this.api, [ownId], { aliases: new Map() });
508
+ const self = names.get(ownId);
509
+ if (self && self.source === "zalo" && self.name) {
510
+ await onSelfName(self.name);
511
+ }
512
+ } catch {
513
+ /* best-effort registry sync */
514
+ }
515
+ }
516
+
494
517
  _send590(listener, kind, syncId, ek, ik, req) {
495
518
  listener.sendWs(
496
519
  {
@@ -564,6 +587,7 @@ export class SyncV2 {
564
587
  initDb(resolve(this.accountDir, "zalo.db"));
565
588
  this._refreshThreadNames(threadMap);
566
589
  await this._refreshDmNames(threadMap);
590
+ await this._refreshSelfName(opts.onSelfName);
567
591
 
568
592
  const L = this.api.listener;
569
593
  const sessions = new Map(); // syncId -> {kind, batches:[], statuses:[], done, err}
@@ -1142,7 +1166,7 @@ export class SyncV2 {
1142
1166
  // it does in a removal's
1143
1167
  // globalDelMsgId and a reaction's
1144
1168
  // gMsgID.
1145
- msgId: syncMsgId(msg),
1169
+ msgId: syncMsgId(msg, threadId),
1146
1170
  threadId,
1147
1171
  senderId: senders.get(msg.senderId).id,
1148
1172
  senderName: senders.get(msg.senderId).name,
package/src/index.js CHANGED
@@ -36,8 +36,11 @@ import { configureStoragePaths } from "./core/credentials.js";
36
36
  import { logoutNeedsSession } from "./core/logout.js";
37
37
  import { checkForUpdates, selfUpdate } from "./utils/update-check.js";
38
38
  import { applyCachedThreadType } from "./utils/thread-type.js";
39
+ import { attachDaemonLogIfChild } from "./core/daemon-channel.js";
39
40
  import { success, error, warning } from "./utils/output.js";
40
41
 
42
+ attachDaemonLogIfChild();
43
+
41
44
  const DISCLAIMER =
42
45
  "This tool uses unofficial Zalo APIs (zca-js) — your account may be banned. Use at your own risk. | Tool này dùng API Zalo không chính thức (zca-js) — account có thể bị ban. Tự chịu trách nhiệm.";
43
46
 
@@ -33,9 +33,72 @@ export function getDefaultConfig() {
33
33
  downloadDir: null, // unset -> the same per-account dir every other command uses
34
34
  autoOpen: true,
35
35
  },
36
+ tls: {
37
+ cert: null,
38
+ key: null,
39
+ },
40
+ oauth: {
41
+ clientId: null,
42
+ clientSecret: null,
43
+ issuerUrl: null,
44
+ clients: [],
45
+ },
46
+ webhookSecret: null,
47
+ httpPort: null,
48
+ host: null,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Deep-merge an override layer over a base MCP config object.
54
+ * @param {object} base
55
+ * @param {object} layer
56
+ * @returns {object}
57
+ */
58
+ export function mergeMCPConfigLayer(base, layer) {
59
+ if (!layer || typeof layer !== "object") return base;
60
+ return {
61
+ ...base,
62
+ ...layer,
63
+ notify: { ...base.notify, ...(layer.notify || {}) },
64
+ limits: { ...base.limits, ...(layer.limits || {}) },
65
+ media: { ...base.media, ...(layer.media || {}) },
66
+ tls: { ...base.tls, ...(layer.tls || {}) },
67
+ oauth: {
68
+ ...base.oauth,
69
+ ...(layer.oauth || {}),
70
+ clients: Array.isArray(layer.oauth?.clients)
71
+ ? layer.oauth.clients
72
+ : Array.isArray(base.oauth?.clients)
73
+ ? base.oauth.clients
74
+ : [],
75
+ },
36
76
  };
37
77
  }
38
78
 
79
+ /**
80
+ * Resolve an environment variable with per-account priority:
81
+ * checks `<baseKey>_<accountId>` first (when `accountId` is provided), then `<baseKey>`.
82
+ *
83
+ * @param {string} baseKey - Base environment variable name (e.g. "ZALO_MCP_AUTH_TOKEN")
84
+ * @param {string|null|undefined} accountId - Zalo account ID (`ownId`)
85
+ * @param {NodeJS.ProcessEnv} [env=process.env] - Environment dictionary
86
+ * @returns {string|null} Trimmed non-empty string, or null
87
+ */
88
+ export function envForAccount(baseKey, accountId, env = process.env) {
89
+ if (accountId !== undefined && accountId !== null && String(accountId).trim()) {
90
+ const scoped = env?.[`${baseKey}_${String(accountId).trim()}`];
91
+ if (typeof scoped === "string" && scoped.trim()) {
92
+ return scoped.trim();
93
+ }
94
+ }
95
+ const globalVal = env?.[baseKey];
96
+ if (typeof globalVal === "string" && globalVal.trim()) {
97
+ return globalVal.trim();
98
+ }
99
+ return null;
100
+ }
101
+
39
102
  /**
40
103
  * Load MCP config from disk, merged with defaults.
41
104
  *
@@ -52,14 +115,7 @@ export function loadMCPConfig(configPath) {
52
115
  try {
53
116
  const raw = readFileSync(configPath || mcpConfigFile(), "utf-8");
54
117
  const saved = JSON.parse(raw);
55
- // Shallow merge: saved values override defaults
56
- return {
57
- ...defaults,
58
- ...saved,
59
- notify: { ...defaults.notify, ...saved.notify },
60
- limits: { ...defaults.limits, ...saved.limits },
61
- media: { ...defaults.media, ...saved.media },
62
- };
118
+ return mergeMCPConfigLayer(defaults, saved);
63
119
  } catch {
64
120
  // File doesn't exist or invalid JSON — use defaults
65
121
  return defaults;
@@ -96,6 +152,52 @@ export function readMCPConfig(configPath) {
96
152
  return { config: loadMCPConfig(configPath), problem: null };
97
153
  }
98
154
 
155
+ /**
156
+ * Resolve per-account MCP config overrides for `--account <ownId>`:
157
+ * 1. `baseConfig.accounts?.[accountId]` inside the main `mcp-config.json`
158
+ * 2. `<accountDir>/mcp-config.json` (when `explicitConfigPath` is not set and the file exists)
159
+ *
160
+ * If `<accountDir>/mcp-config.json` exists but is unreadable or invalid JSON, returns `problem`
161
+ * so `mcp start` refuses rather than falling back to defaults.
162
+ *
163
+ * @param {object} baseConfig - Config returned by `readMCPConfig`
164
+ * @param {{ accountId?: string|null, accountDir?: string|null, explicitConfigPath?: string|null }} [opts]
165
+ * @returns {{ config: object, problem: string|null }}
166
+ */
167
+ export function resolveAccountMCPConfig(baseConfig, { accountId, accountDir, explicitConfigPath } = {}) {
168
+ let merged = baseConfig || getDefaultConfig();
169
+ const id = accountId !== undefined && accountId !== null ? String(accountId).trim() : "";
170
+ if (id && merged.accounts && typeof merged.accounts === "object" && merged.accounts[id]) {
171
+ merged = mergeMCPConfigLayer(merged, merged.accounts[id]);
172
+ }
173
+ if (!explicitConfigPath && accountDir) {
174
+ const perAccountFile = join(accountDir, "mcp-config.json");
175
+ let raw = null;
176
+ try {
177
+ raw = readFileSync(perAccountFile, "utf-8");
178
+ } catch (e) {
179
+ if (e.code !== "ENOENT") {
180
+ return {
181
+ config: merged,
182
+ problem: `cannot read the per-account config file ${perAccountFile} (${e.code || e.message})`,
183
+ };
184
+ }
185
+ }
186
+ if (raw !== null) {
187
+ try {
188
+ const parsed = JSON.parse(raw);
189
+ merged = mergeMCPConfigLayer(merged, parsed);
190
+ } catch (e) {
191
+ return {
192
+ config: merged,
193
+ problem: `the per-account config file ${perAccountFile} is not valid JSON (${e.message})`,
194
+ };
195
+ }
196
+ }
197
+ }
198
+ return { config: merged, problem: null };
199
+ }
200
+
99
201
  /**
100
202
  * Save MCP config to disk.
101
203
  * @param {object} config