feat: MVP v0.5 完成 - 全部14个P0功能
- P0-8: 决策交互卡片(飞书卡片+回调+4种模板) - P0-10: 执行记录REST API(Hono框架+统计接口) - P0-11: 创建流程串联(向导→章程→任务→看板→通知) - P0-12: GitHub Actions CI/CD - P0-14: Dockerfile + docker-compose部署 - 前端入口+Vite配置+项目结构完善 - CHANGELOG + PROGRESS更新
This commit is contained in:
172
src/lib/decision-cards.ts
Normal file
172
src/lib/decision-cards.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 决策交互卡片
|
||||
* 飞书消息卡片格式 + 回调处理
|
||||
*/
|
||||
|
||||
export type DecisionType = 'approve_reject' | 'multi_choice' | 'confirm';
|
||||
|
||||
export interface DecisionCard {
|
||||
title: string;
|
||||
description: string;
|
||||
type: DecisionType;
|
||||
options: DecisionOption[];
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DecisionOption {
|
||||
key: string;
|
||||
label: string;
|
||||
style?: 'primary' | 'danger' | 'default';
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成飞书消息卡片JSON
|
||||
*/
|
||||
export function buildDecisionCard(card: DecisionCard): Record<string, unknown> {
|
||||
const elements: Record<string, unknown>[] = [
|
||||
{
|
||||
tag: 'div',
|
||||
text: { tag: 'plain_text', content: card.description },
|
||||
},
|
||||
];
|
||||
|
||||
// Action buttons
|
||||
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, ...card.metadata },
|
||||
}));
|
||||
|
||||
elements.push({ tag: 'action', actions });
|
||||
|
||||
return {
|
||||
msg_type: 'interactive',
|
||||
card: {
|
||||
header: {
|
||||
title: { tag: 'plain_text', content: card.title },
|
||||
template: 'blue',
|
||||
},
|
||||
elements,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 决策记录
|
||||
*/
|
||||
export interface DecisionRecord {
|
||||
id: string;
|
||||
cardTitle: string;
|
||||
type: DecisionType;
|
||||
options: string[];
|
||||
chosenOption: string | null;
|
||||
status: 'pending' | 'responded' | 'expired';
|
||||
createdAt: string;
|
||||
respondedAt?: string;
|
||||
}
|
||||
|
||||
const pendingDecisions: Map<string, DecisionRecord> = new Map();
|
||||
|
||||
/**
|
||||
* 创建决策请求
|
||||
*/
|
||||
export function createDecision(card: DecisionCard): DecisionRecord {
|
||||
const id = `decision-${Date.now()}`;
|
||||
const record: DecisionRecord = {
|
||||
id,
|
||||
cardTitle: card.title,
|
||||
type: card.type,
|
||||
options: card.options.map((o) => o.key),
|
||||
chosenOption: null,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
pendingDecisions.set(id, record);
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理决策回调
|
||||
*/
|
||||
export function handleDecisionCallback(
|
||||
decisionId: string,
|
||||
actionKey: string
|
||||
): { ok: boolean; record?: DecisionRecord; error?: string } {
|
||||
const 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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待决策列表
|
||||
*/
|
||||
export function getPendingDecisions(): DecisionRecord[] {
|
||||
return Array.from(pendingDecisions.values()).filter((d) => d.status === 'pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的决策卡片模板
|
||||
*/
|
||||
export const DECISION_TEMPLATES = {
|
||||
/** 章程审批 */
|
||||
charterApproval: (projectName: string): DecisionCard => ({
|
||||
title: '📋 项目章程审批',
|
||||
description: `项目「${projectName}」的章程已生成,请审批。`,
|
||||
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: (riskDesc: string, priority: number): DecisionCard => ({
|
||||
title: `⚠️ 风险应对决策(优先级${priority})`,
|
||||
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 },
|
||||
}),
|
||||
|
||||
/** 变更审批 */
|
||||
changeApproval: (changeDesc: string): DecisionCard => ({
|
||||
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: (name: string, date: string): DecisionCard => ({
|
||||
title: `🏁 里程碑达成确认`,
|
||||
description: `里程碑「${name}」(目标:${date})是否已达成?`,
|
||||
type: 'confirm',
|
||||
options: [
|
||||
{ key: 'done', label: '✅ 已达成', style: 'primary', value: 'reached' },
|
||||
{ key: 'missed', label: '❌ 未达成', style: 'danger', value: 'missed' },
|
||||
],
|
||||
metadata: { type: 'milestone', name, date },
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user