/** * import.js — 多源数据导入 REST 路由 * * POST /api/import/linkedin body: { filePath } * POST /api/import/x body: { filePath, personNameOrId } * POST /api/import/meeting body: { title?, date?, summary, rawText?, participants? } */ const express = require('express'); const router = express.Router(); const { LinkedInReader } = require('../services/linkedin/linkedin-reader'); const { XReader } = require('../services/x/x-reader'); const { MeetingImporter } = require('../services/meeting/meeting-importer'); const { getGraphStore } = require('../db/GraphStore'); // ── POST /api/import/linkedin ──────────────────────────────────────── router.post('/linkedin', (req, res) => { try { const { filePath } = req.body; if (!filePath) { return res.status(400).json({ error: 'filePath 为必填字段' }); } const reader = new LinkedInReader(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++; } res.json({ success: true, total: contacts.length, created, updated, message: `LinkedIn 导入完成:新建 ${created} 人,更新 ${updated} 人`, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── POST /api/import/x ─────────────────────────────────────────────── router.post('/x', (req, res) => { try { const { filePath, personNameOrId } = req.body; if (!filePath || !personNameOrId) { return res.status(400).json({ error: 'filePath 和 personNameOrId 为必填字段' }); } const g = getGraphStore(); // 解析联系人 let person = g.getNode(personNameOrId); if (!person) { const results = g.findByName(personNameOrId, 'Person'); person = results[0] || null; } if (!person) { return res.status(404).json({ error: `未找到联系人:${personNameOrId}` }); } const reader = new XReader(filePath); const tweets = reader.parseTweets(); 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++; } res.json({ success: true, imported, personId: person.id, personName: person.name, message: `X 动态导入完成:共导入 ${imported} 条推文`, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── POST /api/import/meeting ───────────────────────────────────────── router.post('/meeting', (req, res) => { try { const { title, date, summary, rawText, participants } = req.body; if (!summary) { return res.status(400).json({ error: 'summary 为必填字段' }); } const importer = new MeetingImporter(); const result = importer.importMeetingNotes({ title, date, summary, rawText, participants }); res.json({ success: true, eventId: result.eventId, linkedParticipants: result.linkedParticipants, skippedParticipants: result.skippedParticipants, message: `会议纪要已导入,关联 ${result.linkedParticipants.length} 位参与者`, }); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;