/** * photo.js — 多模态资产关联 REST 路由 * * POST /api/photo/link body: { imagePath, personNameOrId } * GET /api/photo/person/:id → 该联系人所有照片 * POST /api/photo/scan-dir body: { dirPath } → 扫描目录中所有图片的人脸 */ const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); const { PhotoService } = require('../services/photo/photo-service'); const { getGraphStore } = require('../db/GraphStore'); // 支持的图片扩展名 const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.tif']); // ── POST /api/photo/link ───────────────────────────────────────────── router.post('/link', async (req, res) => { try { const { imagePath, personNameOrId } = req.body; if (!imagePath || !personNameOrId) { return res.status(400).json({ error: 'imagePath 和 personNameOrId 为必填字段' }); } if (!fs.existsSync(imagePath)) { return res.status(400).json({ error: `文件不存在:${imagePath}` }); } const svc = new PhotoService(imagePath); const result = await svc.linkPhotoToPerson(imagePath, personNameOrId); res.json({ success: true, assetId: result.assetId, personId: result.personId, personName: result.personName, exif: result.exif, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── GET /api/photo/person/:id ──────────────────────────────────────── router.get('/person/:id', (req, res) => { try { const personId = req.params.id; const g = getGraphStore(); // 解析联系人 let person = g.getNode(personId); if (!person || person._type !== 'Person') { const results = g.findByName(personId, 'Person'); person = results[0] || null; } if (!person) { return res.status(404).json({ error: `未找到联系人:${personId}` }); } const { outgoing } = g.getEdges(person.id); const photos = []; for (const edge of outgoing) { if (edge.relation !== 'CONTAINS' && edge.relation !== 'ASSOCIATED') continue; const asset = g.getNode(edge.to_id); if (asset && asset._type === 'Asset' && asset.type === 'photo') { photos.push(asset); } } res.json({ success: true, personId: person.id, personName: person.name, count: photos.length, photos, }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ── POST /api/photo/scan-dir ───────────────────────────────────────── router.post('/scan-dir', async (req, res) => { try { const { dirPath } = req.body; if (!dirPath) { return res.status(400).json({ error: 'dirPath 为必填字段' }); } if (!fs.existsSync(dirPath)) { return res.status(400).json({ error: `目录不存在:${dirPath}` }); } const stat = fs.statSync(dirPath); if (!stat.isDirectory()) { return res.status(400).json({ error: `${dirPath} 不是目录` }); } // 枚举目录中的图片文件 const files = fs.readdirSync(dirPath) .filter(f => IMAGE_EXTS.has(path.extname(f).toLowerCase())) .map(f => path.join(dirPath, f)); const results = []; for (const imgPath of files) { try { const svc = new PhotoService(imgPath); const faces = await svc.detectFaces(imgPath); results.push({ file: imgPath, fileName: path.basename(imgPath), faceCount: Array.isArray(faces) ? faces.length : 0, faces: Array.isArray(faces) ? faces : [], }); } catch (e) { results.push({ file: imgPath, fileName: path.basename(imgPath), error: e.message, }); } } res.json({ success: true, dirPath, totalFiles: files.length, results, }); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;