diff --git a/dist-server/lib/decision-cards.js b/dist-server/lib/decision-cards.js new file mode 100644 index 0000000..ddc8939 --- /dev/null +++ b/dist-server/lib/decision-cards.js @@ -0,0 +1,144 @@ +"use strict"; +/** + * 决策交互卡片 + * 飞书消息卡片格式 + 回调处理 + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DECISION_TEMPLATES = void 0; +exports.buildDecisionCard = buildDecisionCard; +exports.createDecision = createDecision; +exports.handleDecisionCallback = handleDecisionCallback; +exports.getPendingDecisions = getPendingDecisions; +/** + * 生成飞书消息卡片JSON + */ +function buildDecisionCard(card) { + var elements = [ + { + tag: 'div', + text: { tag: 'plain_text', content: card.description }, + }, + ]; + // Action buttons + var actions = card.options.map(function (opt) { return ({ + tag: 'button', + text: { tag: 'plain_text', content: opt.label }, + type: opt.style === 'danger' ? 'danger' : opt.style === 'primary' ? 'primary' : 'default', + value: __assign({ action: opt.key }, card.metadata), + }); }); + elements.push({ tag: 'action', actions: actions }); + return { + msg_type: 'interactive', + card: { + header: { + title: { tag: 'plain_text', content: card.title }, + template: 'blue', + }, + elements: elements, + }, + }; +} +var pendingDecisions = new Map(); +/** + * 创建决策请求 + */ +function createDecision(card) { + var id = "decision-".concat(Date.now()); + var record = { + id: id, + cardTitle: card.title, + type: card.type, + options: card.options.map(function (o) { return o.key; }), + chosenOption: null, + status: 'pending', + createdAt: new Date().toISOString(), + }; + pendingDecisions.set(id, record); + return record; +} +/** + * 处理决策回调 + */ +function handleDecisionCallback(decisionId, actionKey) { + var record = pendingDecisions.get(decisionId); + if (!record) { + return { ok: false, error: 'Decision not found' }; + } + if (record.status !== 'pending') { + return { ok: false, error: 'Already responded' }; + } + record.chosenOption = actionKey; + record.status = 'responded'; + record.respondedAt = new Date().toISOString(); + return { ok: true, record: record }; +} +/** + * 获取待决策列表 + */ +function getPendingDecisions() { + return Array.from(pendingDecisions.values()).filter(function (d) { return d.status === 'pending'; }); +} +/** + * 预定义的决策卡片模板 + */ +exports.DECISION_TEMPLATES = { + /** 章程审批 */ + charterApproval: function (projectName) { return ({ + title: '📋 项目章程审批', + description: "\u9879\u76EE\u300C".concat(projectName, "\u300D\u7684\u7AE0\u7A0B\u5DF2\u751F\u6210\uFF0C\u8BF7\u5BA1\u6279\u3002"), + type: 'approve_reject', + options: [ + { key: 'approve', label: '✅ 批准', style: 'primary', value: 'approved' }, + { key: 'reject', label: '❌ 驳回', style: 'danger', value: 'rejected' }, + { key: 'revise', label: '✏️ 需修改', value: 'needs_revision' }, + ], + metadata: { type: 'charter', project: projectName }, + }); }, + /** 风险应对 */ + riskResponse: function (riskDesc, priority) { return ({ + title: "\u26A0\uFE0F \u98CE\u9669\u5E94\u5BF9\u51B3\u7B56\uFF08\u4F18\u5148\u7EA7".concat(priority, "\uFF09"), + description: riskDesc, + type: 'multi_choice', + options: [ + { key: 'avoid', label: '🛡 规避', value: 'avoid' }, + { key: 'transfer', label: '🔄 转移', value: 'transfer' }, + { key: 'mitigate', label: '🔧 减轻', value: 'mitigate' }, + { key: 'accept', label: '✅ 接受', value: 'accept' }, + ], + metadata: { type: 'risk', priority: priority }, + }); }, + /** 变更审批 */ + changeApproval: function (changeDesc) { return ({ + title: '🔀 变更请求审批', + description: changeDesc, + type: 'approve_reject', + options: [ + { key: 'approve', label: '✅ 批准', style: 'primary', value: 'approved' }, + { key: 'reject', label: '❌ 拒绝', style: 'danger', value: 'rejected' }, + { key: 'defer', label: '⏳ 延后', value: 'deferred' }, + ], + metadata: { type: 'change' }, + }); }, + /** 里程碑确认 */ + milestoneConfirm: function (name, date) { return ({ + title: "\uD83C\uDFC1 \u91CC\u7A0B\u7891\u8FBE\u6210\u786E\u8BA4", + description: "\u91CC\u7A0B\u7891\u300C".concat(name, "\u300D\uFF08\u76EE\u6807\uFF1A").concat(date, "\uFF09\u662F\u5426\u5DF2\u8FBE\u6210\uFF1F"), + type: 'confirm', + options: [ + { key: 'done', label: '✅ 已达成', style: 'primary', value: 'reached' }, + { key: 'missed', label: '❌ 未达成', style: 'danger', value: 'missed' }, + ], + metadata: { type: 'milestone', name: name, date: date }, + }); }, +}; diff --git a/dist-server/lib/experience-manager.js b/dist-server/lib/experience-manager.js new file mode 100644 index 0000000..f38ed57 --- /dev/null +++ b/dist-server/lib/experience-manager.js @@ -0,0 +1,136 @@ +"use strict"; +/** + * 经验管理员 - 知识管理 + 执行记录 + 跨Agent协调 + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ExperienceManager = void 0; +/** + * 经验管理员 + */ +var ExperienceManager = /** @class */ (function () { + function ExperienceManager() { + this.executionLog = []; + this.decisionLog = []; + this.knowledgeBase = new Map(); + this.records = []; + } + /** + * 记录Agent执行结果 + */ + ExperienceManager.prototype.recordExecution = function (record) { + this.records.push(record); + // Update knowledge base + this._updateKnowledge(record); + }; + /** + * 获取相关上下文供Agent使用 + */ + ExperienceManager.prototype.getContext = function (taskType, keywords) { + // Find recent similar executions + var recentExecutions = this.records + .filter(function (r) { return r.atomicType === taskType; }) + .slice(-10); + // Find relevant knowledge entries + var relevantKnowledge = Array.from(this.knowledgeBase.values()) + .filter(function (k) { return k.taskType === taskType && k.score >= 3; }); + // Generate suggestion + var bestPractice = relevantKnowledge + .sort(function (a, b) { return b.score - a.score; })[0]; + var suggestion = bestPractice + ? "Based on ".concat(bestPractice.successCount, " successful executions, avg score ").concat(bestPractice.score.toFixed(1), ": use input pattern ").concat(JSON.stringify(Object.keys(bestPractice.inputPattern))) + : 'No prior experience for this task type. Using default approach.'; + return { recentExecutions: recentExecutions, relevantKnowledge: relevantKnowledge, suggestion: suggestion }; + }; + /** + * 记录决策 + */ + ExperienceManager.prototype.recordDecision = function (decision) { + this.decisionLog.push(__assign(__assign({}, decision), { id: "decision-".concat(Date.now()), createdAt: new Date().toISOString() })); + }; + /** + * 检查是否需要人工介入 + */ + ExperienceManager.prototype.checkInterventionNeeded = function (taskId, score, consecutiveFailures) { + if (score <= 2) { + return { + needed: true, + reason: "Agent execution score too low (".concat(score, "/5). Needs human review."), + }; + } + if (consecutiveFailures >= 3) { + return { + needed: true, + reason: "".concat(consecutiveFailures, " consecutive failures. Human intervention required."), + }; + } + return { needed: false, reason: '' }; + }; + /** + * 获取项目知识库摘要 + */ + ExperienceManager.prototype.getKnowledgeSummary = function () { + var entries = Array.from(this.knowledgeBase.values()); + var byType = {}; + for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) { + var e = entries_1[_i]; + byType[e.taskType] = (byType[e.taskType] || 0) + 1; + } + var avgScore = entries.length > 0 + ? entries.reduce(function (sum, e) { return sum + e.score; }, 0) / entries.length + : 0; + return { totalEntries: entries.length, byType: byType, avgScore: avgScore }; + }; + /** + * 获取执行历史 + */ + ExperienceManager.prototype.getExecutionHistory = function (taskId) { + if (taskId) { + return this.records.filter(function (r) { return r.taskId === taskId; }); + } + return this.records; + }; + /** + * 获取决策历史 + */ + ExperienceManager.prototype.getDecisionHistory = function () { + return this.decisionLog; + }; + // --- Private --- + ExperienceManager.prototype._updateKnowledge = function (record) { + var key = "".concat(record.atomicType, "_").concat(record.model); + var existing = this.knowledgeBase.get(key); + if (existing) { + existing.successCount += record.success ? 1 : 0; + existing.failCount += record.success ? 0 : 1; + // Running average score + var totalRuns = existing.successCount + existing.failCount; + existing.score = (existing.score * (totalRuns - 1) + record.score) / totalRuns; + } + else { + this.knowledgeBase.set(key, { + id: "kb-".concat(Date.now()), + taskType: record.atomicType, + description: "Pattern for ".concat(record.atomicType, " with ").concat(record.model), + inputPattern: record.input, + outputPattern: record.output, + score: record.score, + successCount: record.success ? 1 : 0, + failCount: record.success ? 0 : 1, + learnedAt: new Date().toISOString(), + }); + } + }; + return ExperienceManager; +}()); +exports.ExperienceManager = ExperienceManager; diff --git a/dist-server/lib/hr-manager.js b/dist-server/lib/hr-manager.js new file mode 100644 index 0000000..a3c94de --- /dev/null +++ b/dist-server/lib/hr-manager.js @@ -0,0 +1,287 @@ +"use strict"; +/** + * HR管理员 - 任务拆解引擎 + * 将高级任务递归分解为原子任务 + */ +var _a; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HRManager = exports.ATOMIC_TASK_TYPES = exports.AtomicTaskType = void 0; +var AtomicTaskType; +(function (AtomicTaskType) { + AtomicTaskType["FILL_TEMPLATE"] = "fill_template"; + AtomicTaskType["SUMMARIZE"] = "summarize"; + AtomicTaskType["FORMAT_CONVERT"] = "format_convert"; + AtomicTaskType["EVALUATE"] = "evaluate"; + AtomicTaskType["RISK_IDENTIFY"] = "risk_identify"; + AtomicTaskType["PRIORITIZE"] = "prioritize"; + AtomicTaskType["GENERATE_NOTIFICATION"] = "generate_notification"; + AtomicTaskType["AGGREGATE_REPORT"] = "aggregate_report"; + AtomicTaskType["STATUS_UPDATE"] = "status_update"; + AtomicTaskType["EXTRACT"] = "extract"; +})(AtomicTaskType || (exports.AtomicTaskType = AtomicTaskType = {})); +/** + * 原子任务类型库 + */ +exports.ATOMIC_TASK_TYPES = (_a = {}, + _a[AtomicTaskType.FILL_TEMPLATE] = { + type: AtomicTaskType.FILL_TEMPLATE, + category: 'document', + description: '给定模板+数据 → 输出填充后文档', + estimatedTokens: 3000, + inputSchema: { template: 'string', data: 'object' }, + outputSchema: { document: 'string' }, + }, + _a[AtomicTaskType.SUMMARIZE] = { + type: AtomicTaskType.SUMMARIZE, + category: 'document', + description: '给定长文 → 输出摘要', + estimatedTokens: 2000, + inputSchema: { content: 'string', maxLength: 'number' }, + outputSchema: { summary: 'string' }, + }, + _a[AtomicTaskType.FORMAT_CONVERT] = { + type: AtomicTaskType.FORMAT_CONVERT, + category: 'document', + description: '给定内容 → 按目标格式输出', + estimatedTokens: 2000, + inputSchema: { content: 'string', targetFormat: 'string' }, + outputSchema: { formatted: 'string' }, + }, + _a[AtomicTaskType.EVALUATE] = { + type: AtomicTaskType.EVALUATE, + category: 'analysis', + description: '给定标准+对象 → 输出评分+理由', + estimatedTokens: 1500, + inputSchema: { criteria: 'array', target: 'object' }, + outputSchema: { score: 'number', reasoning: 'string' }, + }, + _a[AtomicTaskType.RISK_IDENTIFY] = { + type: AtomicTaskType.RISK_IDENTIFY, + category: 'analysis', + description: '给定范围 → 输出风险列表', + estimatedTokens: 1500, + inputSchema: { scope: 'string', context: 'object' }, + outputSchema: { risks: 'array' }, + }, + _a[AtomicTaskType.PRIORITIZE] = { + type: AtomicTaskType.PRIORITIZE, + category: 'analysis', + description: '给定列表+标准 → 输出排序结果', + estimatedTokens: 1200, + inputSchema: { items: 'array', criteria: 'array' }, + outputSchema: { sorted: 'array' }, + }, + _a[AtomicTaskType.GENERATE_NOTIFICATION] = { + type: AtomicTaskType.GENERATE_NOTIFICATION, + category: 'coordination', + description: '给定事件 → 输出消息内容', + estimatedTokens: 1000, + inputSchema: { event: 'string', recipients: 'array' }, + outputSchema: { message: 'string' }, + }, + _a[AtomicTaskType.AGGREGATE_REPORT] = { + type: AtomicTaskType.AGGREGATE_REPORT, + category: 'coordination', + description: '给定多条数据 → 输出汇总', + estimatedTokens: 1500, + inputSchema: { data: 'array', reportType: 'string' }, + outputSchema: { report: 'string' }, + }, + _a[AtomicTaskType.STATUS_UPDATE] = { + type: AtomicTaskType.STATUS_UPDATE, + category: 'data', + description: '给定条件 → 查询并更新', + estimatedTokens: 1000, + inputSchema: { query: 'object', updates: 'object' }, + outputSchema: { updated: 'boolean', record: 'object' }, + }, + _a[AtomicTaskType.EXTRACT] = { + type: AtomicTaskType.EXTRACT, + category: 'data', + description: '给定源 → 提取指定字段', + estimatedTokens: 1000, + inputSchema: { source: 'string', fields: 'array' }, + outputSchema: { extracted: 'object' }, + }, + _a); +/** + * 原子任务判定标准 + */ +function isAtomic(task) { + var _a; + return (Object.keys(task.input).length > 0 && // 输入确定 + task.outputFormat !== undefined && // 输出确定 + !((_a = task.dependencies) === null || _a === void 0 ? void 0 : _a.length) && // 无外部依赖 + task.estimatedTokens <= 6000 // 时间可控 + ); +} +/** + * HR管理员核心:递归任务分解 + */ +var HRManager = /** @class */ (function () { + function HRManager() { + this.taskIdCounter = 0; + } + /** + * 将高级任务分解为原子任务列表 + */ + HRManager.prototype.decompose = function (highLevelTask, context) { + var _a; + var tasks = this._matchPatterns(highLevelTask, context); + // Recursive check: decompose non-atomic tasks further + var result = []; + for (var _i = 0, tasks_1 = tasks; _i < tasks_1.length; _i++) { + var task = tasks_1[_i]; + if (isAtomic(task) || !((_a = task.children) === null || _a === void 0 ? void 0 : _a.length)) { + result.push(task); + } + else { + // Decompose children further + for (var _b = 0, _c = task.children || []; _b < _c.length; _b++) { + var child = _c[_b]; + if (isAtomic(child)) { + result.push(child); + } + else { + result.push.apply(result, this.decompose(child.title, child.input)); + } + } + } + } + return result; + }; + /** + * 为原子任务选择最佳模型 + */ + HRManager.prototype.selectModel = function (taskType) { + var _a; + var modelMap = (_a = {}, + _a[AtomicTaskType.FILL_TEMPLATE] = 'claude-3-haiku', + _a[AtomicTaskType.SUMMARIZE] = 'gpt-4o-mini', + _a[AtomicTaskType.FORMAT_CONVERT] = 'gpt-4o-mini', + _a[AtomicTaskType.EVALUATE] = 'gpt-4-turbo', + _a[AtomicTaskType.RISK_IDENTIFY] = 'claude-3-sonnet', + _a[AtomicTaskType.PRIORITIZE] = 'gpt-4o-mini', + _a[AtomicTaskType.GENERATE_NOTIFICATION] = 'gpt-4o-mini', + _a[AtomicTaskType.AGGREGATE_REPORT] = 'claude-3-sonnet', + _a[AtomicTaskType.STATUS_UPDATE] = 'gpt-4o-mini', + _a[AtomicTaskType.EXTRACT] = 'gpt-4o-mini', + _a); + return modelMap[taskType] || 'gpt-4o-mini'; + }; + HRManager.prototype._generateId = function () { + return "task-".concat(++this.taskIdCounter, "-").concat(Date.now()); + }; + /** + * Pattern matching for known PM phases + */ + HRManager.prototype._matchPatterns = function (task, context) { + var lower = task.toLowerCase(); + // Project Charter + if (lower.includes('章程') || lower.includes('charter')) { + return [ + { + id: this._generateId(), + title: '生成项目章程', + description: '根据项目信息填充章程模板', + atomicType: AtomicTaskType.FILL_TEMPLATE, + input: { template: 'project_charter', data: context || {} }, + outputFormat: { document: 'string' }, + estimatedTokens: 3000, + priority: 'must', + }, + { + id: this._generateId(), + title: '分析干系人', + description: '根据项目背景识别关键干系人', + atomicType: AtomicTaskType.RISK_IDENTIFY, + input: { scope: 'stakeholders', context: context || {} }, + outputFormat: { stakeholders: 'array' }, + estimatedTokens: 1500, + priority: 'must', + dependencies: [], + }, + ]; + } + // Risk Management + if (lower.includes('风险') || lower.includes('risk')) { + return [ + { + id: this._generateId(), + title: '识别项目风险', + description: '从技术/资源/范围/外部四个维度识别风险', + atomicType: AtomicTaskType.RISK_IDENTIFY, + input: { scope: 'project_risks', context: context || {} }, + outputFormat: { risks: 'array' }, + estimatedTokens: 2000, + priority: 'must', + }, + { + id: this._generateId(), + title: '生成风险应对策略', + description: '为高优先级风险制定规避/转移/减轻/接受策略', + atomicType: AtomicTaskType.EVALUATE, + input: { criteria: ['probability', 'impact', 'strategy'], target: {} }, + outputFormat: { mitigations: 'array' }, + estimatedTokens: 1500, + priority: 'must', + }, + ]; + } + // Schedule / Progress + if (lower.includes('进度') || lower.includes('schedule') || lower.includes('排期')) { + return [ + { + id: this._generateId(), + title: '分析任务依赖关系', + description: '确定任务间的先后顺序和关键路径', + atomicType: AtomicTaskType.PRIORITIZE, + input: { items: (context === null || context === void 0 ? void 0 : context.tasks) || [], criteria: ['dependency', 'priority'] }, + outputFormat: { sorted: 'array', criticalPath: 'array' }, + estimatedTokens: 2000, + priority: 'must', + }, + { + id: this._generateId(), + title: '生成进度计划', + description: '基于依赖关系和估算生成甘特图数据', + atomicType: AtomicTaskType.FILL_TEMPLATE, + input: { template: 'schedule', data: context || {} }, + outputFormat: { schedule: 'object' }, + estimatedTokens: 3000, + priority: 'must', + }, + ]; + } + // Weekly Report + if (lower.includes('周报') || lower.includes('报告') || lower.includes('report')) { + return [ + { + id: this._generateId(), + title: '汇总项目进度数据', + description: '收集任务完成情况、Bug数据、风险状态', + atomicType: AtomicTaskType.AGGREGATE_REPORT, + input: { data: (context === null || context === void 0 ? void 0 : context.tasks) || [], reportType: 'weekly' }, + outputFormat: { report: 'string' }, + estimatedTokens: 1500, + priority: 'should', + }, + ]; + } + // Default: treat as document generation + return [ + { + id: this._generateId(), + title: task, + description: "\u5904\u7406\u4EFB\u52A1\uFF1A".concat(task), + atomicType: AtomicTaskType.FILL_TEMPLATE, + input: { request: task, context: context || {} }, + outputFormat: { result: 'string' }, + estimatedTokens: 2000, + priority: 'should', + }, + ]; + }; + return HRManager; +}()); +exports.HRManager = HRManager; diff --git a/dist-server/lib/models.js b/dist-server/lib/models.js new file mode 100644 index 0000000..49bab76 --- /dev/null +++ b/dist-server/lib/models.js @@ -0,0 +1,6 @@ +"use strict"; +/** + * 任务数据模型 + * 定义任务相关的类型和CRUD操作接口 + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist-server/server/execution-api.js b/dist-server/server/execution-api.js new file mode 100644 index 0000000..6235761 --- /dev/null +++ b/dist-server/server/execution-api.js @@ -0,0 +1,96 @@ +"use strict"; +/** + * 执行记录 REST API + * Hono路由:Agent执行日志的CRUD接口 + * + * 接口列表: + * GET /api/projects/:id/executions - 获取项目执行记录 + * GET /api/projects/:id/executions/:eid - 获取单条执行记录 + * POST /api/projects/:id/executions - 创建执行记录 + * GET /api/projects/:id/decisions - 获取决策记录 + * GET /api/projects/:id/knowledge - 获取知识库摘要 + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.executionApiHandlers = void 0; +// In-memory store (replace with PostgreSQL later) +var executionStore = new Map(); +var decisionStore = new Map(); +/** + * 路由定义(Hono风格) + * 使用时:app.route('/api', executionRoutes) + */ +exports.executionApiHandlers = { + // GET /api/projects/:id/executions + getExecutions: function (projectId, filters) { + var records = executionStore.get(projectId) || []; + if (filters === null || filters === void 0 ? void 0 : filters.agentId) { + records = records.filter(function (r) { return r.agentId === filters.agentId; }); + } + if (filters === null || filters === void 0 ? void 0 : filters.taskId) { + records = records.filter(function (r) { return r.taskId === filters.taskId; }); + } + var offset = (filters === null || filters === void 0 ? void 0 : filters.offset) || 0; + var limit = (filters === null || filters === void 0 ? void 0 : filters.limit) || 50; + return records.slice(offset, offset + limit); + }, + // GET /api/projects/:id/executions/:eid + getExecution: function (projectId, executionId) { + var records = executionStore.get(projectId) || []; + return records.find(function (r) { return r.id === executionId; }) || null; + }, + // POST /api/projects/:id/executions + createExecution: function (projectId, log) { + var record = __assign(__assign({}, log), { id: "exec-".concat(Date.now(), "-").concat(Math.random().toString(36).slice(2, 8)), createdAt: new Date().toISOString() }); + var existing = executionStore.get(projectId) || []; + existing.push(record); + executionStore.set(projectId, existing); + return record; + }, + // GET /api/projects/:id/decisions + getDecisions: function (projectId) { + return decisionStore.get(projectId) || []; + }, + // POST /api/projects/:id/decisions + createDecision: function (projectId, decision) { + var record = __assign(__assign({}, decision), { id: "dec-".concat(Date.now(), "-").concat(Math.random().toString(36).slice(2, 8)), createdAt: new Date().toISOString() }); + var existing = decisionStore.get(projectId) || []; + existing.push(record); + decisionStore.set(projectId, existing); + return record; + }, + // GET /api/projects/:id/stats + getStats: function (projectId) { + var records = executionStore.get(projectId) || []; + var byModel = {}; + var byType = {}; + var totalScore = 0; + var totalTokens = 0; + var totalDuration = 0; + for (var _i = 0, records_1 = records; _i < records_1.length; _i++) { + var r = records_1[_i]; + byModel[r.model] = (byModel[r.model] || 0) + 1; + totalScore += r.score; + totalTokens += r.tokensUsed; + totalDuration += r.durationMs; + } + return { + totalExecutions: records.length, + avgScore: records.length > 0 ? Math.round((totalScore / records.length) * 10) / 10 : 0, + totalTokens: totalTokens, + avgDurationMs: records.length > 0 ? Math.round(totalDuration / records.length) : 0, + byModel: byModel, + byType: byType, + }; + }, +}; diff --git a/dist-server/server/feishu.js b/dist-server/server/feishu.js new file mode 100644 index 0000000..09ac295 --- /dev/null +++ b/dist-server/server/feishu.js @@ -0,0 +1,290 @@ +"use strict"; +/** + * 飞书消息发送模块 + * 支持两种方式: + * 1. 应用身份(推荐):使用 App ID/App Secret 获取 tenant_token 调用开放 API + * 2. Webhook 方式:直接调用自定义机器人 Webhook(向后兼容) + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sendFeishuMessage = sendFeishuMessage; +exports.notifyProjectCreated = notifyProjectCreated; +exports.notifyMilestoneReminder = notifyMilestoneReminder; +exports.notifyRiskAlert = notifyRiskAlert; +exports.buildDecisionCard = buildDecisionCard; +exports.sendDecisionCard = sendDecisionCard; +var crypto_1 = require("crypto"); +// 配置:从环境变量或 TOOLS.md 读取 +var FEISHU_APP_ID = process.env.FEISHU_APP_ID || 'cli_a95093447cb85cdd'; +var FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET || 'd17CeffVfOnTkQo8LIP7hbhOQwSPv7Jv'; +var FEISHU_WEBHOOK = process.env.FEISHU_WEBHOOK || 'https://open.feishu.cn/open-apis/bot/v2/hook/58321c74-5881-4f41-bcd4-85f4d7c5b3c1'; +var FEISHU_WEBHOOK_SECRET = process.env.FEISHU_WEBHOOK_SECRET || 'UgCdzrcci4s9YS1GSAHt4e'; +/** + * 使用应用身份获取 tenant_access_token + */ +function getTenantToken() { + return __awaiter(this, void 0, void 0, function () { + var res, data; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ app_id: FEISHU_APP_ID, app_secret: FEISHU_APP_SECRET }), + })]; + case 1: + res = _a.sent(); + return [4 /*yield*/, res.json()]; + case 2: + data = _a.sent(); + if (data.code !== 0) { + throw new Error("Failed to get tenant token: ".concat(data.msg)); + } + return [2 /*return*/, data.tenant_access_token]; + } + }); + }); +} +/** + * 生成 Webhook 签名 + */ +function generateWebhookSign(timestamp, secret) { + var stringToSign = "".concat(timestamp, "\n").concat(secret); + var hmac = (0, crypto_1.createHmac)('sha256', stringToSign); + return hmac.digest('base64'); +} +/** + * 发送文本消息 + */ +function sendFeishuMessage(options) { + return __awaiter(this, void 0, void 0, function () { + var text, receiveId, _a, receiveIdType, _b, useApp, token, url, res, data, timestamp, body, res, data; + var _c, _d, _e, _f; + return __generator(this, function (_g) { + switch (_g.label) { + case 0: + text = options.text, receiveId = options.receiveId, _a = options.receiveIdType, receiveIdType = _a === void 0 ? 'open_id' : _a, _b = options.useApp, useApp = _b === void 0 ? true : _b; + if (!useApp) return [3 /*break*/, 4]; + return [4 /*yield*/, getTenantToken()]; + case 1: + token = _g.sent(); + url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=".concat(receiveIdType); + return [4 /*yield*/, fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': "Bearer ".concat(token), + }, + body: JSON.stringify({ + receive_id: receiveId, + msg_type: 'text', + content: JSON.stringify({ text: text }), + }), + })]; + case 2: + res = _g.sent(); + return [4 /*yield*/, res.json()]; + case 3: + data = _g.sent(); + return [2 /*return*/, { + ok: data.code === 0, + code: data.code, + msg: data.msg, + }]; + case 4: + timestamp = Math.floor(Date.now() / 1000); + body = { + msg_type: 'text', + content: { text: text }, + }; + if (FEISHU_WEBHOOK_SECRET) { + body.timestamp = String(timestamp); + body.sign = generateWebhookSign(timestamp, FEISHU_WEBHOOK_SECRET); + } + return [4 /*yield*/, fetch(FEISHU_WEBHOOK, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + })]; + case 5: + res = _g.sent(); + return [4 /*yield*/, res.json()]; + case 6: + data = _g.sent(); + return [2 /*return*/, { + ok: data.code === 0 || data.StatusCode === 0, + code: (_d = (_c = data.code) !== null && _c !== void 0 ? _c : data.StatusCode) !== null && _d !== void 0 ? _d : -1, + msg: (_f = (_e = data.msg) !== null && _e !== void 0 ? _e : data.StatusMessage) !== null && _f !== void 0 ? _f : '', + }]; + } + }); + }); +} +/** + * 发送项目创建通知 + */ +function notifyProjectCreated(projectName_1, goal_1, receiveId_1) { + return __awaiter(this, arguments, void 0, function (projectName, goal, receiveId, receiveIdType) { + if (receiveIdType === void 0) { receiveIdType = 'open_id'; } + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, sendFeishuMessage({ + text: "\uD83D\uDE80 \u65B0\u9879\u76EE\u5DF2\u521B\u5EFA\n\n\u9879\u76EE\uFF1A".concat(projectName, "\n\u76EE\u6807\uFF1A").concat(goal, "\n\n\u8BF7\u53CA\u65F6\u67E5\u770B\u5E76\u786E\u8BA4\u9879\u76EE\u7AE0\u7A0B\u3002"), + receiveId: receiveId, + receiveIdType: receiveIdType, + useApp: true, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +/** + * 发送里程碑提醒 + */ +function notifyMilestoneReminder(milestoneName_1, targetDate_1, receiveId_1) { + return __awaiter(this, arguments, void 0, function (milestoneName, targetDate, receiveId, receiveIdType) { + if (receiveIdType === void 0) { receiveIdType = 'open_id'; } + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, sendFeishuMessage({ + text: "\u23F0 \u91CC\u7A0B\u7891\u63D0\u9192\n\n\u91CC\u7A0B\u7891\u300C".concat(milestoneName, "\u300D\u5373\u5C06\u5230\u671F\n\u76EE\u6807\u65E5\u671F\uFF1A").concat(targetDate, "\n\n\u8BF7\u786E\u8BA4\u8FDB\u5EA6\u662F\u5426\u6B63\u5E38\u3002"), + receiveId: receiveId, + receiveIdType: receiveIdType, + useApp: true, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +/** + * 发送风险预警 + */ +function notifyRiskAlert(riskDesc_1, priority_1, receiveId_1) { + return __awaiter(this, arguments, void 0, function (riskDesc, priority, receiveId, receiveIdType) { + var level; + if (receiveIdType === void 0) { receiveIdType = 'open_id'; } + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + level = priority >= 15 ? '🔴 高' : priority >= 10 ? '🟡 中' : '🟢 低'; + return [4 /*yield*/, sendFeishuMessage({ + text: "\u26A0\uFE0F \u98CE\u9669\u9884\u8B66\n\n\u98CE\u9669\uFF1A".concat(riskDesc, "\n\u4F18\u5148\u7EA7\uFF1A").concat(level, "\uFF08").concat(priority, "\u5206\uFF09\n\n\u8BF7\u8BC4\u4F30\u5E76\u5236\u5B9A\u5E94\u5BF9\u63AA\u65BD\u3002"), + receiveId: receiveId, + receiveIdType: receiveIdType, + useApp: true, + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); +} +/** + * 生成飞书卡片消息 + */ +function buildDecisionCard(card) { + var elements = [ + { + tag: 'div', + text: { tag: 'plain_text', content: card.description }, + }, + ]; + var actions = card.options.map(function (opt) { return ({ + tag: 'button', + text: { tag: 'plain_text', content: opt.label }, + type: opt.style === 'danger' ? 'danger' : opt.style === 'primary' ? 'primary' : 'default', + value: { action: opt.key }, + }); }); + elements.push({ tag: 'action', actions: actions }); + return { + msg_type: 'interactive', + card: { + header: { + title: { tag: 'plain_text', content: card.title }, + template: 'blue', + }, + elements: elements, + }, + }; +} +/** + * 发送决策卡片 + */ +function sendDecisionCard(card_1, receiveId_1) { + return __awaiter(this, arguments, void 0, function (card, receiveId, receiveIdType) { + var token, url, res, data; + if (receiveIdType === void 0) { receiveIdType = 'open_id'; } + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getTenantToken()]; + case 1: + token = _a.sent(); + url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=".concat(receiveIdType); + return [4 /*yield*/, fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': "Bearer ".concat(token), + }, + body: JSON.stringify({ + receive_id: receiveId, + msg_type: 'interactive', + card: buildDecisionCard(card), + }), + })]; + case 2: + res = _a.sent(); + return [4 /*yield*/, res.json()]; + case 3: + data = _a.sent(); + return [2 /*return*/, { + ok: data.code === 0, + code: data.code, + msg: data.msg, + }]; + } + }); + }); +} diff --git a/dist-server/server/index.js b/dist-server/server/index.js new file mode 100644 index 0000000..f03e09e --- /dev/null +++ b/dist-server/server/index.js @@ -0,0 +1,86 @@ +"use strict"; +/** + * FlowPilot 后端入口 + * Hono框架,提供REST API + 飞书事件回调 + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DECISION_TEMPLATES = exports.getPendingDecisions = exports.createDecision = exports.handleDecisionCallback = exports.sendDecisionCard = exports.notifyRiskAlert = exports.notifyMilestoneReminder = exports.notifyProjectCreated = exports.sendFeishuMessage = exports.executionApiHandlers = void 0; +exports.handleDecompose = handleDecompose; +exports.handleFeishuCallback = handleFeishuCallback; +var execution_api_1 = require("./execution-api"); +Object.defineProperty(exports, "executionApiHandlers", { enumerable: true, get: function () { return execution_api_1.executionApiHandlers; } }); +var feishu_1 = require("./feishu"); +Object.defineProperty(exports, "sendFeishuMessage", { enumerable: true, get: function () { return feishu_1.sendFeishuMessage; } }); +Object.defineProperty(exports, "notifyProjectCreated", { enumerable: true, get: function () { return feishu_1.notifyProjectCreated; } }); +Object.defineProperty(exports, "notifyMilestoneReminder", { enumerable: true, get: function () { return feishu_1.notifyMilestoneReminder; } }); +Object.defineProperty(exports, "notifyRiskAlert", { enumerable: true, get: function () { return feishu_1.notifyRiskAlert; } }); +Object.defineProperty(exports, "sendDecisionCard", { enumerable: true, get: function () { return feishu_1.sendDecisionCard; } }); +var decision_cards_1 = require("../lib/decision-cards"); +Object.defineProperty(exports, "handleDecisionCallback", { enumerable: true, get: function () { return decision_cards_1.handleDecisionCallback; } }); +Object.defineProperty(exports, "createDecision", { enumerable: true, get: function () { return decision_cards_1.createDecision; } }); +Object.defineProperty(exports, "getPendingDecisions", { enumerable: true, get: function () { return decision_cards_1.getPendingDecisions; } }); +Object.defineProperty(exports, "DECISION_TEMPLATES", { enumerable: true, get: function () { return decision_cards_1.DECISION_TEMPLATES; } }); +var hr_manager_1 = require("../lib/hr-manager"); +var experience_manager_1 = require("../lib/experience-manager"); +// --- Route definitions (to be wired with Hono) --- +/** + * API路由表 + * + * POST /api/projects - 创建项目 + * GET /api/projects/:id - 获取项目 + * GET /api/projects/:id/tasks - 获取任务列表 + * POST /api/projects/:id/tasks - 创建任务 + * PATCH /api/projects/:id/tasks/:tid - 更新任务 + * GET /api/projects/:id/executions - 获取执行记录 + * POST /api/projects/:id/executions - 创建执行记录 + * GET /api/projects/:id/decisions - 获取决策记录 + * GET /api/projects/:id/stats - 获取项目统计 + * POST /api/projects/:id/decompose - 触发任务拆解 + * POST /api/feishu/webhook - 飞书事件回调 + * POST /api/feishu/decision/callback - 飞书决策卡片回调 + */ +/** + * 任务拆解API + */ +function handleDecompose(highLevelTask, context) { + var hrManager = new hr_manager_1.HRManager(); + var experienceManager = new experience_manager_1.ExperienceManager(); + // 1. Decompose + var atomicTasks = hrManager.decompose(highLevelTask, context); + // 2. Get context for each task + var tasksWithContext = atomicTasks.map(function (task) { + var ctx = experienceManager.getContext(task.atomicType || hr_manager_1.AtomicTaskType.FILL_TEMPLATE); + return __assign(__assign({}, task), { model: task.atomicType ? hrManager.selectModel(task.atomicType) : 'gpt-4o-mini', contextSuggestion: ctx.suggestion }); + }); + return { + highLevelTask: highLevelTask, + decomposedCount: tasksWithContext.length, + tasks: tasksWithContext, + }; +} +/** + * 飞书回调处理 + */ +function handleFeishuCallback(event) { + switch (event.type) { + case 'im.message.receive_v1': + // Handle incoming message + return { ok: true, message: 'Message received' }; + case 'card.action.trigger': + // Handle card action (decision callback) + return { ok: true, message: 'Action processed' }; + default: + return { ok: true }; + } +} diff --git a/dist-server/server/main.js b/dist-server/server/main.js new file mode 100644 index 0000000..062800f --- /dev/null +++ b/dist-server/server/main.js @@ -0,0 +1,204 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var hono_1 = require("hono"); +var cors_1 = require("hono/cors"); +var logger_1 = require("hono/logger"); +var execution_api_1 = require("./execution-api"); +var index_1 = require("./index"); +var feishu_1 = require("./feishu"); +// In-memory stores +var projects = {}; +var app = new hono_1.Hono(); +// Middleware +app.use('*', (0, cors_1.cors)()); +app.use('*', (0, logger_1.logger)()); +// Health check +app.get('/api/health', function (c) { return c.json({ status: 'ok', version: '0.5.0', message: 'FlowPilot API is running' }); }); +// Project routes +app.post('/api/projects', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var body, projectId, project, e_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + body = _a.sent(); + projectId = "proj-".concat(Date.now()); + project = __assign(__assign({ id: projectId }, body), { status: 'active', createdAt: new Date().toISOString() }); + projects[projectId] = project; + _a.label = 2; + case 2: + _a.trys.push([2, 4, , 5]); + return [4 /*yield*/, (0, feishu_1.notifyProjectCreated)(project.name || '未命名项目', project.goal || '无目标', 'ou_41d14aca8278e605d98e33b1221777e4', // hardcoded open_id for now + 'open_id')]; + case 3: + _a.sent(); + return [3 /*break*/, 5]; + case 4: + e_1 = _a.sent(); + console.error('Failed to send Feishu notification:', e_1); + return [3 /*break*/, 5]; + case 5: return [2 /*return*/, c.json(project)]; + } + }); +}); }); +app.get('/api/projects/:id', function (c) { + var id = c.req.param('id'); + return c.json(projects[id] || { error: 'Project not found' }); +}); +// Task routes +app.get('/api/projects/:id/tasks', function (c) { + return c.json({ tasks: [], projectId: c.req.param('id') }); +}); +app.post('/api/projects/:id/tasks', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var body; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + body = _a.sent(); + return [2 /*return*/, c.json(__assign(__assign({ id: "task-".concat(Date.now()), projectId: c.req.param('id') }, body), { status: 'todo', createdAt: new Date().toISOString() }))]; + } + }); +}); }); +app.patch('/api/projects/:id/tasks/:taskId', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var body; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + body = _a.sent(); + return [2 /*return*/, c.json(__assign(__assign({ id: c.req.param('taskId'), projectId: c.req.param('id') }, body), { updatedAt: new Date().toISOString() }))]; + } + }); +}); }); +// Execution routes +app.get('/api/projects/:id/executions', function (c) { + var records = execution_api_1.executionApiHandlers.getExecutions(c.req.param('id')); + return c.json({ executions: records }); +}); +app.post('/api/projects/:id/executions', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var body, record; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + body = _a.sent(); + record = execution_api_1.executionApiHandlers.createExecution(c.req.param('id'), body); + return [2 /*return*/, c.json(record, 201)]; + } + }); +}); }); +app.get('/api/projects/:id/stats', function (c) { + var stats = execution_api_1.executionApiHandlers.getStats(c.req.param('id')); + return c.json(stats); +}); +// Decompose route +app.post('/api/projects/:id/decompose', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var body, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + body = _a.sent(); + result = (0, index_1.handleDecompose)(body.task, body.context); + return [2 /*return*/, c.json(result)]; + } + }); +}); }); +// Decision routes +app.get('/api/projects/:id/decisions', function (c) { + var records = execution_api_1.executionApiHandlers.getDecisions(c.req.param('id')); + return c.json({ decisions: records }); +}); +app.post('/api/projects/:id/decisions', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var body, record; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + body = _a.sent(); + record = execution_api_1.executionApiHandlers.createDecision(c.req.param('id'), body); + return [2 /*return*/, c.json(record, 201)]; + } + }); +}); }); +// Feishu webhook +app.post('/api/feishu/webhook', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var event, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + event = _a.sent(); + result = (0, index_1.handleFeishuCallback)(event); + return [2 /*return*/, c.json(result)]; + } + }); +}); }); +// Feishu card callback +app.post('/api/feishu/decision/callback', function (c) { return __awaiter(void 0, void 0, void 0, function () { + var body, action, value; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, c.req.json()]; + case 1: + body = _a.sent(); + action = body.action, value = body.value; + // TODO: handle decision callback, update decision log, etc. + return [2 /*return*/, c.json({ ok: true })]; + } + }); +}); }); +var port = Number(process.env.PORT) || 3001; +console.log("\uD83D\uDE80 FlowPilot API server running on http://localhost:".concat(port)); +exports.default = { + port: port, + fetch: app.fetch, +}; diff --git a/package.json b/package.json index 9381dac..6f665ab 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,12 @@ "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.0", "concurrently": "^9.0.0", "eslint": "^9.0.0", "tsx": "^4.19.0", "typescript": "^5.7.0", "vite": "^6.0.0", - "@vitejs/plugin-react": "^4.3.0", "vitest": "^3.0.0" } } diff --git a/src/lib/flow.ts b/src/lib/flow.ts index 7bf989c..eaef072 100644 --- a/src/lib/flow.ts +++ b/src/lib/flow.ts @@ -6,7 +6,7 @@ import { generateCharter, generateStakeholderRegister, ProjectData } from './charter'; import { HRManager } from './hr-manager'; import { ExperienceManager } from './experience-manager'; -import { notifyProjectCreated, notifyMilestoneReminder, notifyRiskAlert, notifyDecisionRequired } from '../server/feishu'; +import { notifyProjectCreated, notifyMilestoneReminder, notifyRiskAlert, sendDecisionCard } from '../server/feishu'; import { createDecision, DECISION_TEMPLATES, getPendingDecisions } from './decision-cards'; import { getChecklistByPhase, getPhaseCompletion, ChecklistItem } from './checklists'; @@ -74,7 +74,7 @@ export async function runProjectCreationFlow(data: ProjectData): Promise<{ // Step 6: Send notification let notificationSent = false; try { - await notifyProjectCreated(data.name, data.goal); + await notifyProjectCreated(data.name, data.goal, 'ou_41d14aca8278e605d98e33b1221777e4', 'open_id'); notificationSent = true; } catch { notificationSent = false; diff --git a/src/server/feishu.ts b/src/server/feishu.ts index d1e58d7..1906b02 100644 --- a/src/server/feishu.ts +++ b/src/server/feishu.ts @@ -1,64 +1,107 @@ /** * 飞书消息发送模块 - * 通过飞书自定义机器人Webhook发送项目通知 + * 支持两种方式: + * 1. 应用身份(推荐):使用 App ID/App Secret 获取 tenant_token 调用开放 API + * 2. Webhook 方式:直接调用自定义机器人 Webhook(向后兼容) */ -const FEISHU_WEBHOOK = 'https://open.feishu.cn/open-apis/bot/v2/hook/58321c74-5881-4f41-bcd4-85f4d7c5b3c1'; -const FEISHU_SECRET = 'UgCdzrcci4s9YS1GSAHt4e'; - import { createHmac } from 'crypto'; +// 配置:从环境变量或 TOOLS.md 读取 +const FEISHU_APP_ID = process.env.FEISHU_APP_ID || 'cli_a95093447cb85cdd'; +const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET || 'd17CeffVfOnTkQo8LIP7hbhOQwSPv7Jv'; +const FEISHU_WEBHOOK = process.env.FEISHU_WEBHOOK || 'https://open.feishu.cn/open-apis/bot/v2/hook/58321c74-5881-4f41-bcd4-85f4d7c5b3c1'; +const FEISHU_WEBHOOK_SECRET = process.env.FEISHU_WEBHOOK_SECRET || 'UgCdzrcci4s9YS1GSAHt4e'; + /** - * 生成飞书签名 + * 使用应用身份获取 tenant_access_token */ -function generateSign(timestamp: number, secret: string): string { +async function getTenantToken(): Promise { + const res = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ app_id: FEISHU_APP_ID, app_secret: FEISHU_APP_SECRET }), + }); + const data = await res.json(); + if (data.code !== 0) { + throw new Error(`Failed to get tenant token: ${data.msg}`); + } + return data.tenant_access_token; +} + +/** + * 生成 Webhook 签名 + */ +function generateWebhookSign(timestamp: number, secret: string): string { const stringToSign = `${timestamp}\n${secret}`; const hmac = createHmac('sha256', stringToSign); return hmac.digest('base64'); } +/** + * 发送飞书文本消息(支持两种方式) + */ export interface FeishuMessageOptions { /** 消息内容 */ text: string; - /** 是否使用签名校验 */ - sign?: boolean; + /** 接收者 ID(open_id、user_id、chat_id 等) */ + receiveId: string; + /** 接收者 ID 类型:open_id、user_id、chat_id、email */ + receiveIdType?: 'open_id' | 'user_id' | 'chat_id' | 'email'; + /** 是否使用应用身份(默认 true),若为 false 则使用 webhook */ + useApp?: boolean; } /** - * 发送飞书文本消息 + * 发送文本消息 */ export async function sendFeishuMessage(options: FeishuMessageOptions): Promise<{ ok: boolean; code: number; msg: string }> { - const { text, sign = true } = options; - const timestamp = Math.floor(Date.now() / 1000); + const { text, receiveId, receiveIdType = 'open_id', useApp = true } = options; - const body: Record = { - msg_type: 'text', - content: { text }, - }; + if (useApp) { + // 使用应用身份 + const token = await getTenantToken(); + const url = `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`; + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + receive_id: receiveId, + msg_type: 'text', + content: JSON.stringify({ text }), + }), + }); + const data = await res.json(); + return { + ok: data.code === 0, + code: data.code, + msg: data.msg, + }; + } else { + // 使用 webhook + const timestamp = Math.floor(Date.now() / 1000); + const body: Record = { + msg_type: 'text', + content: { text }, + }; + if (FEISHU_WEBHOOK_SECRET) { + body.timestamp = String(timestamp); + body.sign = generateWebhookSign(timestamp, FEISHU_WEBHOOK_SECRET); + } - if (sign) { - body.timestamp = String(timestamp); - body.sign = generateSign(timestamp, FEISHU_SECRET); - } - - try { - const response = await fetch(FEISHU_WEBHOOK, { + const res = await fetch(FEISHU_WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); - - const result = await response.json(); + const data = await res.json(); return { - ok: result.code === 0 || result.StatusCode === 0, - code: result.code ?? result.StatusCode ?? -1, - msg: result.msg ?? result.StatusMessage ?? '', - }; - } catch (error) { - return { - ok: false, - code: -1, - msg: error instanceof Error ? error.message : 'Unknown error', + ok: data.code === 0 || data.StatusCode === 0, + code: data.code ?? data.StatusCode ?? -1, + msg: data.msg ?? data.StatusMessage ?? '', }; } } @@ -66,37 +109,103 @@ export async function sendFeishuMessage(options: FeishuMessageOptions): Promise< /** * 发送项目创建通知 */ -export async function notifyProjectCreated(projectName: string, goal: string): Promise { +export async function notifyProjectCreated(projectName: string, goal: string, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise { await sendFeishuMessage({ text: `🚀 新项目已创建\n\n项目:${projectName}\n目标:${goal}\n\n请及时查看并确认项目章程。`, + receiveId, + receiveIdType, + useApp: true, }); } /** * 发送里程碑提醒 */ -export async function notifyMilestoneReminder(milestoneName: string, targetDate: string): Promise { +export async function notifyMilestoneReminder(milestoneName: string, targetDate: string, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise { await sendFeishuMessage({ text: `⏰ 里程碑提醒\n\n里程碑「${milestoneName}」即将到期\n目标日期:${targetDate}\n\n请确认进度是否正常。`, + receiveId, + receiveIdType, + useApp: true, }); } /** * 发送风险预警 */ -export async function notifyRiskAlert(riskDesc: string, priority: number): Promise { +export async function notifyRiskAlert(riskDesc: string, priority: number, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise { const level = priority >= 15 ? '🔴 高' : priority >= 10 ? '🟡 中' : '🟢 低'; await sendFeishuMessage({ text: `⚠️ 风险预警\n\n风险:${riskDesc}\n优先级:${level}(${priority}分)\n\n请评估并制定应对措施。`, + receiveId, + receiveIdType, + useApp: true, }); } /** - * 发送决策请求 + * 发送决策请求(生成卡片消息) */ -export async function notifyDecisionRequired(title: string, options: string[]): Promise { - const optionList = options.map((o, i) => ` ${i + 1}. ${o}`).join('\n'); - await sendFeishuMessage({ - text: `🔑 需要您的决策\n\n${title}\n\n选项:\n${optionList}\n\n请回复选项编号。`, - }); +export interface DecisionCardOptions { + title: string; + description: string; + options: Array<{ key: string; label: string; style?: 'primary' | 'danger' | 'default' }>; +} + +/** + * 生成飞书卡片消息 + */ +export function buildDecisionCard(card: DecisionCardOptions): Record { + const elements: Record[] = [ + { + tag: 'div', + text: { tag: 'plain_text', content: card.description }, + }, + ]; + + const actions = card.options.map((opt) => ({ + tag: 'button', + text: { tag: 'plain_text', content: opt.label }, + type: opt.style === 'danger' ? 'danger' : opt.style === 'primary' ? 'primary' : 'default', + value: { action: opt.key }, + })); + + elements.push({ tag: 'action', actions }); + + return { + msg_type: 'interactive', + card: { + header: { + title: { tag: 'plain_text', content: card.title }, + template: 'blue', + }, + elements, + }, + }; +} + +/** + * 发送决策卡片 + */ +export async function sendDecisionCard(card: DecisionCardOptions, receiveId: string, receiveIdType: 'open_id' | 'user_id' = 'open_id'): Promise<{ ok: boolean; code: number; msg: string }> { + const token = await getTenantToken(); + const url = `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`; + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + receive_id: receiveId, + msg_type: 'interactive', + card: buildDecisionCard(card), + }), + }); + const data = await res.json(); + return { + ok: data.code === 0, + code: data.code, + msg: data.msg, + }; } diff --git a/src/server/index.ts b/src/server/index.ts index c3fb8bd..eb29346 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,7 +4,7 @@ */ import { executionApiHandlers } from './execution-api'; -import { sendFeishuMessage, notifyProjectCreated, notifyMilestoneReminder, notifyRiskAlert, notifyDecisionRequired } from './feishu'; +import { sendFeishuMessage, notifyProjectCreated, notifyMilestoneReminder, notifyRiskAlert, sendDecisionCard } from './feishu'; import { handleDecisionCallback, createDecision, getPendingDecisions, DECISION_TEMPLATES, DecisionRecord } from '../lib/decision-cards'; import { HRManager, AtomicTaskType } from '../lib/hr-manager'; import { ExperienceManager } from '../lib/experience-manager'; @@ -75,5 +75,5 @@ export function handleFeishuCallback(event: { } // Re-export for convenience -export { executionApiHandlers, sendFeishuMessage, notifyProjectCreated, notifyMilestoneReminder, notifyRiskAlert, notifyDecisionRequired, handleDecisionCallback, createDecision, getPendingDecisions, DECISION_TEMPLATES }; +export { executionApiHandlers, sendFeishuMessage, notifyProjectCreated, notifyMilestoneReminder, notifyRiskAlert, sendDecisionCard, handleDecisionCallback, createDecision, getPendingDecisions, DECISION_TEMPLATES }; export type { DecisionRecord }; diff --git a/src/server/main.ts b/src/server/main.ts index 327672d..2db7edb 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -3,7 +3,13 @@ import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { executionApiHandlers } from './execution-api'; import { handleDecompose, handleFeishuCallback } from './index'; -import { notifyProjectCreated } from './feishu'; +import { sendFeishuMessage, notifyProjectCreated, notifyMilestoneReminder, notifyRiskAlert, sendDecisionCard } from './feishu'; +import { handleDecisionCallback, createDecision, getPendingDecisions, DECISION_TEMPLATES, DecisionRecord } from '../lib/decision-cards'; +import { HRManager, AtomicTaskType } from '../lib/hr-manager'; +import { ExperienceManager } from '../lib/experience-manager'; + +// In-memory stores +const projects: Record = {}; const app = new Hono(); @@ -12,21 +18,38 @@ app.use('*', cors()); app.use('*', logger()); // Health check -app.get('/api/health', (c) => c.json({ status: 'ok', version: '0.5.0' })); +app.get('/api/health', (c) => c.json({ status: 'ok', version: '0.5.0', message: 'FlowPilot API is running' })); // Project routes app.post('/api/projects', async (c) => { const body = await c.req.json(); - return c.json({ - id: `proj-${Date.now()}`, + const projectId = `proj-${Date.now()}`; + const project = { + id: projectId, ...body, status: 'active', createdAt: new Date().toISOString(), - }); + }; + projects[projectId] = project; + + // Send Feishu notification + try { + await notifyProjectCreated( + project.name || '未命名项目', + project.goal || '无目标', + 'ou_41d14aca8278e605d98e33b1221777e4', // hardcoded open_id for now + 'open_id' + ); + } catch (e) { + console.error('Failed to send Feishu notification:', e); + } + + return c.json(project); }); app.get('/api/projects/:id', (c) => { - return c.json({ id: c.req.param('id'), status: 'active' }); + const id = c.req.param('id'); + return c.json(projects[id] || { error: 'Project not found' }); }); // Task routes @@ -99,8 +122,11 @@ app.post('/api/feishu/webhook', async (c) => { }); // Feishu card callback -app.post('/api/feishu/card', async (c) => { +app.post('/api/feishu/decision/callback', async (c) => { const body = await c.req.json(); + // Expecting: { action: string, value: { ... } } + const { action, value } = body; + // TODO: handle decision callback, update decision log, etc. return c.json({ ok: true }); }); diff --git a/test-feishu.mjs b/test-feishu.mjs new file mode 100644 index 0000000..af87867 --- /dev/null +++ b/test-feishu.mjs @@ -0,0 +1,17 @@ + +import { sendFeishuMessage } from './src/server/feishu.js'; +async function test() { + try { + const result = await sendFeishuMessage({ + text: '✅ FlowPilot 后端服务测试消息', + receiveId: 'ou_41d14aca8278e605d98e33b1221777e4', + receiveIdType: 'open_id', + useApp: true, + }); + console.log('Notification sent:', result); + } catch (e) { + console.error('Failed to send:', e.message); + } +} +test(); +