openclaw-channel-dmwork 0.2.17 → 0.2.18
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.ts +41 -0
- package/src/channel.ts +63 -0
- package/src/inbound.ts +195 -8
package/package.json
CHANGED
package/src/api-fetch.ts
CHANGED
|
@@ -147,6 +147,47 @@ export async function fetchBotGroups(params: {
|
|
|
147
147
|
return resp.json();
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* 获取群成员列表
|
|
152
|
+
*/
|
|
153
|
+
export interface GroupMember {
|
|
154
|
+
uid: string;
|
|
155
|
+
name: string;
|
|
156
|
+
role?: string; // admin/member
|
|
157
|
+
robot?: boolean; // 是否是机器人
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function getGroupMembers(params: {
|
|
161
|
+
apiUrl: string;
|
|
162
|
+
botToken: string;
|
|
163
|
+
groupNo: string; // 群 ID (channel_id)
|
|
164
|
+
}): Promise<GroupMember[]> {
|
|
165
|
+
const url = `${params.apiUrl.replace(/\/+$/, "")}/v1/bot/groups/${params.groupNo}/members`;
|
|
166
|
+
try {
|
|
167
|
+
const resp = await fetch(url, {
|
|
168
|
+
method: "GET",
|
|
169
|
+
headers: {
|
|
170
|
+
"Authorization": `Bearer ${params.botToken}`,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (!resp.ok) {
|
|
174
|
+
console.log(`[dmwork] getGroupMembers failed: ${resp.status}`);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const data = await resp.json();
|
|
178
|
+
// Normalize to strict array to prevent silent failures
|
|
179
|
+
const members = Array.isArray(data?.members)
|
|
180
|
+
? data.members
|
|
181
|
+
: Array.isArray(data)
|
|
182
|
+
? data
|
|
183
|
+
: [];
|
|
184
|
+
return members as GroupMember[];
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.log(`[dmwork] getGroupMembers error: ${err}`);
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
/**
|
|
151
192
|
* 获取频道历史消息(用于注入上下文)
|
|
152
193
|
* @param params.log - Optional logger for consistent logging with OpenClaw log system
|
package/src/channel.ts
CHANGED
|
@@ -30,6 +30,41 @@ function getOrCreateHistoryMap(accountId: string): Map<string, any[]> {
|
|
|
30
30
|
return m;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Module-level member mapping: displayName -> uid
|
|
34
|
+
// Used to resolve @mentions in AI replies
|
|
35
|
+
const _memberMaps = new Map<string, Map<string, string>>();
|
|
36
|
+
function getOrCreateMemberMap(accountId: string): Map<string, string> {
|
|
37
|
+
let m = _memberMaps.get(accountId);
|
|
38
|
+
if (!m) {
|
|
39
|
+
m = new Map<string, string>();
|
|
40
|
+
_memberMaps.set(accountId, m);
|
|
41
|
+
}
|
|
42
|
+
return m;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Module-level reverse mapping: uid -> displayName
|
|
46
|
+
// Used to show display names instead of uids in replies
|
|
47
|
+
const _uidToNameMaps = new Map<string, Map<string, string>>();
|
|
48
|
+
function getOrCreateUidToNameMap(accountId: string): Map<string, string> {
|
|
49
|
+
let m = _uidToNameMaps.get(accountId);
|
|
50
|
+
if (!m) {
|
|
51
|
+
m = new Map<string, string>();
|
|
52
|
+
_uidToNameMaps.set(accountId, m);
|
|
53
|
+
}
|
|
54
|
+
return m;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Group member cache timestamps: groupId -> lastFetchedAt (ms)
|
|
58
|
+
const _groupCacheTimestamps = new Map<string, Map<string, number>>();
|
|
59
|
+
function getOrCreateGroupCacheTimestamps(accountId: string): Map<string, number> {
|
|
60
|
+
let m = _groupCacheTimestamps.get(accountId);
|
|
61
|
+
if (!m) {
|
|
62
|
+
m = new Map<string, number>();
|
|
63
|
+
_groupCacheTimestamps.set(accountId, m);
|
|
64
|
+
}
|
|
65
|
+
return m;
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
const meta = {
|
|
34
69
|
id: "dmwork",
|
|
35
70
|
label: "DMWork",
|
|
@@ -104,6 +139,22 @@ export const dmworkPlugin: ChannelPlugin<ResolvedDmworkAccount> = {
|
|
|
104
139
|
channelId = groupPart;
|
|
105
140
|
}
|
|
106
141
|
channelType = ChannelType.Group;
|
|
142
|
+
|
|
143
|
+
// Parse @mentions from message content (e.g., "@chenpipi_bot" -> "chenpipi_bot")
|
|
144
|
+
// Match @username where username is alphanumeric with underscores (typical uid format)
|
|
145
|
+
const contentMentions = content.match(/@([a-zA-Z0-9_]+)/g);
|
|
146
|
+
if (contentMentions) {
|
|
147
|
+
for (const mention of contentMentions) {
|
|
148
|
+
const uid = mention.slice(1); // Remove @ prefix
|
|
149
|
+
if (uid && !mentionUids.includes(uid)) {
|
|
150
|
+
mentionUids.push(uid);
|
|
151
|
+
console.log(`[dmwork] parsed @mention from content: ${uid}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (mentionUids.length > 0) {
|
|
156
|
+
console.log(`[dmwork] sending message with mentionUids: ${mentionUids.join(", ")}`);
|
|
157
|
+
}
|
|
107
158
|
}
|
|
108
159
|
|
|
109
160
|
await sendMessage({
|
|
@@ -207,6 +258,15 @@ export const dmworkPlugin: ChannelPlugin<ResolvedDmworkAccount> = {
|
|
|
207
258
|
|
|
208
259
|
// 4. Group history map — persists across auto-restarts (module-level)
|
|
209
260
|
const groupHistories = getOrCreateHistoryMap(account.accountId);
|
|
261
|
+
|
|
262
|
+
// 4b. Member name->uid map — for resolving @mentions in replies
|
|
263
|
+
const memberMap = getOrCreateMemberMap(account.accountId);
|
|
264
|
+
|
|
265
|
+
// 4c. Reverse map uid->name — for showing display names in replies
|
|
266
|
+
const uidToNameMap = getOrCreateUidToNameMap(account.accountId);
|
|
267
|
+
|
|
268
|
+
// 4d. Group cache timestamps — track when each group's members were last fetched
|
|
269
|
+
const groupCacheTimestamps = getOrCreateGroupCacheTimestamps(account.accountId);
|
|
210
270
|
|
|
211
271
|
// 5. Token refresh state — detect stale cached token
|
|
212
272
|
let hasRefreshedToken = false;
|
|
@@ -232,6 +292,9 @@ export const dmworkPlugin: ChannelPlugin<ResolvedDmworkAccount> = {
|
|
|
232
292
|
message: msg,
|
|
233
293
|
botUid: credentials.robot_id,
|
|
234
294
|
groupHistories,
|
|
295
|
+
memberMap,
|
|
296
|
+
uidToNameMap,
|
|
297
|
+
groupCacheTimestamps,
|
|
235
298
|
log,
|
|
236
299
|
statusSink,
|
|
237
300
|
}).catch((err) => {
|
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 } from "./api-fetch.js";
|
|
2
|
+
import { sendMessage, sendReadReceipt, sendTyping, getChannelMessages, getGroupMembers } 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";
|
|
@@ -51,15 +51,21 @@ function resolveContent(payload: BotMessage["payload"]): string {
|
|
|
51
51
|
return "";
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Cache expiry time: 1 hour
|
|
55
|
+
const GROUP_CACHE_EXPIRY_MS = 60 * 60 * 1000;
|
|
56
|
+
|
|
54
57
|
export async function handleInboundMessage(params: {
|
|
55
58
|
account: ResolvedDmworkAccount;
|
|
56
59
|
message: BotMessage;
|
|
57
60
|
botUid: string;
|
|
58
61
|
groupHistories: Map<string, any[]>;
|
|
62
|
+
memberMap: Map<string, string>; // displayName -> uid mapping
|
|
63
|
+
uidToNameMap: Map<string, string>; // uid -> displayName mapping (reverse)
|
|
64
|
+
groupCacheTimestamps: Map<string, number>; // groupId -> lastFetchedAt
|
|
59
65
|
log?: ChannelLogSink;
|
|
60
66
|
statusSink?: DmworkStatusSink;
|
|
61
67
|
}) {
|
|
62
|
-
const { account, message, botUid, groupHistories, log, statusSink } = params;
|
|
68
|
+
const { account, message, botUid, groupHistories, memberMap, uidToNameMap, groupCacheTimestamps, log, statusSink } = params;
|
|
63
69
|
|
|
64
70
|
await ensureSdkLoaded();
|
|
65
71
|
|
|
@@ -96,11 +102,97 @@ export async function handleInboundMessage(params: {
|
|
|
96
102
|
// --- Mention gating for group messages ---
|
|
97
103
|
const requireMention = account.config.requireMention !== false;
|
|
98
104
|
let historyPrefix = "";
|
|
105
|
+
|
|
106
|
+
// Save original mention uids for reply (exclude bot itself)
|
|
107
|
+
const originalMentionUids: string[] = (message.payload?.mention?.uids ?? []).filter((uid: string) => uid !== botUid);
|
|
108
|
+
|
|
109
|
+
// Helper function to refresh group member cache
|
|
110
|
+
async function refreshGroupMemberCache(forceRefresh = false): Promise<boolean> {
|
|
111
|
+
if (!isGroup) return false;
|
|
112
|
+
|
|
113
|
+
const lastFetched = groupCacheTimestamps.get(sessionId) ?? 0;
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const isExpired = (now - lastFetched) > GROUP_CACHE_EXPIRY_MS;
|
|
116
|
+
|
|
117
|
+
if (!forceRefresh && !isExpired && lastFetched > 0) {
|
|
118
|
+
return false; // Cache is still valid
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
log?.info?.(`dmwork: [CACHE] ${forceRefresh ? 'Force refreshing' : 'Refreshing expired'} group member cache for ${sessionId}`);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const members = await getGroupMembers({
|
|
125
|
+
apiUrl: account.config.apiUrl,
|
|
126
|
+
botToken: account.config.botToken ?? "",
|
|
127
|
+
groupNo: sessionId,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (members.length > 0) {
|
|
131
|
+
for (const m of members) {
|
|
132
|
+
if (m.name && m.uid) {
|
|
133
|
+
memberMap.set(m.name, m.uid);
|
|
134
|
+
uidToNameMap.set(m.uid, m.name);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
groupCacheTimestamps.set(sessionId, now);
|
|
138
|
+
log?.info?.(`dmwork: [CACHE] Loaded ${members.length} members, memberMap size: ${memberMap.size}`);
|
|
139
|
+
return true;
|
|
140
|
+
} else {
|
|
141
|
+
// Set a short backoff (30s) to prevent retry storms on empty responses
|
|
142
|
+
groupCacheTimestamps.set(sessionId, now - GROUP_CACHE_EXPIRY_MS + 30000);
|
|
143
|
+
log?.warn?.(`dmwork: [CACHE] No members returned for group ${sessionId}, backoff 30s`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Set a short backoff (30s) to prevent retry storms on errors
|
|
148
|
+
groupCacheTimestamps.set(sessionId, now - GROUP_CACHE_EXPIRY_MS + 30000);
|
|
149
|
+
log?.error?.(`dmwork: [CACHE] Failed to fetch group members: ${err}, backoff 30s`);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Refresh group member cache if needed (on first message or after expiry)
|
|
155
|
+
if (isGroup) {
|
|
156
|
+
await refreshGroupMemberCache();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build displayName -> uid mapping from message content + mention.uids
|
|
160
|
+
// When user sends "@陈皮皮 @托马斯.福 xxx", the @ names in content correspond to mention.uids in order
|
|
161
|
+
if (isGroup) {
|
|
162
|
+
const allMentionUids: string[] = message.payload?.mention?.uids ?? [];
|
|
163
|
+
// Match all @xxx patterns (including Chinese characters and dots)
|
|
164
|
+
const contentMentions = rawBody.match(/@[\w\u4e00-\u9fa5.]+/g) ?? [];
|
|
165
|
+
|
|
166
|
+
if (contentMentions.length > 0 && allMentionUids.length > 0) {
|
|
167
|
+
log?.debug?.(`dmwork: [MAPPING] content @names: ${JSON.stringify(contentMentions)}, mention.uids: ${JSON.stringify(allMentionUids)}`);
|
|
168
|
+
|
|
169
|
+
// Pair them in order
|
|
170
|
+
const pairCount = Math.min(contentMentions.length, allMentionUids.length);
|
|
171
|
+
for (let i = 0; i < pairCount; i++) {
|
|
172
|
+
const displayName = contentMentions[i].slice(1); // Remove @ prefix
|
|
173
|
+
const uid = allMentionUids[i];
|
|
174
|
+
if (displayName && uid) {
|
|
175
|
+
// Update both mappings
|
|
176
|
+
if (!memberMap.has(displayName)) {
|
|
177
|
+
memberMap.set(displayName, uid);
|
|
178
|
+
log?.debug?.(`dmwork: [MAPPING] learned name->uid mapping`);
|
|
179
|
+
}
|
|
180
|
+
if (!uidToNameMap.has(uid)) {
|
|
181
|
+
uidToNameMap.set(uid, displayName);
|
|
182
|
+
log?.debug?.(`dmwork: [MAPPING] learned uid->name mapping`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
99
188
|
|
|
100
189
|
if (isGroup && requireMention) {
|
|
101
190
|
const mentionUids: string[] = message.payload?.mention?.uids ?? [];
|
|
102
191
|
const mentionAll: boolean = message.payload?.mention?.all === true;
|
|
103
192
|
const isMentioned = mentionAll || mentionUids.includes(botUid);
|
|
193
|
+
|
|
194
|
+
// Debug: log received mention info
|
|
195
|
+
log?.debug?.(`dmwork: [RECV] mention payload: uidsCount=${mentionUids.length}, all=${mentionAll}, originalCount=${originalMentionUids.length}`);
|
|
104
196
|
|
|
105
197
|
if (!isMentioned) {
|
|
106
198
|
// Record as pending history context (manual — avoids SDK format incompatibility)
|
|
@@ -130,10 +222,10 @@ export async function handleInboundMessage(params: {
|
|
|
130
222
|
// Take last N entries (sliding window)
|
|
131
223
|
if (entries.length > historyLimit) {
|
|
132
224
|
entries = entries.slice(-historyLimit);
|
|
133
|
-
groupHistories.set(sessionId, entries);
|
|
225
|
+
groupHistories.set(sessionId, entries); // Persist trimmed array to prevent unbounded growth
|
|
134
226
|
}
|
|
135
227
|
const historyCountBefore = entries.length;
|
|
136
|
-
log?.info?.(`dmwork: [MENTION] 收到@消息 |
|
|
228
|
+
log?.info?.(`dmwork: [MENTION] 收到@消息 | 缓存=${historyCountBefore}条 | historyLimit=${historyLimit}`);
|
|
137
229
|
|
|
138
230
|
// If memory cache is empty, try fetching from API
|
|
139
231
|
if (entries.length === 0 && account.config.botToken) {
|
|
@@ -156,7 +248,6 @@ export async function handleInboundMessage(params: {
|
|
|
156
248
|
body: m.content,
|
|
157
249
|
timestamp: m.timestamp * 1000,
|
|
158
250
|
}));
|
|
159
|
-
groupHistories.set(sessionId, entries); // Persist API-fetched entries
|
|
160
251
|
log?.info?.(`dmwork: [MENTION] 从API获取到 ${entries.length} 条历史消息`);
|
|
161
252
|
} catch (err) {
|
|
162
253
|
log?.error?.(`dmwork: [MENTION] 从API获取历史失败: ${err}`);
|
|
@@ -299,14 +390,110 @@ export async function handleInboundMessage(params: {
|
|
|
299
390
|
const content = contentParts.join("\n").trim();
|
|
300
391
|
if (!content) return;
|
|
301
392
|
|
|
393
|
+
// Build mentionUids from @mentions in content, using memberMap to resolve displayName -> uid
|
|
394
|
+
// The order of mentionUids MUST match the order of @xxx in content for correct linking!
|
|
395
|
+
let replyMentionUids: string[] = [];
|
|
396
|
+
let finalContent = content;
|
|
397
|
+
|
|
398
|
+
if (isGroup) {
|
|
399
|
+
// Parse all @mentions from content (support Chinese, English, dots, underscores, hex uids)
|
|
400
|
+
const contentMentions = content.match(/@[\w\u4e00-\u9fa5.]+/g) ?? [];
|
|
401
|
+
|
|
402
|
+
log?.debug?.(`dmwork: [REPLY] content @mentions count: ${contentMentions.length}`);
|
|
403
|
+
log?.debug?.(`dmwork: [REPLY] memberMap size: ${memberMap.size}, uidToNameMap size: ${uidToNameMap.size}`);
|
|
404
|
+
|
|
405
|
+
// Track if we need to retry after cache refresh
|
|
406
|
+
let unresolvedNames: { name: string; index: number }[] = [];
|
|
407
|
+
|
|
408
|
+
// Helper to resolve a single mention
|
|
409
|
+
const resolveMention = (name: string): { uid: string | null; newContent: string } => {
|
|
410
|
+
// First try memberMap (displayName -> uid)
|
|
411
|
+
let uid = memberMap.get(name);
|
|
412
|
+
let newContent = finalContent;
|
|
413
|
+
|
|
414
|
+
if (uid) {
|
|
415
|
+
log?.debug?.(`dmwork: [REPLY] resolved displayName to uid`);
|
|
416
|
+
return { uid, newContent };
|
|
417
|
+
} else if (/^[a-f0-9]{32}$/i.test(name)) {
|
|
418
|
+
// Looks like a hex uid (32 chars) - try to find display name
|
|
419
|
+
const displayName = uidToNameMap.get(name);
|
|
420
|
+
if (displayName) {
|
|
421
|
+
newContent = newContent.replace(`@${name}`, `@${displayName}`);
|
|
422
|
+
log?.debug?.(`dmwork: [REPLY] replaced uid with displayName`);
|
|
423
|
+
return { uid: name, newContent };
|
|
424
|
+
} else {
|
|
425
|
+
log?.warn?.(`dmwork: [REPLY] unknown hex uid, no displayName found`);
|
|
426
|
+
return { uid: name, newContent };
|
|
427
|
+
}
|
|
428
|
+
} else if (/^[a-zA-Z0-9_]+$/.test(name)) {
|
|
429
|
+
// Looks like a uid format (alphanumeric + underscore)
|
|
430
|
+
const displayName = uidToNameMap.get(name);
|
|
431
|
+
if (displayName) {
|
|
432
|
+
newContent = newContent.replace(`@${name}`, `@${displayName}`);
|
|
433
|
+
log?.debug?.(`dmwork: [REPLY] replaced uid with displayName`);
|
|
434
|
+
return { uid: name, newContent };
|
|
435
|
+
} else {
|
|
436
|
+
log?.debug?.(`dmwork: [REPLY] using mention as uid directly`);
|
|
437
|
+
return { uid: name, newContent };
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
// Chinese name not found - track for retry
|
|
441
|
+
return { uid: null, newContent };
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// First pass: try to resolve all mentions, tracking indices for order preservation
|
|
446
|
+
const resolvedUids: (string | null)[] = [];
|
|
447
|
+
for (const mention of contentMentions) {
|
|
448
|
+
const name = mention.slice(1);
|
|
449
|
+
const result = resolveMention(name);
|
|
450
|
+
finalContent = result.newContent;
|
|
451
|
+
resolvedUids.push(result.uid); // null if unresolved
|
|
452
|
+
if (!result.uid) {
|
|
453
|
+
unresolvedNames.push({ name, index: resolvedUids.length - 1 });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// If we have unresolved names, try refreshing the cache and retry
|
|
458
|
+
if (unresolvedNames.length > 0) {
|
|
459
|
+
log?.info?.(`dmwork: [REPLY] ${unresolvedNames.length} unresolved names, force refreshing cache...`);
|
|
460
|
+
const refreshed = await refreshGroupMemberCache(true);
|
|
461
|
+
|
|
462
|
+
if (refreshed) {
|
|
463
|
+
// Retry unresolved names and insert at original positions
|
|
464
|
+
for (const { name, index } of unresolvedNames) {
|
|
465
|
+
const uid = memberMap.get(name);
|
|
466
|
+
if (uid) {
|
|
467
|
+
resolvedUids[index] = uid; // Insert at original position
|
|
468
|
+
log?.debug?.(`dmwork: [REPLY] after refresh: resolved @${name}`);
|
|
469
|
+
} else {
|
|
470
|
+
log?.warn?.(`dmwork: [REPLY] after refresh: still cannot resolve @${name}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Build final mention UIDs array preserving original order
|
|
477
|
+
replyMentionUids = resolvedUids.filter((uid): uid is string => uid !== null);
|
|
478
|
+
|
|
479
|
+
// Always include the original sender so they get notified of the reply
|
|
480
|
+
if (message.from_uid && !replyMentionUids.includes(message.from_uid)) {
|
|
481
|
+
replyMentionUids.unshift(message.from_uid);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (replyMentionUids.length > 0) {
|
|
485
|
+
log?.debug?.(`dmwork: [REPLY] final mentionUids count: ${replyMentionUids.length}`);
|
|
486
|
+
log?.debug?.(`dmwork: [REPLY] final content length: ${finalContent.length}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
302
490
|
await sendMessage({
|
|
303
491
|
apiUrl: account.config.apiUrl,
|
|
304
492
|
botToken: account.config.botToken ?? "",
|
|
305
493
|
channelId: replyChannelId,
|
|
306
494
|
channelType: replyChannelType,
|
|
307
|
-
content,
|
|
308
|
-
|
|
309
|
-
...(isGroup ? { mentionUids: [message.from_uid] } : {}),
|
|
495
|
+
content: finalContent,
|
|
496
|
+
...(replyMentionUids.length > 0 ? { mentionUids: replyMentionUids } : {}),
|
|
310
497
|
});
|
|
311
498
|
|
|
312
499
|
statusSink?.({ lastOutboundAt: Date.now(), lastError: null });
|