wechat-importer.js 4.8 KB
/**
 * wechat-importer.js — 将微信数据写入 GraphStore 知识图谱
 *
 * 依赖 WechatReader(只读数据库)和 GraphStore(图谱写入)。
 */

const { WechatReader } = require('./wechat-reader');
const { getGraphStore } = require('../../db/GraphStore');

class WechatImporter {
    constructor() {
        this.reader = new WechatReader();
    }

    /**
     * 批量导入微信联系人到知识图谱
     * - name = remark || nickName(优先使用备注名)
     * - wechatId = username
     * - analysisStatus = 'partial'
     * - 去重:按 wechatId 查找已有节点,执行 upsert
     *
     * @param {number} limit - 导入数量上限,默认 5000
     * @returns {{ imported: number, skipped: number, total: number }}
     */
    importContacts(limit = 5000) {
        const contacts = this.reader.getContacts(limit);
        const g = getGraphStore();

        let imported = 0;
        let skipped = 0;
        const total = contacts.length;

        for (const c of contacts) {
            try {
                const name = c.remark || c.nickName;
                if (!name) {
                    skipped++;
                    continue;
                }

                // 按 wechatId 查找已有节点(避免重复)
                const existing = g.search(c.username).find(
                    n => n._type === 'Person' && n.wechatId === c.username
                );

                const nodeData = {
                    ...(existing ? { id: existing.id } : {}),
                    name,
                    nickName:       c.nickName       || undefined,
                    remark:         c.remark         || undefined,
                    wechatId:       c.username,
                    alias:          c.alias          || undefined,
                    notes:          c.description    || undefined,
                    quanPin:        c.quanPin        || undefined,  // 全拼排序键
                    pinYinInitial:  c.pinYinInitial  || undefined,  // 拼音首字母
                    analysisStatus: 'partial',
                };

                // 清理 undefined 字段
                Object.keys(nodeData).forEach(k => {
                    if (nodeData[k] === undefined) delete nodeData[k];
                });

                g.createNode('Person', nodeData);
                imported++;
            } catch (e) {
                console.warn(`[WechatImporter] 导入联系人失败 ${c.username}:`, e.message);
                skipped++;
            }
        }

        return { imported, skipped, total };
    }

    /**
     * 导入指定联系人的全部聊天记录,upsert 到 Event 节点(按 wechatId 去重,重复导入只更新)
     *
     * @param {string} username - 微信 ID(wxid_xxx)
     * @param {number} limit    - 消息条数上限,默认 99999(全部)
     * @returns {{ eventId: string|null, messages: number }}
     */
    importChatHistory(username, limit = 99999) {
        const messages = this.reader.getMessages(username, limit);
        if (messages.length === 0) {
            return { eventId: null, messages: 0 };
        }

        const g = getGraphStore();

        // 最新 5 条作为摘要
        const summaryLines = messages.slice(-5).map(m => {
            const sender = m.senderUsername || username;
            return `[${sender}] ${m.content.slice(0, 80)}`;
        });

        // 最早和最新时间
        const firstTime = messages[0]?.createTime || 0;
        const lastTime  = messages[messages.length - 1]?.createTime || 0;

        function tsToDate(ts) {
            if (!ts) return undefined;
            return new Date(ts * 1000).toISOString().slice(0, 10);
        }

        // 按 wechatId 查找已有 Event,实现 upsert(避免重复导入)
        const existing = g.search(username).find(
            n => n._type === 'Event' && n.wechatId === username && n.type === 'chat'
        );

        const event = g.createNode('Event', {
            ...(existing ? { id: existing.id } : {}),
            type:      'chat',
            summary:   summaryLines.join('\n'),
            date:      tsToDate(lastTime),
            rawText:   messages.map(m => `[${m.senderUsername || username}] ${m.content}`).join('\n'),
            wechatId:  username,
            msgCount:  messages.length,
            startDate: tsToDate(firstTime),
        });

        // 关联 Person(createEdge 内部已去重)
        const personNode = g.search(username).find(
            n => n._type === 'Person' && n.wechatId === username
        );
        if (personNode) {
            g.createEdge(personNode.id, event.id, 'PARTICIPATED', {
                channel: 'wechat',
                msgCount: messages.length,
            });
        }

        return { eventId: event.id, messages: messages.length };
    }
}

module.exports = { WechatImporter };