diff --git a/server/routes/company.js b/server/routes/company.js index 5eb6f95..3385d8a 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -339,4 +339,56 @@ router.delete('/:companyId/schedule/slots/:slotId', (req, res) => { res.json({ success: true }); }); +// ═══════════════════════════════════════ +// DataSource (运营数据接入) +// ═══════════════════════════════════════ + +const { DataSourceService } = require('../services/datasource-service'); +const dataSourceService = new DataSourceService(); + +// GET /api/company/:companyId/data/metrics — 获取运营指标 +router.get('/:companyId/data/metrics', async (req, res) => { + try { + const { source = 'mock', startDate, endDate } = req.query; + const sd = startDate || new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10); + const ed = endDate || new Date().toISOString().slice(0, 10); + const data = await dataSourceService.fetchMetrics(source, { startDate: sd, endDate: ed }); + res.json(data); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// GET /api/company/:companyId/data/trending — 获取热点趋势 +router.get('/:companyId/data/trending', async (req, res) => { + try { + const { source = 'mock', platform = 'weibo', count = '10' } = req.query; + const data = await dataSourceService.fetchTrending(source, { platform, count: parseInt(count, 10) }); + res.json(data); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + +// GET /api/company/:companyId/data/attribution — 归因分析 +router.get('/:companyId/data/attribution', async (req, res) => { + try { + const { source = 'mock' } = req.query; + const now = new Date(); + const currentEnd = now.toISOString().slice(0, 10); + const currentStart = new Date(now.getTime() - 7 * 86400000).toISOString().slice(0, 10); + const previousEnd = new Date(now.getTime() - 7 * 86400000).toISOString().slice(0, 10); + const previousStart = new Date(now.getTime() - 14 * 86400000).toISOString().slice(0, 10); + + const analysis = await dataSourceService.analyzeAttribution( + source, + { startDate: currentStart, endDate: currentEnd }, + { startDate: previousStart, endDate: previousEnd } + ); + res.json(analysis); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); + module.exports = router; diff --git a/server/services/datasource-service.js b/server/services/datasource-service.js new file mode 100644 index 0000000..9850adc --- /dev/null +++ b/server/services/datasource-service.js @@ -0,0 +1,240 @@ +/** + * DataSourceService — 运营数据接入抽象层 + * + * 提供标准化的数据源接口,支持多平台运营数据接入。 + * 每个数据源实现 DataSourceAdapter 接口: + * - fetchMetrics(dateRange) → 获取核心指标 + * - fetchContent(contentId) → 获取内容详情 + * - fetchTrending(params) → 获取热点/趋势 + * + * 已实现的 Adapter: + * - MockAdapter: 本地模拟数据 (开发调试用) + * - FileAdapter: 从本地文件读取数据 (CSV/JSON) + * + * 可扩展: + * - WechatAdapter: 微信公众号后台数据 + * - DouyinAdapter: 抖音数据 + * - CustomAPIAdapter: 自定义 REST API + */ + +const fs = require('fs'); +const path = require('path'); + +const DATA_DIR = path.resolve(__dirname, '../../data'); + +// ════════════════════════════════════════ +// DataSource Adapter 基类 +// ════════════════════════════════════════ + +class DataSourceAdapter { + constructor(config = {}) { + this.config = config; + this.name = 'base'; + } + + /** @returns {Promise} */ + async fetchMetrics(dateRange) { return null; } + /** @returns {Promise} */ + async fetchContent(contentId) { return null; } + /** @returns {Promise} */ + async fetchTrending(params) { return null; } +} + +// ════════════════════════════════════════ +// Mock Adapter — 模拟数据 (开发调试用) +// ════════════════════════════════════════ + +class MockAdapter extends DataSourceAdapter { + constructor(config) { + super(config); + this.name = 'mock'; + } + + async fetchMetrics({ startDate, endDate }) { + const days = []; + const start = new Date(startDate); + const end = new Date(endDate); + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + days.push({ + date: d.toISOString().slice(0, 10), + views: Math.floor(Math.random() * 10000 + 500), + likes: Math.floor(Math.random() * 500 + 20), + shares: Math.floor(Math.random() * 100 + 5), + comments: Math.floor(Math.random() * 200 + 10), + followers: Math.floor(Math.random() * 50 + 1), + revenue: Math.round(Math.random() * 5000) / 100, + }); + } + return { + source: 'mock', + period: { startDate, endDate }, + dailyMetrics: days, + summary: { + totalViews: days.reduce((s, d) => s + d.views, 0), + totalLikes: days.reduce((s, d) => s + d.likes, 0), + avgDailyViews: Math.round(days.reduce((s, d) => s + d.views, 0) / days.length), + }, + }; + } + + async fetchTrending({ platform = 'weibo', count = 10 }) { + const topics = [ + 'AI 革命新进展', '年轻人消费降级', '职场新趋势', '健康科技创新', + '城市生活方式', '教育改革方向', '新能源汽车竞争', '社交媒体新玩法', + '房市最新政策', '投资理财策略', '远程办公生态', '文化IP跨界', + ]; + return { + source: 'mock', + platform, + timestamp: new Date().toISOString(), + topics: topics.slice(0, count).map((t, i) => ({ + rank: i + 1, + title: t, + hotness: Math.floor(Math.random() * 1000000 + 100000), + category: ['tech', 'lifestyle', 'finance', 'culture'][i % 4], + })), + }; + } +} + +// ════════════════════════════════════════ +// File Adapter — 从本地文件读取数据 +// ════════════════════════════════════════ + +class FileAdapter extends DataSourceAdapter { + constructor(config) { + super(config); + this.name = 'file'; + this.dataDir = config.dataDir || path.join(DATA_DIR, '_datasources'); + } + + async fetchMetrics({ startDate, endDate }) { + const metricsPath = path.join(this.dataDir, 'metrics.json'); + if (!fs.existsSync(metricsPath)) return { source: 'file', error: 'metrics.json not found' }; + const allData = JSON.parse(fs.readFileSync(metricsPath, 'utf-8')); + + // 按日期过滤 + const filtered = (allData.dailyMetrics || []) + .filter(d => d.date >= startDate && d.date <= endDate); + + return { + source: 'file', + period: { startDate, endDate }, + dailyMetrics: filtered, + summary: { + totalViews: filtered.reduce((s, d) => s + (d.views || 0), 0), + totalLikes: filtered.reduce((s, d) => s + (d.likes || 0), 0), + avgDailyViews: filtered.length > 0 + ? Math.round(filtered.reduce((s, d) => s + (d.views || 0), 0) / filtered.length) + : 0, + }, + }; + } + + async fetchTrending({ platform = 'local', count = 10 }) { + const trendingPath = path.join(this.dataDir, `trending_${platform}.json`); + if (!fs.existsSync(trendingPath)) { + // fallback to generic trending.json + const genericPath = path.join(this.dataDir, 'trending.json'); + if (!fs.existsSync(genericPath)) return { source: 'file', error: 'trending data not found' }; + return JSON.parse(fs.readFileSync(genericPath, 'utf-8')); + } + return JSON.parse(fs.readFileSync(trendingPath, 'utf-8')); + } +} + +// ════════════════════════════════════════ +// DataSourceService — 统一管理入口 +// ════════════════════════════════════════ + +class DataSourceService { + constructor() { + this.adapters = new Map(); + // 默认注册 Mock 适配器 + this.register('mock', new MockAdapter()); + this.register('file', new FileAdapter()); + } + + /** + * 注册数据源适配器 + */ + register(name, adapter) { + this.adapters.set(name, adapter); + } + + /** + * 获取数据源 + */ + getAdapter(name) { + return this.adapters.get(name); + } + + /** + * 获取指标数据 (自动选择适配器) + * @param {string} source - 'mock' | 'file' | 'wechat' | ... + * @param {object} params - { startDate, endDate } + */ + async fetchMetrics(source, params) { + const adapter = this.adapters.get(source); + if (!adapter) throw new Error(`DataSource adapter "${source}" not found`); + return adapter.fetchMetrics(params); + } + + /** + * 获取热点趋势 + */ + async fetchTrending(source, params) { + const adapter = this.adapters.get(source); + if (!adapter) throw new Error(`DataSource adapter "${source}" not found`); + return adapter.fetchTrending(params); + } + + /** + * 归因分析 — 对比两个时间段的指标变化 + * + * @param {string} source + * @param {object} current - { startDate, endDate } 当前周期 + * @param {object} previous - { startDate, endDate } 上一周期 + * @returns {object} 归因分析结果 + */ + async analyzeAttribution(source, current, previous) { + const currentData = await this.fetchMetrics(source, current); + const previousData = await this.fetchMetrics(source, previous); + + const cs = currentData.summary; + const ps = previousData.summary; + + const changes = {}; + for (const key of Object.keys(cs)) { + const curr = cs[key] || 0; + const prev = ps[key] || 0; + const delta = prev > 0 ? ((curr - prev) / prev * 100).toFixed(1) : 'N/A'; + changes[key] = { + current: curr, + previous: prev, + changePercent: delta, + trend: curr > prev ? '📈 上升' : curr < prev ? '📉 下降' : '➡️ 持平', + }; + } + + // 找出最显著的变化维度 + const significantChanges = Object.entries(changes) + .filter(([_, v]) => v.changePercent !== 'N/A' && Math.abs(parseFloat(v.changePercent)) > 10) + .sort((a, b) => Math.abs(parseFloat(b[1].changePercent)) - Math.abs(parseFloat(a[1].changePercent))); + + return { + source, + currentPeriod: current, + previousPeriod: previous, + changes, + insights: significantChanges.map(([key, val]) => + `${key}: ${val.trend} ${val.changePercent}% (${val.previous} → ${val.current})` + ), + recommendation: significantChanges.length > 0 + ? `主要变化维度: ${significantChanges[0][0]} (${significantChanges[0][1].changePercent}%)` + : '各项指标基本持平,无显著变化', + }; + } +} + +module.exports = { DataSourceService, DataSourceAdapter, MockAdapter, FileAdapter }; diff --git a/server/services/memory-service.js b/server/services/memory-service.js index 0a0d8fd..85d6b77 100644 --- a/server/services/memory-service.js +++ b/server/services/memory-service.js @@ -721,6 +721,9 @@ function autoReflect(agentId, convId, meta, output) { // 更新 memory.md 的"已验证模式"和"失败教训"段 updateMemoryMdSections(agentId, reflection); + // ── Know-how 自动更新: 将经验教训写回 SKILL.md ── + updateAgentKnowhow(agentId, reflection); + console.log(`[Memory] 🪞 反思完成: ${agentId} (${reflection.patterns.length} 模式, ${reflection.lessons.length} 教训${reflection.plan ? ', +1 方案固化' : ''})`); } @@ -986,6 +989,73 @@ function generateDailyReport(agentId) { } } +// ════════════════════════════════════════ +// 9. Phase 3: Know-how 自动更新 +// ════════════════════════════════════════ + +/** + * 将反思产出的经验教训写回 Agent 的 SKILL.md + * 在 SKILL.md 末尾维护一个 "## 经验教训 (自动沉淀)" 段落 + * + * @param {string} agentId + * @param {object} reflection - { patterns: string[], lessons: string[] } + */ +function updateAgentKnowhow(agentId, reflection) { + try { + if (!reflection.patterns.length && !reflection.lessons.length) return; + + const agentDir = getAgentDir(agentId); + + // 查找 SKILL.md(可能在多个路径) + const skillPaths = [ + path.join(agentDir, '.claude', 'skills', 'main_skill', 'SKILL.md'), + path.join(agentDir, 'SKILL.md'), + ]; + let skillPath = null; + for (const sp of skillPaths) { + if (fs.existsSync(sp)) { skillPath = sp; break; } + } + if (!skillPath) return; // 没有 SKILL.md,跳过 + + let content = fs.readFileSync(skillPath, 'utf-8'); + const date = new Date().toISOString().substring(0, 10); + + // 构建新条目 + const entries = []; + for (const p of reflection.patterns) { + entries.push(`- ✅ [${date}] ${p}`); + } + for (const l of reflection.lessons) { + entries.push(`- ⚠️ [${date}] ${l}`); + } + const newBlock = entries.join('\n'); + + // 查找或创建 "经验教训" 段 + const SECTION_HEADER = '## 经验教训 (自动沉淀)'; + if (content.includes(SECTION_HEADER)) { + // 追加到已有段落 + content = content.replace( + new RegExp(`(${SECTION_HEADER.replace(/[()]/g, '\\$&')}\\n)([\\s\\S]*?)(?=\\n## |$)`), + (match, header, existing) => { + const trimmed = existing.trim(); + // 限制最大条目数(保留最近 20 条) + const lines = (trimmed + '\n' + newBlock).split('\n').filter(l => l.startsWith('- ')); + const limited = lines.slice(-20).join('\n'); + return `${header}${limited}\n`; + } + ); + } else { + // 新增段落 + content += `\n\n${SECTION_HEADER}\n\n> 以下经验由系统自动从历史对话中提取沉淀,帮助你避免重复犯错。\n\n${newBlock}\n`; + } + + fs.writeFileSync(skillPath, content, 'utf-8'); + console.log(`[Memory] 📚 Know-how 已更新: ${agentId} SKILL.md (+${entries.length} 条经验)`); + } catch (err) { + console.error(`[Memory] ⚠️ Know-how 更新失败:`, err.message); + } +} + module.exports = { // Phase 1 saveSessionSummary,