Files
Agent-System/web/src/engine/cardActionEngine.ts
T

265 lines
7.6 KiB
TypeScript

import { DocumentAST, DocumentBlock, BlockComment } from '../types/block';
import { ChatMessage, InteractiveCardData } from '../types/workbench';
/**
* 扫描受控文档 AST,寻找与审核审查最匹配的锚定段落块 (如电气安全、EMC、竞品对比、临床痛点)
*/
export function findMatchingBlockForReview(ast: DocumentAST, hint?: string): DocumentBlock {
const children = ast.rootBlock?.children || [];
if (children.length === 0) {
return ast.rootBlock;
}
// 1. 若外部指定了段落 ID 或文本线索,优先精确/包含匹配
if (hint && hint.trim()) {
const trimmed = hint.trim().toLowerCase();
const matched = children.find(
(b) =>
b.id === hint ||
b.content.toLowerCase().includes(trimmed) ||
(b.properties?.tableData &&
JSON.stringify(b.properties.tableData).toLowerCase().includes(trimmed))
);
if (matched) return matched;
}
// 2. 依次匹配关键合规与审查高频词
const keywords = ['电气安全', 'emc', '电磁兼容', '竞品', '对标', '临床痛点', '性能矩阵'];
for (const kw of keywords) {
const matched = children.find(
(b) =>
b.content.toLowerCase().includes(kw) ||
(b.properties?.tableData &&
JSON.stringify(b.properties.tableData).toLowerCase().includes(kw))
);
if (matched) return matched;
}
// 3. 降级:优先返回第 3 个段落块,或第 1 个 H2 标题块,或第 1 个子块
const h2Block = children.find((b) => b.type === 'heading' && b.level === 2);
if (h2Block) return h2Block;
return children[Math.min(2, children.length - 1)];
}
/**
* 构建审核驳回流转确认交互卡片
*/
export function createRejectConfirmationCard(
targetDoc: { docId: string; title: string },
matchingBlock?: DocumentBlock
): InteractiveCardData {
const cardId = 'card-reject-' + Date.now();
const sectionLabel = matchingBlock?.numbering
? `${matchingBlock.numbering} ${matchingBlock.content.slice(0, 24)}`
: '第3节「电气安全与合规风险对标」';
return {
cardId,
cardType: 'CONFIRMATION',
title: '审核驳回与返工确认',
targetDocId: targetDoc.docId,
targetDocTitle: targetDoc.title,
targetSectionId: matchingBlock?.id || 'sec-3',
sectionTitle: sectionLabel,
proposedContent: '【驳回审查意见】关键技术指标与竞品对比不充分,需补充电气安全与EMC合规佐证,退回编制人重新修订。',
impactRisk: '高风险:将退回至草稿编制阶段,正文锁定将解除',
status: 'active',
ttlTurns: 3,
createdAt: new Date().toTimeString().substring(0, 5),
actions: [
{
actionId: 'confirm_reject',
label: '确认驳回并退回草稿',
style: 'danger',
isAffirmativeDefault: true,
payload: {
docId: targetDoc.docId,
reason: '关键竞品性能指标与电气安全指标待补充,退回编制人重新修订。',
targetSection: sectionLabel,
},
},
{
actionId: 'cancel',
label: '放弃操作',
style: 'subtle',
payload: {
docId: targetDoc.docId,
},
},
],
};
}
/**
* 判定用户输入是否属于肯定型双轨回复(好、确认、同意等)
*/
export function isAffirmativeResponse(text: string): boolean {
const lower = text.trim().toLowerCase();
const affirmativeKeywords = ['好', '确认', '同意', '是的', '准许', '批准', '行', '确认驳回', '同意驳回', 'ok', 'yes'];
return affirmativeKeywords.some(
(kw) =>
lower === kw ||
lower.startsWith(kw) ||
(lower.length <= 8 && lower.includes(kw))
);
}
/**
* 查找消息历史中最近一条处于 active 状态的交互卡片消息
*/
export function findLastActiveCard(
messages: ChatMessage[],
sessionId?: string
): { message: ChatMessage; card: InteractiveCardData } | null {
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (sessionId && m.sessionId !== sessionId) continue;
if (m.interactiveCard && m.interactiveCard.status === 'active') {
return { message: m, card: m.interactiveCard };
}
}
return null;
}
/**
* 递减所有 active 卡片的有效交互轮次 (TTL);达到 0 自动归档为 expired
*/
export function decrementActiveCardsTtl(
messages: ChatMessage[],
sessionId?: string
): ChatMessage[] {
return messages.map((m) => {
if (sessionId && m.sessionId !== sessionId) return m;
if (m.interactiveCard && m.interactiveCard.status === 'active') {
const nextTtl = m.interactiveCard.ttlTurns - 1;
return {
...m,
interactiveCard: {
...m.interactiveCard,
ttlTurns: nextTtl,
status: nextTtl <= 0 ? 'expired' : m.interactiveCard.status,
},
};
}
return m;
});
}
/**
* 执行审核驳回对文档 AST 的变更:回退为 drafting 并挂载 issue 批注
*/
export function applyConfirmRejectToDoc(
doc: DocumentAST,
cardId: string,
reviewerName: string,
reason: string,
hint?: string
): { updatedDoc: DocumentAST; matchingBlock: DocumentBlock } {
const matchingBlock = findMatchingBlockForReview(doc, hint);
const commentId = `cmt-reject-${cardId}`;
const issueComment: BlockComment = {
id: commentId,
authorName: reviewerName,
authorRole: 'reviewer',
createdAt: new Date().toTimeString().substring(0, 5),
type: 'issue',
content: reason || '关键竞品性能指标与电气安全指标待补充,退回编制人重新修订。',
resolved: false,
};
// 克隆子块并追加批注
const updatedChildren = (doc.rootBlock.children || []).map((b) => {
if (b.id === matchingBlock.id) {
return {
...b,
comments: [...(b.comments || []), issueComment],
};
}
return b;
});
const updatedDoc: DocumentAST = {
...doc,
lifecycleState: 'drafting',
rootBlock: {
...doc.rootBlock,
children: updatedChildren,
},
};
return { updatedDoc, matchingBlock };
}
/**
* 执行合法窗口期撤销驳回对文档 AST 的变更:恢复为 reviewing 并移除挂载的驳回批注
*/
export function applyUndoRejectToDoc(doc: DocumentAST, cardId: string): DocumentAST {
const commentId = `cmt-reject-${cardId}`;
const updatedChildren = (doc.rootBlock.children || []).map((b) => {
if (b.comments && b.comments.length > 0) {
return {
...b,
comments: b.comments.filter((c) => c.id !== commentId),
};
}
return b;
});
return {
...doc,
lifecycleState: 'reviewing',
rootBlock: {
...doc.rootBlock,
children: updatedChildren,
},
};
}
/**
* 在消息列表中将指定卡片更新为 resolved
*/
export function resolveCardInMessages(
messages: ChatMessage[],
cardId: string,
resolutionSummary: string,
operatorName: string,
resolvedAt?: string
): ChatMessage[] {
return messages.map((m) => {
if (m.interactiveCard && m.interactiveCard.cardId === cardId) {
return {
...m,
interactiveCard: {
...m.interactiveCard,
status: 'resolved',
resolutionSummary,
resolvedAt: resolvedAt || new Date().toISOString(),
resolvedBy: operatorName,
},
};
}
return m;
});
}
/**
* 在消息列表中将指定卡片更新为 cancelled
*/
export function cancelCardInMessages(messages: ChatMessage[], cardId: string): ChatMessage[] {
return messages.map((m) => {
if (m.interactiveCard && m.interactiveCard.cardId === cardId) {
return {
...m,
interactiveCard: {
...m.interactiveCard,
status: 'cancelled',
},
};
}
return m;
});
}