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

334 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)];
}
/**
* 构建审核驳回流转确认交互卡片(仅供审核/批准执行路径;编制人查询请用 createRejectReasonInfoCard)
*/
export function createRejectConfirmationCard(
targetDoc: { docId: string; title: string },
matchingBlock?: DocumentBlock,
options?: { reason?: string; mode?: 'review' | 'final' }
): InteractiveCardData {
const cardId = 'card-reject-' + Date.now();
const sectionLabel = matchingBlock?.numbering
? `${matchingBlock.numbering} ${matchingBlock.content.slice(0, 24)}`
: '第3节「电气安全与合规风险对标」';
const mode = options?.mode || 'review';
const reason =
options?.reason ||
'【驳回审查意见】关键技术指标与竞品对比不充分,需补充电气安全与EMC合规佐证,退回编制人重新修订。';
return {
cardId,
cardType: 'CONFIRMATION',
title: mode === 'final' ? '终审驳回确认' : '审核驳回与返工确认',
targetDocId: targetDoc.docId,
targetDocTitle: targetDoc.title,
targetSectionId: matchingBlock?.id || 'sec-3',
sectionTitle: sectionLabel,
proposedContent: reason,
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,
target_action: mode === 'final' ? 'confirm_final_reject' : 'confirm_reject',
},
},
{
actionId: 'cancel',
label: '放弃驳回',
style: 'subtle',
payload: {
docId: targetDoc.docId,
},
},
],
};
}
/**
* 构建驳回原因说明卡(编制人/查询路径;无危险流转按钮)
*/
export function createRejectReasonInfoCard(
targetDoc: { docId: string; title: string },
reason: string,
opts?: { includeStartRevision?: boolean; sectionLabel?: string; sectionId?: string }
): InteractiveCardData {
const actions: InteractiveCardData['actions'] = [
{
actionId: 'ack_reject_reason',
label: '知道了',
style: 'primary',
isAffirmativeDefault: true,
payload: { docId: targetDoc.docId },
},
];
if (opts?.includeStartRevision !== false) {
actions.push({
actionId: 'start_revision',
label: '开始按意见修订',
style: 'subtle',
payload: { docId: targetDoc.docId },
});
}
return {
cardId: 'card-reject-reason-' + Date.now(),
cardType: 'REMINDER_NOTICE',
title: '驳回原因说明',
targetDocId: targetDoc.docId,
targetDocTitle: targetDoc.title,
targetSectionId: opts?.sectionId,
sectionTitle: opts?.sectionLabel,
proposedContent: reason,
impactRisk: '本卡片为查询说明,不会变更文档生命周期状态',
status: 'active',
ttlTurns: 5,
createdAt: new Date().toTimeString().substring(0, 5),
actions,
};
}
/**
* 从文档 AST 中提取最近一条 issue 类驳回批注
*/
export function extractLatestRejectReasonFromAst(ast: DocumentAST): string | undefined {
const collect = (block: DocumentBlock | undefined): string[] => {
if (!block) return [];
const out: string[] = [];
for (const c of block.comments || []) {
if (c.type === 'issue' || (c.content && /驳回|退回|缺少|需补充/.test(c.content))) {
out.push(c.content);
}
}
for (const child of block.children || []) {
out.push(...collect(child));
}
return out;
};
const reasons = collect(ast.rootBlock);
return reasons.length > 0 ? reasons[reasons.length - 1] : undefined;
}
/**
* 判定用户输入是否属于肯定型双轨回复(好、确认、同意等)
*/
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;
});
}