Files
Agent-System/web/src/hooks/useDocumentLifecycleActions.ts
T

299 lines
9.5 KiB
TypeScript

import type { Dispatch, SetStateAction } from 'react';
import {
approverDecision,
claimDocument,
mapLeaseToAstEditLease,
reviewDecision,
submitForReview,
type DocGovRecord,
} from '../api/docgov';
import { requestReopen } from '../api/projectTrace';
import { fetchProjectSettings } from '../api/projectSettings';
import type { DocumentAST } from '../types/block';
import type { DocLifecycleState, UserProfile } from '../types/docgov';
import type { ChatMessage, DeliverableItem } from '../types/workbench';
export interface DocumentLifecycleInput {
docAST: DocumentAST;
setDocAST: (next: DocumentAST) => void;
activeDocId: string;
currentUser: UserProfile;
activeSessionId: string;
projectId?: number;
workflowInstanceId?: number;
setDeliverables: Dispatch<SetStateAction<DeliverableItem[]>>;
setMessages: Dispatch<SetStateAction<ChatMessage[]>>;
}
export interface DocumentLifecycleActions {
handleClaim: () => Promise<void>;
handleSubmitReview: () => Promise<void>;
handlePassReview: () => Promise<void>;
handleRejectReview: (reason: string) => Promise<void>;
handleApprove: () => Promise<void>;
handleRejectApprove: (reason: string) => Promise<void>;
handleRequestReopen: () => Promise<void>;
}
export function useDocumentLifecycleActions(input: DocumentLifecycleInput): DocumentLifecycleActions {
const {
docAST,
setDocAST,
activeDocId,
currentUser,
activeSessionId,
projectId,
workflowInstanceId,
setDeliverables,
setMessages,
} = input;
const applyDocGovRecord = (doc: DocGovRecord) => {
// Prefer API current_state; never silently keep reviewing after reject if server omitted field.
const apiState = (doc.current_state || '').trim();
const nextState = (apiState || docAST.lifecycleState) as DocLifecycleState;
const nextClaimed = doc.author_claimed ?? docAST.isClaimed;
const lease = mapLeaseToAstEditLease(doc);
const nextAst: DocumentAST = {
...docAST,
lifecycleState: nextState,
isClaimed: nextClaimed,
authorClaimedAt: doc.author_claimed_at || docAST.authorClaimedAt,
authorId: doc.author_id ?? docAST.authorId,
author: doc.author_name || docAST.author,
reviewerId: doc.reviewer_id ?? docAST.reviewerId,
reviewer: doc.reviewer_name || docAST.reviewer,
approverId: doc.approver_id ?? docAST.approverId,
approver: doc.approver_name || docAST.approver,
dutyPolicy: doc.duty_policy || docAST.dutyPolicy,
contentSha256: doc.content_sha256 || docAST.contentSha256,
editLease: lease ?? docAST.editLease,
};
setDocAST(nextAst);
setDeliverables((prev) =>
prev.map((d) =>
d.docId === activeDocId || d.docId === doc.doc_id
? {
...d,
lifecycleState: nextState,
isClaimed: nextClaimed,
authorId: doc.author_id ?? d.authorId,
author: doc.author_name || d.author,
reviewerId: doc.reviewer_id ?? d.reviewerId,
reviewer: doc.reviewer_name || d.reviewer,
approverId: doc.approver_id ?? d.approverId,
approver: doc.approver_name || d.approver,
dutyPolicy: doc.duty_policy || d.dutyPolicy,
editLease: lease ?? d.editLease,
}
: d,
),
);
};
const pushDocGovError = (actionLabel: string, err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
setMessages((prev) => [
...prev,
{
id: 'docgov-err-' + Date.now(),
sessionId: activeSessionId,
role: 'system',
content: `⚠️ ${actionLabel}失败:${message}`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
};
const handleClaim = async () => {
try {
const doc = await claimDocument({
doc_id: docAST.docId,
user_id: currentUser.id,
user_name: currentUser.name,
user_role: 'author',
});
applyDocGovRecord(doc);
} catch (err) {
pushDocGovError('认领文档', err);
}
};
const handleSubmitReview = async () => {
try {
const doc = await submitForReview({
doc_id: docAST.docId,
user_id: currentUser.id,
user_name: currentUser.name,
});
applyDocGovRecord(doc);
} catch (err) {
pushDocGovError('提交送审', err);
}
};
const handlePassReview = async () => {
try {
const doc = await reviewDecision({
doc_id: docAST.docId,
user_id: currentUser.id,
user_name: currentUser.name,
action: 'approve',
});
applyDocGovRecord(doc);
} catch (err) {
pushDocGovError('审核通过', err);
}
};
const handleRejectReview = async (reason: string) => {
try {
const doc = await reviewDecision({
doc_id: docAST.docId,
user_id: currentUser.id,
user_name: currentUser.name,
action: 'reject',
reason,
});
applyDocGovRecord(doc);
setMessages((prev) => [
...prev,
{
id: 'rej-' + Date.now(),
sessionId: activeSessionId,
role: 'system',
content: `⚠️ ${currentUser.name}(审核)驳回了《${docAST.title}》,已退回编制阶段。驳回原因:${reason}`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
} catch (err) {
pushDocGovError('审核驳回', err);
throw err;
}
};
const handleApprove = async () => {
try {
const doc = await approverDecision({
doc_id: docAST.docId,
user_id: currentUser.id,
user_name: currentUser.name,
action: 'approve',
});
applyDocGovRecord(doc);
setMessages((prev) => [
...prev,
{
id: 'appr-' + Date.now(),
sessionId: activeSessionId,
role: 'assistant',
authorName: '水木东方协同参谋',
intentTag: '🏆 终审批准生效',
content: `🎉 祝贺!${currentUser.name}(批准人)已终审批准《${docAST.title}》!系统已自动对该受控交付物进行微粒度章节切片与向量化,成功注入全局标杆案例经验库!`,
timestamp: new Date().toTimeString().substring(0, 5),
recommendedCases: [
{
docId: docAST.docId,
taskKey: docAST.taskKey,
artifactTitle: docAST.title,
sectionTitle: '终审已生效标杆案例',
similarityScore: 1.0,
keyRegulations: ['GB 9706.1-2020', 'YY 0569-2011', 'IEC 60601-1-2'],
summarySnippet:
'本交付物已通过编制、审核、批准法定三级签署闭环,作为往期标杆经验向全平台开放自推荐。',
},
],
},
]);
} catch (err) {
pushDocGovError('终审批准', err);
}
};
const handleRejectApprove = async (reason: string) => {
try {
const doc = await approverDecision({
doc_id: docAST.docId,
user_id: currentUser.id,
user_name: currentUser.name,
action: 'reject',
reason,
rollback_to: 'drafting',
});
applyDocGovRecord(doc);
setMessages((prev) => [
...prev,
{
id: 'rej-appr-' + Date.now(),
sessionId: activeSessionId,
role: 'system',
content: `⚠️ ${currentUser.name}(批准人)驳回了《${docAST.title}》,已退回编制阶段。驳回原因:${reason}`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
} catch (err) {
pushDocGovError('批准驳回', err);
throw err;
}
};
const handleRequestReopen = async () => {
if (!projectId || projectId <= 0) {
pushDocGovError('申请再修订', new Error('缺少项目上下文'));
return;
}
const reason = window.prompt('请输入再修订原因(将通知原批准人审批)');
if (!reason || !reason.trim()) return;
try {
let instanceId = workflowInstanceId || 0;
let stageKey = docAST.version || '';
try {
const settings = await fetchProjectSettings(projectId);
if (settings?.live_instance?.id) {
instanceId = settings.live_instance.id;
}
if (settings?.active_stage_key) {
stageKey = settings.active_stage_key;
}
} catch {
/* keep fallbacks */
}
if (!stageKey) stageKey = 'V0.5';
const stored = await requestReopen(projectId, {
instance_id: instanceId,
stage_key: stageKey,
doc_task_keys: [docAST.taskKey],
reason: reason.trim(),
});
setMessages((prev) => [
...prev,
{
id: 'reopen-' + Date.now(),
sessionId: activeSessionId,
role: 'system',
content:
stored.status === 'approved'
? `《${docAST.title}》再修订申请已自动批准,文档已回编制期。`
: `已提交《${docAST.title}》再修订申请(#${stored.id}),等待原批准人审批。`,
timestamp: new Date().toTimeString().substring(0, 5),
},
]);
if (stored.status === 'approved') {
setDocAST({ ...docAST, lifecycleState: 'drafting' as DocLifecycleState, isClaimed: docAST.isClaimed });
}
} catch (err) {
pushDocGovError('申请再修订', err);
}
};
return {
handleClaim,
handleSubmitReview,
handlePassReview,
handleRejectReview,
handleApprove,
handleRejectApprove,
handleRequestReopen,
};
}