@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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/commands/conv.js +8 -1
- package/src/commands/listen.js +144 -42
- package/src/commands/login.js +16 -3
- package/src/commands/mcp.js +335 -77
- package/src/commands/profile.js +6 -2
- package/src/commands/sync.js +3 -1
- package/src/core/accounts.js +20 -0
- package/src/core/daemon-channel.js +419 -2
- package/src/core/daemon-sync.js +13 -2
- package/src/core/db.js +60 -3
- package/src/core/live-store.js +58 -1
- package/src/core/sync-v2/index.js +27 -3
- package/src/index.js +3 -0
- package/src/mcp/mcp-config.js +110 -8
- package/src/mcp/mcp-http-transport.js +529 -14
- package/src/mcp/mcp-tools.js +234 -3
|
@@ -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
|
-
|
|
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
|
|
package/src/mcp/mcp-config.js
CHANGED
|
@@ -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
|
-
|
|
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
|