// 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 = {}; 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 = {}; 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 = {}; 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}`); });