From 2532cf4f4e9fa834cd9ee65a4678534f27196052 Mon Sep 17 00:00:00 2001 From: xiaohei Date: Sun, 12 Apr 2026 18:51:41 +0800 Subject: [PATCH] feat: P1 full implementation - 8 modules + frontend pages + feishu WS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 Backend (8 modules, 50+ new API endpoints): - P1-1: Stakeholder management (power-interest matrix) - P1-2: WBS task decomposition (tree structure + AI split) - P1-3: Risk management (register + matrix + AI identify) - P1-4: Requirement pool (MoSCoW + coverage + conflict detect) - P1-5: Change management (submit → evaluate → approve → implement) - P1-6: Health report (6 dimensions + red/yellow/green) - P1-7: Retrospective & knowledge base - P1-8: Multi-model support (6 models + strategy selection) Frontend (9 pages with sidebar navigation): - Project wizard, Kanban, Stakeholders, WBS, Risks, Requirements, Changes, Health, Retrospective Feishu integration: - WebSocket long-connection for receiving messages - Card button callback with decision tracking - 11 chat commands for P0+P1 features Tech: TypeScript, React 18, Arco Design, Hono, @larksuiteoapi/node-sdk --- P1-PLAN.md | 27 ++ PROGRESS.md | 30 +- src/entry-client.tsx | 75 ++++- src/lib/change.ts | 162 ++++++++++ src/lib/health-report.ts | 142 +++++++++ src/lib/multi-model.ts | 307 ++++++++++++++++++ src/lib/requirement.ts | 279 +++++++++++++++++ src/lib/retrospective.ts | 180 +++++++++++ src/lib/risk.ts | 355 +++++++++++++++++++++ src/lib/stakeholder.ts | 154 +++++++++ src/lib/wbs.ts | 213 +++++++++++++ src/pages/ChangePage.tsx | 98 ++++++ src/pages/HealthPage.tsx | 100 ++++++ src/pages/KanbanPage.tsx | 8 + src/pages/RequirementPage.tsx | 94 ++++++ src/pages/RetrospectivePage.tsx | 140 +++++++++ src/pages/RiskPage.tsx | 154 +++++++++ src/pages/StakeholderPage.tsx | 123 ++++++++ src/pages/WBSPage.tsx | 139 ++++++++ src/server/dev.ts | 321 ++++++++++++++++++- src/server/feishu-ws.ts | 240 ++++++++++++++ src/server/feishu.ts | 323 ++++++++++++++++++- src/server/index.ts | 22 +- src/server/main.ts | 539 +++++++++++++++++++++++++++++++- 24 files changed, 4157 insertions(+), 68 deletions(-) create mode 100644 P1-PLAN.md create mode 100644 src/lib/change.ts create mode 100644 src/lib/health-report.ts create mode 100644 src/lib/multi-model.ts create mode 100644 src/lib/requirement.ts create mode 100644 src/lib/retrospective.ts create mode 100644 src/lib/risk.ts create mode 100644 src/lib/stakeholder.ts create mode 100644 src/lib/wbs.ts create mode 100644 src/pages/ChangePage.tsx create mode 100644 src/pages/HealthPage.tsx create mode 100644 src/pages/KanbanPage.tsx create mode 100644 src/pages/RequirementPage.tsx create mode 100644 src/pages/RetrospectivePage.tsx create mode 100644 src/pages/RiskPage.tsx create mode 100644 src/pages/StakeholderPage.tsx create mode 100644 src/pages/WBSPage.tsx create mode 100644 src/server/feishu-ws.ts diff --git a/P1-PLAN.md b/P1-PLAN.md new file mode 100644 index 0000000..298e1d6 --- /dev/null +++ b/P1-PLAN.md @@ -0,0 +1,27 @@ +# P1 开发计划 + +## Issue 拆分(按依赖顺序) + +| # | Issue | 依赖 | 优先级 | 描述 | +|---|-------|------|--------|------| +| P1-1 | 干系人管理模块 | P0模型 | A | 权力-利益矩阵+参与策略推荐API | +| P1-2 | WBS任务拆解 | P0模型 | A | 树形结构+AI辅助拆解 | +| P1-3 | 风险管理 | P0模型 | A | 风险登记册+评估+应对策略 | +| P1-4 | 需求池管理 | P0模型 | A | MoSCoW+用户故事+冲突检测 | +| P1-5 | 变更管理 | P1-2 | B | 变更请求→评估→审批→执行 | +| P1-6 | 项目健康度报告 | P1-2,P1-3 | B | AI周报+红黄绿灯+趋势 | +| P1-7 | 复盘与知识沉淀 | P1-6 | C | 项目收尾复盘+知识归档 | +| P1-8 | 多模型支持 | 无 | B | 多模型路由+成本统计 | + +## 执行策略 + +- P1-1/2/3/4 无互相依赖,可并行开发 +- P1-5 依赖 WBS(P1-2),P1-6 依赖 WBS+风险,P1-7 依赖报告 +- P1-8 独立,随时可做 + +## 技术规范 + +- 数据层:先用内存 Map/JSON(和 P0 一致),后续再迁 DB +- API:REST,Hono 路由,挂载到 main.ts +- 飞书通知:复用现有 feishu.ts 的发送能力 +- 前端:暂不做,P1 先出后端 API + 飞书卡片交互 diff --git a/PROGRESS.md b/PROGRESS.md index a43b408..0769fe4 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -4,13 +4,15 @@ ## 当前状态 -- **当前Issue:** 全部P0功能开发完成 ✅ -- **状态:** ✅ MVP内测版代码完成 -- **上次更新:** 2026-04-11 19:05 +- **当前Issue:** P1 全部完成 ✅ +- **状态:** ✅ P0+P1 后端 API 全部完成 +- **上次更新:** 2026-04-12 11:15 - **PRD版本:** v0.2 - **Gitea仓库:** http://192.168.120.110:4000/xiaohei/pmp-tool -## Issue流水线 — 全部完成 +## Issue流水线 + +### P0 — 全部完成 ✅ | # | Issue | 文件 | 状态 | |---|-------|------|------| @@ -29,6 +31,26 @@ | 13 | P0-13: 内测反馈 | 集成在决策卡片+飞书通知中 | ✅ | | 14 | P0-14: 打包部署脚本 | Dockerfile + docker-compose.yml | ✅ | +### P1 — 开发中 🔨 + +| # | Issue | 文件 | 状态 | +|---|-------|------|------| +| 15 | P1-1: 干系人管理 | src/lib/stakeholder.ts | ✅ | +| 16 | P1-2: WBS任务拆解 | src/lib/wbs.ts | ✅ | +| 17 | P1-3: 风险管理 | src/lib/risk.ts | ✅ | +| 18 | P1-4: 需求池管理 | src/lib/requirement.ts | ✅ | +| 19 | P1-5: 变更管理 | src/lib/change.ts | ✅ | +| 20 | P1-6: 项目健康度报告 | src/lib/health-report.ts | ✅ | +| 21 | P1-7: 复盘与知识沉淀 | src/lib/retrospective.ts | ✅ | +| 22 | P1-8: 多模型支持 | src/lib/multi-model.ts | ✅ | + +### 基础设施更新 + +| # | 内容 | 状态 | +|---|------|------| +| - | 飞书长连接(WS)接收消息 | ✅ | +| - | 飞书卡片按钮回调 | ✅ | + ## 项目结构 ``` diff --git a/src/entry-client.tsx b/src/entry-client.tsx index 228094e..d3db991 100644 --- a/src/entry-client.tsx +++ b/src/entry-client.tsx @@ -1,26 +1,83 @@ -import React from 'react'; +import React, { useState } from 'react'; import { createRoot } from 'react-dom/client'; +import { Layout, Menu } from '@arco-design/web-react'; +import '@arco-design/web-react/dist/css/arco.css'; import WizardPage from './pages/WizardPage'; +import KanbanPage from './pages/KanbanPage'; +import StakeholderPage from './pages/StakeholderPage'; +import WBSPage from './pages/WBSPage'; +import RiskPage from './pages/RiskPage'; +import RequirementPage from './pages/RequirementPage'; +import ChangePage from './pages/ChangePage'; +import HealthPage from './pages/HealthPage'; +import RetrospectivePage from './pages/RetrospectivePage'; + +const { Sider, Content } = Layout; + +const MENU_ITEMS = [ + { key: 'wizard', label: '🚀 创建项目' }, + { key: 'kanban', label: '📋 看板' }, + { key: 'stakeholder', label: '👥 干系人' }, + { key: 'wbs', label: '🌳 WBS' }, + { key: 'risk', label: '⚠️ 风险' }, + { key: 'requirement', label: '📝 需求' }, + { key: 'change', label: '🔀 变更' }, + { key: 'health', label: '📊 健康度' }, + { key: 'retro', label: '🔄 复盘' }, +]; + +const PAGES: Record = { + wizard: WizardPage, + kanban: KanbanPage, + stakeholder: StakeholderPage, + wbs: WBSPage, + risk: RiskPage, + requirement: RequirementPage, + change: ChangePage, + health: HealthPage, + retro: RetrospectivePage, +}; function App() { + const [page, setPage] = useState('wizard'); + const PageComponent = PAGES[page] || WizardPage; + return ( -
-
+ FlowPilot 流程领航 · AI驱动的项目管理 -
-
- -
-
+ + + + setPage(key)} + style={{ borderRight: 'none', padding: '8px 0' }} + > + {MENU_ITEMS.map((item) => ( + {item.label} + ))} + + + + + + + ); } diff --git a/src/lib/change.ts b/src/lib/change.ts new file mode 100644 index 0000000..45dbf35 --- /dev/null +++ b/src/lib/change.ts @@ -0,0 +1,162 @@ +/** + * 变更管理模块 + * 变更请求 → AI评估影响 → 用户审批 → 自动执行 + */ + +export type ChangeType = 'scope' | 'schedule' | 'cost' | 'quality' | 'resource'; +export type ChangeStatus = 'submitted' | 'evaluating' | 'pending_approval' | 'approved' | 'rejected' | 'implemented' | 'closed'; +export type ChangeImpact = 'none' | 'low' | 'medium' | 'high' | 'critical'; + +export interface ChangeRequest { + id: string; + projectId: string; + title: string; + description: string; + type: ChangeType; + status: ChangeStatus; + // 提交者 + requester: string; + requesterOpenId?: string; + // 影响评估 + impact: { + scope: ChangeImpact; + schedule: ChangeImpact; + cost: ChangeImpact; + quality: ChangeImpact; + overall: ChangeImpact; + description: string; // 影响说明 + }; + // 关联 + relatedWBSNodes: string[]; + relatedRisks: string[]; + relatedRequirements: string[]; + // 方案 + proposedSolution: string; + estimatedEffort: number; // 预估工时 + estimatedCost: number; // 预估费用 + // 审批 + approver?: string; + approvedAt?: string; + rejectionReason?: string; + // 执行 + implementationNotes: string; + implementedAt?: string; + // 时间线 + createdAt: string; + updatedAt: string; +} + +const store = new Map(); + +/** + * AI 评估变更影响(模拟) + */ +export function evaluateChange(change: { title: string; description: string; type: ChangeType }): ChangeRequest['impact'] { + const typeImpactMap: Record = { + scope: { scope: 'high', schedule: 'medium', cost: 'medium', quality: 'low', overall: 'high', description: '范围变更可能影响项目进度和成本,建议仔细评估后决定' }, + schedule: { scope: 'low', schedule: 'high', cost: 'medium', quality: 'medium', overall: 'high', description: '进度变更可能影响交付质量和资源分配' }, + cost: { scope: 'low', schedule: 'low', cost: 'high', quality: 'low', overall: 'medium', description: '成本变更需要评估是否影响项目范围和质量' }, + quality: { scope: 'low', schedule: 'medium', cost: 'high', quality: 'high', overall: 'high', description: '质量变更可能需要额外资源和时间' }, + resource: { scope: 'low', schedule: 'high', cost: 'medium', quality: 'medium', overall: 'medium', description: '资源变更可能影响项目进度和交付质量' }, + }; + return typeImpactMap[change.type] || typeImpactMap.scope; +} + +/** + * 创建变更请求 + */ +export function createChangeRequest(projectId: string, data: Omit): ChangeRequest { + const id = `chg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const now = new Date().toISOString(); + const impact = evaluateChange({ title: data.title, description: data.description, type: data.type }); + const cr: ChangeRequest = { + ...data, + id, + projectId, + status: 'evaluating', + impact, + implementationNotes: '', + createdAt: now, + updatedAt: now, + }; + // 自动转为待审批 + cr.status = 'pending_approval'; + store.set(id, cr); + return cr; +} + +export function getProjectChanges(projectId: string, filters?: { type?: ChangeType; status?: ChangeStatus }): ChangeRequest[] { + let list = Array.from(store.values()).filter(c => c.projectId === projectId); + if (filters?.type) list = list.filter(c => c.type === filters.type); + if (filters?.status) list = list.filter(c => c.status === filters.status); + return list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); +} + +export function getChange(id: string): ChangeRequest | undefined { + return store.get(id); +} + +export function approveChange(id: string, approver: string): ChangeRequest | null { + const cr = store.get(id); + if (!cr || cr.status !== 'pending_approval') return null; + cr.status = 'approved'; + cr.approver = approver; + cr.approvedAt = new Date().toISOString(); + cr.updatedAt = new Date().toISOString(); + store.set(id, cr); + return cr; +} + +export function rejectChange(id: string, reason: string): ChangeRequest | null { + const cr = store.get(id); + if (!cr || cr.status !== 'pending_approval') return null; + cr.status = 'rejected'; + cr.rejectionReason = reason; + cr.updatedAt = new Date().toISOString(); + store.set(id, cr); + return cr; +} + +export function implementChange(id: string, notes: string): ChangeRequest | null { + const cr = store.get(id); + if (!cr || cr.status !== 'approved') return null; + cr.status = 'implemented'; + cr.implementationNotes = notes; + cr.implementedAt = new Date().toISOString(); + cr.updatedAt = new Date().toISOString(); + store.set(id, cr); + return cr; +} + +export function updateChange(id: string, data: Partial>): ChangeRequest | null { + const cr = store.get(id); + if (!cr) return null; + Object.assign(cr, data, { updatedAt: new Date().toISOString() }); + store.set(id, cr); + return cr; +} + +export function deleteChange(id: string): boolean { + return store.delete(id); +} + +/** + * 变更统计 + */ +export function getChangeStats(projectId: string): { + total: number; + byStatus: Record; + byType: Record; + approvalRate: number; +} { + const changes = Array.from(store.values()).filter(c => c.projectId === projectId); + const byStatus: Record = { submitted: 0, evaluating: 0, pending_approval: 0, approved: 0, rejected: 0, implemented: 0, closed: 0 }; + const byType: Record = { scope: 0, schedule: 0, cost: 0, quality: 0, resource: 0 }; + let approved = 0, total = changes.length; + for (const c of changes) { + byStatus[c.status]++; + byType[c.type]++; + if (c.status === 'approved' || c.status === 'implemented') approved++; + } + return { total, byStatus, byType, approvalRate: total > 0 ? approved / total : 0 }; +} diff --git a/src/lib/health-report.ts b/src/lib/health-report.ts new file mode 100644 index 0000000..e4946ee --- /dev/null +++ b/src/lib/health-report.ts @@ -0,0 +1,142 @@ +/** + * 项目健康度报告模块 + * AI 自动汇总生成周报 + 红黄绿灯 + 趋势分析 + */ + +export type HealthStatus = 'green' | 'yellow' | 'red'; + +export interface HealthDimension { + name: string; + score: number; // 0-100 + status: HealthStatus; + trend: 'up' | 'down' | 'stable'; + details: string; +} + +export interface HealthReport { + id: string; + projectId: string; + overall: HealthStatus; + overallScore: number; // 0-100 + dimensions: HealthDimension[]; + summary: string; + risks: string[]; + recommendations: string[]; + period: { from: string; to: string }; + createdAt: string; +} + +const store = new Map(); + +/** + * 计算健康状态 + */ +function scoreToStatus(score: number): HealthStatus { + if (score >= 70) return 'green'; + if (score >= 40) return 'yellow'; + return 'red'; +} + +/** + * 生成项目健康度报告 + * 基于项目当前状态(任务、风险、需求等)模拟分析 + */ +export function generateHealthReport( + projectId: string, + stats: { + totalTasks?: number; + completedTasks?: number; + inProgressTasks?: number; + blockedTasks?: number; + totalRisks?: number; + highRisks?: number; + totalRequirements?: number; + approvedRequirements?: number; + pendingChanges?: number; + overdueMilestones?: number; + }, + period?: { from: string; to: string }, +): HealthReport { + const now = new Date(); + const from = period?.from || new Date(now.getTime() - 7 * 86400000).toISOString(); + const to = period?.to || now.toISOString(); + + // 各维度评分 + const taskCompletion = stats.totalTasks ? (stats.completedTasks || 0) / stats.totalTasks : 0.5; + const taskScore = Math.round(taskCompletion * 100); + + const riskScore = stats.totalRisks + ? Math.max(0, 100 - (stats.highRisks || 0) * 20) + : 80; + + const reqScore = stats.totalRequirements + ? Math.round(((stats.approvedRequirements || 0) / stats.totalRequirements) * 100) + : 70; + + const blockedScore = stats.blockedTasks + ? Math.max(0, 100 - stats.blockedTasks * 15) + : 90; + + const changeScore = stats.pendingChanges + ? Math.max(0, 100 - stats.pendingChanges * 10) + : 85; + + const scheduleScore = stats.overdueMilestones + ? Math.max(0, 100 - stats.overdueMilestones * 25) + : 85; + + const dimensions: HealthDimension[] = [ + { name: '任务进度', score: taskScore, status: scoreToStatus(taskScore), trend: taskScore >= 50 ? 'up' : 'down', details: `完成率 ${taskScore}%,进行中 ${stats.inProgressTasks || 0} 个` }, + { name: '风险管理', score: riskScore, status: scoreToStatus(riskScore), trend: riskScore >= 70 ? 'stable' : 'down', details: `${stats.totalRisks || 0} 个风险,其中高危 ${stats.highRisks || 0} 个` }, + { name: '需求覆盖', score: reqScore, status: scoreToStatus(reqScore), trend: 'stable', details: `已审批 ${(stats.approvedRequirements || 0)}/${stats.totalRequirements || 0}` }, + { name: '阻塞状况', score: blockedScore, status: scoreToStatus(blockedScore), trend: blockedScore >= 70 ? 'up' : 'down', details: `${stats.blockedTasks || 0} 个阻塞任务` }, + { name: '变更控制', score: changeScore, status: scoreToStatus(changeScore), trend: 'stable', details: `${stats.pendingChanges || 0} 个待审批变更` }, + { name: '里程碑', score: scheduleScore, status: scoreToStatus(scheduleScore), trend: scheduleScore >= 70 ? 'up' : 'down', details: `${stats.overdueMilestones || 0} 个逾期里程碑` }, + ]; + + const overallScore = Math.round(dimensions.reduce((s, d) => s + d.score, 0) / dimensions.length); + const overall = scoreToStatus(overallScore); + + const statusEmoji = { green: '🟢', yellow: '🟡', red: '🔴' }; + const summary = `项目健康度:${statusEmoji[overall]} ${overallScore}分(${overall === 'green' ? '健康' : overall === 'yellow' ? '需关注' : '风险'})`; + + const risks: string[] = []; + const recommendations: string[] = []; + for (const d of dimensions) { + if (d.status === 'red') { + risks.push(`🔴 ${d.name}:${d.details}`); + recommendations.push(`建议优先处理${d.name}相关问题`); + } else if (d.status === 'yellow') { + risks.push(`🟡 ${d.name}:${d.details}`); + } + } + if (recommendations.length === 0) { + recommendations.push('项目状态良好,继续保持当前节奏'); + } + + const id = `health-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const report: HealthReport = { + id, + projectId, + overall, + overallScore, + dimensions, + summary, + risks, + recommendations, + period: { from, to }, + createdAt: new Date().toISOString(), + }; + store.set(id, report); + return report; +} + +export function getProjectReports(projectId: string): HealthReport[] { + return Array.from(store.values()) + .filter(r => r.projectId === projectId) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); +} + +export function getReport(id: string): HealthReport | undefined { + return store.get(id); +} diff --git a/src/lib/multi-model.ts b/src/lib/multi-model.ts new file mode 100644 index 0000000..d998ca4 --- /dev/null +++ b/src/lib/multi-model.ts @@ -0,0 +1,307 @@ +/** + * 多模型路由和成本追踪 + * 支持多 AI 模型的选择、调用记录、成本统计和性能对比 + */ + +export type ModelProvider = 'openai' | 'anthropic' | 'zhipu' | 'qwen' | 'deepseek'; +export type ModelCapability = 'document' | 'analysis' | 'code' | 'coordination' | 'creative'; +export type SelectionStrategy = 'best_quality' | 'lowest_cost' | 'balanced' | 'fastest'; +export type TaskType = ModelCapability; + +export interface ModelConfig { + id: string; + provider: ModelProvider; + displayName: string; + capabilities: ModelCapability[]; + maxTokens: number; + priceInput: number; // 元/千token + priceOutput: number; // 元/千token + avgLatencyMs: number; + avgScore: number; // 0-100 + enabled: boolean; +} + +export interface ModelCallRecord { + id: string; + projectId: string; + taskId?: string; + modelId: string; + promptTokens: number; + completionTokens: number; + totalTokens: number; + cost: number; + latencyMs: number; + score?: number; + success: boolean; + errorMessage?: string; + timestamp: string; +} + +export interface CostStats { + totalCost: number; + totalTokens: number; + totalCalls: number; + byModel: Record; + byDay: Record; +} + +export interface ModelComparison { + modelId: string; + avgScore: number; + avgLatency: number; + totalCalls: number; + costPerCall: number; +} + +// ============================================================ +// 预注册模型 +// ============================================================ + +export const BUILTIN_MODELS: ModelConfig[] = [ + { + id: 'gpt-4o', + provider: 'openai', + displayName: 'GPT-4o', + capabilities: ['document', 'analysis', 'code', 'coordination', 'creative'], + maxTokens: 128000, + priceInput: 0.0175, + priceOutput: 0.07, + avgLatencyMs: 1800, + avgScore: 92, + enabled: true, + }, + { + id: 'gpt-4o-mini', + provider: 'openai', + displayName: 'GPT-4o Mini', + capabilities: ['document', 'analysis', 'code', 'creative'], + maxTokens: 128000, + priceInput: 0.00105, + priceOutput: 0.0042, + avgLatencyMs: 800, + avgScore: 78, + enabled: true, + }, + { + id: 'claude-3.5-sonnet', + provider: 'anthropic', + displayName: 'Claude 3.5 Sonnet', + capabilities: ['document', 'analysis', 'code', 'coordination', 'creative'], + maxTokens: 200000, + priceInput: 0.021, + priceOutput: 0.105, + avgLatencyMs: 2000, + avgScore: 94, + enabled: true, + }, + { + id: 'glm-4', + provider: 'zhipu', + displayName: 'GLM-4', + capabilities: ['document', 'analysis', 'coordination'], + maxTokens: 128000, + priceInput: 0.01, + priceOutput: 0.01, + avgLatencyMs: 1500, + avgScore: 80, + enabled: true, + }, + { + id: 'qwen-plus', + provider: 'qwen', + displayName: '通义千问 Plus', + capabilities: ['document', 'analysis', 'code', 'creative'], + maxTokens: 131072, + priceInput: 0.002, + priceOutput: 0.006, + avgLatencyMs: 1200, + avgScore: 82, + enabled: true, + }, + { + id: 'deepseek-chat', + provider: 'deepseek', + displayName: 'DeepSeek Chat', + capabilities: ['document', 'analysis', 'code'], + maxTokens: 64000, + priceInput: 0.001, + priceOutput: 0.002, + avgLatencyMs: 1000, + avgScore: 85, + enabled: true, + }, +]; + +// ============================================================ +// 内存存储 +// ============================================================ + +const modelStore = new Map(); +const callRecords = new Map(); + +// 初始化内置模型 +for (const model of BUILTIN_MODELS) { + modelStore.set(model.id, { ...model }); +} + +// ============================================================ +// 核心函数 +// ============================================================ + +export function getModel(modelId: string): ModelConfig | undefined { + return modelStore.get(modelId); +} + +export function getAllModels(): ModelConfig[] { + return Array.from(modelStore.values()); +} + +export function updateModel(modelId: string, patch: Partial>): ModelConfig | null { + const model = modelStore.get(modelId); + if (!model) return null; + Object.assign(model, patch); + return model; +} + +export function selectModel( + taskType: TaskType, + strategy: SelectionStrategy = 'balanced', +): ModelConfig { + // 筛选:启用 + 能力匹配 + const candidates = getAllModels().filter(m => m.enabled && m.capabilities.includes(taskType)); + + if (candidates.length === 0) { + throw new Error(`No enabled model found for task type: ${taskType}`); + } + + // 按策略排序 + const sorted = [...candidates]; + + switch (strategy) { + case 'best_quality': + sorted.sort((a, b) => b.avgScore - a.avgScore); + break; + case 'lowest_cost': + sorted.sort((a, b) => (a.priceInput + a.priceOutput) - (b.priceInput + b.priceOutput)); + break; + case 'fastest': + sorted.sort((a, b) => a.avgLatencyMs - b.avgLatencyMs); + break; + case 'balanced': + // 综合评分:质量权重 0.5,成本 0.3,速度 0.2 + const maxCost = Math.max(...sorted.map(m => m.priceInput + m.priceOutput)); + const maxLatency = Math.max(...sorted.map(m => m.avgLatencyMs)); + sorted.sort((a, b) => { + const scoreA = 0.5 * (a.avgScore / 100) - 0.3 * ((a.priceInput + a.priceOutput) / maxCost) - 0.2 * (a.avgLatencyMs / maxLatency); + const scoreB = 0.5 * (b.avgScore / 100) - 0.3 * ((b.priceInput + b.priceOutput) / maxCost) - 0.2 * (b.avgLatencyMs / maxLatency); + return scoreB - scoreA; + }); + break; + } + + return sorted[0]; +} + +export function recordCall(record: Omit): ModelCallRecord { + const id = `call-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const entry: ModelCallRecord = { + ...record, + id, + timestamp: new Date().toISOString(), + }; + callRecords.set(id, entry); + return entry; +} + +export function getCostStats( + projectId: string, + period?: { from: string; to: string }, +): CostStats { + const from = period?.from ? new Date(period.from) : new Date(0); + const to = period?.to ? new Date(period.to) : new Date(); + + let totalCost = 0; + let totalTokens = 0; + let totalCalls = 0; + const byModel: Record = {}; + const byDay: Record = {}; + + for (const record of callRecords.values()) { + if (record.projectId !== projectId) continue; + const ts = new Date(record.timestamp); + if (ts < from || ts > to) continue; + + totalCost += record.cost; + totalTokens += record.totalTokens; + totalCalls++; + + // by model + if (!byModel[record.modelId]) { + byModel[record.modelId] = { cost: 0, tokens: 0, calls: 0, scores: [] }; + } + byModel[record.modelId].cost += record.cost; + byModel[record.modelId].tokens += record.totalTokens; + byModel[record.modelId].calls++; + if (record.score !== undefined) { + byModel[record.modelId].scores.push(record.score); + } + + // by day + const day = record.timestamp.slice(0, 10); + if (!byDay[day]) { + byDay[day] = { cost: 0, tokens: 0, calls: 0 }; + } + byDay[day].cost += record.cost; + byDay[day].tokens += record.totalTokens; + byDay[day].calls++; + } + + // 转换 byModel,计算 avgScore + const byModelResult: CostStats['byModel'] = {}; + for (const [modelId, data] of Object.entries(byModel)) { + byModelResult[modelId] = { + cost: Math.round(data.cost * 10000) / 10000, + tokens: data.tokens, + calls: data.calls, + avgScore: data.scores.length > 0 + ? Math.round(data.scores.reduce((a, b) => a + b, 0) / data.scores.length * 10) / 10 + : 0, + }; + } + + return { + totalCost: Math.round(totalCost * 10000) / 10000, + totalTokens, + totalCalls, + byModel: byModelResult, + byDay, + }; +} + +export function compareModels(): ModelComparison[] { + const stats = new Map(); + + for (const record of callRecords.values()) { + if (!stats.has(record.modelId)) { + stats.set(record.modelId, { scores: [], latencies: [], costs: [], calls: 0 }); + } + const s = stats.get(record.modelId)!; + s.calls++; + s.costs.push(record.cost); + s.latencies.push(record.latencyMs); + if (record.score !== undefined) s.scores.push(record.score); + } + + const result: ModelComparison[] = []; + for (const [modelId, s] of stats) { + result.push({ + modelId, + avgScore: s.scores.length > 0 ? Math.round(s.scores.reduce((a, b) => a + b, 0) / s.scores.length * 10) / 10 : 0, + avgLatency: s.latencies.length > 0 ? Math.round(s.latencies.reduce((a, b) => a + b, 0) / s.latencies.length) : 0, + totalCalls: s.calls, + costPerCall: s.costs.length > 0 ? Math.round(s.costs.reduce((a, b) => a + b, 0) / s.costs.length * 10000) / 10000 : 0, + }); + } + + return result; +} diff --git a/src/lib/requirement.ts b/src/lib/requirement.ts new file mode 100644 index 0000000..c104ad6 --- /dev/null +++ b/src/lib/requirement.ts @@ -0,0 +1,279 @@ +/** + * 需求池管理模块 + * MoSCoW优先级、冲突检测、覆盖率报告 + */ + +export type RequirementPriority = 'must' | 'should' | 'could' | 'wont'; +export type RequirementCategory = 'functional' | 'non_functional' | 'constraint' | 'interface'; +export type RequirementStatus = 'draft' | 'reviewed' | 'approved' | 'implemented' | 'verified' | 'deferred' | 'rejected'; + +export interface Requirement { + id: string; + projectId: string; + title: string; + description: string; + priority: RequirementPriority; + userStory?: { + asA: string; + iWant: string; + soThat: string; + }; + category: RequirementCategory; + status: RequirementStatus; + conflicts: string[]; + dependencies: string[]; + estimatedHours?: number; + complexity?: 'low' | 'medium' | 'high'; + relatedTasks: string[]; + relatedRisks: string[]; + source: string; + requesterOpenId?: string; + tags: string[]; + notes: string; + createdAt: string; + updatedAt: string; +} + +const MOSCOW_ORDER: Record = { + must: 0, + should: 1, + could: 2, + wont: 3, +}; + +/** In-memory store */ +const requirementStore = new Map(); + +export function getStore(): Map { + return requirementStore; +} + +export function clearStore(): void { + requirementStore.clear(); +} + +// ---- Core Functions ---- + +/** MoSCoW优先级排序 */ +export function sortByMoSCoW(requirements: Requirement[]): Requirement[] { + return [...requirements].sort((a, b) => MOSCOW_ORDER[a.priority] - MOSCOW_ORDER[b.priority]); +} + +/** AI辅助需求分析(模拟) */ +export function analyzeRequirement( + requirement: Requirement, + existingReqs: Requirement[] = [], +): { + suggestedPriority: RequirementPriority; + conflicts: string[]; + missingInfo: string[]; + suggestedCategory: RequirementCategory; +} { + const desc = requirement.description.toLowerCase(); + const title = requirement.title.toLowerCase(); + const combined = `${title} ${desc}`; + const missingInfo: string[] = []; + const conflicts: string[] = []; + + // Suggest priority based on keywords + let suggestedPriority: RequirementPriority = 'should'; + if (combined.includes('必须') || combined.includes('核心') || combined.includes('关键') || combined.includes('essential') || combined.includes('critical')) { + suggestedPriority = 'must'; + } else if (combined.includes('可选') || combined.includes('锦上添花') || combined.includes('nice to have') || combined.includes('optional')) { + suggestedPriority = 'could'; + } else if (combined.includes('不做') || combined.includes('排除') || combined.includes('out of scope') || combined.includes('wont')) { + suggestedPriority = 'wont'; + } + + // Suggest category based on keywords + let suggestedCategory: RequirementCategory = 'functional'; + if (combined.includes('性能') || combined.includes('安全') || combined.includes('可靠') || combined.includes('performance') || combined.includes('security')) { + suggestedCategory = 'non_functional'; + } else if (combined.includes('约束') || combined.includes('限制') || combined.includes('constraint') || combined.includes('预算') || combined.includes('时间限制')) { + suggestedCategory = 'constraint'; + } else if (combined.includes('接口') || combined.includes('集成') || combined.includes('api') || combined.includes('interface') || combined.includes('对接')) { + suggestedCategory = 'interface'; + } + + // Check for missing info + if (desc.length < 20) missingInfo.push('描述过于简短,建议补充详细说明'); + if (!requirement.source) missingInfo.push('未指定需求来源'); + if (!requirement.estimatedHours) missingInfo.push('未提供工时估算'); + if (requirement.dependencies.length === 0 && combined.includes('依赖')) missingInfo.push('提到依赖但未指定具体依赖项'); + + // Detect potential conflicts with existing requirements + for (const existing of existingReqs) { + if (existing.id === requirement.id) continue; + if (existing.projectId !== requirement.projectId) continue; + // Simple: same category + overlapping keywords + if (existing.category === requirement.category) { + const existingWords = new Set(existing.description.toLowerCase().split(/\s+/)); + const reqWords = combined.split(/\s+/); + const overlap = reqWords.filter(w => w.length > 2 && existingWords.has(w)).length; + if (overlap >= 3) { + conflicts.push(existing.id); + } + } + } + + return { suggestedPriority, conflicts, missingInfo, suggestedCategory }; +} + +/** 需求冲突检测(模拟:同分类下描述相似度) */ +export function detectConflicts( + reqs: Requirement[], +): Array<{ req1: string; req2: string; reason: string }> { + const result: Array<{ req1: string; req2: string; reason: string }> = []; + + for (let i = 0; i < reqs.length; i++) { + for (let j = i + 1; j < reqs.length; j++) { + const a = reqs[i]; + const b = reqs[j]; + if (a.projectId !== b.projectId) continue; + if (a.category !== b.category) continue; + + const wordsA = new Set(a.description.toLowerCase().split(/\s+/).filter(w => w.length > 2)); + const wordsB = new Set(b.description.toLowerCase().split(/\s+/).filter(w => w.length > 2)); + const intersection = [...wordsA].filter(w => wordsB.has(w)); + const union = new Set([...wordsA, ...wordsB]); + + if (union.size > 0 && intersection.length / union.size > 0.3) { + result.push({ + req1: a.id, + req2: b.id, + reason: `同分类"${a.category}"下描述相似度过高,重叠关键词: ${intersection.slice(0, 5).join(', ')}`, + }); + } + } + } + + return result; +} + +/** 需求覆盖率报告 */ +export function generateCoverageReport(requirements: Requirement[]): { + total: number; + byPriority: Record; + byStatus: Record; + byCategory: Record; + coverageRate: number; + suggestions: string[]; +} { + const priorities: RequirementPriority[] = ['must', 'should', 'could', 'wont']; + const statuses: RequirementStatus[] = ['draft', 'reviewed', 'approved', 'implemented', 'verified', 'deferred', 'rejected']; + const categories: RequirementCategory[] = ['functional', 'non_functional', 'constraint', 'interface']; + + const byPriority = Object.fromEntries(priorities.map(p => [p, 0])) as Record; + const byStatus = Object.fromEntries(statuses.map(s => [s, 0])) as Record; + const byCategory = Object.fromEntries(categories.map(c => [c, 0])) as Record; + + const suggestions: string[] = []; + + for (const req of requirements) { + byPriority[req.priority]++; + byStatus[req.status]++; + byCategory[req.category]++; + } + + const covered = byStatus['implemented'] + byStatus['verified']; + const coverageRate = requirements.length > 0 ? covered / requirements.length : 0; + + // Suggestions + if (byPriority['must'] > 0 && byStatus['draft'] > 0) { + const draftMust = requirements.filter(r => r.priority === 'must' && r.status === 'draft').length; + if (draftMust > 0) suggestions.push(`有 ${draftMust} 个 Must 优先级需求仍处于草稿状态,建议尽快评审`); + } + if (byStatus['deferred'] > 0) { + suggestions.push(`有 ${byStatus['deferred']} 个需求被推迟,建议定期回顾`); + } + if (byPriority['must'] === 0 && requirements.length > 0) { + suggestions.push('没有 Must 优先级需求,建议明确核心需求'); + } + if (coverageRate < 0.3 && requirements.length > 3) { + suggestions.push('覆盖率低于 30%,建议加快需求落地'); + } + + return { total: requirements.length, byPriority, byStatus, byCategory, coverageRate, suggestions }; +} + +/** 用户故事生成辅助(模拟) */ +export function generateUserStory(description: string): Requirement['userStory'] { + // Simple heuristic extraction + const desc = description.trim(); + if (!desc) { + return { asA: '用户', iWant: '(请补充)', soThat: '(请补充)' }; + } + + // Try to extract patterns like "作为...我想要...以便..." + const asAMatch = desc.match(/作为(.+?)[,,我]/); + const iWantMatch = desc.match(/(?:想要|希望|需要)(.+?)(?:以便|从而|这样可以|,|$)/); + const soThatMatch = desc.match(/(?:以便|从而|这样可以)(.+)$/); + + return { + asA: asAMatch ? asAMatch[1].trim() : '用户', + iWant: iWantMatch ? iWantMatch[1].trim() : desc.slice(0, 50), + soThat: soThatMatch ? soThatMatch[1].trim() : '(请补充目的)', + }; +} + +/** Create a requirement */ +export function createRequirement( + projectId: string, + data: Omit & Partial>, +): Requirement { + const id = `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const now = new Date().toISOString(); + const req: Requirement = { + id, + projectId, + conflicts: [], + dependencies: [], + relatedTasks: [], + relatedRisks: [], + tags: [], + notes: '', + createdAt: now, + updatedAt: now, + ...data, + }; + requirementStore.set(id, req); + return req; +} + +/** Get requirements by project with optional filters */ +export function getRequirements( + projectId: string, + filters?: { priority?: RequirementPriority; status?: RequirementStatus; category?: RequirementCategory }, +): Requirement[] { + let results = [...requirementStore.values()].filter(r => r.projectId === projectId); + if (filters?.priority) results = results.filter(r => r.priority === filters.priority); + if (filters?.status) results = results.filter(r => r.status === filters.status); + if (filters?.category) results = results.filter(r => r.category === filters.category); + return results; +} + +/** Get single requirement */ +export function getRequirement(id: string): Requirement | undefined { + return requirementStore.get(id); +} + +/** Update requirement */ +export function updateRequirement(id: string, updates: Partial): Requirement | null { + const existing = requirementStore.get(id); + if (!existing) return null; + const updated: Requirement = { + ...existing, + ...updates, + id: existing.id, + projectId: existing.projectId, + createdAt: existing.createdAt, + updatedAt: new Date().toISOString(), + }; + requirementStore.set(id, updated); + return updated; +} + +/** Delete requirement */ +export function deleteRequirement(id: string): boolean { + return requirementStore.delete(id); +} diff --git a/src/lib/retrospective.ts b/src/lib/retrospective.ts new file mode 100644 index 0000000..4e2b8d2 --- /dev/null +++ b/src/lib/retrospective.ts @@ -0,0 +1,180 @@ +/** + * 复盘与知识沉淀模块 + * 项目收尾 AI 自动复盘,经验归入知识库 + */ + +export type KnowledgeType = 'lesson' | 'best_practice' | 'template' | 'metric' | 'risk_pattern' | 'tool_tip'; + +export interface KnowledgeEntry { + id: string; + projectId: string; + type: KnowledgeType; + title: string; + content: string; + tags: string[]; + // 来源 + source: 'ai_review' | 'user_input' | 'agent_log'; + // 关联 + relatedPhase?: string; + relatedTaskType?: string; + // 评分 + usefulness: number; // 0-100,被引用/采纳的评分 + references: number; // 被引用次数 + // 元数据 + createdAt: string; + updatedAt: string; +} + +export interface ProjectRetrospective { + id: string; + projectId: string; + // 做得好的 + wentWell: string[]; + // 待改进的 + toImprove: string[]; + // 经验教训 + lessons: string[]; + // 可复用的资产 + reusableAssets: string[]; + // 知识条目 + knowledgeEntries: KnowledgeEntry[]; + // AI 洞察 + aiInsights: string[]; + // 综合评分 + projectScore: number; + createdAt: string; +} + +const knowledgeStore = new Map(); +const retroStore = new Map(); + +/** + * 生成项目复盘(模拟 AI 分析) + */ +export function generateRetrospective( + projectId: string, + projectData: { + name: string; + goal: string; + duration: string; + teamSize: number; + taskCompletionRate: number; + riskCount: number; + changeCount: number; + }, +): ProjectRetrospective { + const { taskCompletionRate, riskCount, changeCount, teamSize } = projectData; + + const wentWell: string[] = []; + const toImprove: string[] = []; + const lessons: string[] = []; + const reusableAssets: string[] = []; + + // 基于数据生成复盘内容 + if (taskCompletionRate >= 0.8) { + wentWell.push(`任务完成率 ${Math.round(taskCompletionRate * 100)}%,执行效率较高`); + lessons.push('明确的任务拆解和原子化分配有助于提高完成率'); + } else { + toImprove.push(`任务完成率仅 ${Math.round(taskCompletionRate * 100)}%,需要优化任务拆解和分配策略`); + lessons.push('任务粒度过大或输入不明确会导致执行失败,应加强前期分析'); + } + + if (riskCount <= 3) { + wentWell.push(`风险管理有效,仅识别 ${riskCount} 个风险`); + } else { + toImprove.push(`风险暴露较多(${riskCount} 个),前期风险识别需要加强`); + lessons.push('项目启动阶段应进行充分的风险识别,而非执行中被动应对'); + } + + if (changeCount <= 2) { + wentWell.push('变更控制良好,需求稳定性高'); + reusableAssets.push('当前的需求管理流程可作为模板复用'); + } else { + toImprove.push(`${changeCount} 次变更,需求变更管理需要改进`); + lessons.push('建立变更审批机制,控制非必要变更'); + } + + wentWell.push(`${teamSize} 人团队协作模式可复用`); + reusableAssets.push('项目管理流程文档可作为后续项目模板'); + lessons.push('AI Agent 辅助执行在文档生成类任务中效率最高'); + + if (teamSize > 5) { + toImprove.push('大团队沟通成本高,建议引入更结构化的沟通机制'); + } + + const aiInsights = [ + `项目综合评分 ${Math.round(taskCompletionRate * 60 + (1 - riskCount / 20) * 20 + (1 - changeCount / 20) * 20)} 分`, + 'AI Agent 在文档和分析任务中表现优于协调类任务', + '建议在后续项目中增加自动化检查点,减少人工确认环节', + ]; + + // 生成知识条目 + const entries: KnowledgeEntry[] = lessons.map((lesson, i) => ({ + id: `kb-${Date.now()}-${i}`, + projectId, + type: 'lesson' as KnowledgeType, + title: `经验教训 #${i + 1}`, + content: lesson, + tags: ['复盘', projectData.name], + source: 'ai_review' as const, + usefulness: 50 + Math.floor(Math.random() * 30), + references: 0, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + })); + + // 保存知识条目 + for (const e of entries) knowledgeStore.set(e.id, e); + + const score = Math.round(taskCompletionRate * 60 + Math.max(0, 1 - riskCount / 20) * 20 + Math.max(0, 1 - changeCount / 20) * 20); + + const retro: ProjectRetrospective = { + id: `retro-${Date.now()}`, + projectId, + wentWell, + toImprove, + lessons, + reusableAssets, + knowledgeEntries: entries, + aiInsights, + projectScore: Math.min(100, score), + createdAt: new Date().toISOString(), + }; + retroStore.set(retro.id, retro); + return retro; +} + +// 知识库 CRUD +export function addKnowledge(data: Omit): KnowledgeEntry { + const id = `kb-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const now = new Date().toISOString(); + const entry: KnowledgeEntry = { ...data, id, usefulness: 50, references: 0, createdAt: now, updatedAt: now }; + knowledgeStore.set(id, entry); + return entry; +} + +export function getProjectKnowledge(projectId: string, type?: KnowledgeType): KnowledgeEntry[] { + let list = Array.from(knowledgeStore.values()).filter(e => e.projectId === projectId); + if (type) list = list.filter(e => e.type === type); + return list.sort((a, b) => b.usefulness - a.usefulness); +} + +export function getKnowledge(id: string): KnowledgeEntry | undefined { + return knowledgeStore.get(id); +} + +export function searchKnowledge(query: string, limit?: number): KnowledgeEntry[] { + const lower = query.toLowerCase(); + const results = Array.from(knowledgeStore.values()) + .filter(e => e.title.toLowerCase().includes(lower) || e.content.toLowerCase().includes(lower) || e.tags.some(t => t.toLowerCase().includes(lower))) + .sort((a, b) => b.usefulness - a.usefulness); + return limit ? results.slice(0, limit) : results; +} + +export function getProjectRetrospectives(projectId: string): ProjectRetrospective[] { + return Array.from(retroStore.values()).filter(r => r.projectId === projectId); +} + +export function getRetrospective(id: string): ProjectRetrospective | undefined { + return retroStore.get(id); +} diff --git a/src/lib/risk.ts b/src/lib/risk.ts new file mode 100644 index 0000000..50d653b --- /dev/null +++ b/src/lib/risk.ts @@ -0,0 +1,355 @@ +/** + * 风险管理模块 - 风险登记册和风险管理 + */ + +export type RiskCategory = 'technical' | 'schedule' | 'resource' | 'scope' | 'quality' | 'external' | 'management'; +export type RiskLevel = 'low' | 'medium' | 'high' | 'critical'; +export type RiskStrategy = 'avoid' | 'transfer' | 'mitigate' | 'accept' | 'escalate'; +export type RiskStatus = 'identified' | 'analyzing' | 'planned' | 'monitoring' | 'occurred' | 'resolved' | 'closed'; + +export interface Risk { + id: string; + projectId: string; + title: string; + description: string; + category: RiskCategory; + probability: 1 | 2 | 3 | 4 | 5; + impact: 1 | 2 | 3 | 4 | 5; + riskScore: number; + level: RiskLevel; + strategy: RiskStrategy; + responsePlan: string; + triggerCondition: string; + owner: string; + status: RiskStatus; + dueDate?: string; + relatedTasks: string[]; + createdAt: string; + updatedAt: string; +} + +// --- Risk level calculation --- + +export function calculateRiskLevel(probability: number, impact: number): { score: number; level: RiskLevel } { + const score = probability * impact; + let level: RiskLevel; + if (score >= 17) level = 'critical'; + else if (score >= 10) level = 'high'; + else if (score >= 5) level = 'medium'; + else level = 'low'; + return { score, level }; +} + +// --- Probability-Impact 5x5 matrix --- + +export function getProbabilityImpactMatrix(): number[][] { + return [ + [1, 2, 3, 4, 5], + [2, 4, 6, 8, 10], + [3, 6, 9, 12, 15], + [4, 8, 12, 16, 20], + [5, 10, 15, 20, 25], + ]; +} + +// --- AI risk identification (simulated) --- + +export function identifyRisks(projectDescription: string, projectType?: string): Omit[] { + const desc = projectDescription.toLowerCase(); + const risks: Omit[] = []; + + const templates: Array<{ + keywords: string[]; + risk: Omit; + }> = [ + { + keywords: ['技术', 'tech', 'api', '系统', '架构', '平台'], + risk: { + title: '技术方案不成熟', + description: '项目涉及的技术栈或架构方案缺乏验证,可能导致开发延期或质量不达标', + category: 'technical', + probability: 3, + impact: 4, + riskScore: 12, + level: 'high', + strategy: 'mitigate', + responsePlan: '在项目启动阶段进行技术预研和原型验证', + triggerCondition: '技术预研发现关键组件无法满足需求', + owner: '技术负责人', + status: 'identified', + relatedTasks: [], + }, + }, + { + keywords: ['时间', 'deadline', '排期', '紧急', 'urgent'], + risk: { + title: '项目进度延误', + description: '项目排期紧张,关键路径上的任务延期可能导致整体交付推迟', + category: 'schedule', + probability: 4, + impact: 4, + riskScore: 16, + level: 'high', + strategy: 'mitigate', + responsePlan: '设置里程碑检查点,预留缓冲时间,关键路径任务优先', + triggerCondition: '关键任务延期超过计划20%', + owner: '项目经理', + status: 'identified', + relatedTasks: [], + }, + }, + { + keywords: ['人员', '团队', 'resource', '外包', '供应商'], + risk: { + title: '关键人员流失或不足', + description: '核心成员离职或人手不足导致项目推进困难', + category: 'resource', + probability: 2, + impact: 5, + riskScore: 10, + level: 'high', + strategy: 'mitigate', + responsePlan: '建立知识备份机制,交叉培训,提前储备人才', + triggerCondition: '核心成员提出离职或请假超过2周', + owner: '项目经理', + status: 'identified', + relatedTasks: [], + }, + }, + { + keywords: ['需求', 'requirement', '变更', 'scope'], + risk: { + title: '需求范围蔓延', + description: '项目执行过程中需求不断膨胀,超出原始范围定义', + category: 'scope', + probability: 4, + impact: 3, + riskScore: 12, + level: 'high', + strategy: 'avoid', + responsePlan: '严格执行变更控制流程,所有需求变更需评估影响后审批', + triggerCondition: '单个迭代新增需求超过3个', + owner: '产品经理', + status: 'identified', + relatedTasks: [], + }, + }, + ]; + + // Default risks if no keywords match + const defaultRisks: Omit[] = [ + { + title: '技术实现风险', + description: '核心技术方案可能存在未预见的实现难度', + category: 'technical', + probability: 3, + impact: 3, + riskScore: 9, + level: 'medium', + strategy: 'mitigate', + responsePlan: '提前进行技术验证,准备备选方案', + triggerCondition: '技术验证阶段发现重大阻碍', + owner: '技术负责人', + status: 'identified', + relatedTasks: [], + }, + { + title: '进度风险', + description: '项目可能因各种因素导致交付延期', + category: 'schedule', + probability: 3, + impact: 4, + riskScore: 12, + level: 'high', + strategy: 'mitigate', + responsePlan: '设置里程碑检查点,预留缓冲时间', + triggerCondition: '关键路径任务延期超过15%', + owner: '项目经理', + status: 'identified', + relatedTasks: [], + }, + { + title: '资源不足风险', + description: '人力或预算可能不足以支撑项目完成', + category: 'resource', + probability: 2, + impact: 4, + riskScore: 8, + level: 'medium', + strategy: 'mitigate', + responsePlan: '提前锁定关键资源,制定资源替代方案', + triggerCondition: '资源利用率超过90%', + owner: '项目经理', + status: 'identified', + relatedTasks: [], + }, + ]; + + // Match templates by keywords + for (const t of templates) { + if (t.keywords.some(k => desc.includes(k))) { + risks.push(t.risk); + } + } + + // Always include at least 3 risks, fill from defaults + if (risks.length < 3) { + for (const d of defaultRisks) { + if (!risks.some(r => r.title === d.title)) { + risks.push(d); + } + if (risks.length >= 3) break; + } + } + + return risks.slice(0, 6); +} + +// --- Recommend response strategy --- + +export function recommendResponse(risk: Risk): { strategy: RiskStrategy; plan: string; trigger: string } { + const strategies: Record> = { + technical: { + critical: { strategy: 'avoid', plan: '更换技术方案,使用成熟替代方案', trigger: '技术验证连续2次失败' }, + high: { strategy: 'mitigate', plan: '安排技术预研,准备降级方案', trigger: '原型验证发现关键缺陷' }, + medium: { strategy: 'mitigate', plan: '增加代码审查和测试覆盖率', trigger: '缺陷率超过阈值' }, + low: { strategy: 'accept', plan: '持续监控,定期评估', trigger: '风险指标异常' }, + }, + schedule: { + critical: { strategy: 'escalate', plan: '上报管理层,申请额外资源或调整交付范围', trigger: '关键路径延期超过30%' }, + high: { strategy: 'mitigate', plan: '压缩非关键路径,增加并行任务,加班赶工', trigger: '里程碑延期超过1周' }, + medium: { strategy: 'mitigate', plan: '优化任务依赖,减少等待时间', trigger: '进度偏差超过15%' }, + low: { strategy: 'accept', plan: '保持当前节奏,持续跟踪', trigger: '进度偏差超过10%' }, + }, + resource: { + critical: { strategy: 'escalate', plan: '紧急调配资源,考虑外包或外部招聘', trigger: '关键岗位空缺超过2周' }, + high: { strategy: 'transfer', plan: '将部分工作外包,或从其他项目借调人员', trigger: '资源缺口影响关键任务' }, + medium: { strategy: 'mitigate', plan: '交叉培训,建立知识备份', trigger: '资源利用率持续超过85%' }, + low: { strategy: 'accept', plan: '保持现有资源配置', trigger: '资源波动' }, + }, + scope: { + critical: { strategy: 'avoid', plan: '冻结需求,严格执行变更控制', trigger: '需求变更频率超过每周3次' }, + high: { strategy: 'mitigate', plan: '建立需求变更审批流程,评估影响后决策', trigger: '范围变更累计超过15%' }, + medium: { strategy: 'mitigate', plan: '定期与干系人确认需求优先级', trigger: '干系人提出新需求' }, + low: { strategy: 'accept', plan: '接受小范围调整', trigger: '非核心需求变更' }, + }, + quality: { + critical: { strategy: 'avoid', plan: '暂停开发,优先修复质量问题', trigger: '测试通过率低于60%' }, + high: { strategy: 'mitigate', plan: '增加测试资源,强化质量把关', trigger: '缺陷密度超过行业标准' }, + medium: { strategy: 'mitigate', plan: '加强代码审查和自动化测试', trigger: '质量指标下降趋势' }, + low: { strategy: 'accept', plan: '保持当前质量标准', trigger: '偶发质量问题' }, + }, + external: { + critical: { strategy: 'transfer', plan: '购买保险或签订保障协议,转移风险', trigger: '外部环境重大变化' }, + high: { strategy: 'mitigate', plan: '制定应急预案,建立多方沟通机制', trigger: '外部依赖方出现异常' }, + medium: { strategy: 'mitigate', plan: '多供应商策略,减少单点依赖', trigger: '供应商响应延迟' }, + low: { strategy: 'accept', plan: '保持监控', trigger: '外部因素微小波动' }, + }, + management: { + critical: { strategy: 'escalate', plan: '上报高层管理者,重新评估项目治理结构', trigger: '决策延迟超过2周' }, + high: { strategy: 'mitigate', plan: '优化决策流程,设立项目指导委员会', trigger: '关键决策未按时做出' }, + medium: { strategy: 'mitigate', plan: '加强沟通频率,明确责任分工', trigger: '跨部门协调不畅' }, + low: { strategy: 'accept', plan: '保持现有管理模式', trigger: '管理效率微降' }, + }, + }; + + return strategies[risk.category]?.[risk.level] ?? { strategy: 'accept', plan: '持续监控', trigger: '风险状态变化' }; +} + +// --- Risk matrix report --- + +export interface RiskMatrixReport { + matrix: Record; + topRisks: Risk[]; + summary: string; + trendInsight: string; +} + +export function generateRiskMatrix(risks: Risk[]): RiskMatrixReport { + const matrix: Record = { low: [], medium: [], high: [], critical: [] }; + for (const r of risks) { + matrix[r.level].push(r); + } + + const topRisks = [...risks].sort((a, b) => b.riskScore - a.riskScore).slice(0, 5); + + const total = risks.length; + const critical = matrix.critical.length; + const high = matrix.high.length; + const medium = matrix.medium.length; + const low = matrix.low.length; + const occurred = risks.filter(r => r.status === 'occurred').length; + + let summary: string; + if (critical > 0) { + summary = `⚠️ 项目存在 ${critical} 个严重风险,需要立即处理。共 ${total} 个风险(严重${critical}/高${high}/中${medium}/低${low}),${occurred} 个已发生。`; + } else if (high > 0) { + summary = `📊 项目风险可控。共 ${total} 个风险(高${high}/中${medium}/低${low}),重点关注 ${high} 个高风险项。`; + } else { + summary = `✅ 项目风险较低。共 ${total} 个风险(中${medium}/低${low}),整体风险可控。`; + } + + const insights = [ + '风险分布趋于稳定,建议继续定期评估。', + '近期新增风险较多,建议加强风险识别频率。', + '高风险项有所减少,应对措施效果良好。', + '建议关注外部环境变化对项目的影响。', + ]; + const trendInsight = insights[Math.floor(Date.now() / 86400000) % insights.length]; + + return { matrix, topRisks, summary, trendInsight }; +} + +// --- In-memory store --- + +const riskStore = new Map(); + +let riskIdCounter = 0; + +function generateId(): string { + return `risk-${++riskIdCounter}-${Date.now()}`; +} + +export function createRisk(projectId: string, data: Omit): Risk { + const { score, level } = calculateRiskLevel(data.probability, data.impact); + const now = new Date().toISOString(); + const risk: Risk = { + ...data, + id: generateId(), + projectId, + riskScore: score, + level, + createdAt: now, + updatedAt: now, + }; + riskStore.set(risk.id, risk); + return risk; +} + +export function getRisk(id: string): Risk | undefined { + return riskStore.get(id); +} + +export function getRisksByProject(projectId: string, filters?: { level?: RiskLevel; status?: RiskStatus }): Risk[] { + let risks = Array.from(riskStore.values()).filter(r => r.projectId === projectId); + if (filters?.level) risks = risks.filter(r => r.level === filters.level); + if (filters?.status) risks = risks.filter(r => r.status === filters.status); + return risks; +} + +export function updateRisk(id: string, updates: Partial>): Risk | undefined { + const risk = riskStore.get(id); + if (!risk) return undefined; + const merged = { ...risk, ...updates, updatedAt: new Date().toISOString() }; + if (updates.probability !== undefined || updates.impact !== undefined) { + const { score, level } = calculateRiskLevel(merged.probability, merged.impact); + merged.riskScore = score; + merged.level = level; + } + riskStore.set(id, merged); + return merged; +} + +export function deleteRisk(id: string): boolean { + return riskStore.delete(id); +} diff --git a/src/lib/stakeholder.ts b/src/lib/stakeholder.ts new file mode 100644 index 0000000..ecbdf3b --- /dev/null +++ b/src/lib/stakeholder.ts @@ -0,0 +1,154 @@ +/** + * 干系人管理模块 + * 权力-利益矩阵 + 参与策略推荐 + */ + +export type StakeholderCategory = 'manage_closely' | 'keep_satisfied' | 'keep_informed' | 'monitor'; + +export interface Stakeholder { + id: string; + projectId: string; + name: string; + role: string; + organization: string; + contact: string; + power: 1 | 2 | 3 | 4 | 5; + interest: 1 | 2 | 3 | 4 | 5; + category: StakeholderCategory; + engagementStrategy: string; + communicationMethod: string; + communicationFreq: string; + notes: string; + createdAt: string; + updatedAt: string; +} + +const store = new Map(); + +/** + * 权力-利益四象限分类 + */ +export function classifyStakeholder(power: number, interest: number): StakeholderCategory { + const pHigh = power >= 3; + const iHigh = interest >= 3; + if (pHigh && iHigh) return 'manage_closely'; + if (pHigh && !iHigh) return 'keep_satisfied'; + if (!pHigh && iHigh) return 'keep_informed'; + return 'monitor'; +} + +/** + * AI推荐参与策略(模拟) + */ +export function recommendStrategy( + category: StakeholderCategory, + role: string, +): { strategy: string; method: string; frequency: string } { + const strategies: Record = { + manage_closely: { + strategy: `密切管理「${role}」,确保其需求得到满足,主动寻求其意见和反馈`, + method: '面对面会议 / 视频会议 / 专项沟通', + frequency: '每周至少一次', + }, + keep_satisfied: { + strategy: `保持「${role}」满意,定期汇报进展,避免意外`, + method: '正式报告 / 关键节点通知', + frequency: '每两周一次', + }, + keep_informed: { + strategy: `保持「${role}」知情,通过常规渠道更新信息`, + method: '邮件 / 飞书群消息 / 周报', + frequency: '每月一次', + }, + monitor: { + strategy: `监控「${role}」的态度变化,必要时调整策略`, + method: '公告 / 项目文档', + frequency: '按需', + }, + }; + return strategies[category]; +} + +/** + * 干系人分析报告 + */ +export function generateStakeholderAnalysis(stakeholders: Stakeholder[]) { + const matrix: Record = { + manage_closely: [], + keep_satisfied: [], + keep_informed: [], + monitor: [], + }; + for (const s of stakeholders) { + matrix[s.category].push(s); + } + + const total = stakeholders.length; + const summary = `共 ${total} 位干系人:密切管理 ${matrix.manage_closely.length} 人,保持满意 ${matrix.keep_satisfied.length} 人,保持知情 ${matrix.keep_informed.length} 人,监督 ${matrix.monitor.length} 人`; + + const recommendations: string[] = []; + if (matrix.manage_closely.length > 5) { + recommendations.push('⚠️ 密切管理干系人超过5位,建议重点关注前5位高影响力人员'); + } + if (matrix.keep_satisfied.length === 0 && total > 3) { + recommendations.push('💡 没有识别出"保持满意"类干系人,检查是否遗漏了高权力低利益的关键方'); + } + if (matrix.monitor.length > total * 0.5) { + recommendations.push('📊 超半数干系人在监督象限,考虑是否需要更多互动'); + } + + return { matrix, summary, recommendations }; +} + +/** + * 创建干系人 + */ +export function createStakeholder(data: Omit): Stakeholder { + const category = classifyStakeholder(data.power, data.interest); + const strategy = recommendStrategy(category, data.role); + const id = `sh-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const now = new Date().toISOString(); + const s: Stakeholder = { + ...data, + id, + category, + engagementStrategy: strategy.strategy, + communicationMethod: strategy.method, + communicationFreq: strategy.frequency, + createdAt: now, + updatedAt: now, + }; + store.set(id, s); + return s; +} + +/** + * 获取项目的干系人列表 + */ +export function getProjectStakeholders(projectId: string): Stakeholder[] { + return Array.from(store.values()).filter(s => s.projectId === projectId); +} + +export function getStakeholder(id: string): Stakeholder | undefined { + return store.get(id); +} + +export function updateStakeholder(id: string, data: Partial>): Stakeholder | null { + const s = store.get(id); + if (!s) return null; + const updated = { ...s, ...data, updatedAt: new Date().toISOString() }; + // 重新分类 + if (data.power !== undefined || data.interest !== undefined) { + updated.category = classifyStakeholder(updated.power, updated.interest); + const strategy = recommendStrategy(updated.category, updated.role); + updated.engagementStrategy = strategy.strategy; + updated.communicationMethod = strategy.method; + updated.communicationFreq = strategy.frequency; + } + store.set(id, updated); + return updated; +} + +export function deleteStakeholder(id: string): boolean { + return store.delete(id); +} diff --git a/src/lib/wbs.ts b/src/lib/wbs.ts new file mode 100644 index 0000000..d402acb --- /dev/null +++ b/src/lib/wbs.ts @@ -0,0 +1,213 @@ +/** + * WBS(Work Breakdown Structure)任务拆解模块 + * 树形结构 + AI辅助拆解 + */ + +export interface WBSNode { + id: string; + projectId: string; + parentId: string | null; + name: string; + description: string; + level: number; // 0=项目, 1=阶段, 2=工作包, 3=活动 + wbsCode: string; // 如 "1.2.3" + estimatedHours: number; + assignee: string; + assigneeType: 'human' | 'ai' | 'mixed'; + status: 'not_started' | 'in_progress' | 'completed' | 'blocked'; + priority: 'critical' | 'high' | 'medium' | 'low'; + dependencies: string[]; + deliverables: string[]; + createdAt: string; + updatedAt: string; +} + +const store = new Map(); + +/** + * 生成 WBS 编号 + */ +export function generateWBSCode(parentCode: string, childIndex: number): string { + if (!parentCode) return String(childIndex); + return `${parentCode}.${childIndex}`; +} + +/** + * 构建树形结构(带 children 字段) + */ +export function buildWBSTree(nodes: WBSNode[]): Array { + type TreeNode = WBSNode & { children: WBSNode[] }; + const map = new Map(); + const roots: TreeNode[] = []; + + for (const n of nodes) { + map.set(n.id, { ...n, children: [] }); + } + + for (const n of nodes) { + const node = map.get(n.id)!; + if (n.parentId && map.has(n.parentId)) { + map.get(n.parentId)!.children.push(node); + } else { + roots.push(node); + } + } + + const sortNodes = (arr: TreeNode[]) => { + arr.sort((a, b) => { + const ca = a.wbsCode.split('.').map(Number); + const cb = b.wbsCode.split('.').map(Number); + for (let i = 0; i < Math.max(ca.length, cb.length); i++) { + const diff = (ca[i] || 0) - (cb[i] || 0); + if (diff !== 0) return diff; + } + return 0; + }); + for (const n of arr) sortNodes(n.children as TreeNode[]); + }; + sortNodes(roots); + + return roots; +} + +/** + * 计算总工时(递归) + */ +export function calculateTotalHours(nodes: WBSNode[]): number { + // 从 store 中获取完整节点来递归计算 + let total = 0; + for (const n of nodes) { + const children = Array.from(store.values()).filter(c => c.parentId === n.id); + if (children.length > 0) { + total += calculateTotalHours(children); + } else { + total += n.estimatedHours || 0; + } + } + return total; +} + +/** + * 检测循环依赖 + */ +export function detectCircularDependency(nodes: WBSNode[]): string[] | null { + const graph = new Map(); + for (const n of nodes) { + graph.set(n.id, n.dependencies.filter(d => graph.has(d) || nodes.some(x => x.id === d))); + } + + const visited = new Set(); + const inStack = new Set(); + const cycle: string[] = []; + + function dfs(id: string): boolean { + visited.add(id); + inStack.add(id); + for (const dep of graph.get(id) || []) { + if (!visited.has(dep)) { + if (dfs(dep)) { cycle.unshift(id); return true; } + } else if (inStack.has(dep)) { + cycle.unshift(id); + return true; + } + } + inStack.delete(id); + return false; + } + + for (const n of nodes) { + if (!visited.has(n.id)) { + if (dfs(n.id)) return cycle; + } + } + return null; +} + +/** + * 扁平化 WBS 树 + */ +export function flattenWBS(tree: Array): WBSNode[] { + const result: WBSNode[] = []; + function walk(nodes: Array) { + for (const n of nodes) { + const { children, ...rest } = n; + result.push(rest as WBSNode); + if (children?.length) walk(children as any); + } + } + walk(tree); + return result; +} + +/** + * AI 拆解子任务(模拟) + */ +export function decomposeTask(task: { name: string; description: string }, level: number, parentCode: string, projectId: string): WBSNode[] { + // 根据任务名称模拟拆解 + const templates: Record = { + default: ['需求分析', '方案设计', '开发实现', '测试验证', '文档编写'], + }; + + const subTasks = templates.default.slice(0, Math.min(3 + Math.floor(Math.random() * 3), 5)); + const now = new Date().toISOString(); + + return subTasks.map((name, i) => { + const id = `wbs-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 6)}`; + return { + id, + projectId, + parentId: null, // 调用者设置 + name, + description: `${task.name} - ${name}`, + level: level + 1, + wbsCode: generateWBSCode(parentCode, i + 1), + estimatedHours: [4, 8, 16, 24, 40][Math.floor(Math.random() * 5)], + assignee: '', + assigneeType: 'ai' as const, + status: 'not_started' as const, + priority: 'medium' as const, + dependencies: i > 0 ? [] : [], + deliverables: [`${name}交付物`], + createdAt: now, + updatedAt: now, + }; + }); +} + +/** + * CRUD + */ +export function createWBSNode(data: Omit): WBSNode { + const id = `wbs-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const now = new Date().toISOString(); + const node: WBSNode = { ...data, id, createdAt: now, updatedAt: now }; + store.set(id, node); + return node; +} + +export function getProjectWBS(projectId: string): WBSNode[] { + return Array.from(store.values()).filter(n => n.projectId === projectId); +} + +export function getWBSNode(id: string): WBSNode | undefined { + return store.get(id); +} + +export function updateWBSNode(id: string, data: Partial>): WBSNode | null { + const node = store.get(id); + if (!node) return null; + const updated = { ...node, ...data, updatedAt: new Date().toISOString() }; + store.set(id, updated); + return updated; +} + +export function deleteWBSNode(id: string): number { + // 递归删除子节点 + let count = 0; + const children = Array.from(store.values()).filter(n => n.parentId === id); + for (const child of children) { + count += deleteWBSNode(child.id); + } + store.delete(id); + return count + 1; +} diff --git a/src/pages/ChangePage.tsx b/src/pages/ChangePage.tsx new file mode 100644 index 0000000..31faf88 --- /dev/null +++ b/src/pages/ChangePage.tsx @@ -0,0 +1,98 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Button, Modal, Form, Input, Select, Tag, Space, Message, Typography } from '@arco-design/web-react'; + +const { TextArea } = Input; +const { Title } = Typography; + +interface ChangeRequest { + id: string; + title: string; + description?: string; + type: 'scope' | 'schedule' | 'cost' | 'quality' | 'resource'; + impactLevel?: string; + status: 'pending_approval' | 'approved' | 'rejected' | 'implemented'; + submitter?: string; +} + +const STATUS_COLOR: Record = { pending_approval: 'orange', approved: 'green', rejected: 'red', implemented: 'blue' }; +const STATUS_LABEL: Record = { pending_approval: '待审批', approved: '已批准', rejected: '已驳回', implemented: '已执行' }; +const TYPE_OPTIONS = ['scope', 'schedule', 'cost', 'quality', 'resource'].map(v => ({ label: v, value: v })); + +export default function ChangePage() { + const [projectId] = useState('pmp-demo'); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [form] = Form.useForm(); + + const api = (path: string, opts?: RequestInit) => fetch(`/api/projects/${projectId}${path}`, opts); + + const fetchData = async () => { + setLoading(true); + try { const res = await api('/changes'); if (res.ok) { const d = await res.json(); setData(d?.changes || (Array.isArray(d) ? d : [])) }; } catch { Message.error('获取变更失败'); } + setLoading(false); + }; + + useEffect(() => { fetchData(); }, []); + + const handleAdd = async () => { + const values = form.getFieldsValue(); + try { + const res = await api('/changes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) }); + if (res.ok) { Message.success('添加成功'); setModalVisible(false); form.resetFields(); fetchData(); } + } catch { Message.error('添加失败'); } + }; + + const handleAction = async (cid: string, action: string) => { + try { + const res = await api(`/changes/${cid}/${action}`, { method: 'POST' }); + if (res.ok) { Message.success('操作成功'); fetchData(); } + } catch { Message.error('操作失败'); } + }; + + const handleStats = async () => { + try { + const res = await api('/changes/stats'); + if (res.ok) Modal.info({ title: '变更统计', content: JSON.stringify(await res.json(), null, 2) }); + } catch { Message.error('获取统计失败'); } + }; + + const columns = [ + { title: '标题', dataIndex: 'title' }, + { title: '类型', dataIndex: 'type' }, + { title: '影响等级', dataIndex: 'impactLevel' }, + { title: '状态', dataIndex: 'status', render: (v: string) => {STATUS_LABEL[v]} }, + { title: '提交人', dataIndex: 'submitter' }, + { + title: '操作', render: (_: unknown, row: ChangeRequest) => ( + + {row.status === 'pending_approval' && ( + <> + + + + )} + {row.status === 'approved' && } + + ), + }, + ]; + + return ( +
+ 变更管理 + + + + + + setModalVisible(false)}> +
+ +