analysis-tools.js 12.7 KB
/**
 * AI 深度分析 MCP 工具集 — Phase 5
 *
 * 工具:
 *   detect_communities        — 标签传播社区发现
 *   get_followup_reminders    — 超 N 天未互动提醒
 *   activate_contact_profile  — 全量聚合分析,更新 analysisStatus
 *   get_conversation_insights — 对话数据分析(话题分布、互动热度)
 */

const { getGraphStore } = require('../../../db/GraphStore');

/** 解析 name_or_id,先精确查 id 再按名称模糊查 */
function resolvePerson(nameOrId) {
    const g = getGraphStore();
    let node = g.getNode(nameOrId);
    if (!node || node._type !== 'Person') {
        const results = g.findByName(nameOrId, 'Person');
        node = results[0] || null;
    }
    return node;
}

function createAnalysisTools() {
    return [
        // ── 5.1 社区发现 ─────────────────────────────────────────────
        {
            name: 'detect_communities',
            description: '运行社区发现算法(标签传播),识别人脉中的圈子群体,返回分组结果。',
            parameters: {
                type: 'object',
                properties: {
                    max_iterations: {
                        type: 'number',
                        description: '最大迭代次数,默认 10',
                    },
                },
            },

            async execute(params = {}) {
                try {
                    const g = getGraphStore();
                    const result = g.detectCommunities(params.max_iterations || 10);

                    if (result.communities.length === 0) {
                        return '图谱中暂无 Person 节点,无法进行社区发现。';
                    }

                    const lines = [
                        `🔍 社区发现完成,共识别 ${result.communities.length} 个圈子:\n`,
                    ];
                    for (const c of result.communities) {
                        const names = c.nodes.map(n => n.name).join('、');
                        lines.push(`圈子 ${c.id}(${c.nodes.length} 人):${names}`);
                    }
                    return lines.join('\n');
                } catch (err) {
                    return `❌ 社区发现失败:${err.message}`;
                }
            },
        },

        // ── 5.2 跟进提醒 ─────────────────────────────────────────────
        {
            name: 'get_followup_reminders',
            description: '获取超过 N 天未互动的联系人列表,生成跟进提醒建议。',
            parameters: {
                type: 'object',
                properties: {
                    days_since: {
                        type: 'number',
                        description: '超过几天未互动,默认 30',
                    },
                    limit: {
                        type: 'number',
                        description: '最多返回多少人,默认 20',
                    },
                },
            },

            async execute(params = {}) {
                try {
                    const g = getGraphStore();
                    const days = params.days_since || 30;
                    const candidates = g.getFollowUpCandidates(days, params.limit || 20);

                    if (candidates.length === 0) {
                        return `✅ 近 ${days} 天内所有重要联系人均已互动,暂无跟进提醒。`;
                    }

                    const lines = [
                        `⏰ 超过 ${days} 天未互动的联系人(${candidates.length} 人):\n`,
                    ];
                    for (const c of candidates) {
                        const lastStr = c.lastInteraction || '从未互动';
                        lines.push(`• ${c.person.name}(${c.daysSince} 天前:${lastStr})`);
                        lines.push(`  → ${c.suggestion}`);
                    }
                    return lines.join('\n');
                } catch (err) {
                    return `❌ 跟进提醒获取失败:${err.message}`;
                }
            },
        },

        // ── 5.3 激活联系人档案 ───────────────────────────────────────
        {
            name: 'activate_contact_profile',
            description: '对指定联系人执行全量聚合分析,更新 analysisStatus 为 complete,返回分析摘要。',
            parameters: {
                type: 'object',
                required: ['person_name_or_id'],
                properties: {
                    person_name_or_id: {
                        type: 'string',
                        description: '联系人姓名或节点 id',
                    },
                },
            },

            async execute(params) {
                try {
                    const g = getGraphStore();
                    const person = resolvePerson(params.person_name_or_id);
                    if (!person) {
                        return `❌ 未找到联系人:${params.person_name_or_id}`;
                    }

                    // 获取所有边和关联节点
                    const { outgoing, incoming } = g.getEdges(person.id);
                    const allEdges = [...outgoing, ...incoming];

                    // 统计互动次数(Event 节点)
                    let interactionCount = 0;
                    const relatedOrgs = new Set();
                    const recentEvents = [];
                    const now = Date.now();
                    let latestEventDate = null;

                    for (const edge of allEdges) {
                        const neighborId = edge.from_id === person.id ? edge.to_id : edge.from_id;
                        const neighbor = g.getNode(neighborId);
                        if (!neighbor) continue;

                        if (neighbor._type === 'Event') {
                            interactionCount++;
                            const dateStr = neighbor.date || neighbor.createdAt || null;
                            if (dateStr) {
                                recentEvents.push({ name: neighbor.name || neighbor.summary, date: dateStr });
                                if (!latestEventDate || dateStr > latestEventDate) {
                                    latestEventDate = dateStr;
                                }
                            }
                        } else if (neighbor._type === 'Organization') {
                            relatedOrgs.add(neighbor.name || neighborId);
                        }
                    }

                    // 最近 3 个事件
                    recentEvents.sort((a, b) => (b.date > a.date ? 1 : -1));
                    const topEvents = recentEvents.slice(0, 3);

                    // 更新 analysisStatus = 'complete'
                    g.setAnalysisStatus(person.id, 'complete');

                    const daysSinceLast = latestEventDate
                        ? Math.floor((now - new Date(latestEventDate).getTime()) / (1000 * 60 * 60 * 24))
                        : null;

                    const summary = [
                        `📊 联系人档案激活完成:${person.name}`,
                        `─────────────────────────────`,
                        `互动次数:${interactionCount} 次`,
                        `关联组织:${relatedOrgs.size > 0 ? [...relatedOrgs].join('、') : '(无)'}`,
                        `最近互动:${latestEventDate || '(无记录)'}${daysSinceLast !== null ? `(${daysSinceLast} 天前)` : ''}`,
                    ];

                    if (topEvents.length > 0) {
                        summary.push('\n最近事件:');
                        topEvents.forEach(e => summary.push(`  • ${e.date} — ${e.name || '(无标题)'}`));
                    }

                    summary.push('\nanalysisStatus 已更新为 complete。');
                    return summary.join('\n');
                } catch (err) {
                    return `❌ 档案激活失败:${err.message}`;
                }
            },
        },

        // ── 5.4 对话洞察 ─────────────────────────────────────────────
        {
            name: 'get_conversation_insights',
            description: '分析联系人的对话数据,返回话题分布、互动热度、待跟进事项。',
            parameters: {
                type: 'object',
                required: ['person_name_or_id'],
                properties: {
                    person_name_or_id: {
                        type: 'string',
                        description: '联系人姓名或节点 id',
                    },
                },
            },

            async execute(params) {
                try {
                    const g = getGraphStore();
                    const person = resolvePerson(params.person_name_or_id);
                    if (!person) {
                        return `❌ 未找到联系人:${params.person_name_or_id}`;
                    }

                    const { outgoing, incoming } = g.getEdges(person.id);
                    const allEdges = [...outgoing, ...incoming];

                    const typeDist = {};    // Event type 分布
                    const monthDist = {};   // 按月分布(互动热度)
                    const hashtags = {};    // 话题标签(来自 X 动态)

                    let totalEvents = 0;

                    for (const edge of allEdges) {
                        const neighborId = edge.from_id === person.id ? edge.to_id : edge.from_id;
                        const neighbor = g.getNode(neighborId);
                        if (!neighbor || neighbor._type !== 'Event') continue;

                        totalEvents++;

                        // 类型分布
                        const t = neighbor.type || 'other';
                        typeDist[t] = (typeDist[t] || 0) + 1;

                        // 月度热度
                        const dateStr = neighbor.date || neighbor.createdAt;
                        if (dateStr) {
                            const month = String(dateStr).slice(0, 7); // YYYY-MM
                            monthDist[month] = (monthDist[month] || 0) + 1;
                        }

                        // 话题标签
                        if (Array.isArray(neighbor.hashtags)) {
                            for (const tag of neighbor.hashtags) {
                                if (tag) hashtags[tag] = (hashtags[tag] || 0) + 1;
                            }
                        }
                    }

                    // 按月排序,取最近 6 个月
                    const sortedMonths = Object.entries(monthDist)
                        .sort((a, b) => b[0].localeCompare(a[0]))
                        .slice(0, 6);

                    // 按频率排序 hashtags
                    const topHashtags = Object.entries(hashtags)
                        .sort((a, b) => b[1] - a[1])
                        .slice(0, 10);

                    const insights = {
                        personId: person.id,
                        personName: person.name,
                        totalEvents,
                        typeDistribution: typeDist,
                        recentActivity: Object.fromEntries(sortedMonths),
                        topHashtags: Object.fromEntries(topHashtags),
                        analysisStatus: person._analysisStatus,
                    };

                    const lines = [
                        `📈 联系人洞察:${person.name}`,
                        `─────────────────────────────`,
                        `关联事件总数:${totalEvents}`,
                        `事件类型分布:${JSON.stringify(typeDist)}`,
                        `近期互动热度(月):${JSON.stringify(Object.fromEntries(sortedMonths))}`,
                    ];

                    if (topHashtags.length > 0) {
                        lines.push(`常见话题标签:${topHashtags.map(([k, v]) => `${k}(${v})`).join('、')}`);
                    }

                    lines.push('\n---\n原始数据:\n```json\n' + JSON.stringify(insights, null, 2) + '\n```');
                    return lines.join('\n');
                } catch (err) {
                    return `❌ 对话洞察分析失败:${err.message}`;
                }
            },
        },
    ];
}

module.exports = { createAnalysisTools };