Files
pmp-tool/src/server/dev.ts
xiaohei 2532cf4f4e
Some checks failed
CI / lint-and-typecheck (push) Failing after 42s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
feat: P1 full implementation - 8 modules + frontend pages + feishu WS
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
2026-04-12 18:51:41 +08:00

374 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Dev server entry - runs Hono on Node.js
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { createServer } from 'http';
import { serve } from '@hono/node-server';
import { executionApiHandlers } from './execution-api';
import { handleDecompose } from './index';
import {
notifyProjectCreated,
onMessage,
onCardAction,
handleFeishuEvent,
handleCardCallback,
replyMessage,
updateCard,
FeishuMessageEvent,
FeishuCardActionRequest,
} from './feishu';
import { handleDecisionCallback, createDecision, getPendingDecisions } from '../lib/decision-cards';
const projects: Record<string, any> = {};
const app = new Hono();
app.use('*', cors());
app.use('*', logger());
app.get('/api/health', (c) => c.json({ status: 'ok', version: '0.5.0', message: 'FlowPilot API is running' }));
// Create project + send Feishu notification
app.post('/api/projects', async (c) => {
const body = await c.req.json();
const projectId = `proj-${Date.now()}`;
const project = { id: projectId, ...body, status: 'active', createdAt: new Date().toISOString() };
projects[projectId] = project;
try {
await notifyProjectCreated(project.name || '未命名项目', project.goal || '', 'ou_41d14aca8278e605d98e33b1221777e4', 'open_id');
console.log('✅ Feishu notification sent for project:', project.name);
} catch (e: any) {
console.error('❌ Feishu notification failed:', e.message);
}
return c.json(project);
});
app.get('/api/projects/:id', (c) => c.json(projects[c.req.param('id')] || { error: 'not found' }));
app.get('/api/projects/:id/tasks', (c) => c.json({ tasks: [], projectId: c.req.param('id') }));
app.post('/api/projects/:id/tasks', async (c) => {
const body = await c.req.json();
return c.json({ id: `task-${Date.now()}`, projectId: c.req.param('id'), ...body, status: 'todo', createdAt: new Date().toISOString() });
});
app.patch('/api/projects/:id/tasks/:taskId', async (c) => {
const body = await c.req.json();
return c.json({ id: c.req.param('taskId'), ...body, updatedAt: new Date().toISOString() });
});
app.get('/api/projects/:id/executions', (c) => c.json({ executions: executionApiHandlers.getExecutions(c.req.param('id')) }));
app.post('/api/projects/:id/executions', async (c) => { const b = await c.req.json(); return c.json(executionApiHandlers.createExecution(c.req.param('id'), b), 201); });
app.get('/api/projects/:id/stats', (c) => c.json(executionApiHandlers.getStats(c.req.param('id'))));
app.post('/api/projects/:id/decompose', async (c) => {
const b = await c.req.json();
return c.json(handleDecompose(b.task, b.context));
});
app.get('/api/projects/:id/decisions', (c) => c.json({ decisions: executionApiHandlers.getDecisions(c.req.param('id')) }));
app.post('/api/projects/:id/decisions', async (c) => { const b = await c.req.json(); return c.json(executionApiHandlers.createDecision(c.req.param('id'), b), 201); });
// ============================================================
// 飞书消息处理
// ============================================================
onMessage(async (event: FeishuMessageEvent) => {
const { message, sender } = event;
const msgType = message.message_type;
const chatType = message.chat_type;
const openId = sender.sender_id.open_id;
if (msgType !== 'text') {
await replyMessage(message.message_id, '暂只支持文本消息,敬请谅解 🙏');
return;
}
let text = '';
try {
const parsed = JSON.parse(message.content);
text = (parsed.text || '').trim();
} catch {
text = message.content;
}
console.log(`[Feishu] Text message: "${text}" from ${openId} (${chatType})`);
const lower = text.toLowerCase();
if (lower === '/help' || lower === '帮助') {
await replyMessage(message.message_id,
'📋 FlowPilot 指令列表:\n' +
'• /help - 显示帮助\n' +
'• /status - 查看项目状态\n' +
'• /decisions - 查看待决策\n' +
'• /tasks - 查看任务列表\n' +
'\n也可以直接发消息我会尝试处理。'
);
} else if (lower === '/status' || lower === '状态') {
const projectCount = Object.keys(projects).length;
await replyMessage(message.message_id,
`📊 FlowPilot 状态:\n` +
`• 项目数:${projectCount}\n` +
`• 版本v0.5.0\n` +
`• 状态:运行中 ✅`
);
} else if (lower === '/decisions' || lower === '决策') {
const pending = getPendingDecisions();
if (pending.length === 0) {
await replyMessage(message.message_id, '当前没有待决策项 ✅');
} else {
const list = pending.map((d, i) => `${i + 1}. ${d.cardTitle}${d.type}`).join('\n');
await replyMessage(message.message_id, `⏳ 待决策项:\n${list}`);
}
} else {
await replyMessage(message.message_id, `收到:${text}\n输入 /help 查看可用指令。`);
}
});
// ============================================================
// 飞书卡片按钮回调
// ============================================================
onCardAction(async (action: FeishuCardActionRequest) => {
const value = action.action!.value;
const decisionId = value.decisionId || '';
const actionKey = value.action || '';
console.log(`[Feishu] Card button clicked: decisionId=${decisionId}, action=${actionKey}, user=${action.open_id}`);
if (!decisionId) {
return { toast: { type: 'error' as const, content: '无效的决策' } };
}
const result = handleDecisionCallback(decisionId, actionKey);
if (!result.ok) {
return { toast: { type: 'error' as const, content: result.error || '处理失败' } };
}
const optionLabel = actionKey === 'approve' ? '✅ 已批准'
: actionKey === 'reject' ? '❌ 已驳回'
: `已选择:${actionKey}`;
const updatedCard = {
header: {
title: { tag: 'plain_text', content: `${result.record?.cardTitle || '决策'} - ${optionLabel}` },
template: actionKey === 'approve' ? 'green' : actionKey === 'reject' ? 'red' : 'blue',
},
elements: [
{ tag: 'div', text: { tag: 'plain_text', content: `决策结果:${optionLabel}\n操作人${action.open_id}\n时间${new Date().toLocaleString('zh-CN')}` } },
],
};
await updateCard(action.open_message_id, updatedCard);
return { toast: { type: 'success' as const, content: optionLabel } };
});
// ============================================================
// 飞书路由
// ============================================================
app.post('/api/feishu/event', async (c) => {
const body = await c.req.json();
const headers: Record<string, string> = {};
for (const h of ['x-lark-signature', 'x-lark-request-timestamp', 'x-lark-request-nonce']) {
const v = c.req.header(h);
if (v) headers[h] = v;
}
const result = await handleFeishuEvent(body, headers);
return c.json(result.body, result.status as any);
});
app.post('/api/feishu/card', async (c) => {
const body = await c.req.json();
const result = await handleCardCallback(body);
return c.json(result.body, result.status as any);
});
// 兼容旧路由
app.post('/api/feishu/webhook', async (c) => {
const body = await c.req.json();
const headers: Record<string, string> = {};
for (const h of ['x-lark-signature', 'x-lark-request-timestamp', 'x-lark-request-nonce']) {
const v = c.req.header(h);
if (v) headers[h] = v;
}
const result = await handleFeishuEvent(body, headers);
return c.json(result.body, result.status as any);
});
app.post('/api/feishu/decision/callback', async (c) => {
const body = await c.req.json();
const result = await handleCardCallback(body);
return c.json(result.body, result.status as any);
});
// ============================================================
// 多模型路由
// ============================================================
import {
getAllModels, getModel, updateModel, selectModel, recordCall, getCostStats, compareModels,
} from '../lib/multi-model';
app.get('/api/models', (c) => c.json(getAllModels()));
app.get('/api/models/:modelId', (c) => {
const model = getModel(c.req.param('modelId'));
return model ? c.json(model) : c.json({ error: 'Model not found' }, 404);
});
app.patch('/api/models/:modelId', async (c) => {
const body = await c.req.json();
const updated = updateModel(c.req.param('modelId'), body);
return updated ? c.json(updated) : c.json({ error: 'Model not found' }, 404);
});
app.post('/api/models/select', async (c) => {
const { taskType, strategy } = await c.req.json();
try {
const model = selectModel(taskType, strategy);
return c.json(model);
} catch (e: any) {
return c.json({ error: e.message }, 400);
}
});
app.get('/api/projects/:id/model-costs', (c) => {
const from = c.req.query('from');
const to = c.req.query('to');
const period = (from && to) ? { from, to } : undefined;
return c.json(getCostStats(c.req.param('id'), period));
});
app.get('/api/projects/:id/model-comparison', (c) => c.json(compareModels()));
app.post('/api/projects/:id/model-calls', async (c) => {
const body = await c.req.json();
const entry = recordCall({ ...body, projectId: c.req.param('id') });
return c.json(entry, 201);
});
// ============================================================
import {
createChangeRequest, getProjectChanges, getChange, approveChange, rejectChange, implementChange, updateChange, deleteChange, getChangeStats,
} from '../lib/change';
import { generateHealthReport, getProjectReports, getReport } from '../lib/health-report';
import { generateRetrospective, addKnowledge, getProjectKnowledge, getKnowledge, searchKnowledge, getProjectRetrospectives, getRetrospective } from '../lib/retrospective';
// 干系人路由
// ============================================================
import {
createStakeholder, getProjectStakeholders, getStakeholder, updateStakeholder, deleteStakeholder, generateStakeholderAnalysis, recommendStrategy,
} from '../lib/stakeholder';
import {
createWBSNode, getProjectWBS, getWBSNode, updateWBSNode, deleteWBSNode, buildWBSTree, decomposeTask,
} from '../lib/wbs';
import {
createRisk, getRisksByProject, getRisk, updateRisk, deleteRisk, identifyRisks, recommendResponse, generateRiskMatrix,
} from '../lib/risk';
import {
createRequirement, getRequirements, getRequirement, updateRequirement, deleteRequirement, analyzeRequirement, detectConflicts as detectRequirementConflicts, generateCoverageReport, generateUserStory, sortByMoSCoW,
} from '../lib/requirement';
app.post('/api/projects/:id/stakeholders', async (c) => {
const body = await c.req.json();
return c.json(createStakeholder({ ...body, projectId: c.req.param('id') }), 201);
});
app.get('/api/projects/:id/stakeholders', (c) => c.json({ stakeholders: getProjectStakeholders(c.req.param('id')) }));
app.get('/api/projects/:id/stakeholders/:sid', (c) => { const s = getStakeholder(c.req.param('sid')); return s ? c.json(s) : c.json({ error: 'Not found' }, 404); });
app.patch('/api/projects/:id/stakeholders/:sid', async (c) => { const body = await c.req.json(); const u = updateStakeholder(c.req.param('sid'), body); return u ? c.json(u) : c.json({ error: 'Not found' }, 404); });
app.delete('/api/projects/:id/stakeholders/:sid', (c) => deleteStakeholder(c.req.param('sid')) ? c.json({ ok: true }) : c.json({ error: 'Not found' }, 404));
app.post('/api/projects/:id/stakeholders/analyze', (c) => c.json(generateStakeholderAnalysis(getProjectStakeholders(c.req.param('id')))));
app.post('/api/projects/:id/stakeholders/:sid/strategy', (c) => { const s = getStakeholder(c.req.param('sid')); if (!s) return c.json({ error: 'Not found' }, 404); return c.json(recommendStrategy(s.category, s.role)); });
app.post('/api/projects/:id/wbs', async (c) => { const body = await c.req.json(); return c.json(createWBSNode({ ...body, projectId: c.req.param('id') }), 201); });
app.get('/api/projects/:id/wbs', (c) => { const nodes = getProjectWBS(c.req.param('id')); return c.json({ tree: buildWBSTree(nodes), total: nodes.length }); });
app.get('/api/projects/:id/wbs/:nodeId', (c) => { const n = getWBSNode(c.req.param('nodeId')); return n ? c.json(n) : c.json({ error: 'Not found' }, 404); });
app.patch('/api/projects/:id/wbs/:nodeId', async (c) => { const body = await c.req.json(); const u = updateWBSNode(c.req.param('nodeId'), body); return u ? c.json(u) : c.json({ error: 'Not found' }, 404); });
app.delete('/api/projects/:id/wbs/:nodeId', (c) => c.json({ ok: true, deletedCount: deleteWBSNode(c.req.param('nodeId')) }));
app.post('/api/projects/:id/wbs/:nodeId/decompose', async (c) => {
const parent = getWBSNode(c.req.param('nodeId'));
if (!parent) return c.json({ error: 'Not found' }, 404);
const children = decomposeTask({ name: parent.name, description: parent.description }, parent.level, parent.wbsCode, parent.projectId);
const saved = children.map(child => createWBSNode({ ...child, parentId: parent.id }));
return c.json({ parent: parent.id, children: saved });
});
app.post('/api/projects/:id/risks', async (c) => { const body = await c.req.json(); return c.json(createRisk(c.req.param("id"), body), 201); });
app.get('/api/projects/:id/risks', (c) => {
let list = getRisksByProject(c.req.param('id'));
const level = c.req.query('level'); if (level) list = list.filter((r: any) => r.level === level);
const status = c.req.query('status'); if (status) list = list.filter((r: any) => r.status === status);
return c.json({ risks: list });
});
app.get('/api/projects/:id/risks/:rid', (c) => { const r = getRisk(c.req.param('rid')); return r ? c.json(r) : c.json({ error: 'Not found' }, 404); });
app.patch('/api/projects/:id/risks/:rid', async (c) => { const body = await c.req.json(); const u = updateRisk(c.req.param('rid'), body); return u ? c.json(u) : c.json({ error: 'Not found' }, 404); });
app.delete('/api/projects/:id/risks/:rid', (c) => deleteRisk(c.req.param('rid')) ? c.json({ ok: true }) : c.json({ error: 'Not found' }, 404));
app.post('/api/projects/:id/risks/identify', async (c) => { const { description, type } = await c.req.json(); return c.json({ risks: identifyRisks(description || '', type) }); });
app.post('/api/projects/:id/risks/:rid/respond', (c) => { const r = getRisk(c.req.param('rid')); if (!r) return c.json({ error: 'Not found' }, 404); return c.json(recommendResponse(r)); });
app.get('/api/projects/:id/risks/matrix', (c) => c.json(generateRiskMatrix(getRisksByProject(c.req.param('id')))));
app.post('/api/projects/:id/requirements', async (c) => { const body = await c.req.json(); return c.json(createRequirement(c.req.param("id"), body), 201); });
app.get('/api/projects/:id/requirements', (c) => {
let list = getRequirements(c.req.param('id'));
const p = c.req.query('priority'); if (p) list = list.filter((r: any) => r.priority === p);
const s = c.req.query('status'); if (s) list = list.filter((r: any) => r.status === s);
const cat = c.req.query('category'); if (cat) list = list.filter((r: any) => r.category === cat);
return c.json({ requirements: sortByMoSCoW(list) });
});
app.get('/api/projects/:id/requirements/:rid', (c) => { const r = getRequirement(c.req.param('rid')); return r ? c.json(r) : c.json({ error: 'Not found' }, 404); });
app.patch('/api/projects/:id/requirements/:rid', async (c) => { const body = await c.req.json(); const u = updateRequirement(c.req.param('rid'), body); return u ? c.json(u) : c.json({ error: 'Not found' }, 404); });
app.delete('/api/projects/:id/requirements/:rid', (c) => deleteRequirement(c.req.param('rid')) ? c.json({ ok: true }) : c.json({ error: 'Not found' }, 404));
app.post('/api/projects/:id/requirements/analyze', async (c) => c.json(analyzeRequirement(await c.req.json())));
app.post('/api/projects/:id/requirements/detect-conflicts', (c) => c.json({ conflicts: detectRequirementConflicts(getRequirements(c.req.param('id'))) }));
app.get('/api/projects/:id/requirements/coverage', (c) => c.json(generateCoverageReport(getRequirements(c.req.param('id')))));
app.post('/api/projects/:id/requirements/:rid/user-story', (c) => {
const r = getRequirement(c.req.param('rid')); if (!r) return c.json({ error: 'Not found' }, 404);
const story = generateUserStory(r.description);
const u = updateRequirement(c.req.param('rid'), { userStory: story } as any);
return c.json(u);
});
// ============================================================
// 变更管理路由
// ============================================================
app.post('/api/projects/:id/changes', async (c) => { const body = await c.req.json(); return c.json(createChangeRequest(c.req.param('id'), body), 201); });
app.get('/api/projects/:id/changes', (c) => { const type = c.req.query('type') as any; const status = c.req.query('status') as any; return c.json({ changes: getProjectChanges(c.req.param('id'), { type, status }) }); });
app.get('/api/projects/:id/changes/stats', (c) => c.json(getChangeStats(c.req.param('id'))));
app.get('/api/projects/:id/changes/:cid', (c) => { const cr = getChange(c.req.param('cid')); return cr ? c.json(cr) : c.json({ error: 'Not found' }, 404); });
app.patch('/api/projects/:id/changes/:cid', async (c) => { const body = await c.req.json(); const u = updateChange(c.req.param('cid'), body); return u ? c.json(u) : c.json({ error: 'Not found' }, 404); });
app.delete('/api/projects/:id/changes/:cid', (c) => deleteChange(c.req.param('cid')) ? c.json({ ok: true }) : c.json({ error: 'Not found' }, 404));
app.post('/api/projects/:id/changes/:cid/approve', async (c) => { const { approver } = await c.req.json(); const cr = approveChange(c.req.param('cid'), approver); return cr ? c.json(cr) : c.json({ error: 'Cannot approve' }, 400); });
app.post('/api/projects/:id/changes/:cid/reject', async (c) => { const { reason } = await c.req.json(); const cr = rejectChange(c.req.param('cid'), reason || ''); return cr ? c.json(cr) : c.json({ error: 'Cannot reject' }, 400); });
app.post('/api/projects/:id/changes/:cid/implement', async (c) => { const { notes } = await c.req.json(); const cr = implementChange(c.req.param('cid'), notes || ''); return cr ? c.json(cr) : c.json({ error: 'Cannot implement' }, 400); });
// 健康度报告
app.post('/api/projects/:id/health-report', async (c) => { const stats = await c.req.json(); return c.json(generateHealthReport(c.req.param('id'), stats), 201); });
app.get('/api/projects/:id/health-reports', (c) => c.json({ reports: getProjectReports(c.req.param('id')) }));
app.get('/api/projects/:id/health-reports/:rid', (c) => { const r = getReport(c.req.param('rid')); return r ? c.json(r) : c.json({ error: 'Not found' }, 404); });
// 复盘与知识库
app.post('/api/projects/:id/retrospective', async (c) => { const data = await c.req.json(); return c.json(generateRetrospective(c.req.param('id'), data), 201); });
app.get('/api/projects/:id/retrospectives', (c) => c.json({ retrospectives: getProjectRetrospectives(c.req.param('id')) }));
app.get('/api/projects/:id/retrospectives/:rid', (c) => { const r = getRetrospective(c.req.param('rid')); return r ? c.json(r) : c.json({ error: 'Not found' }, 404); });
app.post('/api/projects/:id/knowledge', async (c) => { const body = await c.req.json(); return c.json(addKnowledge({ ...body, projectId: c.req.param('id') }), 201); });
app.get('/api/projects/:id/knowledge', (c) => { const type = c.req.query('type') as any; return c.json({ knowledge: getProjectKnowledge(c.req.param('id'), type) }); });
app.get('/api/knowledge/search', (c) => { const q = c.req.query('q') || ''; const limit = Number(c.req.query('limit')) || 10; return c.json({ results: searchKnowledge(q, limit) }); });
app.get('/api/knowledge/:kid', (c) => { const e = getKnowledge(c.req.param('kid')); return e ? c.json(e) : c.json({ error: 'Not found' }, 404); });
// 启动飞书长连接
import { startFeishuWS, setProjectsStore } from './feishu-ws';
setProjectsStore(projects);
startFeishuWS().catch(err => console.error('❌ Feishu WS 启动失败:', err));
const port = Number(process.env.PORT) || 3001;
serve({ fetch: app.fetch, port }, () => {
console.log(`🚀 FlowPilot API running on http://localhost:${port}`);
});