feat(web): fold long workflow stages and batch their approval

Later stages with more than four documents start collapsed, and one action can put the whole stage under human approval or take it back out. The workflow chip still names the design-control flow when the catalog has not loaded.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-19 10:30:32 +00:00
co-authored by Cursor
parent 5bf10c7536
commit 6367f865d5
6 changed files with 105 additions and 7 deletions
@@ -49,7 +49,7 @@ updated: 2026-09-19
| FE App.tsx | 部分。登录壳不再静态引用导出引擎、文件预览、管理台、块编辑器和图表。入口包 2.83 MB → 1.00 MB(br 626 KB → 235 KB)。`loginShellImports.test.ts`。工作台主体仍在 `App.tsx`,未按 hook 拆文件 |
| H1-del | 关闭理由:`allowLegacyTestPrincipal` 仍被 `AllowInMemory && ContextDbHost==""` 的 dev/test 内存测试使用。删除会把这批测试变成未登录 401。保留告警,删除另开、先改测试夹具 |
仍开放:rdev P2 前端规划 UI、P3 三样本真 PI、P4 键名空间化、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。P2 的对话点名已接:`生成项目建议书` 命中 `rd_proposal`,长标题压过短标题。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。
仍开放:rdev P2 的 PrerequisiteBanner、P3 三样本真 PI、P4 键名空间化、首次加载 U1/U6/U7、dev 容器重建与治理库 PG。P2 规划界面已能折叠长阶段并批量纳入人工审批。U4 登录扇出收口仍有效,浏览器请求次数证据还没有。H2 批注/审计的项目成员检查在 Postgres 上走 `RequireProjectAccess`;内存授权器在无库时把任意正用户 id 视为成员,这是 `TestAuthorizeDocRevision_ReviewerAndNonMemberCannotWrite` 的隔离前提,不能改成默认拒绝。H2 的 HTTP 负例因此要在 `TEST_DATABASE_URL` 上补,本机未设置该变量。M7 的投影意图已随账本提交(goose `00053`);容器 schema 仍是 52,要等占槽重建后才升到 53。
红线关闭(不是实现):
+1 -1
View File
@@ -57,7 +57,7 @@
| 对话数据治理(D7) | 留存/删除口径、库·备份·日志控制、敏感字段加密边界 | deferred | deferred_enhancement(违规则升 blocker) | [计划](../remediation/2026-09-16-feature-matrix-production-readiness-plan.md) Scope Boundaries;[收口 §7.1](../remediation/2026-09-16-partial-deferred-completion-plan.md);[D7 ADR 草案](../adr/2026-09-17-d7-chat-data-retention.md);[W7 盘点](../audit/2026-09-17-d7-data-inventory.md);[报告](../test-reports/2026-09-17-w7-d7-inventory-test-report.md) | **after W7**:只读盘点完成(sessions/messages/KB/日志/备份/event)。**未发现**已确认基线违规,未写 gap-matrix。口径仍 `pending-compliance-signoff`;禁止按 365/180/30 物理删除。本行不升 done | `chat` |
| 智能体小队 | 角色-任务-工作区-黑板 | done | non_blocker | [agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6)属 backlog | `squad` |
| 工作流治理 | 三正式工作流、设置页/DutyMode、动作级鉴权、真实 PI 三工作流 E2E、full/none 混合长链 | done | non_blocker | [三工作流整改](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[409/职责](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md);[PI E2E](../test-reports/2026-09-16-pi-three-workflow-e2e-test-report.md);[Wave2 full/none](../test-reports/2026-09-16-wave2-real-pi-evidence-test-report.md) | full 跨用户审批 + none 免审批不阻塞已签。纯软件自举仍独立 planned | `workflow` |
| 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[short-term](../archive/ops/2026-09-17-rdev-workflow-integration.md);[模板深度](../archive/ops/2026-09-17-rdev-template-depth.md);[S0+S1 首批报告](../test-reports/2026-09-17-rdev-template-depth-test-report.md);[S1 续批报告](../test-reports/2026-09-17-rdev-template-depth-s1-continued-test-report.md);[写作规范](../system-design/project-workflow/08-rdev模板写作规范.md);[backlog](../backlog/2026-09-17-rdev-phase2-backlog.md) | **P0+P1 done**。**模板深度 S0+S1 首批 + S1 续批 done**:60/60 模板已补强;真 PI 抽样 PASS(含 `rd_planning_review` + `rd_design_vv`,项目 6446)。剩余:S3 overlay 与 R2/R3 已接。P2 名称匹配已接:`生成项目建议书` → `rd_proposal`,更长标题优先(`TestIntentRecognition_RDevProposalAndLongerTitleWins`)。P2 前端规划 UI、P3 三样本真 PI E2E、P4 键名空间化仍未做 | `workflow-def` / `rdev-templates` |
| 研发设计控制(rdev) | 医疗第 4 工作流:13×60 manifest/模板、`squad.rdev_doc.v1` kind 编排、main-completed 硬 precheck、StageKey 动态化;不含前端/真 PI/键名空间化 | partial | deferred_enhancement | [设计](../system-design/project-workflow/07-研发工作流rdev接入-设计与实施计划.md);[short-term](../archive/ops/2026-09-17-rdev-workflow-integration.md);[模板深度](../archive/ops/2026-09-17-rdev-template-depth.md);[S0+S1 首批报告](../test-reports/2026-09-17-rdev-template-depth-test-report.md);[S1 续批报告](../test-reports/2026-09-17-rdev-template-depth-s1-continued-test-report.md);[写作规范](../system-design/project-workflow/08-rdev模板写作规范.md);[backlog](../backlog/2026-09-17-rdev-phase2-backlog.md) | **P0+P1 done**。**模板深度 S0+S1 首批 + S1 续批 done**:60/60 模板已补强;真 PI 抽样 PASS(含 `rd_planning_review` + `rd_design_vv`,项目 6446)。剩余:S3 overlay 与 R2/R3 已接。P2 名称匹配已接。规划界面:任务多于 4 的后续阶段默认折叠,可对本阶段批量纳入或取消人工审批(`projectSettingsDuties.test.ts`);工作流条在目录不可用时仍显示「研发设计控制流程」。PrerequisiteBanner、P3 三样本真 PI、P4 键名空间化仍未做 | `workflow-def` / `rdev-templates` |
| 纯软件项目自举 | 软件项目创建、Bootstrap、Delivery、受控工程交接 | partial | deferred_enhancement(明确不挡首发) | [设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[ADR](../adr/2026-09-16-software-project-bootstrap-keys.md);[L1 报告](../test-reports/2026-09-16-software-project-bootstrap-test-report.md);[W5a](../test-reports/2026-09-17-w5a-software-bootstrap-ui-pi-test-report.md) | **before**:L1 + UI 仍克隆医疗模板。**after W5a**:catalog 驱动创建、软件不挂医疗 key、docs_only 真 PI 建议 delivery 且不自动激活。**SWBOOT-10 仍 stub**(W5b);本行不升 done | `workflow-design` |
| IAM / 鉴权 | 项目级 API 鉴权 + RBAC;会话/阶段授权 | done | 加固 | [v3 full inventory](../audit/authorization-matrix-v3-full.md);[session 中断](../test-reports/2026-09-16-iam-route-inventory-test-report.md);[fail-closed](../test-reports/2026-09-16-wave1-iam-fail-closed-test-report.md);[410 策略](../audit/2026-09-16-bug11-410-retire-decision.md);[R13](../audit/2026-09-16-r13-data-safety-checklist.md) | 全路由 inventory + session 中断负例 + 410 冻结 + R13 基线已签 | `authz` |
| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | non_blocker | [docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[正文 UI](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[canonical hash](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md) | 项目 143 只读;不终止 | `docgov` |
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { X, ChevronRight, ChevronLeft, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
import { X, ChevronRight, ChevronDown, ChevronLeft, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
import {
fetchProjectSettings,
fetchDefaultWorkflowPlan,
@@ -36,6 +36,9 @@ import {
resolveClientDocTaskDuty,
toggleAuthorOnly,
toggleHumanApproval,
stageStartsCollapsed,
applyStageHumanApproval,
stageApprovalKeys,
} from '../../lib/projectSettingsDuties';
import { ACTIVATION_UNMANAGED_FREEZE_HINT } from '../../lib/blockedGovernanceCopy';
import type { ProjectSettingsIssue } from '../../api/projectSettingsError';
@@ -69,6 +72,7 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
const [error, setError] = useState<string | null>(null);
const [settings, setSettings] = useState<ProjectSettings | null>(null);
const [plan, setPlan] = useState<WorkflowPlan | null>(null);
const [openStages, setOpenStages] = useState<Record<string, boolean>>({});
const [preview, setPreview] = useState<ActivationPreview | null>(null);
const [selectedKey, setSelectedKey] = useState('');
const [terminateReason, setTerminateReason] = useState('');
@@ -148,6 +152,24 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
setPlan({ ...plan, stages });
};
const applyStageApproval = (stageIdx: number, managed: boolean) => {
if (!plan) return;
const stage = plan.stages[stageIdx];
if (!stage) return;
const keys = stageApprovalKeys(stage.doc_tasks, showEditor);
const stages = plan.stages.map((item, index) =>
index === stageIdx
? { ...item, doc_tasks: applyStageHumanApproval(item.doc_tasks, managed, showEditor) }
: item,
);
setPlan({ ...plan, stages });
setRequireApprovalKeys((current) => {
if (managed) return Array.from(new Set([...current, ...keys]));
const drop = new Set(keys);
return current.filter((key) => !drop.has(key));
});
};
const handleSavePlan = async () => {
if (!plan || !settings || !settings.can_edit_plan) return;
@@ -499,10 +521,42 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
请先保存一次规划以从工作流定义加载阶段结构,或返回上一步重新选择。
</div>
)}
{plan.stages.map((st, si) => (
{plan.stages.map((st, si) => {
const expanded = openStages[st.stage_key] ?? !stageStartsCollapsed(st.doc_tasks.length, si);
return (
<div key={st.stage_key} style={{ border: '1px solid var(--border)', borderRadius: '8px', padding: '12px' }}>
<div style={{ fontWeight: 600, marginBottom: '8px' }}>{st.stage_name || st.stage_key}</div>
{st.doc_tasks.map((dt, ti) => {
<button
type="button"
onClick={() => setOpenStages((prev) => ({ ...prev, [st.stage_key]: !expanded }))}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
width: '100%',
background: 'transparent',
border: 'none',
padding: 0,
cursor: 'pointer',
fontWeight: 600,
marginBottom: expanded ? '8px' : 0,
textAlign: 'left',
}}
>
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
{st.stage_name || st.stage_key}
<span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>· {st.doc_tasks.length} 项</span>
</button>
{expanded && showEditor && (
<div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}>
<button type="button" onClick={() => applyStageApproval(si, true)} style={{ fontSize: '11px', cursor: 'pointer' }}>
本阶段全部纳入人工审批
</button>
<button type="button" onClick={() => applyStageApproval(si, false)} style={{ fontSize: '11px', cursor: 'pointer' }}>
本阶段全部取消人工审批
</button>
</div>
)}
{expanded && st.doc_tasks.map((dt, ti) => {
const resolved = resolveClientDocTaskDuty(dt);
const managed =
resolved.mode !== 'none' ||
@@ -627,7 +681,8 @@ export const ProjectSettingsModal: React.FC<ProjectSettingsModalProps> = ({
);
})}
</div>
))}
);
})}
</div>
)}
@@ -18,6 +18,7 @@ const FALLBACK_OPTIONS = [
{ key: 'main', label: '商务对接流程', icon: '⚡' },
{ key: 'product_manager', label: '智能产品经理流程', icon: '📋' },
{ key: 'test_manager', label: '智能测试工程师流程', icon: '🧪' },
{ key: 'rdev', label: '研发设计控制流程', icon: '📐' },
];
export const ProjectWorkflowSwitcher: React.FC<ProjectWorkflowSwitcherProps> = ({
+20
View File
@@ -10,6 +10,9 @@ import {
resolveClientDocTaskDuty,
toggleAuthorOnly,
toggleHumanApproval,
stageStartsCollapsed,
applyStageHumanApproval,
stageApprovalKeys,
} from './projectSettingsDuties';
function plan(tasks: WorkflowPlan['stages'][number]['doc_tasks']): WorkflowPlan {
@@ -170,4 +173,21 @@ describe('projectSettingsDuties', () => {
assert.equal(off.duty_policy, '');
assert.deepEqual(off.duties, { author_user_id: 7 });
});
it('folds later rdev stages and batches human approval keys', () => {
assert.equal(stageStartsCollapsed(2, 0), false);
assert.equal(stageStartsCollapsed(12, 0), false);
assert.equal(stageStartsCollapsed(12, 1), true);
const tasks = [
{ task_key: 'rd_proposal', enabled: true, approval_configurable: true, duties: { author_user_id: 1, reviewer_user_id: 2, approver_user_id: 3 } },
{ task_key: 'rd_design_plan', enabled: true, approval_configurable: false, duties: {} },
{ task_key: 'rd_sw_plan', enabled: false, approval_configurable: true, duties: {} },
];
assert.deepEqual(stageApprovalKeys(tasks, true), ['rd_proposal']);
const cleared = applyStageHumanApproval(tasks, false, true);
assert.equal(cleared[0].duty_policy, '');
assert.deepEqual(cleared[0].duties, {});
assert.equal(cleared[1].approval_configurable, false);
assert.equal(cleared[2].enabled, false);
});
});
+22
View File
@@ -55,6 +55,28 @@ export function toggleHumanApproval(task: DocTaskPlan, managed: boolean): DocTas
return { ...task };
}
// 研发流程一个阶段常有十几份文档。第一阶段保持展开,后面任务较多的阶段先折叠。
export function stageStartsCollapsed(taskCount: number, stageIndex: number): boolean {
return stageIndex > 0 && taskCount > 4;
}
export function applyStageHumanApproval(
tasks: DocTaskPlan[],
managed: boolean,
planning: boolean,
): DocTaskPlan[] {
return tasks.map((task) => {
if (!task.enabled || !canToggleHumanApproval(task, planning)) return task;
return toggleHumanApproval(task, managed);
});
}
export function stageApprovalKeys(tasks: DocTaskPlan[], planning: boolean): string[] {
return tasks
.filter((task) => task.enabled && canToggleHumanApproval(task, planning))
.map((task) => task.task_key);
}
/** 切换兼任模式:开启时只保留编制人并清空审核/批准;关闭时清除 duty_policy。 */
export function toggleAuthorOnly(task: DocTaskPlan, enabled: boolean): DocTaskPlan {
if (!enabled) {