import-tools.js 7.45 KB
/**
 * 多源数据导入 MCP 工具集 — Phase 4
 *
 * 工具:
 *   import_linkedin_export  — 解析 LinkedIn Connections.csv 批量导入人脉
 *   import_x_timeline       — 解析 X (Twitter) tweets.js 导入动态
 *   import_meeting_notes    — 将会议纪要导入图谱
 */

const { getGraphStore } = require('../../../db/GraphStore');
const { LinkedInReader } = require('../../../services/linkedin/linkedin-reader');
const { XReader } = require('../../../services/x/x-reader');
const { MeetingImporter } = require('../../../services/meeting/meeting-importer');

// 解析 name_or_id(复用 index.js 逻辑的本地版本)
function resolveNode(nameOrId, type) {
    const g = getGraphStore();
    let node = g.getNode(nameOrId);
    if (!node) {
        const results = type
            ? g.findByName(nameOrId, type)
            : g.search(nameOrId);
        node = results[0] || null;
    }
    return node;
}

function createImportTools() {
    return [
        // ── 4.1 LinkedIn 批量导入 ────────────────────────────────────
        {
            name: 'import_linkedin_export',
            description: '解析 LinkedIn 导出的 Connections.csv 文件,将联系人批量导入知识图谱。filePath 为文件绝对路径。',
            parameters: {
                type: 'object',
                required: ['filePath'],
                properties: {
                    filePath: {
                        type: 'string',
                        description: 'LinkedIn Connections.csv 文件的绝对路径',
                    },
                },
            },

            async execute(params) {
                try {
                    const reader = new LinkedInReader(params.filePath);
                    const contacts = reader.parse();
                    const g = getGraphStore();

                    let created = 0;
                    let updated = 0;

                    for (const c of contacts) {
                        const fullName = [c.firstName, c.lastName].filter(Boolean).join(' ');
                        if (!fullName) continue;

                        const existing = g.findByName(fullName, 'Person');
                        const id = existing[0]?.id;

                        g.createNode('Person', {
                            ...(id ? { id } : {}),
                            name: fullName,
                            email: c.email || undefined,
                            company: c.company || undefined,
                            role: c.position || undefined,
                            linkedin: c.connectedOn ? `connected:${c.connectedOn}` : undefined,
                            source: 'linkedin',
                        });

                        if (id) updated++; else created++;
                    }

                    return `✅ LinkedIn 导入完成:新建 ${created} 人,更新 ${updated} 人,共处理 ${contacts.length} 条记录。`;
                } catch (err) {
                    return `❌ LinkedIn 导入失败:${err.message}`;
                }
            },
        },

        // ── 4.2 X (Twitter) 动态导入 ──────────────────────────────────
        {
            name: 'import_x_timeline',
            description: '解析 X (Twitter) 导出的 tweets.js 文件,将动态作为 Event:post 节点导入图谱,并关联到指定联系人。',
            parameters: {
                type: 'object',
                required: ['filePath', 'person_name_or_id'],
                properties: {
                    filePath: {
                        type: 'string',
                        description: 'tweets.js 文件的绝对路径',
                    },
                    person_name_or_id: {
                        type: 'string',
                        description: '关联到哪个联系人(姓名或节点 id)',
                    },
                },
            },

            async execute(params) {
                try {
                    const person = resolveNode(params.person_name_or_id, 'Person');
                    if (!person) {
                        return `❌ 未找到联系人:${params.person_name_or_id}`;
                    }

                    const reader = new XReader(params.filePath);
                    const tweets = reader.parseTweets();
                    const g = getGraphStore();

                    let imported = 0;

                    for (const tw of tweets) {
                        const eventNode = g.createNode('Event', {
                            type: 'post',
                            summary: tw.text.slice(0, 140),
                            rawText: tw.text,
                            date: tw.createdAt
                                ? new Date(tw.createdAt).toISOString().slice(0, 10)
                                : undefined,
                            tweetId: tw.id,
                            hashtags: tw.hashtags,
                            source: 'x_twitter',
                        });

                        g.createEdge(person.id, eventNode.id, 'ASSOCIATED');
                        imported++;
                    }

                    return `✅ X 动态导入完成:共导入 ${imported} 条推文,已关联到「${person.name}」。`;
                } catch (err) {
                    return `❌ X 动态导入失败:${err.message}`;
                }
            },
        },

        // ── 4.3 会议纪要导入 ──────────────────────────────────────────
        {
            name: 'import_meeting_notes',
            description: '将会议纪要文本导入图谱,创建 Event:meeting 节点并关联参与者。',
            parameters: {
                type: 'object',
                required: ['summary'],
                properties: {
                    title:        { type: 'string', description: '会议标题' },
                    date:         { type: 'string', description: '会议日期(YYYY-MM-DD)' },
                    summary:      { type: 'string', description: '会议摘要(必填)' },
                    rawText:      { type: 'string', description: '完整会议纪要原文' },
                    participants: {
                        type: 'array',
                        items: { type: 'string' },
                        description: '参与者姓名或节点 id 列表',
                    },
                },
            },

            async execute(params) {
                try {
                    const importer = new MeetingImporter();
                    const result = importer.importMeetingNotes(params);

                    const lines = [
                        `✅ 会议纪要已导入,事件 id:${result.eventId}`,
                        `关联参与者(${result.linkedParticipants.length}):${result.linkedParticipants.join('、') || '(无)'}`,
                    ];
                    if (result.skippedParticipants.length > 0) {
                        lines.push(`⚠️ 未找到以下参与者(已跳过):${result.skippedParticipants.join('、')}`);
                    }
                    return lines.join('\n');
                } catch (err) {
                    return `❌ 会议纪要导入失败:${err.message}`;
                }
            },
        },
    ];
}

module.exports = { createImportTools };