| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691 |
- const fs = require('fs');
- const path = require('path');
- const { outputsRoot } = require('./output-paths');
- const { readQiweiGuid } = require('./credentials');
- const {
- readCustomerByExternalUserId,
- readCustomerByPhone,
- writeCustomer,
- readBrokerById,
- readBrokerByWecomUserId,
- normalizePhone
- } = require('./customer-broker-store');
- const {
- resolveBrokerIdForGuid,
- resolveWecomUserIdForGuid
- } = require('./device-broker-mapping');
- const {
- saveWebhookEventStructured,
- isDuplicateEvent,
- markEventProcessed,
- updateWebhookEventStatus
- } = require('./webhook-store');
- const { parseWebhookEnvelope, ParsedWebhookEvent, SystemMsgType, decodeChangedMemberList } = require('./webhook-types');
- const {
- qiweiAutoCreateGroup,
- qiweiCheckFriendStatus
- } = require('../tools/qiwei-customer-ops-run');
- const {
- readImportedMapping,
- writeImportedMapping,
- readConfirmedMapping,
- writeConfirmedMapping,
- updateConfirmedMapping,
- updateImportedMapping,
- appendWebhookMessage,
- messageExists
- } = require('../tools/qiwei-group-management-run');
- const { enqueuePortraitUpdate } = require('../tools/qiwei-portrait-tags-run');
- const { qiweiTranscribeVoice } = require('../tools/qiwei-voice-run');
- const { getGroupOperationsStore } = require('./group-operations-store');
- const REQUIRED_METHODS = {
- getWxContactList: '/contact/getWxContactList'
- };
- function webhookConfigPath() {
- return path.join(outputsRoot(), 'webhook', 'webhook-config.json');
- }
- function readWebhookConfig() {
- try {
- if (!fs.existsSync(webhookConfigPath())) return { callbackUrl: null, secret: null, autoCreateGroup: true };
- return { callbackUrl: null, secret: null, autoCreateGroup: true, ...JSON.parse(fs.readFileSync(webhookConfigPath(), 'utf8')) };
- } catch {
- return { callbackUrl: null, secret: null, autoCreateGroup: true };
- }
- }
- function groupsDir() {
- return path.join(outputsRoot(), 'groups');
- }
- function customersDir() {
- return path.join(outputsRoot(), 'customers');
- }
- function ensureGroupsDir() {
- fs.mkdirSync(groupsDir(), { recursive: true });
- }
- function hasActiveGroup(brokerId, customerId) {
- const mapping = readConfirmedMapping();
- for (const entry of Object.values(mapping)) {
- if (entry.brokerId === brokerId && entry.customerId === customerId && entry.status === 'ACTIVE') {
- return true;
- }
- }
- return false;
- }
- function recordConfirmedGroup({ roomId, roomName, brokerId, customerId, externalUserId, source = 'webhook-auto' }) {
- const mapping = readConfirmedMapping();
- mapping[roomId] = {
- roomId,
- roomName: roomName || mapping[roomId]?.roomName,
- brokerId,
- customerId,
- externalUserId,
- status: 'ACTIVE',
- reviewStatus: 'AUTO_CONFIRMED',
- source,
- confirmedAt: new Date().toISOString(),
- ...(mapping[roomId] || {})
- };
- writeConfirmedMapping(mapping);
- return mapping[roomId];
- }
- function resolveDeviceGuid(input = {}) {
- if (input.guid) return input.guid;
- if (input.deviceGuid) return input.deviceGuid;
- return readQiweiGuid();
- }
- function buildContext(input = {}) {
- const { buildContext: sharedBuildContext } = require('./shared-gateway');
- return sharedBuildContext(input);
- }
- function gatewayCall(ctx, method, params) {
- const { gatewayCall: sharedGatewayCall } = require('./shared-gateway');
- return sharedGatewayCall(ctx, method, params);
- }
- function assertMethodsInCatalog(methods) {
- const { assertMethodsInCatalog: sharedAssert } = require('./shared-gateway');
- return sharedAssert(methods);
- }
- async function findNewlyAddedContact(guid) {
- if (!guid) return undefined;
- try {
- assertMethodsInCatalog({ getWxContactList: REQUIRED_METHODS.getWxContactList });
- const ctx = buildContext({ guid });
- const data = await gatewayCall(ctx, REQUIRED_METHODS.getWxContactList, {
- guid,
- currentSeq: 0,
- limit: 50,
- bizType: 1
- });
- const list = Array.isArray(data && data.contactList) ? data.contactList : [];
- for (const contact of list) {
- const userId = String(contact.userId || contact.externalUserId || '');
- if (!userId) continue;
- const customer = readCustomerByExternalUserId(userId);
- if (customer) {
- console.log(`[Webhook] 在联系人列表中找到匹配客户: ${userId}`);
- return userId;
- }
- }
- console.log(`[Webhook] 设备 ${guid} 的联系人列表中没有匹配到客户`);
- return undefined;
- } catch (err) {
- console.warn(`[Webhook] 设备 ${guid} 获取联系人列表失败:`, err.message);
- return undefined;
- }
- }
- async function findNewlyAddedContactAcrossDevices() {
- // 目标项目简化为:尝试从当前 guid 获取联系人列表
- const guid = readQiweiGuid();
- if (!guid) {
- console.warn('[Webhook] 缺少 guid,无法跨设备查找联系人');
- return undefined;
- }
- return findNewlyAddedContact(guid);
- }
- function buildGroupName(customer, broker) {
- const template = customer.groupNameTemplate || '{name} 专属服务群';
- const phone = normalizePhone(customer.phone);
- const phoneSuffix = phone.length >= 4 ? phone.slice(-4) : '';
- const customerLabel = customer.name || (phoneSuffix ? `客户${phoneSuffix}` : '客户');
- return template
- .replace(/{name}/g, customerLabel)
- .replace(/{phone}/g, phone)
- .replace(/{brokerName}/g, broker.name || '');
- }
- async function triggerAutoCreateGroup(event, eventFilePath = null) {
- const guid = resolveDeviceGuid(event);
- let externalUserId = event.externalUserId;
- if (!externalUserId && event.msgType === SystemMsgType.CONTACT_EXTERNAL_CHANGE) {
- externalUserId = await findNewlyAddedContact(guid);
- if (!externalUserId) {
- externalUserId = await findNewlyAddedContactAcrossDevices();
- }
- }
- if (!externalUserId) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', '无法提取 externalUserId');
- return { success: false, ignored: true, reason: '无法提取 externalUserId' };
- }
- let customer = readCustomerByExternalUserId(externalUserId);
- if (!customer && event.contactNickname) {
- console.log(`[Webhook] 未通过 externalUserId 匹配客户,尝试备注/昵称: ${event.contactNickname}`);
- }
- if (!customer) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', `externalUserId=${externalUserId} 未匹配到客户档案`);
- return { success: false, ignored: true, reason: '未匹配到客户档案' };
- }
- const customerId = customer.customerId;
- let brokerId = customer.brokerId;
- if (!brokerId) {
- brokerId = resolveBrokerIdForGuid(guid);
- if (!brokerId) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', `客户 ${customerId} 无归属经纪人`);
- return { success: false, ignored: true, reason: '客户无归属经纪人' };
- }
- customer.brokerId = brokerId;
- }
- const broker = readBrokerById(brokerId);
- if (!broker) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', `经纪人 ${brokerId} 不存在`);
- return { success: false, ignored: true, reason: '经纪人不存在' };
- }
- if (!guid) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', '没有可用的企微设备');
- return { success: false, ignored: true, reason: '没有可用的企微设备' };
- }
- let shouldSaveCustomer = false;
- if (!customer.externalUserId) {
- customer.externalUserId = externalUserId;
- shouldSaveCustomer = true;
- }
- if (customer.friendRequestStatus !== 'ACCEPTED') {
- customer.friendRequestStatus = 'ACCEPTED';
- shouldSaveCustomer = true;
- }
- if (shouldSaveCustomer) {
- writeCustomer(customer);
- }
- const checkResult = await qiweiCheckFriendStatus({
- guid,
- customers: customer.phone ? [{ phone: customer.phone, name: customer.name }] : [],
- externalUserIds: customer.phone ? [] : [externalUserId]
- });
- const confirmed = checkResult.status === 'ok' && checkResult.data && checkResult.data.details && checkResult.data.details.some(d => d.statusText === 'already_friend');
- if (!confirmed) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', '二次确认:客户尚未成为好友');
- return { success: false, ignored: true, reason: '客户尚未成为好友' };
- }
- if (hasActiveGroup(brokerId, customerId)) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', '客户已有活跃服务群,跳过建群');
- return { success: false, ignored: true, reason: '已有活跃服务群' };
- }
- const supportMemberIds = Array.isArray(customer.supportBrokerId)
- ? customer.supportBrokerId
- : customer.supportBrokerId ? [customer.supportBrokerId] : [];
- const resolvedSupportIds = [];
- for (const supportId of supportMemberIds) {
- if (!supportId || supportId === brokerId) continue;
- const supportBroker = readBrokerById(supportId);
- if (supportBroker && supportBroker.wecomUserId) {
- resolvedSupportIds.push(supportBroker.wecomUserId);
- }
- }
- const groupName = buildGroupName(customer, broker);
- const createResult = await qiweiAutoCreateGroup({
- guid,
- memberList: [externalUserId],
- supportMemberIds: resolvedSupportIds,
- groupName,
- isOuterRoom: 1
- });
- if (createResult.status !== 'ok' || !createResult.data || !createResult.data.roomId) {
- const reason = createResult.assistantMessage || '建群接口调用失败';
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'ERROR', reason);
- return { success: false, error: reason };
- }
- const roomId = createResult.data.roomId;
- const roomName = createResult.data.groupName || groupName;
- recordConfirmedGroup({
- roomId,
- roomName,
- brokerId,
- customerId,
- externalUserId,
- source: 'webhook-auto'
- });
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'AUTO_GROUP_CREATED', `自动建群成功: roomId=${roomId}, groupName=${roomName}`);
- console.log(`[Webhook] 自动建群成功: brokerId=${brokerId}, customerId=${customerId}, roomId=${roomId}`);
- return { success: true, roomId, roomName };
- }
- async function identifyServiceGroupMembers(event, raw, changedMemberListRaw) {
- let brokerId = null;
- // 1. 优先 senderId 匹配 broker.wecomUserId
- const senderId = raw.senderId ? String(raw.senderId) : '';
- if (senderId) {
- const broker = readBrokerByWecomUserId(senderId);
- if (broker) {
- brokerId = broker.brokerId;
- console.log(`[Webhook] GROUP_CREATED: 通过 senderId=${senderId} 识别经纪人: ${brokerId}`);
- }
- }
- // 2. 回退 event.guid -> device -> brokerId
- if (!brokerId && event.guid) {
- brokerId = resolveBrokerIdForGuid(event.guid);
- if (brokerId) {
- console.log(`[Webhook] GROUP_CREATED: 通过 guid=${event.guid} 回退识别经纪人: ${brokerId}`);
- }
- }
- // 3. 再回退群成员中匹配 broker.wecomUserId
- let memberIds = decodeChangedMemberList(changedMemberListRaw);
- if (!brokerId && memberIds.length) {
- for (const memberId of memberIds) {
- const broker = readBrokerByWecomUserId(memberId);
- if (broker) {
- brokerId = broker.brokerId;
- console.log(`[Webhook] GROUP_CREATED: 通过群成员 ${memberId} 识别经纪人: ${brokerId}`);
- break;
- }
- }
- }
- // 客户识别:排除经纪人后匹配 customer.externalUserId
- let customerId = null;
- let customerExternalUserId = null;
- const brokerWecomUserIds = new Set();
- if (senderId) brokerWecomUserIds.add(senderId);
- if (brokerId) {
- const broker = readBrokerById(brokerId);
- if (broker && broker.wecomUserId) brokerWecomUserIds.add(broker.wecomUserId);
- }
- const potentialCustomers = memberIds.filter(id => !brokerWecomUserIds.has(id));
- for (const memberId of potentialCustomers) {
- const customer = readCustomerByExternalUserId(memberId);
- if (customer) {
- customerId = customer.customerId;
- customerExternalUserId = memberId;
- console.log(`[Webhook] GROUP_CREATED: 通过成员 ${memberId} 识别客户: ${customerId}`);
- break;
- }
- }
- return { brokerId, customerId, customerExternalUserId };
- }
- async function handleGroupCreateWebhook(event, eventFilePath = null) {
- const raw = event.raw || {};
- const roomId = String(raw.fromRoomId || '');
- if (!roomId || roomId === '0') {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', '缺少 fromRoomId');
- return { success: false, ignored: true, reason: '缺少 fromRoomId' };
- }
- const changedMemberListRaw = raw.changedMemberList;
- const { brokerId, customerId, customerExternalUserId } = await identifyServiceGroupMembers(event, raw, changedMemberListRaw);
- const memberIds = decodeChangedMemberList(changedMemberListRaw);
- if (brokerId && customerId) {
- updateConfirmedMapping(roomId, {
- roomId,
- brokerId,
- customerId,
- externalUserId: customerExternalUserId,
- status: 'ACTIVE',
- reviewStatus: 'AUTO_CONFIRMED',
- memberCount: memberIds.length,
- seenAt: new Date().toISOString()
- });
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'PROCESSED', `自动捕获群创建: roomId=${roomId}, brokerId=${brokerId}, customerId=${customerId}`);
- console.log(`[Webhook] GROUP_CREATED: 新群 ${roomId} 已录入 (brokerId=${brokerId}, customerId=${customerId})`);
- return { success: true, brokerId, customerId };
- }
- if (brokerId && !customerId) {
- updateImportedMapping(roomId, {
- roomId,
- brokerId,
- customerId: 'orphan',
- status: 'IMPORTED',
- reviewStatus: 'IMPORTED',
- memberCount: memberIds.length,
- seenAt: new Date().toISOString()
- });
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'PROCESSED', `群 ${roomId} 已创建 orphan 记录 (brokerId=${brokerId})`);
- console.log(`[Webhook] GROUP_CREATED: 群 ${roomId} 已创建 orphan 记录(经纪人=${brokerId})`);
- return { success: true, orphan: true, brokerId };
- }
- updateImportedMapping(roomId, {
- roomId,
- brokerId: null,
- customerId: customerId || 'orphan',
- externalUserId: customerExternalUserId,
- status: 'IMPORTED',
- reviewStatus: 'IMPORTED',
- memberCount: memberIds.length,
- seenAt: new Date().toISOString()
- });
- const debugInfo = `senderId=${raw.senderId || 'N/A'}, guid=${event.guid || 'N/A'}`;
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'PROCESSED', `无法识别经纪人 (${debugInfo}),已导入待补充`);
- console.log(`[Webhook] GROUP_CREATED: 群 ${roomId} 无法识别经纪人,已导入 (${debugInfo})`);
- return { success: true, imported: true };
- }
- function resolveSenderType(senderId, msgType, customerId, customerExternalUserId, brokerWecomUserId) {
- if (!senderId) return 'UNKNOWN';
- if (customerExternalUserId && senderId === customerExternalUserId) return 'CUSTOMER';
- if (brokerWecomUserId && senderId === brokerWecomUserId) return 'BROKER';
- // 通过 customer 档案进一步判断
- if (customerId) {
- const customer = readCustomerById(customerId);
- if (customer) {
- if (customer.externalUserId && senderId === customer.externalUserId) return 'CUSTOMER';
- if (customer.supportBrokerId) {
- const supportIds = Array.isArray(customer.supportBrokerId) ? customer.supportBrokerId : [customer.supportBrokerId];
- for (const supportId of supportIds) {
- const supportBroker = readBrokerById(supportId);
- if (supportBroker && supportBroker.wecomUserId === senderId) return 'SUPPORT';
- }
- }
- }
- }
- const broker = readBrokerByWecomUserId(senderId);
- if (broker) return 'BROKER';
- return 'UNKNOWN';
- }
- function ensureRelatedContact(customerId, externalUserId, name) {
- if (!customerId || !externalUserId) return null;
- const filePath = path.join(customersDir(), `${customerId}-related-${externalUserId}.json`);
- if (fs.existsSync(filePath)) return filePath;
- const record = {
- customerId,
- externalUserId,
- name: name || '',
- createdAt: new Date().toISOString()
- };
- fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf8');
- return filePath;
- }
- function extractMessageContent(raw) {
- if (!raw) return '';
- if (typeof raw.content === 'string' && raw.content) return raw.content;
- if (typeof raw.msgContent === 'string' && raw.msgContent) return raw.msgContent;
- if (raw.msgData) {
- if (typeof raw.msgData.content === 'string') return raw.msgData.content;
- if (typeof raw.msgData.text === 'string') return raw.msgData.text;
- }
- return '';
- }
- async function processVoiceMessage(msgUniqueId, msgType, msgData) {
- const isVoice = msgType === 16 || msgType === 34 || String(msgType) === '16' || String(msgType) === '34';
- if (!isVoice) return {};
- const voiceUrl = msgData && (msgData.voiceUrl || msgData.url || msgData.cdnUrl);
- const base64Audio = msgData && (msgData.base64 || msgData.base64Data);
- if (!voiceUrl && !base64Audio) return {};
- try {
- const result = await qiweiTranscribeVoice({
- voiceUrl,
- base64Audio,
- transcribe: true
- });
- if (result.status === 'ok' && result.data) {
- return {
- voiceTranscript: result.data.transcript || result.data.text || '',
- voiceLocalPath: result.data.filePath || result.data.localPath || ''
- };
- }
- } catch (err) {
- console.warn(`[Webhook] 语音转写失败 ${msgUniqueId}:`, err.message);
- }
- return {};
- }
- function isKnownRoom(roomId) {
- const confirmed = readConfirmedMapping();
- if (confirmed[roomId]) return true;
- const imported = readImportedMapping();
- if (imported[roomId]) return true;
- return false;
- }
- async function storeGroupMessageFromWebhook(event, eventFilePath = null) {
- const raw = event.raw || {};
- const roomId = String(raw.fromRoomId || '');
- if (!roomId || roomId === '0') {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', '非群消息,跳过');
- return { success: false, ignored: true, reason: '非群消息' };
- }
- const msgUniqueId = String(raw.msgUniqueIdentifier || '');
- if (!msgUniqueId) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', '缺少 msgUniqueIdentifier');
- return { success: false, ignored: true, reason: '缺少 msgUniqueIdentifier' };
- }
- const msgType = Number(raw.msgType) || 0;
- const SKIP_MSG_TYPES = new Set([2001, 2005]);
- if (SKIP_MSG_TYPES.has(msgType)) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', `msgType=${msgType} 为通知类消息,跳过`);
- return { success: false, ignored: true, reason: '通知类消息' };
- }
- if (!isKnownRoom(roomId)) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', `roomId=${roomId} 不在群映射中,跳过`);
- return { success: false, ignored: true, reason: '不在群映射中' };
- }
- if (messageExists(roomId, msgUniqueId)) {
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'IGNORED', `消息 ${msgUniqueId} 已存在`);
- return { success: false, ignored: true, reason: '消息已存在' };
- }
- const content = extractMessageContent(raw);
- const timestamp = Number(raw.timestamp) || 0;
- const senderId = String(raw.senderId || '');
- // 解析群对应的客户/经纪人
- const confirmed = readConfirmedMapping();
- const imported = readImportedMapping();
- const groupMapping = confirmed[roomId] || imported[roomId] || {};
- const customerId = groupMapping.customerId;
- const customerExternalUserId = groupMapping.externalUserId;
- const brokerId = groupMapping.brokerId;
- const broker = brokerId ? readBrokerById(brokerId) : null;
- const brokerWecomUserId = broker ? broker.wecomUserId : null;
- const senderType = resolveSenderType(senderId, msgType, customerId, customerExternalUserId, brokerWecomUserId);
- if (senderType === 'UNKNOWN' && senderId) {
- ensureRelatedContact(customerId, senderId, String(raw.senderName || ''));
- }
- const voiceMeta = await processVoiceMessage(msgUniqueId, msgType, raw.msgData);
- const message = {
- msgId: msgUniqueId,
- seq: raw.seq || 0,
- senderId,
- senderName: String(raw.senderName || ''),
- senderType,
- msgType: String(msgType),
- content,
- timestamp: timestamp ? new Date(timestamp * 1000).toISOString() : new Date().toISOString(),
- isRevoked: raw.isRevoked ? true : false,
- ...voiceMeta,
- rawData: raw
- };
- appendWebhookMessage(roomId, message);
- // 兼容写入账号隔离的社群运营事实表;失败不回滚已经落盘的旧消息。
- const operationAccountKey = resolveDeviceGuid(event);
- if (operationAccountKey) {
- try {
- const operationStore = getGroupOperationsStore();
- operationStore.upsertGroup(operationAccountKey, {
- roomId,
- roomName: groupMapping.roomName || '',
- ownerId: brokerId || '',
- groupType: groupMapping.groupType || 'customer'
- }, 'webhook');
- operationStore.ingestMessages(operationAccountKey, roomId, [{
- ...message,
- messageId: msgUniqueId,
- senderRole: senderType === 'CUSTOMER' ? 'customer' : ['BROKER', 'SUPPORT'].includes(senderType) ? 'staff' : 'unknown',
- sentAt: message.timestamp
- }]);
- } catch (error) {
- console.warn(`[Webhook] 社群运营事实表写入失败: ${error.message}`);
- }
- } else {
- console.warn('[Webhook] 事件缺少设备 guid,已保留旧消息但跳过社群运营账号隔离入库');
- }
- // 更新 mapping 的 lastMsgAt / lastSyncSeq
- if (confirmed[roomId]) {
- confirmed[roomId].lastMsgAt = message.timestamp;
- confirmed[roomId].lastSyncSeq = Math.max(confirmed[roomId].lastSyncSeq || 0, Number(raw.seq) || 0);
- writeConfirmedMapping(confirmed);
- } else if (imported[roomId]) {
- imported[roomId].lastMsgAt = message.timestamp;
- imported[roomId].lastSyncSeq = Math.max(imported[roomId].lastSyncSeq || 0, Number(raw.seq) || 0);
- writeImportedMapping(imported);
- }
- // 触发客户画像更新
- if (customerExternalUserId && senderType === 'CUSTOMER') {
- enqueuePortraitUpdate(customerExternalUserId, 'WEBHOOK');
- }
- if (eventFilePath) updateWebhookEventStatus(eventFilePath, 'PROCESSED', `群消息已入库: ${content.substring(0, 80)}`);
- console.log(`[Webhook] 群消息已入库: roomId=${roomId}, seq=${raw.seq}`);
- return { success: true };
- }
- async function processWebhookEvents(envelope) {
- const events = parseWebhookEnvelope(envelope);
- if (!events.length) {
- console.log('[Webhook] 回调中无有效事件');
- return { processed: 0, ignored: 0, errors: 0 };
- }
- const result = { processed: 0, ignored: 0, errors: 0 };
- for (const event of events) {
- if (isDuplicateEvent(event.eventId)) {
- console.log(`[Webhook] 重复事件已跳过: ${event.eventId}`);
- result.ignored++;
- continue;
- }
- const eventFilePath = saveWebhookEventStructured(event, envelope.source || 'callback', envelope.__rawBody);
- markEventProcessed(event.eventId);
- try {
- const config = readWebhookConfig();
- const autoCreateGroupEnabled = config.autoCreateGroup !== false;
- if (event.parsedType === ParsedWebhookEvent.NEW_MESSAGE) {
- const msgResult = await storeGroupMessageFromWebhook(event, eventFilePath);
- if (msgResult.success) result.processed++;
- else if (msgResult.ignored) result.ignored++;
- else result.errors++;
- continue;
- }
- if (event.parsedType === ParsedWebhookEvent.GROUP_CREATED) {
- const groupResult = await handleGroupCreateWebhook(event, eventFilePath);
- if (groupResult.success) result.processed++;
- else if (groupResult.ignored) result.ignored++;
- else result.errors++;
- continue;
- }
- if (autoCreateGroupEnabled && (
- event.parsedType === ParsedWebhookEvent.CONTACT_ADDED_OR_CHANGED ||
- event.parsedType === ParsedWebhookEvent.FRIEND_REQUEST_RECEIVED
- )) {
- const autoResult = await triggerAutoCreateGroup(event, eventFilePath);
- if (autoResult.success) result.processed++;
- else if (autoResult.ignored) result.ignored++;
- else result.errors++;
- continue;
- }
- updateWebhookEventStatus(eventFilePath, 'IGNORED', `事件类型 ${event.parsedType} 不需要自动处理`);
- result.ignored++;
- } catch (err) {
- console.error('[Webhook] 事件处理异常:', err && err.message ? err.message : err);
- updateWebhookEventStatus(eventFilePath, 'ERROR', String(err && err.message ? err.message : err));
- result.errors++;
- }
- }
- return result;
- }
- module.exports = {
- processWebhookEvents,
- triggerAutoCreateGroup,
- findNewlyAddedContact,
- findNewlyAddedContactAcrossDevices,
- resolveDeviceGuid,
- handleGroupCreateWebhook,
- identifyServiceGroupMembers,
- storeGroupMessageFromWebhook,
- resolveSenderType,
- ensureRelatedContact,
- readConfirmedMapping,
- writeConfirmedMapping,
- recordConfirmedGroup,
- hasActiveGroup
- };
|