|
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
const path = require('path');
|
|
|
const { okResult, errorResult } = require('../core/result-envelope');
|
|
|
const { createRunDir, latestPath, outputsRoot, writeRunManifest } = require('../core/output-paths');
|
|
|
-const { recordGroupSync, recordCustomersFromGroups } = require('../core/dashboard-state');
|
|
|
+const { recordCustomersFromGroups } = require('../core/dashboard-state');
|
|
|
const { getGroupOperationsStore } = require('../core/group-operations-store');
|
|
|
const {
|
|
|
buildContext,
|
|
|
@@ -15,7 +15,6 @@ const {
|
|
|
const REQUIRED_METHODS = {
|
|
|
getRoomList: '/room/getRoomList',
|
|
|
batchGetRoomDetail: '/room/batchGetRoomDetail',
|
|
|
- syncMsg: '/msg/syncMsg',
|
|
|
getSessionPage: '/session/getSessionPage'
|
|
|
};
|
|
|
|
|
|
@@ -69,24 +68,6 @@ function ensureGroupsDir() {
|
|
|
fs.mkdirSync(groupsDir(), { recursive: true });
|
|
|
}
|
|
|
|
|
|
-function ensureMessagesDir() {
|
|
|
- fs.mkdirSync(messagesDir(), { recursive: true });
|
|
|
-}
|
|
|
-
|
|
|
-function messageSyncCursorPath(accountKey) {
|
|
|
- const safeAccount = String(accountKey || 'local-default').trim().replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 100) || 'local-default';
|
|
|
- ensureGroupsDir();
|
|
|
- return path.join(groupsDir(), `message-sync-cursor-${safeAccount}.json`);
|
|
|
-}
|
|
|
-
|
|
|
-function readMessageSyncCursor(accountKey) {
|
|
|
- return safeReadJson(messageSyncCursorPath(accountKey), { travelSyncKey: 0, updatedAt: null });
|
|
|
-}
|
|
|
-
|
|
|
-function writeMessageSyncCursor(accountKey, cursor) {
|
|
|
- fs.writeFileSync(messageSyncCursorPath(accountKey), JSON.stringify({ ...cursor, updatedAt: new Date().toISOString() }, null, 2), 'utf8');
|
|
|
-}
|
|
|
-
|
|
|
function ensureRoomMessagesDir(roomId) {
|
|
|
return fs.mkdirSync(roomMessagesDir(roomId), { recursive: true });
|
|
|
}
|
|
|
@@ -97,84 +78,6 @@ function messageFilePath(roomId, seq, msgUniqueId) {
|
|
|
return path.join(roomMessagesDir(roomId), `${seq}-${safeUniqueId}.json`);
|
|
|
}
|
|
|
|
|
|
-let gbkEncodeMap = null;
|
|
|
-
|
|
|
-function getGbkEncodeMap() {
|
|
|
- if (gbkEncodeMap) return gbkEncodeMap;
|
|
|
- gbkEncodeMap = new Map();
|
|
|
- const decoder = new TextDecoder('gbk');
|
|
|
- for (let first = 0x81; first <= 0xfe; first++) {
|
|
|
- for (let second = 0x40; second <= 0xfe; second++) {
|
|
|
- if (second === 0x7f) continue;
|
|
|
- const bytes = Buffer.from([first, second]);
|
|
|
- const char = decoder.decode(bytes);
|
|
|
- if (!char || char === '\uFFFD' || char.length !== 1) continue;
|
|
|
- if (!gbkEncodeMap.has(char)) gbkEncodeMap.set(char, bytes);
|
|
|
- }
|
|
|
- }
|
|
|
- return gbkEncodeMap;
|
|
|
-}
|
|
|
-
|
|
|
-function repairUtf8DecodedAsGbk(value) {
|
|
|
- const text = String(value || '');
|
|
|
- if (!/[\u4e00-\u9fa5]/.test(text)) return text;
|
|
|
- const map = getGbkEncodeMap();
|
|
|
- const chunks = [];
|
|
|
- for (const char of text) {
|
|
|
- const code = char.codePointAt(0);
|
|
|
- if (code <= 0x7f) {
|
|
|
- chunks.push(Buffer.from([code]));
|
|
|
- continue;
|
|
|
- }
|
|
|
- const bytes = map.get(char);
|
|
|
- if (!bytes) return text;
|
|
|
- chunks.push(bytes);
|
|
|
- }
|
|
|
- const repaired = Buffer.concat(chunks).toString('utf8');
|
|
|
- if (repaired.includes('\uFFFD')) return text;
|
|
|
- return /[\u4e00-\u9fa5]/.test(repaired) ? repaired : text;
|
|
|
-}
|
|
|
-
|
|
|
-function cleanExtractedText(value) {
|
|
|
- const cleaned = String(value || '').replace(/[\u0000-\u001f\u007f]/g, '').trim();
|
|
|
- return repairUtf8DecodedAsGbk(cleaned);
|
|
|
-}
|
|
|
-
|
|
|
-function extractUtf8TextFromBase64(value) {
|
|
|
- if (!value) return '';
|
|
|
- let buffer;
|
|
|
- try {
|
|
|
- buffer = Buffer.from(String(value), 'base64');
|
|
|
- } catch {
|
|
|
- return '';
|
|
|
- }
|
|
|
- const candidates = [];
|
|
|
- for (let i = 0; i < buffer.length - 1; i++) {
|
|
|
- const len = buffer[i];
|
|
|
- if (!len || len > 240 || i + 1 + len > buffer.length) continue;
|
|
|
- const text = cleanExtractedText(buffer.slice(i + 1, i + 1 + len).toString('utf8'));
|
|
|
- if (!text || text.includes('\uFFFD')) continue;
|
|
|
- if (/[\u4e00-\u9fa5]/.test(text)) candidates.push(text);
|
|
|
- }
|
|
|
- return candidates.sort((a, b) => b.length - a.length)[0] || '';
|
|
|
-}
|
|
|
-
|
|
|
-function extractMessageContent(msg = {}) {
|
|
|
- const rawText = extractUtf8TextFromBase64(msg.base64RawData || msg.msgData?.extras?.base64RawData);
|
|
|
- if (rawText) return rawText;
|
|
|
- if (typeof msg.content === 'string' && msg.content) return cleanExtractedText(msg.content);
|
|
|
- if (typeof msg.msgContent === 'string' && msg.msgContent) return cleanExtractedText(msg.msgContent);
|
|
|
- if (msg.msgData) {
|
|
|
- if (typeof msg.msgData.content === 'string' && msg.msgData.content) return cleanExtractedText(msg.msgData.content);
|
|
|
- if (typeof msg.msgData.text === 'string' && msg.msgData.text) return cleanExtractedText(msg.msgData.text);
|
|
|
- if (Array.isArray(msg.msgData.moreDetail)) {
|
|
|
- return cleanExtractedText(msg.msgData.moreDetail.map(item => item && item.text).filter(Boolean).join(''));
|
|
|
- }
|
|
|
- if (typeof msg.msgData.notifyTitle === 'string' && msg.msgData.notifyTitle) return cleanExtractedText(msg.msgData.notifyTitle);
|
|
|
- }
|
|
|
- return '';
|
|
|
-}
|
|
|
-
|
|
|
function appendWebhookMessage(roomId, message) {
|
|
|
if (!roomId || !message) return null;
|
|
|
const seq = Number(message.seq) || 0;
|
|
|
@@ -516,45 +419,12 @@ async function enrichRoomsWithDetails(ctx, roomIds, chunkSize = 50) {
|
|
|
});
|
|
|
const roomList = Array.isArray(data && data.roomList) ? data.roomList : [];
|
|
|
for (const room of roomList) {
|
|
|
- rooms.push(createRoomRecord(room, 'messages'));
|
|
|
+ rooms.push(createRoomRecord(room, 'roomDetail'));
|
|
|
}
|
|
|
}
|
|
|
return rooms;
|
|
|
}
|
|
|
|
|
|
-async function scanRoomsFromMessages(ctx, maxPages = 300, maxTotalMessages = 20000) {
|
|
|
- assertMethodsInCatalog({ syncMsg: REQUIRED_METHODS.syncMsg });
|
|
|
- const roomIds = new Set();
|
|
|
- let msgSeq = 0;
|
|
|
- let hasMore = true;
|
|
|
- let pages = 0;
|
|
|
- let totalMessages = 0;
|
|
|
-
|
|
|
- while (hasMore && pages < maxPages && totalMessages < maxTotalMessages) {
|
|
|
- pages++;
|
|
|
- const data = await gatewayCall(ctx, REQUIRED_METHODS.syncMsg, {
|
|
|
- guid: ctx.guid,
|
|
|
- msgSeq,
|
|
|
- limit: 500
|
|
|
- });
|
|
|
- const msgList = Array.isArray(data && data.syncMsgList) ? data.syncMsgList : [];
|
|
|
- hasMore = Boolean(data.hasMore);
|
|
|
- msgSeq = data.travelSyncKey ?? msgSeq + 1;
|
|
|
- totalMessages += msgList.length;
|
|
|
-
|
|
|
- for (const msg of msgList) {
|
|
|
- const roomId = String(msg.fromRoomId || '');
|
|
|
- if (roomId) roomIds.add(roomId);
|
|
|
- }
|
|
|
-
|
|
|
- if (!msgList.length) break;
|
|
|
- }
|
|
|
-
|
|
|
- const roomIdList = Array.from(roomIds);
|
|
|
- const details = await enrichRoomsWithDetails(ctx, roomIdList, 50);
|
|
|
- return { rooms: details, pages, rawRoomIds: roomIdList, totalMessages };
|
|
|
-}
|
|
|
-
|
|
|
function listGroupFiles() {
|
|
|
ensureGroupsDir();
|
|
|
const files = fs.readdirSync(groupsDir())
|
|
|
@@ -648,9 +518,8 @@ function writeRejectedMapping(mapping) {
|
|
|
}
|
|
|
|
|
|
function resolveScope(input) {
|
|
|
- const validScopes = ['self', 'session', 'messages', 'all'];
|
|
|
+ const validScopes = ['self', 'session', 'all'];
|
|
|
if (input.scope && validScopes.includes(input.scope)) return input.scope;
|
|
|
- if (input.fromMessages === true) return 'messages';
|
|
|
return 'all';
|
|
|
}
|
|
|
|
|
|
@@ -671,8 +540,7 @@ const qiweiSyncExternalGroups = safeResult(async function qiweiSyncExternalGroup
|
|
|
assertMethodsInCatalog({
|
|
|
getRoomList: REQUIRED_METHODS.getRoomList,
|
|
|
batchGetRoomDetail: REQUIRED_METHODS.batchGetRoomDetail,
|
|
|
- getSessionPage: REQUIRED_METHODS.getSessionPage,
|
|
|
- syncMsg: REQUIRED_METHODS.syncMsg
|
|
|
+ getSessionPage: REQUIRED_METHODS.getSessionPage
|
|
|
});
|
|
|
const ctx = buildContext(input);
|
|
|
requireGuid(ctx);
|
|
|
@@ -681,13 +549,10 @@ const qiweiSyncExternalGroups = safeResult(async function qiweiSyncExternalGroup
|
|
|
const maxPages = Math.max(1, Math.min(300, Number(input.maxPages || 100)));
|
|
|
const includeInternalGroups = input.includeInternalGroups === true;
|
|
|
const autoClassify = input.autoClassify !== false;
|
|
|
- const scanFromMessages = input.scanFromMessages === true;
|
|
|
- const previewMessages = Math.max(0, Math.min(100, Number(input.previewMessages || 0)));
|
|
|
- const previewOnlyForUnsure = input.previewOnlyForUnsure !== false;
|
|
|
const classifier = buildClassifierConfig(input);
|
|
|
|
|
|
const collected = [];
|
|
|
- const perSource = { roomList: { rooms: [], pages: 0 }, session: { rooms: [], pages: 0 }, messages: { rooms: [], pages: 0, totalMessages: 0 } };
|
|
|
+ const perSource = { roomList: { rooms: [], pages: 0 }, session: { rooms: [], pages: 0 } };
|
|
|
|
|
|
if (scope === 'self' || scope === 'all') {
|
|
|
try {
|
|
|
@@ -709,16 +574,6 @@ const qiweiSyncExternalGroups = safeResult(async function qiweiSyncExternalGroup
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (scope === 'messages' || (scope === 'all' && scanFromMessages)) {
|
|
|
- try {
|
|
|
- const result = await scanRoomsFromMessages(ctx, maxPages);
|
|
|
- perSource.messages = result;
|
|
|
- collected.push(...result.rooms);
|
|
|
- } catch (err) {
|
|
|
- // 继续其他来源
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
const deduped = dedupRooms(collected);
|
|
|
const roomsNeedingDetails = deduped
|
|
|
.filter(r => Number(r.memberCount || 0) > 0 && (!Array.isArray(r.members) || !r.members.length))
|
|
|
@@ -761,21 +616,7 @@ const qiweiSyncExternalGroups = safeResult(async function qiweiSyncExternalGroup
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- let text = room.roomName || '';
|
|
|
- let classification = classifyGroupByName(text, classifier);
|
|
|
-
|
|
|
- if (previewMessages > 0 && previewOnlyForUnsure && classification.reviewStatus !== 'AUTO_CONFIRMED') {
|
|
|
- try {
|
|
|
- const previews = await fetchPreviewMessages(ctx, room.roomId, previewMessages);
|
|
|
- if (previews.length) {
|
|
|
- text += ' ' + previews.join(' ');
|
|
|
- classification = classifyGroupByName(text, classifier);
|
|
|
- }
|
|
|
- } catch {
|
|
|
- // 忽略消息拉取失败,保留初次分类结果
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+ const classification = classifyGroupByName(room.roomName || '', classifier);
|
|
|
classifiedRooms.push({ ...room, ...classification });
|
|
|
}
|
|
|
|
|
|
@@ -788,8 +629,6 @@ const qiweiSyncExternalGroups = safeResult(async function qiweiSyncExternalGroup
|
|
|
maxPages,
|
|
|
includeInternalGroups,
|
|
|
autoClassify,
|
|
|
- previewMessages,
|
|
|
- previewOnlyForUnsure,
|
|
|
matchMode: classifier.matchMode,
|
|
|
threshold: classifier.threshold,
|
|
|
total: classifiedRooms.length,
|
|
|
@@ -823,16 +662,13 @@ const qiweiSyncExternalGroups = safeResult(async function qiweiSyncExternalGroup
|
|
|
scanned: classifiedRooms.length,
|
|
|
selfCount: perSource.roomList.rooms.length,
|
|
|
sessionCount: perSource.session.rooms.length,
|
|
|
- messageCount: perSource.messages.rooms.length,
|
|
|
mergedCount: deduped.length,
|
|
|
externalCount: filtered.length,
|
|
|
autoConfirmed: classifiedRooms.filter(r => r.reviewStatus === 'AUTO_CONFIRMED').length,
|
|
|
suggested: classifiedRooms.filter(r => r.reviewStatus === 'SUGGESTED').length,
|
|
|
imported: classifiedRooms.filter(r => r.reviewStatus === 'IMPORTED').length,
|
|
|
discoveredCustomers: discoveredCustomers.discovered,
|
|
|
- previewMessages,
|
|
|
- previewOnlyForUnsure,
|
|
|
- pages: perSource.roomList.pages + perSource.session.pages + perSource.messages.pages
|
|
|
+ pages: perSource.roomList.pages + perSource.session.pages
|
|
|
},
|
|
|
data: {
|
|
|
rooms: classifiedRooms.slice(0, 20),
|
|
|
@@ -926,37 +762,6 @@ const qiweiListExternalGroups = safeResult(async function qiweiListExternalGroup
|
|
|
});
|
|
|
});
|
|
|
|
|
|
-async function fetchPreviewMessages(ctx, roomId, previewMessages) {
|
|
|
- const texts = [];
|
|
|
- let msgSeq = 0;
|
|
|
- let hasMore = true;
|
|
|
- let pages = 0;
|
|
|
- const maxPages = Math.min(10, Math.ceil(previewMessages / 100));
|
|
|
-
|
|
|
- while (hasMore && pages < maxPages && texts.length < previewMessages) {
|
|
|
- pages++;
|
|
|
- const data = await gatewayCall(ctx, REQUIRED_METHODS.syncMsg, {
|
|
|
- guid: ctx.guid,
|
|
|
- msgSeq,
|
|
|
- limit: 100
|
|
|
- });
|
|
|
- const msgList = Array.isArray(data && data.syncMsgList) ? data.syncMsgList : [];
|
|
|
- hasMore = Boolean(data.hasMore);
|
|
|
- msgSeq = data.travelSyncKey ?? msgSeq + 1;
|
|
|
-
|
|
|
- for (const msg of msgList) {
|
|
|
- if (String(msg.fromRoomId || '') !== roomId) continue;
|
|
|
- const content = String(msg.content || msg.msgContent || '').trim();
|
|
|
- if (content) texts.push(content);
|
|
|
- if (texts.length >= previewMessages) break;
|
|
|
- }
|
|
|
-
|
|
|
- if (!msgList.length) break;
|
|
|
- }
|
|
|
-
|
|
|
- return texts;
|
|
|
-}
|
|
|
-
|
|
|
const qiweiAnalyzeGroupMembers = safeResult(async function qiweiAnalyzeGroupMembers(input = {}) {
|
|
|
assertMethodsInCatalog({ batchGetRoomDetail: REQUIRED_METHODS.batchGetRoomDetail });
|
|
|
const ctx = buildContext(input);
|
|
|
@@ -967,29 +772,18 @@ const qiweiAnalyzeGroupMembers = safeResult(async function qiweiAnalyzeGroupMemb
|
|
|
|
|
|
const classifier = buildClassifierConfig(input);
|
|
|
const autoClassify = input.autoClassify !== false;
|
|
|
- const previewMessages = Math.max(0, Math.min(100, Number(input.previewMessages || 0)));
|
|
|
const updateSnapshot = input.updateSnapshot === true;
|
|
|
|
|
|
const details = await enrichRoomsWithDetails(ctx, roomIds, 20);
|
|
|
|
|
|
const classified = [];
|
|
|
for (const room of details) {
|
|
|
- let text = room.roomName || '';
|
|
|
- if (previewMessages > 0) {
|
|
|
- try {
|
|
|
- const previews = await fetchPreviewMessages(ctx, room.roomId, previewMessages);
|
|
|
- if (previews.length) text += ' ' + previews.join(' ');
|
|
|
- } catch {
|
|
|
- // 忽略消息拉取失败
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
if (!autoClassify) {
|
|
|
classified.push({ ...room, reviewStatus: 'IMPORTED', confidence: 0, reason: '自动分类已关闭', matchedKeywords: [] });
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- const classification = classifyGroupByName(text, classifier);
|
|
|
+ const classification = classifyGroupByName(room.roomName || '', classifier);
|
|
|
classified.push({ ...room, ...classification });
|
|
|
}
|
|
|
|
|
|
@@ -1174,114 +968,6 @@ const qiweiRejectExternalGroup = safeResult(async function qiweiRejectExternalGr
|
|
|
});
|
|
|
});
|
|
|
|
|
|
-const qiweiSyncGroupMessages = safeResult(async function qiweiSyncGroupMessages(input = {}) {
|
|
|
- assertMethodsInCatalog({ syncMsg: REQUIRED_METHODS.syncMsg });
|
|
|
- const ctx = buildContext(input);
|
|
|
- requireGuid(ctx);
|
|
|
-
|
|
|
- const roomIds = Array.isArray(input.roomIds) ? input.roomIds.map(String).filter(Boolean) : null;
|
|
|
- const maxPages = Math.max(1, Math.min(300, Number(input.maxPages || 100)));
|
|
|
- const maxMessagesPerRoom = Math.max(1, Math.min(5000, Number(input.maxMessagesPerRoom || 1000)));
|
|
|
-
|
|
|
- let targetRoomIds = roomIds;
|
|
|
- if (!targetRoomIds) {
|
|
|
- const mapping = readConfirmedMapping();
|
|
|
- targetRoomIds = Object.keys(mapping);
|
|
|
- if (!targetRoomIds.length) return errorResult('未指定 roomIds 且没有已确认的客户群映射,请先调用 qiwei_confirm_external_group');
|
|
|
- }
|
|
|
-
|
|
|
- const targetSet = new Set(targetRoomIds);
|
|
|
- const messagesByRoom = new Map(targetRoomIds.map(roomId => [roomId, []]));
|
|
|
- const cursorBefore = readMessageSyncCursor(ctx.guid);
|
|
|
- let msgSeq = Number(cursorBefore.travelSyncKey || 0);
|
|
|
- let nextCursor = msgSeq;
|
|
|
- let hasMore = true;
|
|
|
- let pages = 0;
|
|
|
- let scannedMessages = 0;
|
|
|
-
|
|
|
- while (hasMore && pages < maxPages) {
|
|
|
- pages++;
|
|
|
- const data = await gatewayCall(ctx, REQUIRED_METHODS.syncMsg, { guid: ctx.guid, msgSeq, limit: 500 });
|
|
|
- const msgList = Array.isArray(data && data.syncMsgList) ? data.syncMsgList : [];
|
|
|
- hasMore = Boolean(data.hasMore);
|
|
|
- nextCursor = data.travelSyncKey ?? msgSeq;
|
|
|
- scannedMessages += msgList.length;
|
|
|
- for (const msg of msgList) {
|
|
|
- const roomId = String(msg.fromRoomId || '');
|
|
|
- if (!targetSet.has(roomId)) continue;
|
|
|
- const roomMessages = messagesByRoom.get(roomId);
|
|
|
- if (roomMessages.length >= maxMessagesPerRoom) continue;
|
|
|
- roomMessages.push({
|
|
|
- msgId: msg.msgUniqueIdentifier || msg.msgServerId || `${roomId}_${msg.seq}`,
|
|
|
- seq: msg.seq,
|
|
|
- senderId: msg.senderId || '',
|
|
|
- senderName: msg.senderName || '',
|
|
|
- receiverId: msg.receiverId || '',
|
|
|
- fromRoomId: roomId,
|
|
|
- msgType: String(msg.msgType),
|
|
|
- content: extractMessageContent(msg),
|
|
|
- timestamp: msg.timestamp ? new Date(msg.timestamp * 1000).toISOString() : new Date().toISOString(),
|
|
|
- rawData: msg
|
|
|
- });
|
|
|
- }
|
|
|
- if (!msgList.length || nextCursor === msgSeq) break;
|
|
|
- msgSeq = nextCursor;
|
|
|
- }
|
|
|
-
|
|
|
- if (!hasMore) writeMessageSyncCursor(ctx.guid, { travelSyncKey: nextCursor, pages, scannedMessages });
|
|
|
-
|
|
|
- const results = [];
|
|
|
- const mapping = readConfirmedMapping();
|
|
|
- const operationStore = getGroupOperationsStore();
|
|
|
- for (const roomId of targetRoomIds) {
|
|
|
- const messages = messagesByRoom.get(roomId) || [];
|
|
|
- if (messages.length) {
|
|
|
- ensureMessagesDir();
|
|
|
- let writtenCount = 0;
|
|
|
- for (const message of messages) {
|
|
|
- if (appendWebhookMessage(roomId, message)) writtenCount++;
|
|
|
- }
|
|
|
- try {
|
|
|
- operationStore.upsertGroup(ctx.guid, { roomId, roomName: mapping[roomId]?.roomName || '' }, 'history-sync');
|
|
|
- operationStore.ingestMessages(ctx.guid, roomId, messages.map(message => ({
|
|
|
- ...message,
|
|
|
- messageId: message.msgId,
|
|
|
- senderRole: 'unknown',
|
|
|
- sentAt: message.timestamp
|
|
|
- })));
|
|
|
- } catch (error) {
|
|
|
- console.warn(`[群消息同步] 社群运营事实表写入失败: ${error.message}`);
|
|
|
- }
|
|
|
-
|
|
|
- // 更新 confirmed-mapping 的 lastMsgAt / lastSyncSeq
|
|
|
- if (mapping[roomId]) {
|
|
|
- const maxSeq = Math.max(...messages.map(m => Number(m.seq) || 0));
|
|
|
- const lastMsg = messages[messages.length - 1];
|
|
|
- mapping[roomId].lastMsgAt = lastMsg.timestamp || new Date().toISOString();
|
|
|
- mapping[roomId].lastSyncSeq = Math.max(mapping[roomId].lastSyncSeq || 0, maxSeq);
|
|
|
- recordGroupSync(roomId, {
|
|
|
- messageCount: writtenCount,
|
|
|
- lastMsgAt: lastMsg.timestamp || new Date().toISOString(),
|
|
|
- lastSyncSeq: mapping[roomId].lastSyncSeq
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- results.push({ roomId, newCount: writtenCount, fileCount: messages.length });
|
|
|
- } else {
|
|
|
- results.push({ roomId, newCount: 0, fileCount: 0 });
|
|
|
- }
|
|
|
- }
|
|
|
- writeConfirmedMapping(mapping);
|
|
|
-
|
|
|
- const totalNew = results.reduce((sum, r) => sum + r.newCount, 0);
|
|
|
- return okResult({
|
|
|
- assistantMessage: `群消息同步完成:${targetRoomIds.length} 个群,共 ${totalNew} 条新消息。`,
|
|
|
- summary: { rooms: targetRoomIds.length, totalNew, syncedRooms: results.filter(r => r.newCount > 0).length, pages, scannedMessages, cursorAdvanced: !hasMore && nextCursor !== Number(cursorBefore.travelSyncKey || 0) },
|
|
|
- data: { results, cursor: { before: cursorBefore.travelSyncKey || 0, after: !hasMore ? nextCursor : cursorBefore.travelSyncKey || 0, complete: !hasMore } },
|
|
|
- files: results.filter(r => r.filePath).map(r => path.join(outputsRoot(), r.filePath))
|
|
|
- });
|
|
|
-});
|
|
|
-
|
|
|
module.exports = {
|
|
|
qiweiSyncExternalGroups,
|
|
|
qiweiListExternalGroups,
|
|
|
@@ -1290,8 +976,8 @@ module.exports = {
|
|
|
qiweiAddExternalGroup,
|
|
|
qiweiConfigureGroupKeywords,
|
|
|
qiweiRejectExternalGroup,
|
|
|
- qiweiSyncGroupMessages,
|
|
|
appendWebhookMessage,
|
|
|
+ messageExists,
|
|
|
readRoomMessages,
|
|
|
readConfirmedMapping,
|
|
|
writeConfirmedMapping,
|