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:
weichen
2026-09-13 17:08:35 +00:00
co-authored by Cursor
parent d5ce81228c
commit cf3d126436
8 changed files with 75 additions and 3 deletions
@@ -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) => {
+1
View File
@@ -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),
+26
View File
@@ -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',
);
});
});
+8 -2
View File
@@ -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';
+1
View File
@@ -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;
+2
View File
@@ -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;
+2
View File
@@ -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;