feat(web): never name-escalate server controlled zero duty ids
Mark server summaries dutiesAuthoritative so id 0 stays fail-closed viewer instead of matching names like a legacy local document. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -99,5 +99,34 @@ describe('mergeWorkflowDocumentsFromServer', () => {
|
||||
const asA = mergeWorkflowDocumentsFromServer([], [serverDoc], 56, { id: 9, name: '编制人A' });
|
||||
expect(asA[0].author).toBe('编制人A');
|
||||
expect(asA[0].authorId).toBe(9);
|
||||
expect(asA[0].dutiesAuthoritative).toBe(true);
|
||||
});
|
||||
|
||||
it('marks server controlled docs authoritative even when duty ids are 0', () => {
|
||||
const merged = mergeWorkflowDocumentsFromServer(
|
||||
[],
|
||||
[
|
||||
{
|
||||
doc_id: 'doc_proj56_intake',
|
||||
project_id: 56,
|
||||
task_key: 'intake',
|
||||
title: '用户初始需求收集表',
|
||||
lifecycle_state: 'drafting',
|
||||
is_template_only: false,
|
||||
author_id: 0,
|
||||
author_name: '编制人A',
|
||||
reviewer_id: 0,
|
||||
reviewer_name: '审核人B',
|
||||
approver_id: 0,
|
||||
approver_name: '批准人C',
|
||||
},
|
||||
],
|
||||
56,
|
||||
{ id: 99, name: '编制人A' },
|
||||
);
|
||||
expect(merged[0].dutiesAuthoritative).toBe(true);
|
||||
expect(merged[0].governanceMode).toBe('controlled');
|
||||
expect(merged[0].authorId).toBe(0);
|
||||
expect(merged[0].author).toBe('编制人A');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,12 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
|
||||
reviewer: item.reviewer,
|
||||
approver: item.approver,
|
||||
projectPermission: permissions[item.docId],
|
||||
isLegacyLocalDoc: !item.authorId && !item.reviewerId && !item.approverId,
|
||||
dutiesAuthoritative: item.dutiesAuthoritative,
|
||||
governanceMode: item.governanceMode,
|
||||
isLegacyLocalDoc:
|
||||
item.dutiesAuthoritative !== true &&
|
||||
item.governanceMode !== 'controlled' &&
|
||||
/^doc-p\d+-/.test(item.docId),
|
||||
});
|
||||
|
||||
deliverables.forEach((item) => {
|
||||
|
||||
@@ -268,6 +268,7 @@ export function markdownToBlockAST(
|
||||
reviewerId: meta?.reviewerId,
|
||||
approver: meta?.approver || '王总监',
|
||||
approverId: meta?.approverId,
|
||||
dutiesAuthoritative: meta?.dutiesAuthoritative,
|
||||
revisionEtag: meta?.revisionEtag,
|
||||
summary: meta?.summary || '由项目助手自动生成的受控交付物',
|
||||
updatedAt: meta?.updatedAt || new Date().toISOString().replace('T', ' ').substring(0, 16),
|
||||
|
||||
@@ -58,4 +58,30 @@ describe('resolveCurrentDocRole', () => {
|
||||
'viewer',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not name-promote when server controlled duties are zero ids', () => {
|
||||
assert.equal(
|
||||
resolveCurrentDocRole({
|
||||
currentUser: { id: 99, name: '编制人A' },
|
||||
authorId: 0,
|
||||
reviewerId: 0,
|
||||
approverId: 0,
|
||||
author: '编制人A',
|
||||
reviewer: '审核人B',
|
||||
approver: '批准人C',
|
||||
dutiesAuthoritative: true,
|
||||
governanceMode: 'controlled',
|
||||
isLegacyLocalDoc: true,
|
||||
}),
|
||||
'viewer',
|
||||
);
|
||||
assert.equal(
|
||||
resolveCurrentDocRole({
|
||||
currentUser: { id: 99, name: '编制人A' },
|
||||
author: '编制人A',
|
||||
governanceMode: 'controlled',
|
||||
}),
|
||||
'viewer',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,19 +10,25 @@ export type ResolveCurrentDocRoleInput = {
|
||||
approver?: string;
|
||||
projectPermission?: string;
|
||||
isLegacyLocalDoc?: boolean;
|
||||
dutiesAuthoritative?: boolean;
|
||||
governanceMode?: 'controlled' | 'freeform';
|
||||
};
|
||||
|
||||
function hasAuthoritativeDutyId(input: ResolveCurrentDocRoleInput): boolean {
|
||||
return Boolean(input.authorId || input.reviewerId || input.approverId);
|
||||
}
|
||||
|
||||
function isAuthoritativeDutySource(input: ResolveCurrentDocRoleInput): boolean {
|
||||
return input.dutiesAuthoritative === true || input.governanceMode === 'controlled';
|
||||
}
|
||||
|
||||
/**
|
||||
* 按权威 user id 解析文档职责。未知身份 fail-closed 为 viewer。
|
||||
* name 匹配仅用于无职责 id 的旧本地文档。
|
||||
* name 匹配仅用于真正旧本地文档;服务端受控即使 duty id 为 0 也不得按 name 提权。
|
||||
*/
|
||||
export function resolveCurrentDocRole(input: ResolveCurrentDocRoleInput): UserRole {
|
||||
const uid = input.currentUser.id;
|
||||
if (hasAuthoritativeDutyId(input)) {
|
||||
if (isAuthoritativeDutySource(input) || hasAuthoritativeDutyId(input)) {
|
||||
if (input.authorId && input.authorId === uid) return 'author';
|
||||
if (input.reviewerId && input.reviewerId === uid) return 'reviewer';
|
||||
if (input.approverId && input.approverId === uid) return 'approver';
|
||||
|
||||
@@ -90,6 +90,7 @@ export function mergeWorkflowDocumentsFromServer(
|
||||
};
|
||||
|
||||
applyAuthoritativeDuties(base, s, slotIdx < 0);
|
||||
base.dutiesAuthoritative = true;
|
||||
|
||||
base.docId = s.doc_id;
|
||||
base.title = s.title || base.title;
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface DocumentAST {
|
||||
reviewerId?: number;
|
||||
approver: string;
|
||||
approverId?: number;
|
||||
/** 服务端受控职责为权威来源;即使 id 为 0 也不得按 name 提权。 */
|
||||
dutiesAuthoritative?: boolean;
|
||||
/** 打开受控文档时取得的 revision-state ETag,人工 PATCH 必须使用该基线。 */
|
||||
revisionEtag?: string;
|
||||
summary?: string;
|
||||
|
||||
@@ -313,6 +313,8 @@ export interface DeliverableItem {
|
||||
reviewerId?: number;
|
||||
approver: string;
|
||||
approverId?: number;
|
||||
/** 来自服务端 summary 的职责;true 时禁止 name fallback 提权。 */
|
||||
dutiesAuthoritative?: boolean;
|
||||
updatedAt: string;
|
||||
summary?: string;
|
||||
sourceMessageSnippet?: string;
|
||||
|
||||
Reference in New Issue
Block a user