refactor(web): move document lifecycle actions into a hook
Keep claim, review, and approval on one server record so the open document and the sidebar stay in the same state. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -46,7 +46,7 @@ updated: 2026-09-19
|
||||
| FE 缓存 | done(版本字段)。窗口缓存写入 `{schema,data}`,无 schema 的旧载荷丢弃。`window cache: 无 schema` |
|
||||
| FE lucide | 关闭理由:`package-lock.json` 锁定 `lucide-react@1.42.0`,来源 `registry.npmjs.org`,带 integrity。`^1.16.0` 是合法的 1.x 范围,不是来源不明的包 |
|
||||
| M7 | done(出站意图 + 同键重放)。`document_content_projections` 与账本同一事务写入;投影成功才销账。`TestDocRevision_PatchOrder_RetryProjectsWithoutSecondLedgerCommit`。账本库与正文库仍不是一个 SQL 事务,进程退出后靠未投影行重放,不靠客户端记忆 |
|
||||
| FE App.tsx | 部分。登录壳不再静态引用重模块。本地缓存写入在 `useWorkbenchPersistence`。登录后的项目/成员/会话同步在 `useAuthenticatedWorkspaceSync`。受控文档补水与重试在 `useControlledDocumentHydration`。自由文档、工作流对账和编辑租约轮询在 `useServerDocumentSync`。工作台主体仍在 `App.tsx` |
|
||||
| FE App.tsx | 部分。登录壳不再静态引用重模块。本地缓存写入在 `useWorkbenchPersistence`。登录后的项目/成员/会话同步在 `useAuthenticatedWorkspaceSync`。受控文档补水与重试在 `useControlledDocumentHydration`。自由文档、工作流对账和编辑租约轮询在 `useServerDocumentSync`。认领、送审、审核和批准在 `useDocumentLifecycleActions`。发送消息与块编辑仍在 `App.tsx` |
|
||||
| H1-del | 关闭理由:`allowLegacyTestPrincipal` 仍被 `AllowInMemory && ContextDbHost==""` 的 dev/test 内存测试使用。删除会把这批测试变成未登录 401。保留告警,删除另开、先改测试夹具 |
|
||||
|
||||
仍开放:rdev P3 三样本真 PI、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。公式引擎已离开入口包(`index-j0BIrjIR.js` 737.38 kB,br 170,451 B,包内无 katex),U3 仍缺浏览器瀑布,不关。P4 键名空间化已在本分支落地:新清单用带前缀的任务键,旧键与旧 doc_id 仍可读,`TestPublishedTaskKeysAreNamespacedAndDisjoint`。意图短语仍输出旧键,执行路径按别名接上。P2 规划界面已折叠长阶段、可批量纳入人工审批,并在选中 rdev 时提示 CDO 前置。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。2026-09-19 在 `fix/audit-remainder-closeout` 跑过 `cd web && npm test && npm run build`:node:test 414 pass / 0 fail,vitest 32 pass;入口 `index-BFf6n8IA.js` 1,005.01 kB,br 235,285 B。CURRENT 文首 schema 52 与三套容器一致,00053 要等重建后才升。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。
|
||||
|
||||
+17
-190
@@ -49,14 +49,8 @@ import { mergeWorkflowDocumentsFromServer } from './lib/deliverableSync';
|
||||
import { resolveCurrentDocRole } from './lib/currentDocRole';
|
||||
import { holdsDocDuty } from './lib/docGovActionVisibility';
|
||||
import {
|
||||
claimDocument,
|
||||
submitForReview,
|
||||
reviewDecision,
|
||||
approverDecision,
|
||||
proposeModification,
|
||||
decideModificationProposal,
|
||||
mapLeaseToAstEditLease,
|
||||
type DocGovRecord,
|
||||
} from './api/docgov';
|
||||
import { canAddBlockComment } from './lib/commentPolicy';
|
||||
import {
|
||||
@@ -194,6 +188,7 @@ import {
|
||||
import { useChatWindow } from './hooks/useChatWindow';
|
||||
import { useAuthenticatedWorkspaceSync } from './hooks/useAuthenticatedWorkspaceSync';
|
||||
import { useControlledDocumentHydration } from './hooks/useControlledDocumentHydration';
|
||||
import { useDocumentLifecycleActions } from './hooks/useDocumentLifecycleActions';
|
||||
import { useServerDocumentSync } from './hooks/useServerDocumentSync';
|
||||
import { useWorkbenchPersistence } from './hooks/useWorkbenchPersistence';
|
||||
import { useTurnRuntime } from './hooks/useTurnRuntime';
|
||||
@@ -3109,190 +3104,22 @@ export default function App() {
|
||||
setDocAST({ ...docAST });
|
||||
};
|
||||
|
||||
// 法定生命周期流转(真实 docgov HTTP)
|
||||
const applyDocGovRecord = (doc: DocGovRecord) => {
|
||||
const nextState = (doc.current_state || docAST.lifecycleState) as DocLifecycleState;
|
||||
const nextClaimed = doc.author_claimed ?? docAST.isClaimed;
|
||||
const lease = mapLeaseToAstEditLease(doc);
|
||||
const nextAst: DocumentAST = {
|
||||
...docAST,
|
||||
lifecycleState: nextState,
|
||||
isClaimed: nextClaimed,
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
const {
|
||||
handleClaim,
|
||||
handleSubmitReview,
|
||||
handlePassReview,
|
||||
handleRejectReview,
|
||||
handleApprove,
|
||||
handleRejectApprove,
|
||||
} = useDocumentLifecycleActions({
|
||||
docAST,
|
||||
setDocAST,
|
||||
activeDocId,
|
||||
currentUser,
|
||||
activeSessionId,
|
||||
setDeliverables,
|
||||
setMessages,
|
||||
});
|
||||
|
||||
// 导出 DOCX (Word 渲染器)
|
||||
const handleExportDocx = async () => {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
test('document lifecycle actions live in one hook', () => {
|
||||
const app = readFileSync(new URL('../App.tsx', import.meta.url), 'utf8');
|
||||
const hook = readFileSync(new URL('./useDocumentLifecycleActions.ts', import.meta.url), 'utf8');
|
||||
assert.equal((app.match(/useDocumentLifecycleActions\(/g) || []).length, 1);
|
||||
assert.equal(app.includes('claimDocument('), false);
|
||||
assert.equal(app.includes('approverDecision('), false);
|
||||
assert.equal(hook.includes('claimDocument'), true);
|
||||
assert.equal(hook.includes('submitForReview'), true);
|
||||
assert.equal(hook.includes('reviewDecision'), true);
|
||||
assert.equal(hook.includes('approverDecision'), true);
|
||||
assert.equal(hook.includes("rollback_to: 'drafting'"), true);
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
approverDecision,
|
||||
claimDocument,
|
||||
mapLeaseToAstEditLease,
|
||||
reviewDecision,
|
||||
submitForReview,
|
||||
type DocGovRecord,
|
||||
} from '../api/docgov';
|
||||
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;
|
||||
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>;
|
||||
}
|
||||
|
||||
export function useDocumentLifecycleActions(input: DocumentLifecycleInput): DocumentLifecycleActions {
|
||||
const {
|
||||
docAST,
|
||||
setDocAST,
|
||||
activeDocId,
|
||||
currentUser,
|
||||
activeSessionId,
|
||||
setDeliverables,
|
||||
setMessages,
|
||||
} = input;
|
||||
|
||||
const applyDocGovRecord = (doc: DocGovRecord) => {
|
||||
const nextState = (doc.current_state || docAST.lifecycleState) as DocLifecycleState;
|
||||
const nextClaimed = doc.author_claimed ?? docAST.isClaimed;
|
||||
const lease = mapLeaseToAstEditLease(doc);
|
||||
const nextAst: DocumentAST = {
|
||||
...docAST,
|
||||
lifecycleState: nextState,
|
||||
isClaimed: nextClaimed,
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleClaim,
|
||||
handleSubmitReview,
|
||||
handlePassReview,
|
||||
handleRejectReview,
|
||||
handleApprove,
|
||||
handleRejectApprove,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user