dsh-msg9-kit 0.2.0 → 0.2.2
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 +24 -3
- package/README.zh-CN.md +13 -2
- package/lib/client.js +82 -80
- package/lib/index.js +192 -45
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -280,7 +280,7 @@ function deleteContact(apiUrl, apiKey, address, signal) {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// src/host/store.ts
|
|
283
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
283
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
284
284
|
import { homedir } from "node:os";
|
|
285
285
|
import { dirname, join } from "node:path";
|
|
286
286
|
function stateFilePath() {
|
|
@@ -324,11 +324,73 @@ async function saveState(state) {
|
|
|
324
324
|
}
|
|
325
325
|
var writeQueue = Promise.resolve();
|
|
326
326
|
function enqueueWrite(task) {
|
|
327
|
-
const run = writeQueue.then(
|
|
327
|
+
const run = writeQueue.then(async () => {
|
|
328
|
+
const release = await acquireStateLock();
|
|
329
|
+
try {
|
|
330
|
+
return await task();
|
|
331
|
+
} finally {
|
|
332
|
+
await release();
|
|
333
|
+
}
|
|
334
|
+
});
|
|
328
335
|
writeQueue = run.catch(() => {
|
|
329
336
|
});
|
|
330
337
|
return run;
|
|
331
338
|
}
|
|
339
|
+
var LOCK_STALE_MS = 3e4;
|
|
340
|
+
var LOCK_RETRY_MS = 100;
|
|
341
|
+
var LOCK_MAX_ATTEMPTS = 50;
|
|
342
|
+
async function acquireStateLock() {
|
|
343
|
+
const lockPath = `${stateFilePath()}.lock`;
|
|
344
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
345
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
346
|
+
let handle;
|
|
347
|
+
try {
|
|
348
|
+
handle = await open(lockPath, "wx", 384);
|
|
349
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
350
|
+
await handle.close();
|
|
351
|
+
return async () => {
|
|
352
|
+
await rm(lockPath, { force: true });
|
|
353
|
+
};
|
|
354
|
+
} catch (error) {
|
|
355
|
+
await handle?.close().catch(() => {
|
|
356
|
+
});
|
|
357
|
+
if (error.code !== "EEXIST") throw error;
|
|
358
|
+
if (await isStaleLock(lockPath)) {
|
|
359
|
+
await rm(lockPath, { force: true });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (attempt >= LOCK_MAX_ATTEMPTS) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`msg9-kit state file is locked by another process (${lockPath}); multiple dsh instances sharing one DSH_HOME are not supported`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function isStaleLock(lockPath) {
|
|
372
|
+
let info;
|
|
373
|
+
try {
|
|
374
|
+
info = await stat(lockPath);
|
|
375
|
+
} catch {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) return true;
|
|
379
|
+
const raw = await readFile(lockPath, "utf8").catch(() => "");
|
|
380
|
+
let pid = NaN;
|
|
381
|
+
try {
|
|
382
|
+
pid = Number(JSON.parse(raw).pid);
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid) {
|
|
386
|
+
try {
|
|
387
|
+
process.kill(pid, 0);
|
|
388
|
+
} catch {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
332
394
|
async function getOwner() {
|
|
333
395
|
const envKey = process.env.MSG9_OWNER_KEY;
|
|
334
396
|
if (envKey) {
|
|
@@ -359,10 +421,11 @@ async function getWorkspaceInbox(key) {
|
|
|
359
421
|
const state = await loadState();
|
|
360
422
|
return state.workspaces[key];
|
|
361
423
|
}
|
|
362
|
-
async function upsertWorkspaceInbox(key,
|
|
424
|
+
async function upsertWorkspaceInbox(key, patch) {
|
|
363
425
|
return enqueueWrite(async () => {
|
|
364
426
|
const state = await loadState();
|
|
365
|
-
|
|
427
|
+
const clean = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== void 0));
|
|
428
|
+
state.workspaces[key] = { ...state.workspaces[key] ?? {}, ...clean };
|
|
366
429
|
await saveState(state);
|
|
367
430
|
});
|
|
368
431
|
}
|
|
@@ -591,10 +654,12 @@ function maskKey(key) {
|
|
|
591
654
|
|
|
592
655
|
// src/host/service.ts
|
|
593
656
|
var DEFAULT_WORKSPACE = { key: "default", title: "default", path: "(unknown)" };
|
|
657
|
+
var OWNER_PROBE_RETRY_MS = 6e4;
|
|
658
|
+
var ownerProbeFailedAt = 0;
|
|
594
659
|
async function ownerContext() {
|
|
595
660
|
const owner = await getOwner();
|
|
596
661
|
const apiUrl = owner?.api_url || defaultApiUrl();
|
|
597
|
-
if (owner?.api_key && (owner.slug === void 0 || owner.address_domain === void 0) && !process.env.MSG9_OWNER_KEY) {
|
|
662
|
+
if (owner?.api_key && (owner.slug === void 0 || owner.address_domain === void 0) && !process.env.MSG9_OWNER_KEY && Date.now() - ownerProbeFailedAt >= OWNER_PROBE_RETRY_MS) {
|
|
598
663
|
try {
|
|
599
664
|
const me = await ownerMe(apiUrl, owner.api_key);
|
|
600
665
|
const probed = {
|
|
@@ -604,8 +669,10 @@ async function ownerContext() {
|
|
|
604
669
|
address_domain: typeof me.address_domain === "string" ? me.address_domain : null
|
|
605
670
|
};
|
|
606
671
|
await setOwner(probed);
|
|
672
|
+
ownerProbeFailedAt = 0;
|
|
607
673
|
return { owner: probed, apiUrl };
|
|
608
674
|
} catch {
|
|
675
|
+
ownerProbeFailedAt = Date.now();
|
|
609
676
|
}
|
|
610
677
|
}
|
|
611
678
|
return { owner, apiUrl };
|
|
@@ -704,31 +771,39 @@ async function migrateInbox(workspace, oldInbox, oldOwnerKey) {
|
|
|
704
771
|
throw new Error(L("\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u79DF\u6237\uFF0C\u65E0\u6CD5\u8FC1\u79FB\u3002", "No tenant is bound; cannot migrate."));
|
|
705
772
|
}
|
|
706
773
|
const agent = await provisionUnderOwner(apiUrl, owner, workspace, workspaceProfile(workspace));
|
|
707
|
-
|
|
774
|
+
let signingSeed;
|
|
775
|
+
try {
|
|
776
|
+
const material = generateSigningMaterial();
|
|
777
|
+
await setSigningKey(apiUrl, agent.api_key, material.publicKey);
|
|
778
|
+
signingSeed = material.seed;
|
|
779
|
+
} catch {
|
|
780
|
+
}
|
|
781
|
+
const inbox = {
|
|
708
782
|
address: agent.address,
|
|
709
783
|
api_key: agent.api_key,
|
|
710
784
|
api_url: apiUrl,
|
|
711
785
|
title: workspace.title,
|
|
712
|
-
path: workspace.path
|
|
713
|
-
|
|
714
|
-
|
|
786
|
+
path: workspace.path,
|
|
787
|
+
...signingSeed ? { signing_seed: signingSeed } : {}
|
|
788
|
+
};
|
|
715
789
|
if (oldInbox.address === agent.address) {
|
|
790
|
+
await replaceWorkspaceInbox(workspace.key, inbox);
|
|
716
791
|
return { inbox, oldDisabled: false, forwarding: false, movedMail: null };
|
|
717
792
|
}
|
|
718
|
-
let forwarding = false;
|
|
719
|
-
let note;
|
|
720
793
|
try {
|
|
721
794
|
await setForwarding(oldInbox.api_url, oldInbox.api_key, agent.address);
|
|
722
|
-
forwarding = true;
|
|
723
795
|
} catch (error) {
|
|
724
|
-
|
|
725
|
-
"\u65E7\u5730\u5740\u8F6C\u53D1\u8BBE\u7F6E\u5931\u8D25\uFF08{reason}\uFF09\
|
|
726
|
-
"Could not set forwarding on the old address ({reason}) \u2014
|
|
727
|
-
{ reason: error?.message ?? String(error) }
|
|
728
|
-
);
|
|
796
|
+
throw new Error(L(
|
|
797
|
+
"\u65E7\u5730\u5740 {old} \u7684\u8F6C\u53D1\u8BBE\u7F6E\u5931\u8D25\uFF08{reason}\uFF09\uFF0C\u8FC1\u79FB\u5DF2\u4E2D\u6B62\uFF1A\u672C\u5730\u914D\u7F6E\u672A\u6539\u52A8\uFF0C\u4ECD\u6307\u5411\u65E7\u4FE1\u7BB1\u3002",
|
|
798
|
+
"Could not set forwarding on the old address {old} ({reason}); migration aborted \u2014 local state still points at the old inbox.",
|
|
799
|
+
{ old: oldInbox.address, reason: error?.message ?? String(error) }
|
|
800
|
+
));
|
|
729
801
|
}
|
|
802
|
+
await replaceWorkspaceInbox(workspace.key, inbox);
|
|
803
|
+
const forwarding = true;
|
|
730
804
|
let oldDisabled = false;
|
|
731
805
|
let movedMail = null;
|
|
806
|
+
let note;
|
|
732
807
|
if (oldOwnerKey) {
|
|
733
808
|
try {
|
|
734
809
|
const moved = await ownerMoveMail(oldInbox.api_url, oldOwnerKey, oldInbox.address, { to: agent.address });
|
|
@@ -774,26 +849,51 @@ function createBridgeEventBus() {
|
|
|
774
849
|
size: () => listeners.size
|
|
775
850
|
};
|
|
776
851
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
852
|
+
var UNREAD_TTL_MS = 1e4;
|
|
853
|
+
var unreadCache;
|
|
854
|
+
var unreadInflight;
|
|
855
|
+
function invalidateUnreadCache() {
|
|
856
|
+
unreadCache = void 0;
|
|
857
|
+
}
|
|
858
|
+
async function computeUnread(deps, signal, options) {
|
|
859
|
+
const ttl = options?.ttlMs ?? UNREAD_TTL_MS;
|
|
860
|
+
if (unreadInflight) return unreadInflight;
|
|
861
|
+
if (unreadCache && Date.now() - unreadCache.at < ttl) return unreadCache.view;
|
|
862
|
+
void signal;
|
|
863
|
+
unreadInflight = (async () => {
|
|
864
|
+
const state = await deps.loadState();
|
|
865
|
+
const rows = Object.entries(state.workspaces).filter(([, inbox]) => Boolean(inbox.api_key));
|
|
866
|
+
const settled = await Promise.allSettled(
|
|
867
|
+
rows.map(async ([key, inbox]) => {
|
|
868
|
+
const page = await deps.api.listInbox(inbox.api_url, inbox.api_key, { folder: "all", limit: 1 });
|
|
869
|
+
return [key, Number(page?.unread_count ?? 0), Number(page?.total ?? (page?.messages ?? []).length)];
|
|
870
|
+
})
|
|
871
|
+
);
|
|
872
|
+
const previous = unreadCache?.view;
|
|
873
|
+
const byKey = {};
|
|
874
|
+
const totalByKey = {};
|
|
875
|
+
let total = 0;
|
|
876
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
877
|
+
const [key] = rows[index];
|
|
878
|
+
const result = settled[index];
|
|
879
|
+
if (result.status === "fulfilled") {
|
|
880
|
+
const [, count, mailboxSize] = result.value;
|
|
881
|
+
byKey[key] = count;
|
|
882
|
+
totalByKey[key] = mailboxSize;
|
|
883
|
+
total += count;
|
|
884
|
+
} else if (previous && key in previous.byKey) {
|
|
885
|
+
byKey[key] = previous.byKey[key];
|
|
886
|
+
totalByKey[key] = previous.totalByKey[key] ?? 0;
|
|
887
|
+
total += byKey[key];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const view = { total, byKey, totalByKey };
|
|
891
|
+
unreadCache = { at: Date.now(), view };
|
|
892
|
+
return view;
|
|
893
|
+
})().finally(() => {
|
|
894
|
+
unreadInflight = void 0;
|
|
895
|
+
});
|
|
896
|
+
return unreadInflight;
|
|
797
897
|
}
|
|
798
898
|
function defaultBridgeDeps(ctx, override = {}) {
|
|
799
899
|
return {
|
|
@@ -860,19 +960,35 @@ function hostnameOf(host) {
|
|
|
860
960
|
function isLoopbackHostname(hostname) {
|
|
861
961
|
return hostname === "localhost" || hostname === "::1" || hostname === "0:0:0:0:0:0:0:1" || /^127(\.\d{1,3}){3}$/.test(hostname);
|
|
862
962
|
}
|
|
963
|
+
function isLoopbackAddress(address) {
|
|
964
|
+
const normalized = address.toLowerCase().replace(/^::ffff:/, "");
|
|
965
|
+
return normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || /^127(\.\d{1,3}){3}$/.test(normalized);
|
|
966
|
+
}
|
|
863
967
|
function isTrustedRequest(req) {
|
|
864
968
|
const host = hostnameOf(req.headers.host);
|
|
865
969
|
if (!host) return false;
|
|
866
970
|
const origin = req.headers.origin;
|
|
867
971
|
if (origin) return isSameOrigin(origin, req.headers.host ?? "");
|
|
972
|
+
const remote = req.socket?.remoteAddress;
|
|
973
|
+
if (remote && !isLoopbackAddress(remote)) return false;
|
|
868
974
|
return isLoopbackHostname(host);
|
|
869
975
|
}
|
|
976
|
+
function portOf(hostHeader) {
|
|
977
|
+
if (hostHeader.startsWith("[")) {
|
|
978
|
+
const end = hostHeader.indexOf("]");
|
|
979
|
+
if (end < 0) return void 0;
|
|
980
|
+
const rest = hostHeader.slice(end + 1);
|
|
981
|
+
return rest.startsWith(":") ? rest.slice(1) : void 0;
|
|
982
|
+
}
|
|
983
|
+
const colon = hostHeader.lastIndexOf(":");
|
|
984
|
+
return colon > 0 ? hostHeader.slice(colon + 1) : void 0;
|
|
985
|
+
}
|
|
870
986
|
function isSameOrigin(origin, hostHeader) {
|
|
871
987
|
try {
|
|
872
988
|
const parsed = new URL(origin);
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const expected =
|
|
989
|
+
const originHost = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
990
|
+
if (originHost !== hostnameOf(hostHeader)) return false;
|
|
991
|
+
const expected = portOf(hostHeader) ?? (parsed.protocol === "https:" ? "443" : "80");
|
|
876
992
|
const actual = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
877
993
|
return actual === expected;
|
|
878
994
|
} catch {
|
|
@@ -1104,7 +1220,7 @@ function createMsg9Bridge(deps) {
|
|
|
1104
1220
|
description: row.profile?.description ?? null,
|
|
1105
1221
|
capabilities: row.profile?.capabilities ?? []
|
|
1106
1222
|
}));
|
|
1107
|
-
return ok(res, { agents, total: page.total ?? agents.length });
|
|
1223
|
+
return ok(res, { agents, total: page.total ?? agents.length, org_id: page.org_id ?? null, org_label: page.org_label ?? null });
|
|
1108
1224
|
}
|
|
1109
1225
|
if (method === "GET" && path === `${BRIDGE_PREFIX}/directory`) {
|
|
1110
1226
|
const { apiUrl } = await ownerContext();
|
|
@@ -1255,6 +1371,7 @@ function createMsg9Bridge(deps) {
|
|
|
1255
1371
|
const { inbox } = await inboxFor(key, signal);
|
|
1256
1372
|
await deps.api.markRead(inbox.api_url, inbox.api_key, messageId, "human", signal);
|
|
1257
1373
|
await setMessageMark(key, messageId, { read_by: "human" });
|
|
1374
|
+
invalidateUnreadCache();
|
|
1258
1375
|
deps.events?.emit("read");
|
|
1259
1376
|
return ok(res, { message_id: messageId, read: true });
|
|
1260
1377
|
}
|
|
@@ -1272,6 +1389,7 @@ function createMsg9Bridge(deps) {
|
|
|
1272
1389
|
});
|
|
1273
1390
|
}
|
|
1274
1391
|
await setMessageMark(key, messageId, { read_by: "human", processed_by: "human" });
|
|
1392
|
+
invalidateUnreadCache();
|
|
1275
1393
|
deps.events?.emit("done");
|
|
1276
1394
|
return ok(res, { message_id: messageId, processed: true });
|
|
1277
1395
|
}
|
|
@@ -1301,6 +1419,7 @@ function createMsg9Bridge(deps) {
|
|
|
1301
1419
|
const mailDomain = typeof me.mail_domain === "string" ? me.mail_domain : void 0;
|
|
1302
1420
|
const addressDomain2 = typeof me.address_domain === "string" ? me.address_domain : null;
|
|
1303
1421
|
await setOwner({ api_key: ownerKey, api_url: apiUrl, id, name: name2, slug, mail_domain: mailDomain, address_domain: addressDomain2 });
|
|
1422
|
+
invalidateUnreadCache();
|
|
1304
1423
|
return ok(res, {
|
|
1305
1424
|
owner: {
|
|
1306
1425
|
name: name2 ?? null,
|
|
@@ -1322,6 +1441,7 @@ function createMsg9Bridge(deps) {
|
|
|
1322
1441
|
if (!existing?.api_key) throw new BridgeError(404, "unknown-workspace", `no inbox is registered as "${key}"`);
|
|
1323
1442
|
const workspace = deps.listWorkspaces().find((row) => row.key === key) ?? { key, title: existing.title, path: existing.path };
|
|
1324
1443
|
const result = await migrateInbox(workspace, existing, str(body.old_owner_key));
|
|
1444
|
+
invalidateUnreadCache();
|
|
1325
1445
|
deps.log(`migrated ${key}: ${existing.address} -> ${result.inbox.address}`);
|
|
1326
1446
|
deps.events?.emit("migrate");
|
|
1327
1447
|
return ok(res, {
|
|
@@ -1347,6 +1467,7 @@ function createMsg9Bridge(deps) {
|
|
|
1347
1467
|
title ? { ...workspace, title } : workspace,
|
|
1348
1468
|
signal
|
|
1349
1469
|
);
|
|
1470
|
+
if (provisioned) invalidateUnreadCache();
|
|
1350
1471
|
return ok(res, { key: workspace.key, address: inbox.address, provisioned });
|
|
1351
1472
|
}
|
|
1352
1473
|
if (method === "POST" && path === `${BRIDGE_PREFIX}/resolve`) {
|
|
@@ -1782,7 +1903,7 @@ ${formatMessage(message, Math.max(0, args.body_limit ?? 0))}`;
|
|
|
1782
1903
|
);
|
|
1783
1904
|
}
|
|
1784
1905
|
const { api_key } = await ownerRotateAgentKey(owner.api_url, owner.api_key, inbox.address, exec?.signal);
|
|
1785
|
-
await upsertWorkspaceInbox(workspace.key, {
|
|
1906
|
+
await upsertWorkspaceInbox(workspace.key, { api_key });
|
|
1786
1907
|
return L(
|
|
1787
1908
|
"\u5DF2\u8F6E\u6362\u300C{title}\u300D({address}) \u7684 key\uFF0C\u65B0 key \u5DF2\u4FDD\u5B58\u3002",
|
|
1788
1909
|
'Rotated the key for "{title}" ({address}); the new key is saved.',
|
|
@@ -2006,6 +2127,16 @@ function unseenMessages(messages, lastSeenId, lastSeenAt) {
|
|
|
2006
2127
|
}
|
|
2007
2128
|
return messages;
|
|
2008
2129
|
}
|
|
2130
|
+
function createNonReentrant(task) {
|
|
2131
|
+
let running = false;
|
|
2132
|
+
return () => {
|
|
2133
|
+
if (running) return;
|
|
2134
|
+
running = true;
|
|
2135
|
+
void task().finally(() => {
|
|
2136
|
+
running = false;
|
|
2137
|
+
});
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2009
2140
|
async function pollOnce(deps, rt) {
|
|
2010
2141
|
const state = await deps.loadState();
|
|
2011
2142
|
for (const [key, inbox] of Object.entries(state.workspaces)) {
|
|
@@ -2014,6 +2145,13 @@ async function pollOnce(deps, rt) {
|
|
|
2014
2145
|
await pollInbox(deps, rt, key, inbox);
|
|
2015
2146
|
} catch (error) {
|
|
2016
2147
|
deps.log(`watch poll failed for ${key}: ${error?.message ?? String(error)}`);
|
|
2148
|
+
const status = error?.status;
|
|
2149
|
+
if (status === 429) {
|
|
2150
|
+
const serverWait = error?.retryAfter;
|
|
2151
|
+
const backoff = typeof serverWait === "number" && serverWait > 0 ? serverWait * 1e3 : 6e4;
|
|
2152
|
+
deps.log(`watch poll rate-limited for ${key}; backing off ${backoff / 1e3}s`);
|
|
2153
|
+
await (deps.sleep ?? defaultSleep)(backoff);
|
|
2154
|
+
}
|
|
2017
2155
|
}
|
|
2018
2156
|
}
|
|
2019
2157
|
}
|
|
@@ -2094,7 +2232,10 @@ async function pollInbox(deps, rt, key, inbox) {
|
|
|
2094
2232
|
const all = page.messages ?? [];
|
|
2095
2233
|
if (page.next_cursor) {
|
|
2096
2234
|
await deps.setWatchState(key, { watch_cursor: page.next_cursor });
|
|
2097
|
-
if (all[0]) await deps.setWatchState(key, {
|
|
2235
|
+
if (all[0]) await deps.setWatchState(key, {
|
|
2236
|
+
watch_last_message_id: all[0].message_id,
|
|
2237
|
+
...all[0].created_at ? { watch_last_seen_at: all[0].created_at } : {}
|
|
2238
|
+
});
|
|
2098
2239
|
return;
|
|
2099
2240
|
}
|
|
2100
2241
|
const fresh = unseenMessages(all, inbox.watch_last_message_id, inbox.watch_last_seen_at);
|
|
@@ -2249,6 +2390,7 @@ function startWatcher(ctx, agents, getRegistry, log, events, reconcileUnread) {
|
|
|
2249
2390
|
isPaused: () => getNotifyPaused(),
|
|
2250
2391
|
resolveAgentById: (id) => agents.get(id),
|
|
2251
2392
|
batchWindowMs: Math.max(0, Number(process.env.MSG9_WATCH_BATCH_MS ?? 12e3) || 12e3),
|
|
2393
|
+
sleep: defaultSleep,
|
|
2252
2394
|
resolveAgent: async ({ inbox }) => {
|
|
2253
2395
|
const registry = getRegistry();
|
|
2254
2396
|
if (registry?.resolveByPath) {
|
|
@@ -2285,7 +2427,7 @@ function startWatcher(ctx, agents, getRegistry, log, events, reconcileUnread) {
|
|
|
2285
2427
|
loopControllers.clear();
|
|
2286
2428
|
};
|
|
2287
2429
|
const startPolling = () => {
|
|
2288
|
-
pollTimer ??= setInterval(() =>
|
|
2430
|
+
pollTimer ??= setInterval(createNonReentrant(() => pollOnce(deps, rt)), WATCH_POLL_MS);
|
|
2289
2431
|
};
|
|
2290
2432
|
const reconcile = async () => {
|
|
2291
2433
|
if (streamUnsupported || master.signal.aborted) return;
|
|
@@ -2371,15 +2513,18 @@ export {
|
|
|
2371
2513
|
computeUnread,
|
|
2372
2514
|
createBridgeEventBus,
|
|
2373
2515
|
createMsg9Bridge,
|
|
2516
|
+
createNonReentrant,
|
|
2374
2517
|
createWatchRuntime,
|
|
2375
2518
|
defaultBridgeDeps,
|
|
2376
2519
|
ensureInbox,
|
|
2377
2520
|
flushBatch,
|
|
2378
2521
|
inject,
|
|
2522
|
+
invalidateUnreadCache,
|
|
2379
2523
|
isTrustedRequest,
|
|
2380
2524
|
listWorkspaces,
|
|
2381
2525
|
loadState,
|
|
2382
2526
|
matchWorkspaceByPath,
|
|
2527
|
+
migrateInbox,
|
|
2383
2528
|
name,
|
|
2384
2529
|
ownerContext,
|
|
2385
2530
|
pluginNotice,
|
|
@@ -2387,8 +2532,10 @@ export {
|
|
|
2387
2532
|
renderMailNotice,
|
|
2388
2533
|
resolveInbox,
|
|
2389
2534
|
resolveWorkspace,
|
|
2535
|
+
setOwner,
|
|
2390
2536
|
setWorkspaceRegistry,
|
|
2391
2537
|
stateFilePath,
|
|
2392
2538
|
streamInboxLoop,
|
|
2393
|
-
unseenMessages
|
|
2539
|
+
unseenMessages,
|
|
2540
|
+
upsertWorkspaceInbox
|
|
2394
2541
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-msg9-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "dsh (DeepSeek Harness) msg9 kit: one msg9.io inbox per workspace. Model tools (send/receive/read/contacts/resolve) plus a /msg9 command in the agent, and a ✉ sidebar icon that opens the same mailbox in the GUI — inbox, outbox and address book, with an unread badge. 给每个 dsh workspace 一个 msg9.io 收件箱:Agent 用工具收发,界面里点 ✉ 就能看同一份邮件。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "node scripts/build.mjs",
|
|
61
61
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
62
|
-
"test": "node tests/smoke.test.mjs && node tests/client.test.mjs && node tests/cordis.test.mjs && node tests/watch.test.mjs && node tests/watch-integration.test.mjs",
|
|
62
|
+
"test": "node tests/smoke.test.mjs && node tests/client.test.mjs && node tests/cordis.test.mjs && node tests/watch.test.mjs && node tests/watch-integration.test.mjs && node tests/host-fixes.test.mjs",
|
|
63
63
|
"prepare": "npm run build",
|
|
64
64
|
"prepack": "npm run build"
|
|
65
65
|
},
|