openclaw-channel-dmwork 0.5.12 → 0.5.14
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/package.json +1 -1
- package/src/api-fetch.test.ts +80 -0
- package/src/api-fetch.ts +37 -0
- package/src/channel.ts +46 -10
- package/src/inbound.ts +48 -6
- package/src/mention-utils.test.ts +72 -0
- package/src/mention-utils.ts +41 -3
- package/src/multi-bot-isolation.test.ts +208 -0
- package/node_modules/crypto-js/CONTRIBUTING.md +0 -28
- package/node_modules/crypto-js/LICENSE +0 -24
- package/node_modules/crypto-js/README.md +0 -275
- package/node_modules/crypto-js/aes.js +0 -234
- package/node_modules/crypto-js/blowfish.js +0 -471
- package/node_modules/crypto-js/bower.json +0 -39
- package/node_modules/crypto-js/cipher-core.js +0 -895
- package/node_modules/crypto-js/core.js +0 -807
- package/node_modules/crypto-js/crypto-js.js +0 -6657
- package/node_modules/crypto-js/docs/QuickStartGuide.wiki +0 -470
- package/node_modules/crypto-js/enc-base64.js +0 -136
- package/node_modules/crypto-js/enc-base64url.js +0 -148
- package/node_modules/crypto-js/enc-hex.js +0 -18
- package/node_modules/crypto-js/enc-latin1.js +0 -18
- package/node_modules/crypto-js/enc-utf16.js +0 -149
- package/node_modules/crypto-js/enc-utf8.js +0 -18
- package/node_modules/crypto-js/evpkdf.js +0 -134
- package/node_modules/crypto-js/format-hex.js +0 -66
- package/node_modules/crypto-js/format-openssl.js +0 -18
- package/node_modules/crypto-js/hmac-md5.js +0 -18
- package/node_modules/crypto-js/hmac-ripemd160.js +0 -18
- package/node_modules/crypto-js/hmac-sha1.js +0 -18
- package/node_modules/crypto-js/hmac-sha224.js +0 -18
- package/node_modules/crypto-js/hmac-sha256.js +0 -18
- package/node_modules/crypto-js/hmac-sha3.js +0 -18
- package/node_modules/crypto-js/hmac-sha384.js +0 -18
- package/node_modules/crypto-js/hmac-sha512.js +0 -18
- package/node_modules/crypto-js/hmac.js +0 -143
- package/node_modules/crypto-js/index.js +0 -18
- package/node_modules/crypto-js/lib-typedarrays.js +0 -76
- package/node_modules/crypto-js/md5.js +0 -268
- package/node_modules/crypto-js/mode-cfb.js +0 -80
- package/node_modules/crypto-js/mode-ctr-gladman.js +0 -116
- package/node_modules/crypto-js/mode-ctr.js +0 -58
- package/node_modules/crypto-js/mode-ecb.js +0 -40
- package/node_modules/crypto-js/mode-ofb.js +0 -54
- package/node_modules/crypto-js/package.json +0 -42
- package/node_modules/crypto-js/pad-ansix923.js +0 -49
- package/node_modules/crypto-js/pad-iso10126.js +0 -44
- package/node_modules/crypto-js/pad-iso97971.js +0 -40
- package/node_modules/crypto-js/pad-nopadding.js +0 -30
- package/node_modules/crypto-js/pad-pkcs7.js +0 -18
- package/node_modules/crypto-js/pad-zeropadding.js +0 -47
- package/node_modules/crypto-js/pbkdf2.js +0 -145
- package/node_modules/crypto-js/rabbit-legacy.js +0 -190
- package/node_modules/crypto-js/rabbit.js +0 -192
- package/node_modules/crypto-js/rc4.js +0 -139
- package/node_modules/crypto-js/ripemd160.js +0 -267
- package/node_modules/crypto-js/sha1.js +0 -150
- package/node_modules/crypto-js/sha224.js +0 -80
- package/node_modules/crypto-js/sha256.js +0 -199
- package/node_modules/crypto-js/sha3.js +0 -326
- package/node_modules/crypto-js/sha384.js +0 -83
- package/node_modules/crypto-js/sha512.js +0 -326
- package/node_modules/crypto-js/tripledes.js +0 -779
- package/node_modules/crypto-js/x64-core.js +0 -304
- package/node_modules/curve25519-js/.prettierrc +0 -6
- package/node_modules/curve25519-js/LICENSE +0 -19
- package/node_modules/curve25519-js/README.md +0 -111
- package/node_modules/curve25519-js/lib/index.d.ts +0 -69
- package/node_modules/curve25519-js/lib/index.js +0 -1669
- package/node_modules/curve25519-js/package.json +0 -34
- package/node_modules/curve25519-js/tsconfig.json +0 -20
- package/node_modules/curve25519-js/tslint.json +0 -10
- package/node_modules/md5-typescript/.idea/Md5-typescript.iml +0 -13
- package/node_modules/md5-typescript/.idea/jsLibraryMappings.xml +0 -6
- package/node_modules/md5-typescript/.idea/libraries/tsconfig_roots.xml +0 -13
- package/node_modules/md5-typescript/.idea/modules.xml +0 -8
- package/node_modules/md5-typescript/.idea/vcs.xml +0 -6
- package/node_modules/md5-typescript/.idea/workspace.xml +0 -345
- package/node_modules/md5-typescript/LICENSE +0 -21
- package/node_modules/md5-typescript/README.md +0 -13
- package/node_modules/md5-typescript/dist/bundles/bundle.umd.js +0 -208
- package/node_modules/md5-typescript/dist/bundles/bundle.umd.min.js +0 -1
- package/node_modules/md5-typescript/dist/index.d.ts +0 -43
- package/node_modules/md5-typescript/dist/index.js +0 -198
- package/node_modules/md5-typescript/dist/index.js.map +0 -1
- package/node_modules/md5-typescript/dist/index.metadata.json +0 -1
- package/node_modules/md5-typescript/package.json +0 -31
- package/node_modules/md5-typescript/rollup.config.js +0 -7
- package/node_modules/md5-typescript/tsconfig.json +0 -28
package/package.json
CHANGED
package/src/api-fetch.test.ts
CHANGED
|
@@ -794,3 +794,83 @@ describe("uploadFileToCOS putParams ContentType", () => {
|
|
|
794
794
|
expect(capturedParams.ContentType).toBe("text/plain; charset=utf-8");
|
|
795
795
|
});
|
|
796
796
|
});
|
|
797
|
+
|
|
798
|
+
// --- fetchUserInfo ---
|
|
799
|
+
import { fetchUserInfo } from "./api-fetch.js";
|
|
800
|
+
|
|
801
|
+
describe("fetchUserInfo", () => {
|
|
802
|
+
it("returns user info on success", async () => {
|
|
803
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
804
|
+
ok: true,
|
|
805
|
+
status: 200,
|
|
806
|
+
json: () => Promise.resolve({ uid: "s14_abc", name: "Alice", avatar: "https://example.com/a.png" }),
|
|
807
|
+
}) as any;
|
|
808
|
+
|
|
809
|
+
const result = await fetchUserInfo({
|
|
810
|
+
apiUrl: "http://localhost:8090",
|
|
811
|
+
botToken: "tok",
|
|
812
|
+
uid: "s14_abc",
|
|
813
|
+
});
|
|
814
|
+
expect(result).toEqual({ uid: "s14_abc", name: "Alice", avatar: "https://example.com/a.png" });
|
|
815
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
816
|
+
"http://localhost:8090/v1/bot/user/info?uid=s14_abc",
|
|
817
|
+
expect.objectContaining({ method: "GET" }),
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("returns null on 404 (endpoint not implemented)", async () => {
|
|
822
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
823
|
+
ok: false,
|
|
824
|
+
status: 404,
|
|
825
|
+
}) as any;
|
|
826
|
+
|
|
827
|
+
const result = await fetchUserInfo({
|
|
828
|
+
apiUrl: "http://localhost:8090",
|
|
829
|
+
botToken: "tok",
|
|
830
|
+
uid: "s14_abc",
|
|
831
|
+
});
|
|
832
|
+
expect(result).toBeNull();
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("returns null on 500 error", async () => {
|
|
836
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
837
|
+
ok: false,
|
|
838
|
+
status: 500,
|
|
839
|
+
}) as any;
|
|
840
|
+
|
|
841
|
+
const result = await fetchUserInfo({
|
|
842
|
+
apiUrl: "http://localhost:8090",
|
|
843
|
+
botToken: "tok",
|
|
844
|
+
uid: "s14_abc",
|
|
845
|
+
log: { error: vi.fn() },
|
|
846
|
+
});
|
|
847
|
+
expect(result).toBeNull();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it("returns null on network error", async () => {
|
|
851
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED")) as any;
|
|
852
|
+
|
|
853
|
+
const result = await fetchUserInfo({
|
|
854
|
+
apiUrl: "http://localhost:8090",
|
|
855
|
+
botToken: "tok",
|
|
856
|
+
uid: "s14_abc",
|
|
857
|
+
log: { error: vi.fn() },
|
|
858
|
+
});
|
|
859
|
+
expect(result).toBeNull();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it("returns null when response has no name", async () => {
|
|
863
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
864
|
+
ok: true,
|
|
865
|
+
status: 200,
|
|
866
|
+
json: () => Promise.resolve({ uid: "s14_abc" }),
|
|
867
|
+
}) as any;
|
|
868
|
+
|
|
869
|
+
const result = await fetchUserInfo({
|
|
870
|
+
apiUrl: "http://localhost:8090",
|
|
871
|
+
botToken: "tok",
|
|
872
|
+
uid: "s14_abc",
|
|
873
|
+
});
|
|
874
|
+
expect(result).toBeNull();
|
|
875
|
+
});
|
|
876
|
+
});
|
package/src/api-fetch.ts
CHANGED
|
@@ -649,3 +649,40 @@ export async function editMessage(params: {
|
|
|
649
649
|
content_edit: params.contentEdit,
|
|
650
650
|
}, params.signal);
|
|
651
651
|
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Fetch user info by UID. Requires backend `/v1/bot/user/info` endpoint.
|
|
655
|
+
* Returns null if the endpoint is unavailable (404) or returns an error,
|
|
656
|
+
* so callers can gracefully degrade.
|
|
657
|
+
*/
|
|
658
|
+
export async function fetchUserInfo(params: {
|
|
659
|
+
apiUrl: string;
|
|
660
|
+
botToken: string;
|
|
661
|
+
uid: string;
|
|
662
|
+
log?: { info?: (msg: string) => void; error?: (msg: string) => void };
|
|
663
|
+
}): Promise<{ uid: string; name: string; avatar?: string } | null> {
|
|
664
|
+
const url = `${params.apiUrl.replace(/\/+$/, "")}/v1/bot/user/info?uid=${encodeURIComponent(params.uid)}`;
|
|
665
|
+
try {
|
|
666
|
+
const resp = await fetch(url, {
|
|
667
|
+
method: "GET",
|
|
668
|
+
headers: { Authorization: `Bearer ${params.botToken}` },
|
|
669
|
+
signal: AbortSignal.timeout(5000),
|
|
670
|
+
});
|
|
671
|
+
if (resp.status === 404) {
|
|
672
|
+
// Endpoint not implemented yet — silent degrade
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
if (!resp.ok) {
|
|
676
|
+
params.log?.error?.(`dmwork: fetchUserInfo(${params.uid}) failed: ${resp.status}`);
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const data = await resp.json() as { uid?: string; name?: string; avatar?: string };
|
|
680
|
+
if (data?.name) {
|
|
681
|
+
return { uid: data.uid ?? params.uid, name: data.name, avatar: data.avatar };
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
} catch (err) {
|
|
685
|
+
params.log?.error?.(`dmwork: fetchUserInfo(${params.uid}) error: ${String(err)}`);
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
package/src/channel.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveDmworkAccount,
|
|
12
12
|
type ResolvedDmworkAccount,
|
|
13
13
|
} from "./accounts.js";
|
|
14
|
-
import { registerBot, sendMessage, sendHeartbeat, sendMediaMessage, inferContentType, ensureTextCharset, fetchBotGroups, getGroupMd, parseImageDimensions, parseImageDimensionsFromFile, getUploadCredentials, uploadFileToCOS } from "./api-fetch.js";
|
|
14
|
+
import { registerBot, sendMessage, sendHeartbeat, sendMediaMessage, inferContentType, ensureTextCharset, fetchBotGroups, getGroupMd, getGroupMembers, parseImageDimensions, parseImageDimensionsFromFile, getUploadCredentials, uploadFileToCOS } from "./api-fetch.js";
|
|
15
15
|
import { WKSocket } from "./socket.js";
|
|
16
16
|
import { handleInboundMessage, type DmworkStatusSink } from "./inbound.js";
|
|
17
17
|
import { ChannelType, MessageType, type BotMessage, type MessagePayload } from "./types.js";
|
|
@@ -125,16 +125,26 @@ function getOrCreateGroupCacheTimestamps(accountId: string): Map<string, number>
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
// --- Group → Account mapping: tracks which
|
|
128
|
+
// --- Group → Account mapping: tracks which accounts are active in each group ---
|
|
129
129
|
// Used by handleAction to resolve the correct account when framework passes wrong accountId
|
|
130
|
-
|
|
130
|
+
// A group may have multiple bots (1:N), so we store a Set of accountIds per group.
|
|
131
|
+
const _groupToAccount = new Map<string, Set<string>>(); // groupNo → Set<accountId>
|
|
131
132
|
|
|
132
133
|
export function registerGroupToAccount(groupNo: string, accountId: string): void {
|
|
133
|
-
_groupToAccount.
|
|
134
|
+
let accounts = _groupToAccount.get(groupNo);
|
|
135
|
+
if (!accounts) {
|
|
136
|
+
accounts = new Set<string>();
|
|
137
|
+
_groupToAccount.set(groupNo, accounts);
|
|
138
|
+
}
|
|
139
|
+
accounts.add(accountId);
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
export function resolveAccountForGroup(groupNo: string): string | undefined {
|
|
137
|
-
|
|
143
|
+
const accounts = _groupToAccount.get(groupNo);
|
|
144
|
+
if (!accounts || accounts.size === 0) return undefined;
|
|
145
|
+
// Only resolve when exactly one bot owns the group; multi-bot → ambiguous
|
|
146
|
+
if (accounts.size === 1) return accounts.values().next().value;
|
|
147
|
+
return undefined;
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
// --- Cache cleanup: evict groups inactive for >4 hours ---
|
|
@@ -155,7 +165,8 @@ function cleanupStaleCaches(): void {
|
|
|
155
165
|
if (lastAccess < cutoff) {
|
|
156
166
|
_historyMaps.get(accountId)?.delete(groupId);
|
|
157
167
|
_memberMaps.get(accountId)?.delete(groupId);
|
|
158
|
-
|
|
168
|
+
// Note: uidToNameMap is a flat uid→name map (not keyed by groupId),
|
|
169
|
+
// so we don't delete from it here — names remain valid across groups.
|
|
159
170
|
_groupCacheTimestamps.get(accountId)?.delete(groupId);
|
|
160
171
|
activityMap.delete(groupId);
|
|
161
172
|
}
|
|
@@ -252,11 +263,14 @@ export const dmworkPlugin: ChannelPlugin<ResolvedDmworkAccount> = {
|
|
|
252
263
|
handleAction: async (ctx: any) => {
|
|
253
264
|
// Resolve correct accountId: framework may pass wrong one when agent has multiple accounts.
|
|
254
265
|
// Use currentChannelId to look up which account actually owns the group.
|
|
266
|
+
// When multiple bots share the same group, do NOT correct — the caller's accountId is authoritative.
|
|
255
267
|
let accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
256
268
|
const currentChannelId = ctx.toolContext?.currentChannelId;
|
|
257
269
|
if (currentChannelId) {
|
|
258
270
|
const rawGroupNo = currentChannelId.replace(/^dmwork:/, '');
|
|
259
271
|
const correctAccountId = resolveAccountForGroup(rawGroupNo);
|
|
272
|
+
// Only correct when resolveAccountForGroup returns a definitive answer
|
|
273
|
+
// (exactly one bot owns the group); multi-bot → undefined → no correction
|
|
260
274
|
if (correctAccountId && correctAccountId !== accountId) {
|
|
261
275
|
ctx.log?.info?.(`dmwork: handleAction accountId corrected: ${accountId} → ${correctAccountId} (group=${rawGroupNo})`);
|
|
262
276
|
accountId = correctAccountId;
|
|
@@ -593,27 +607,49 @@ export const dmworkPlugin: ChannelPlugin<ResolvedDmworkAccount> = {
|
|
|
593
607
|
// Check for updates in background (fire-and-forget)
|
|
594
608
|
checkForUpdates(account.config.apiUrl, log).catch(() => {});
|
|
595
609
|
|
|
596
|
-
// Prefetch GROUP.md for all groups (fire-and-forget)
|
|
610
|
+
// Prefetch GROUP.md and group members for all groups (fire-and-forget)
|
|
597
611
|
const groupMdCache = getOrCreateGroupMdCache(account.accountId);
|
|
598
612
|
(async () => {
|
|
599
613
|
try {
|
|
600
614
|
const groups = await fetchBotGroups({ apiUrl: account.config.apiUrl, botToken: account.config.botToken!, log });
|
|
601
615
|
registerBotGroupIds(groups.map(g => g.group_no));
|
|
616
|
+
let mdCount = 0;
|
|
617
|
+
let memberCount = 0;
|
|
602
618
|
for (const g of groups) {
|
|
619
|
+
// Prefetch GROUP.md
|
|
603
620
|
try {
|
|
604
621
|
const md = await getGroupMd({ apiUrl: account.config.apiUrl, botToken: account.config.botToken!, groupNo: g.group_no, log });
|
|
605
622
|
if (md.content) {
|
|
606
623
|
groupMdCache.set(g.group_no, { content: md.content, version: md.version });
|
|
624
|
+
mdCount++;
|
|
607
625
|
}
|
|
608
626
|
} catch {
|
|
609
627
|
// Ignore per-group failures (group may not have GROUP.md)
|
|
610
628
|
}
|
|
629
|
+
// Prefetch group members → fill uidToNameMap for SenderName resolution
|
|
630
|
+
try {
|
|
631
|
+
const members = await getGroupMembers({ apiUrl: account.config.apiUrl, botToken: account.config.botToken!, groupNo: g.group_no, log });
|
|
632
|
+
const prefetchMemberMap = getOrCreateMemberMap(account.accountId);
|
|
633
|
+
const prefetchUidMap = getOrCreateUidToNameMap(account.accountId);
|
|
634
|
+
for (const m of members) {
|
|
635
|
+
if (m.uid && m.name) {
|
|
636
|
+
prefetchMemberMap.set(m.name, m.uid);
|
|
637
|
+
prefetchUidMap.set(m.uid, m.name);
|
|
638
|
+
memberCount++;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
// Ignore per-group failures
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (mdCount > 0) {
|
|
646
|
+
log?.info?.(`dmwork: prefetched GROUP.md for ${mdCount} groups`);
|
|
611
647
|
}
|
|
612
|
-
if (
|
|
613
|
-
log?.info?.(`dmwork: prefetched
|
|
648
|
+
if (memberCount > 0) {
|
|
649
|
+
log?.info?.(`dmwork: prefetched ${memberCount} member names from ${groups.length} groups`);
|
|
614
650
|
}
|
|
615
651
|
} catch (err) {
|
|
616
|
-
log?.error?.(`dmwork:
|
|
652
|
+
log?.error?.(`dmwork: group prefetch failed: ${String(err)}`);
|
|
617
653
|
}
|
|
618
654
|
})();
|
|
619
655
|
|
package/src/inbound.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChannelLogSink, OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import { sendMessage, sendReadReceipt, sendTyping, getChannelMessages, getGroupMembers, getGroupMd, postJson, sendMediaMessage, inferContentType, ensureTextCharset, parseImageDimensions, parseImageDimensionsFromFile, getUploadCredentials, uploadFileToCOS } from "./api-fetch.js";
|
|
2
|
+
import { sendMessage, sendReadReceipt, sendTyping, getChannelMessages, getGroupMembers, getGroupMd, postJson, sendMediaMessage, inferContentType, ensureTextCharset, parseImageDimensions, parseImageDimensionsFromFile, getUploadCredentials, uploadFileToCOS, fetchUserInfo } from "./api-fetch.js";
|
|
3
3
|
import type { ResolvedDmworkAccount } from "./accounts.js";
|
|
4
4
|
import type { BotMessage } from "./types.js";
|
|
5
5
|
import { ChannelType, MessageType } from "./types.js";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
extractMentionUids,
|
|
11
11
|
convertContentForLLM,
|
|
12
12
|
buildSenderPrefix,
|
|
13
|
+
resolveSenderName,
|
|
13
14
|
parseStructuredMentions,
|
|
14
15
|
convertStructuredMentions,
|
|
15
16
|
buildEntitiesFromFallback,
|
|
@@ -1035,6 +1036,14 @@ export async function handleInboundMessage(params: {
|
|
|
1035
1036
|
const resolved = resolveContent(message.payload, account.config.apiUrl, log, account.config.cdnUrl);
|
|
1036
1037
|
let rawBody = resolved.text;
|
|
1037
1038
|
let inboundMediaUrl = resolved.mediaUrl;
|
|
1039
|
+
|
|
1040
|
+
// Opportunistic uid→name cache fill from MultipleForward payloads
|
|
1041
|
+
if (message.payload?.type === MessageType.MultipleForward && Array.isArray(message.payload.users)) {
|
|
1042
|
+
for (const u of message.payload.users as Array<{ uid?: string; name?: string }>) {
|
|
1043
|
+
if (u.uid && u.name) uidToNameMap.set(u.uid, u.name);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1038
1047
|
// For Image/GIF/Voice/Video: download media to local temp file so Core reads
|
|
1039
1048
|
// local files instead of remote URLs (avoids hang on large/slow downloads in Core)
|
|
1040
1049
|
const mediaDownloadTypes = [MessageType.Image, MessageType.GIF, MessageType.Voice, MessageType.Video];
|
|
@@ -1091,6 +1100,10 @@ export async function handleInboundMessage(params: {
|
|
|
1091
1100
|
quotePrefix = `[Quoted message from ${replyFrom}]: ${replyContent}\n---\n`;
|
|
1092
1101
|
log?.info?.(`dmwork: message quotes a reply (${quotePrefix.length} chars)`);
|
|
1093
1102
|
}
|
|
1103
|
+
// Cache reply sender name for uid→name resolution (opportunistic fill)
|
|
1104
|
+
if (replyData.from_uid && replyData.from_name) {
|
|
1105
|
+
uidToNameMap.set(replyData.from_uid, replyData.from_name);
|
|
1106
|
+
}
|
|
1094
1107
|
}
|
|
1095
1108
|
|
|
1096
1109
|
// --- Mention gating for group messages ---
|
|
@@ -1105,15 +1118,18 @@ export async function handleInboundMessage(params: {
|
|
|
1105
1118
|
await refreshGroupMemberCache({ sessionId, memberMap, uidToNameMap, groupCacheTimestamps, apiUrl: account.config.apiUrl, botToken: account.config.botToken ?? "", log });
|
|
1106
1119
|
}
|
|
1107
1120
|
|
|
1108
|
-
|
|
1121
|
+
// Compute isMentioned at top level so it's available for WasMentioned in finalizeInboundContext
|
|
1122
|
+
let isMentioned = false;
|
|
1123
|
+
if (isGroup) {
|
|
1109
1124
|
const mentionUids = extractMentionUids(message.payload?.mention);
|
|
1110
|
-
// mention.all can be boolean `true` or numeric `1` depending on API version
|
|
1111
1125
|
const mentionAllRaw = message.payload?.mention?.all;
|
|
1112
1126
|
const mentionAll: boolean = mentionAllRaw === true || mentionAllRaw === 1;
|
|
1113
|
-
|
|
1114
|
-
|
|
1127
|
+
isMentioned = mentionAll || mentionUids.includes(botUid);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (isGroup && requireMention) {
|
|
1115
1131
|
// Debug: log received mention info
|
|
1116
|
-
log?.debug?.(`dmwork: [RECV] mention payload:
|
|
1132
|
+
log?.debug?.(`dmwork: [RECV] mention payload: isMentioned=${isMentioned}, originalCount=${originalMentionUids.length}`);
|
|
1117
1133
|
|
|
1118
1134
|
if (!isMentioned) {
|
|
1119
1135
|
// Record as pending history context (manual — avoids SDK format incompatibility)
|
|
@@ -1302,6 +1318,29 @@ export async function handleInboundMessage(params: {
|
|
|
1302
1318
|
// Inject GROUP.md as GroupSystemPrompt for group messages
|
|
1303
1319
|
const groupSystemPrompt = isGroup && groupMdCache ? groupMdCache.get(message.channel_id!)?.content : undefined;
|
|
1304
1320
|
|
|
1321
|
+
// Resolve sender display name — async fallback for DM users not in cache
|
|
1322
|
+
let senderName = resolveSenderName(message.from_uid, uidToNameMap);
|
|
1323
|
+
if (!senderName && !isGroup) {
|
|
1324
|
+
// DM user not in any group cache — try backend user info API
|
|
1325
|
+
// Skip if we already tried and failed (negative cache sentinel "")
|
|
1326
|
+
const cached = uidToNameMap.get(message.from_uid);
|
|
1327
|
+
if (cached === undefined) {
|
|
1328
|
+
const userInfo = await fetchUserInfo({
|
|
1329
|
+
apiUrl: account.config.apiUrl,
|
|
1330
|
+
botToken: account.config.botToken ?? "",
|
|
1331
|
+
uid: message.from_uid,
|
|
1332
|
+
log,
|
|
1333
|
+
});
|
|
1334
|
+
if (userInfo?.name) {
|
|
1335
|
+
senderName = userInfo.name;
|
|
1336
|
+
uidToNameMap.set(message.from_uid, userInfo.name);
|
|
1337
|
+
} else {
|
|
1338
|
+
// Negative cache — prevent repeated API calls for unknown UIDs
|
|
1339
|
+
uidToNameMap.set(message.from_uid, "");
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1305
1344
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
1306
1345
|
Body: body,
|
|
1307
1346
|
BodyForAgent: body, // ← 关键!AI 实际读取的是这个字段!
|
|
@@ -1321,6 +1360,9 @@ export async function handleInboundMessage(params: {
|
|
|
1321
1360
|
ChatType: isGroup ? "group" : "direct",
|
|
1322
1361
|
ConversationLabel: fromLabel,
|
|
1323
1362
|
SenderId: message.from_uid,
|
|
1363
|
+
SenderName: senderName,
|
|
1364
|
+
SenderUsername: message.from_uid,
|
|
1365
|
+
WasMentioned: isGroup ? isMentioned : undefined,
|
|
1324
1366
|
MessageSid: String(message.message_id),
|
|
1325
1367
|
Timestamp: message.timestamp ? message.timestamp * 1000 : undefined,
|
|
1326
1368
|
GroupSubject: isGroup ? message.channel_id : undefined,
|
|
@@ -486,3 +486,75 @@ describe("边界情况", () => {
|
|
|
486
486
|
expect(allEntities[1]).toEqual({ uid: "uid_bob", offset: 13, length: 4 });
|
|
487
487
|
});
|
|
488
488
|
});
|
|
489
|
+
|
|
490
|
+
// --- extractBaseUid & resolveSenderName ---
|
|
491
|
+
import { extractBaseUid, resolveSenderName } from "./mention-utils.js";
|
|
492
|
+
|
|
493
|
+
describe("extractBaseUid", () => {
|
|
494
|
+
it("strips space prefix", () => {
|
|
495
|
+
expect(extractBaseUid("s14_abc123")).toBe("abc123");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("handles multi-digit space id", () => {
|
|
499
|
+
expect(extractBaseUid("s1234_user456")).toBe("user456");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("returns uid unchanged when no space prefix", () => {
|
|
503
|
+
expect(extractBaseUid("abc123")).toBe("abc123");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("returns uid unchanged for 's' without underscore", () => {
|
|
507
|
+
expect(extractBaseUid("system")).toBe("system");
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("does not strip non-numeric space prefix (e.g. service_bot)", () => {
|
|
511
|
+
expect(extractBaseUid("service_bot")).toBe("service_bot");
|
|
512
|
+
expect(extractBaseUid("support_team")).toBe("support_team");
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
describe("resolveSenderName", () => {
|
|
517
|
+
it("returns direct match", () => {
|
|
518
|
+
const map = new Map([["s14_abc", "Alice"]]);
|
|
519
|
+
expect(resolveSenderName("s14_abc", map)).toBe("Alice");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("returns undefined when no match", () => {
|
|
523
|
+
const map = new Map([["s14_abc", "Alice"]]);
|
|
524
|
+
expect(resolveSenderName("s14_xyz", map)).toBeUndefined();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("falls back to base uid (non-space entry)", () => {
|
|
528
|
+
const map = new Map([["abc", "Alice"]]);
|
|
529
|
+
expect(resolveSenderName("s14_abc", map)).toBe("Alice");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("falls back to cross-space variant", () => {
|
|
533
|
+
// User known as s10_abc in one space, DM from s14_abc
|
|
534
|
+
const map = new Map([["s10_abc", "Alice"]]);
|
|
535
|
+
expect(resolveSenderName("s14_abc", map)).toBe("Alice");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("does not cross-space fallback for non-prefixed uid", () => {
|
|
539
|
+
// uid "abc" without space prefix should not scan
|
|
540
|
+
const map = new Map([["s10_abc", "Alice"]]);
|
|
541
|
+
expect(resolveSenderName("abc", map)).toBeUndefined();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("prefers direct match over cross-space", () => {
|
|
545
|
+
const map = new Map([["s14_abc", "Alice-14"], ["s10_abc", "Alice-10"]]);
|
|
546
|
+
expect(resolveSenderName("s14_abc", map)).toBe("Alice-14");
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe("buildSenderPrefix with cross-space", () => {
|
|
551
|
+
it("shows name(uid) for cross-space hit", () => {
|
|
552
|
+
const map = new Map([["s10_abc", "Alice"]]);
|
|
553
|
+
expect(buildSenderPrefix("s14_abc", map)).toBe("Alice(s14_abc)");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("shows raw uid when no match", () => {
|
|
557
|
+
const map = new Map([["s10_xyz", "Bob"]]);
|
|
558
|
+
expect(buildSenderPrefix("s14_abc", map)).toBe("s14_abc");
|
|
559
|
+
});
|
|
560
|
+
});
|
package/src/mention-utils.ts
CHANGED
|
@@ -319,13 +319,51 @@ export function convertContentForLLM(
|
|
|
319
319
|
// ── Sender prefix utility ────────────────────────────────────────────────────
|
|
320
320
|
|
|
321
321
|
/**
|
|
322
|
-
|
|
323
|
-
*
|
|
322
|
+
/**
|
|
323
|
+
* Extract the base uid from a space-prefixed uid.
|
|
324
|
+
* "s14_abc123" → "abc123", "abc123" → "abc123"
|
|
325
|
+
*/
|
|
326
|
+
export function extractBaseUid(uid: string): string {
|
|
327
|
+
// Space-prefixed format: s{digits}_{baseUid}
|
|
328
|
+
const match = uid.match(/^s(\d+)_(.+)$/);
|
|
329
|
+
if (match) return match[2];
|
|
330
|
+
return uid;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Resolve sender display name from uidToNameMap with cross-space fallback.
|
|
335
|
+
* 1. Direct lookup: uidToNameMap.get(from_uid)
|
|
336
|
+
* 2. Base uid fallback: strip space prefix and scan map for matching base uid
|
|
337
|
+
* (covers DM users who appear in groups under a different space prefix)
|
|
324
338
|
*/
|
|
339
|
+
export function resolveSenderName(
|
|
340
|
+
fromUid: string,
|
|
341
|
+
uidToNameMap: Map<string, string>,
|
|
342
|
+
): string | undefined {
|
|
343
|
+
// Direct hit (same space or no space prefix)
|
|
344
|
+
const direct = uidToNameMap.get(fromUid);
|
|
345
|
+
if (direct) return direct;
|
|
346
|
+
|
|
347
|
+
// Cross-space fallback: extract base uid and scan
|
|
348
|
+
const baseUid = extractBaseUid(fromUid);
|
|
349
|
+
if (baseUid !== fromUid) {
|
|
350
|
+
// Check if the base uid itself is in the map (non-space account)
|
|
351
|
+
const baseHit = uidToNameMap.get(baseUid);
|
|
352
|
+
if (baseHit) return baseHit;
|
|
353
|
+
|
|
354
|
+
// Scan for any space-prefixed variant with the same base uid
|
|
355
|
+
for (const [uid, name] of uidToNameMap) {
|
|
356
|
+
if (extractBaseUid(uid) === baseUid) return name;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
325
363
|
export function buildSenderPrefix(
|
|
326
364
|
fromUid: string,
|
|
327
365
|
uidToNameMap: Map<string, string>,
|
|
328
366
|
): string {
|
|
329
|
-
const name =
|
|
367
|
+
const name = resolveSenderName(fromUid, uidToNameMap);
|
|
330
368
|
return name ? `${name}(${fromUid})` : fromUid;
|
|
331
369
|
}
|