/** * OkrService — OKR (Objective - Key Result) 目标管理 * * 数据目录: * - 公司级: data/companies/{companyId}/okr/ * - 全局(向后兼容): data/okr/ * * 每个 Objective: {okrDir}/O-xxx/objective.json * 索引文件: {okrDir}/_index.json * * ID 设计: 点分层级 (见 docs/todo/system-design/27-naming-conventions.md 六) * O-001 → Objective (第1层) * O-001.1 → Key Result (第2层) * O-001.1.1 → Sub-Task (第3层+) * * 命名沿革: 本服务原名 GoalsService、目录 goals/、ID 前缀 G-, * 已按 27-NAMING 六 正名为 OKR。存量数据迁移见 scripts/migrate-goals-to-okr.js。 */ const fs = require('fs'); const path = require('path'); const DATA_DIR = path.join(__dirname, '../../data'); class OkrService { /** * @param {string} [companyId] - 公司 ID。不传则使用 data/okr/ (向后兼容) */ constructor(companyId) { this.companyId = companyId || null; this.okrDir = companyId ? path.join(DATA_DIR, 'companies', companyId, 'okr') : path.join(DATA_DIR, 'okr'); if (!fs.existsSync(this.okrDir)) { fs.mkdirSync(this.okrDir, { recursive: true }); } } // ═══════════════════════════════════════ // Objective CRUD // ═══════════════════════════════════════ /** * 创建新 Objective * @param {object} opts - { title, description?, owner } * @returns {object} 新创建的 objective */ create({ title, description = '', owner }) { const id = this._nextObjectiveId(); const now = new Date().toISOString(); const objective = { id, title, description, owner, status: 'active', createdAt: now, updatedAt: now, nodes: {}, }; // 写入 objective.json const objectiveDir = path.join(this.okrDir, id); fs.mkdirSync(objectiveDir, { recursive: true }); this._saveObjective(objective); // 写入初始 progress this._saveProgress(id, this._calcProgress(objective)); // 更新索引 this._rebuildIndex(); // 变更日志 this._log(id, `Objective 创建: ${title} (owner: ${owner})`); console.log(`[OKR] 🎯 创建目标 ${id}: ${title}`); return objective; } /** * 获取 Objective 完整数据 */ get(objectiveId) { const fp = path.join(this.okrDir, objectiveId, 'objective.json'); if (!fs.existsSync(fp)) return null; return JSON.parse(fs.readFileSync(fp, 'utf-8')); } /** * 列出所有 Objective (轻量索引) */ list() { const indexPath = path.join(this.okrDir, '_index.json'); if (!fs.existsSync(indexPath)) { this._rebuildIndex(); } try { return JSON.parse(fs.readFileSync(indexPath, 'utf-8')); } catch { return []; } } /** * 更新 Objective 元数据 */ updateObjective(objectiveId, updates) { const objective = this.get(objectiveId); if (!objective) return null; if (updates.title !== undefined) objective.title = updates.title; if (updates.description !== undefined) objective.description = updates.description; if (updates.status !== undefined) objective.status = updates.status; if (updates.owner !== undefined) objective.owner = updates.owner; objective.updatedAt = new Date().toISOString(); this._saveObjective(objective); this._rebuildIndex(); return objective; } // ═══════════════════════════════════════ // Node CRUD — OKR 节点操作 // ═══════════════════════════════════════ /** * 添加节点 — 自动分配下一个子 ID * * @param {string} objectiveId - O-001 * @param {string} parentNodeId - null=顶级 KeyResult, O-001.1=KR * @param {object} node - { title, owner, priority? } * @returns {object} { nodeId, objective } */ addNode(objectiveId, parentNodeId, { title, owner, priority = 'P1' }) { const objective = this.get(objectiveId); if (!objective) return null; // 计算下一个子 ID const prefix = parentNodeId || objectiveId; const existingChildren = Object.keys(objective.nodes) .filter(id => { const parts = id.split('.'); const prefixParts = prefix.split('.'); // 必须是直接子节点: 层级 = parent + 1 return parts.length === prefixParts.length + 1 && id.startsWith(prefix + '.'); }); const nextNum = existingChildren.length > 0 ? Math.max(...existingChildren.map(id => parseInt(id.split('.').pop()))) + 1 : 1; const nodeId = `${prefix}.${nextNum}`; // 创建节点 objective.nodes[nodeId] = { title, owner, status: 'pending', priority, children: [], createdAt: new Date().toISOString(), }; // 更新父节点的 children if (parentNodeId && objective.nodes[parentNodeId]) { if (!objective.nodes[parentNodeId].children) { objective.nodes[parentNodeId].children = []; } objective.nodes[parentNodeId].children.push(nodeId); } objective.updatedAt = new Date().toISOString(); this._saveObjective(objective); this._saveProgress(objectiveId, this._calcProgress(objective)); this._rebuildIndex(); this._log(objectiveId, `添加节点 ${nodeId}: ${title} (owner: ${owner})`); console.log(`[OKR] 📋 ${objectiveId} 添加节点 ${nodeId}: ${title}`); return { nodeId, objective }; } /** * 更新节点状态/内容 */ updateNode(objectiveId, nodeId, updates) { const objective = this.get(objectiveId); if (!objective || !objective.nodes[nodeId]) return null; const node = objective.nodes[nodeId]; const oldStatus = node.status; if (updates.title !== undefined) node.title = updates.title; if (updates.owner !== undefined) node.owner = updates.owner; if (updates.status !== undefined) { node.status = updates.status; if (updates.status === 'done' && !node.completedAt) { node.completedAt = new Date().toISOString(); } } if (updates.priority !== undefined) node.priority = updates.priority; if (updates.taskId !== undefined) node.taskId = updates.taskId; if (updates.output !== undefined) node.output = updates.output; if (updates.summary !== undefined) node.summary = updates.summary; node.updatedAt = new Date().toISOString(); objective.updatedAt = node.updatedAt; this._saveObjective(objective); this._saveProgress(objectiveId, this._calcProgress(objective)); this._rebuildIndex(); if (oldStatus !== node.status) { this._log(objectiveId, `节点 ${nodeId} 状态变更: ${oldStatus} → ${node.status}`); } return { nodeId, node, objective }; } /** * 获取单个节点 + 上下文信息 */ getNode(objectiveId, nodeId) { const objective = this.get(objectiveId); if (!objective || !objective.nodes[nodeId]) return null; const parsed = this.parseNodeId(nodeId); const parentNode = parsed.parentId ? objective.nodes[parsed.parentId] : null; return { ...objective.nodes[nodeId], nodeId, objectiveId, objectiveTitle: objective.title, level: parsed.level, levelName: parsed.levelName, parentId: parsed.parentId, parentTitle: parentNode?.title || null, }; } // ═══════════════════════════════════════ // 进度计算 // ═══════════════════════════════════════ getProgress(objectiveId) { const fp = path.join(this.okrDir, objectiveId, 'progress.json'); if (!fs.existsSync(fp)) { const objective = this.get(objectiveId); if (!objective) return null; const progress = this._calcProgress(objective); this._saveProgress(objectiveId, progress); return progress; } return JSON.parse(fs.readFileSync(fp, 'utf-8')); } // ═══════════════════════════════════════ // 工具方法 // ═══════════════════════════════════════ /** * 解析节点 ID — 从 ID 提取层级信息 */ parseNodeId(nodeId) { const parts = nodeId.split('.'); const levelNames = ['Objective', 'KeyResult', 'SubTask', 'SubTask']; return { objectiveId: parts[0], level: parts.length, parentId: parts.length > 1 ? parts.slice(0, -1).join('.') : null, levelName: levelNames[Math.min(parts.length - 1, 3)], }; } /** * 为 Agent 生成 Objective 上下文注入文本 */ buildObjectiveContext(objectiveId, nodeId) { const objective = this.get(objectiveId); if (!objective) return ''; const progress = this._calcProgress(objective); let text = `\n\n## 🎯 当前目标\n\n`; text += `**${objectiveId}** — ${objective.title}\n`; text += `全局进度: ${Math.round(progress.progress * 100)}% (${progress.done}/${progress.total} 完成)\n`; if (nodeId && objective.nodes[nodeId]) { const node = objective.nodes[nodeId]; const parsed = this.parseNodeId(nodeId); const parentNode = parsed.parentId ? objective.nodes[parsed.parentId] : null; text += `\n你负责的节点:\n`; text += `- **${nodeId}**「${node.title}」\n`; text += `- 层级: ${parsed.levelName} (第${parsed.level}层)\n`; if (parentNode) { text += `- 父级: ${parsed.parentId}「${parentNode.title}」\n`; } text += `- 状态: ${node.status}\n`; } return text; } /** * 获取某 Agent 当前参与的活跃 Objective 上下文文本(汇总所有活跃节点) */ getActiveContextForAgent(agentId) { if (!fs.existsSync(this.okrDir)) return ''; const index = this.list(); let combinedText = ''; for (const entry of index) { if (entry.status === 'done') continue; const objective = this.get(entry.id); if (!objective) continue; let hasActiveNodes = false; let nodesText = ''; for (const [nodeId, node] of Object.entries(objective.nodes)) { if (node.owner === agentId && node.status !== 'done' && node.status !== 'failed') { if (!hasActiveNodes) { const progress = this._calcProgress(objective); combinedText += `\n\n## 🎯 当前参与的目标\n\n`; combinedText += `**${entry.id}** — ${objective.title}\n`; combinedText += `全局进度: ${Math.round(progress.progress * 100)}% (${progress.done}/${progress.total} 完成)\n`; combinedText += `\n你负责的活跃节点:\n`; hasActiveNodes = true; } const parsed = this.parseNodeId(nodeId); const parentNode = parsed.parentId ? objective.nodes[parsed.parentId] : null; nodesText += `- **${nodeId}**「${node.title}」\n`; nodesText += ` - 层级: ${parsed.levelName} (第${parsed.level}层)\n`; if (parentNode) { nodesText += ` - 父级: ${parsed.parentId}「${parentNode.title}」\n`; } nodesText += ` - 状态: ${node.status}\n`; } } combinedText += nodesText; } return combinedText; } // ═══════════════════════════════════════ // 私有方法 // ═══════════════════════════════════════ _nextObjectiveId() { // 扫描现有 G-xxx 目录 if (!fs.existsSync(this.okrDir)) return 'O-001'; const dirs = fs.readdirSync(this.okrDir, { withFileTypes: true }) .filter(d => d.isDirectory() && d.name.startsWith('O-')) .map(d => parseInt(d.name.replace('O-', ''))) .filter(n => !isNaN(n)); const next = dirs.length > 0 ? Math.max(...dirs) + 1 : 1; return `O-${String(next).padStart(3, '0')}`; } _saveObjective(objective) { const objectiveDir = path.join(this.okrDir, objective.id); if (!fs.existsSync(objectiveDir)) fs.mkdirSync(objectiveDir, { recursive: true }); fs.writeFileSync(path.join(objectiveDir, 'objective.json'), JSON.stringify(objective, null, 2), 'utf-8'); } _saveProgress(objectiveId, progress) { const objectiveDir = path.join(this.okrDir, objectiveId); if (!fs.existsSync(objectiveDir)) return; fs.writeFileSync(path.join(objectiveDir, 'progress.json'), JSON.stringify(progress, null, 2), 'utf-8'); } _calcProgress(objective) { const nodes = objective.nodes || {}; const allIds = Object.keys(nodes); // 叶子节点 = 没有 children 或 children 为空的节点 const leafIds = allIds.filter(id => { const n = nodes[id]; return !n.children || n.children.length === 0; }); const total = leafIds.length; const done = leafIds.filter(id => nodes[id].status === 'done').length; const inProgressStates = ['in_progress', 'researching', 'planning', 'executing', 'verifying', 'pending_approval']; const inProgress = leafIds.filter(id => inProgressStates.includes(nodes[id].status)).length; const pending = total - done - inProgress; // 按 owner 分组 const byOwner = {}; for (const id of leafIds) { const owner = nodes[id].owner || 'unknown'; if (!byOwner[owner]) byOwner[owner] = { total: 0, done: 0 }; byOwner[owner].total++; if (nodes[id].status === 'done') byOwner[owner].done++; } return { objectiveId: objective.id, progress: total > 0 ? done / total : 0, total, done, inProgress, pending, byOwner, updatedAt: new Date().toISOString(), }; } _rebuildIndex() { if (!fs.existsSync(this.okrDir)) return; const dirs = fs.readdirSync(this.okrDir, { withFileTypes: true }) .filter(d => d.isDirectory() && d.name.startsWith('O-')); const index = dirs.map(d => { const objective = this.get(d.name); if (!objective) return null; const progress = this._calcProgress(objective); return { id: objective.id, title: objective.title, owner: objective.owner, status: objective.status, progress: progress.progress, nodeCount: Object.keys(objective.nodes).length, doneCount: progress.done, totalCount: progress.total, updatedAt: objective.updatedAt, }; }).filter(Boolean); fs.writeFileSync( path.join(this.okrDir, '_index.json'), JSON.stringify(index, null, 2), 'utf-8' ); } _log(objectiveId, message) { const logPath = path.join(this.okrDir, objectiveId, 'history.log'); const ts = new Date().toISOString(); fs.appendFileSync(logPath, `[${ts}] ${message}\n`, 'utf-8'); } } module.exports = { OkrService };