- P0-8: 决策交互卡片(飞书卡片+回调+4种模板) - P0-10: 执行记录REST API(Hono框架+统计接口) - P0-11: 创建流程串联(向导→章程→任务→看板→通知) - P0-12: GitHub Actions CI/CD - P0-14: Dockerfile + docker-compose部署 - 前端入口+Vite配置+项目结构完善 - CHANGELOG + PROGRESS更新
173 lines
4.7 KiB
TypeScript
173 lines
4.7 KiB
TypeScript
/**
|
||
* 决策交互卡片
|
||
* 飞书消息卡片格式 + 回调处理
|
||
*/
|
||
|
||
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 },
|
||
}),
|
||
};
|