Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c113f68bf4 | |||
| 5ff8899b48 |
@@ -0,0 +1,37 @@
|
||||
# draw-prototype-flow
|
||||
|
||||
把产品想法或需求文档画成一份可编辑的 Draw.io 低保真原型,并同步表达用户动线、页面状态、角色权限和关键规则。
|
||||
|
||||
## 什么时候使用
|
||||
|
||||
- 想从零梳理 Web、移动端或后台系统的页面原型与用户流程。
|
||||
- 已有 `.drawio` 原型,希望补齐跳转、异常态、权限或规则说明。
|
||||
- 需要一张能用于产品评审、开发对齐和测试设计的产品说明板。
|
||||
|
||||
## 使用前准备
|
||||
|
||||
最好提供产品目标、目标用户、核心任务和已有需求材料。缺少细节时,Agent 会先列出必要假设;只有关键范围无法判断时才会询问。
|
||||
|
||||
可以指定「快速 / 标准 / 完整」档位;不指定时默认按产品评审所需的标准档交付。
|
||||
|
||||
## 使用示例
|
||||
|
||||
```text
|
||||
请用 draw-prototype-flow 为一个企业访客预约系统画低保真原型和用户动线。
|
||||
覆盖访客申请、员工审批、前台核验,以及驳回、过期和无权限状态,输出 editable.drawio。
|
||||
```
|
||||
|
||||
```text
|
||||
检查 current.drawio,把所有主按钮的去向、审批状态和错误恢复路径补完整,并保持现有视觉风格。
|
||||
```
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
1. 整理角色、页面、动作、状态和规则。
|
||||
2. 绘制可编辑页面、关键状态和带标签的用户动线。
|
||||
3. 补充页面索引、权限矩阵或字段规则等必要规格。
|
||||
4. 校验 Draw.io 结构与跨图引用,并说明假设和未决项。
|
||||
|
||||
## 如何判断完成
|
||||
|
||||
最终会得到可在 diagrams.net/Draw.io 打开的 `.drawio` 源文件。核心任务应能从入口走到结果,每个关键按钮有明确去向,异常状态有恢复动作,原型、动线和规格使用同一套页面与状态 ID。
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: draw-prototype-flow
|
||||
description: >-
|
||||
将产品想法、需求文档或现有系统整理成可编辑的低保真 Draw.io 原型、用户动线和产品规则说明板。
|
||||
当用户提出画原型图、线框图、用户流程、用户动线、页面跳转、交互状态、wireframe、user flow、
|
||||
UX flow、产品方案可视化,或要求检查和完善现有 .drawio 产品原型时使用。适合同时表达页面、弹窗、
|
||||
异常态、角色权限、状态流转、字段规则与操作反馈;不用于像素级高保真 UI、纯技术架构图或单张插画。
|
||||
---
|
||||
|
||||
# 绘制低保真原型与用户动线
|
||||
|
||||
把原型、动线和关键规则放进同一套可追踪的产品模型,交付原生可编辑的 `.drawio` 文件。
|
||||
|
||||
## 使用资源
|
||||
|
||||
- 快速单页原型复制 `assets/prototype-flow-template.drawio`,保留其画板尺寸、语义色和基础组件,再替换占位内容。
|
||||
- 多页面原型复制 `assets/scaffold-spec.example.json`,填写页面清单和 `transitions`,再运行 `scripts/scaffold_drawio.py` 生成可编辑骨架与总览连线,禁止手工从空 XML 重复搭建外壳。
|
||||
- 开始绘制前读取 `references/design-patterns.md`;遇到复杂业务、多个角色或超过 8 个页面时,再读取 `references/product-model.md`。
|
||||
- 完成后运行 `scripts/validate_drawio.py`。该脚本可检查常见的压缩或未压缩 Draw.io 文件;Agent 输出时仍设置 `compressed="false"`,便于审阅、差异比较和后续修改。
|
||||
|
||||
## 工作流
|
||||
|
||||
### 1. 确认交付边界
|
||||
|
||||
从用户材料中提取目标用户、核心任务、终端形态、必须覆盖的流程和输出路径。信息不全但不影响主流程时,采用合理假设并在交付说明中列出;只有会改变产品范围或关键流程时才询问。
|
||||
|
||||
默认交付:
|
||||
|
||||
- 原生可编辑 `.drawio`;
|
||||
- 一张端到端动线总览;
|
||||
- 核心任务的页面与关键状态;
|
||||
- 页面索引、状态流转和角色权限等必要规格;
|
||||
- 可选的 PNG/SVG 预览,仅作为查看副本,不能代替源文件。
|
||||
|
||||
根据请求选择交付档位,避免把概念验证画成大而全的规格库:
|
||||
|
||||
- **快速**:1 条核心动线、3~8 个画板、最高风险的 1~2 个异常;适合早期讨论。
|
||||
- **标准(默认)**:核心任务、8~20 个画板、动线总览、关键状态和必要的权限/字段规则;适合产品评审。
|
||||
- **完整**:多角色、多业务域、系统异常、状态机、权限矩阵和详细规格;适合开发与测试对齐。
|
||||
|
||||
用户没有指定时,根据材料成熟度选择,先说明档位和不覆盖的内容。
|
||||
|
||||
### 2. 先建模,再画图
|
||||
|
||||
建立稳定 ID,避免用会随排序变化的显示序号充当引用:
|
||||
|
||||
- 角色:`R-APPLICANT`、`R-APPROVER`;
|
||||
- 页面:`P-LOGIN`、`P-ORDER-LIST`;
|
||||
- 状态:`S-DRAFT`、`S-PENDING`;
|
||||
- 动作:`A-SUBMIT`、`A-REJECT`。
|
||||
|
||||
至少整理以下关系:
|
||||
|
||||
1. 角色 → 目标 → 入口;
|
||||
2. 页面/弹窗 → 可见信息 → 可执行动作;
|
||||
3. 动作 → 前置条件 → 成功去向 → 失败反馈;
|
||||
4. 业务对象 → 状态 → 可编辑性 → 角色权限;
|
||||
5. 字段 → 必填/值域/联动 → 校验反馈。
|
||||
|
||||
复杂任务使用 `references/product-model.md` 中的清单模板。先消除断头页面、无去向按钮和无入口状态,再开始排版。
|
||||
|
||||
### 3. 选择画布结构
|
||||
|
||||
- 不超过 12 个交互画板:可放在一个 Draw.io 页面,按「主流程 → 异常态 → 规格」排列。
|
||||
- 超过 12 个交互画板:默认拆成多个 Draw.io 页面,例如 `00-总览`、`10-申请人`、`20-审批人`、`30-异常状态`、`40-规格`。总览页保留所有跨域跳转。
|
||||
- 用户明确需要一张巨幅说明板时才使用单页矩阵;每行最多 5 个 1600×900 画板,横向间距 120,纵向间距 260,先放原型、再放流程和规格。
|
||||
|
||||
这里的“交互画板”只统计用户可进入的页面、弹窗和异常状态;动线总览、页面索引、字段字典和权限矩阵不参与 12 个阈值计算。生成多页骨架:
|
||||
|
||||
```bash
|
||||
python3 <skill-dir>/scripts/scaffold_drawio.py scaffold-spec.json output.drawio
|
||||
```
|
||||
|
||||
每个画板标题使用「显示序号 · 页面名 · 状态」;底部元信息写角色、入口、核心功能和下一步,但跨页引用使用稳定 ID,例如 `下一步:P-DETAIL`,不要手写易漂移的序号。
|
||||
|
||||
### 4. 绘制页面与状态
|
||||
|
||||
遵循 `references/design-patterns.md` 的视觉语法,并保持低保真:
|
||||
|
||||
- 使用一致的应用外壳、导航、标题层级、表单、表格、按钮和反馈组件;
|
||||
- 主操作、成功、警告、危险和辅助跳转使用固定语义色;
|
||||
- 按钮、字段、弹窗和表格单元格保持独立图元,禁止把整个页面栅格化为图片;
|
||||
- 为核心任务画成功路径,也覆盖与业务相关的加载、空数据、校验失败、权限不足、并发冲突和不可逆确认;
|
||||
- 弹窗作为触发页面的明确状态,必须同时画取消/返回路径和确认后的结果;
|
||||
- 内容优先使用真实语义的示例数据,避免大段 Lorem Ipsum。
|
||||
|
||||
### 5. 绘制动线
|
||||
|
||||
在总览中按角色或阶段使用泳道。所有跳转必须使用 Draw.io edge,并设置真实的 `source`、`target`:
|
||||
|
||||
- 节点写页面稳定 ID 与名称;
|
||||
- 连线标签写用户动作或系统结果,如「提交审批」「保存失败」「自动完结」;
|
||||
- 判断节点写条件,分支在线上写具体结果;
|
||||
- 成功用绿、警告/可恢复异常用橙、破坏性操作用红、普通导航用蓝;
|
||||
- 连线优先正交,绕开画板与文字,不用无来源或无目标的装饰箭头;
|
||||
- 每个入口可达,每个非终态有去向,每个终态有清晰结果。
|
||||
|
||||
### 6. 补齐产品规格
|
||||
|
||||
原型之后至少提供页面与动作索引。按复杂度增加:
|
||||
|
||||
- 角色 × 页面 × 操作权限矩阵;
|
||||
- 状态流转、触发动作、操作者和结果;
|
||||
- 字段字典、值域、必填与联动规则;
|
||||
- 异常反馈与恢复动作;
|
||||
- 报表、导出或批处理口径。
|
||||
|
||||
不要为凑数量复制页面。规格应解释原型无法准确表达的规则,并使用与原型相同的稳定 ID。
|
||||
|
||||
### 7. 一致性检查
|
||||
|
||||
逐项核对:
|
||||
|
||||
1. 每个核心任务都能从入口走到明确结果;
|
||||
2. 每个可点击主操作都在动线或规格中有去向;
|
||||
3. 每条连线的源按钮、目标页面和标签语义一致;
|
||||
4. 页面状态、角色权限、字段规则和按钮可编辑性不冲突;
|
||||
5. 确认弹窗、失败反馈和恢复路径成对出现;
|
||||
6. 所有引用使用稳定 ID,显示序号没有被当作唯一标识;
|
||||
7. 文字没有明显截断,图元不重叠,连线不穿过主要内容。
|
||||
|
||||
运行结构校验:
|
||||
|
||||
```bash
|
||||
python3 <skill-dir>/scripts/validate_drawio.py <output.drawio> --min-frames 1
|
||||
```
|
||||
|
||||
存在两个以上交互画板时追加 `--min-edges 1`,确保至少有一条真实动线;正式交付按核心转移清单设置更高的最小连线数。
|
||||
|
||||
多页面或正式交付追加 `--strict`;如果环境有 Draw.io 导出工具,再导出 PNG/SVG 并目视检查全图和 2~3 个关键画板。不要把包含私有需求的文件上传到公共在线转换服务。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- `.drawio` 能被 XML 解析并在 diagrams.net/Draw.io 打开;
|
||||
- 图元和文字可单独编辑,关键跳转是有源和目标的连接线;
|
||||
- 核心流程、关键分支、角色权限和业务状态相互一致;
|
||||
- 校验无 error;正式交付在 `--strict` 下也无 warning,或逐条说明保留原因;
|
||||
- 向用户列出产物路径、采用的假设、覆盖范围以及仍待产品决策的问题。
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="app.diagrams.net" agent="draw-prototype-flow" version="24.7.17" type="device" compressed="false">
|
||||
<diagram id="prototype-template" name="00-原型与动线">
|
||||
<mxGraphModel dx="1200" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="label-p01" value="01 · P-LIST · 任务列表" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="0" y="15" width="1000" height="40" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="0" y="70" width="1600" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-nav" value="产品名称
工作台
我的任务
数据概览
系统设置" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#061B3A;strokeColor=#061B3A;fontColor=#C8D6EA;fontSize=16;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=32;spacingLeft=32;" vertex="1" parent="1"><mxGeometry x="0" y="70" width="260" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-title" value="我的任务" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="310" y="120" width="500" height="50" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-desc" value="查看进度、继续处理并创建新任务" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=14;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="310" y="170" width="600" height="30" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-filter" value="状态:全部 关键词:请输入名称 查询" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;arcSize=8;" vertex="1" parent="1"><mxGeometry x="310" y="225" width="1240" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-new" value="+ 创建任务" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#1768E8;strokeColor=#1768E8;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1330" y="130" width="220" height="48" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-table-head" value="编号 名称 状态 更新时间 操作" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#EAF0F8;strokeColor=#D9E2EF;fontColor=#263750;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="310" y="335" width="1240" height="54" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-table-row1" value="T-001 示例任务 A 待处理 2026-08-04 10:30 查看 编辑" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="310" y="389" width="1240" height="62" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-table-row2" value="T-002 示例任务 B 已完成 2026-08-03 16:20 查看" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F7F9FC;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="310" y="451" width="1240" height="62" as="geometry"/></mxCell>
|
||||
<mxCell id="p01-meta" value="角色:R-USER|入口:登录后首页|功能:筛选、查看、创建|下一步:P-EDIT" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="310" y="915" width="1240" height="30" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="label-p02" value="02 · P-EDIT · 创建任务 · 编辑中" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1720" y="15" width="1000" height="40" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="1720" y="70" width="1600" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-nav" value="产品名称
工作台
我的任务
数据概览
系统设置" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#061B3A;strokeColor=#061B3A;fontColor=#C8D6EA;fontSize=16;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=32;spacingLeft=32;" vertex="1" parent="1"><mxGeometry x="1720" y="70" width="260" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-title" value="创建任务" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="2030" y="120" width="500" height="50" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-form" value="" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2030" y="210" width="1240" height="570" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-field1" value="任务名称 *
请输入任务名称" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=14;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2080" y="270" width="540" height="74" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-field2" value="任务类型 *
请选择类型 ▾" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=14;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2670" y="270" width="540" height="74" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-field3" value="任务说明
请输入必要的背景、范围和完成标准" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=14;spacingLeft=14;arcSize=8;" vertex="1" parent="1"><mxGeometry x="2080" y="390" width="1130" height="180" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-cancel" value="取消" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#AFC1DA;fontColor=#27405F;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="2720" y="700" width="200" height="50" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-submit" value="提交" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#1768E8;strokeColor=#1768E8;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="2970" y="700" width="240" height="50" as="geometry"/></mxCell>
|
||||
<mxCell id="p02-meta" value="角色:R-USER|入口:P-LIST / A-CREATE|状态:S-EDITING|下一步:P-CONFIRM" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="2030" y="915" width="1240" height="30" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="label-p03" value="03 · P-CONFIRM · 提交确认" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="3440" y="15" width="1000" height="40" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="3440" y="70" width="1600" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-context" value="创建任务页面(保留用户已填写内容)" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#718198;fontSize=18;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="3540" y="170" width="1400" height="650" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-overlay" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#10284B;strokeColor=none;opacity=25;" vertex="1" parent="1"><mxGeometry x="3440" y="70" width="1600" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-modal" value="" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#CBD6E5;arcSize=10;" vertex="1" parent="1"><mxGeometry x="3750" y="300" width="980" height="420" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-title" value="确认提交任务?" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=26;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="3820" y="355" width="600" height="45" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-desc" value="提交后将进入处理流程。你仍可在任务详情中查看进度。" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#40516A;fontSize=16;fontFamily=Microsoft YaHei;align=left;whiteSpace=wrap;" vertex="1" parent="1"><mxGeometry x="3820" y="425" width="820" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-back" value="返回修改" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#AFC1DA;fontColor=#27405F;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="4080" y="590" width="220" height="52" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-confirm" value="确认提交" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#18864B;strokeColor=#18864B;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="4340" y="590" width="260" height="52" as="geometry"/></mxCell>
|
||||
<mxCell id="p03-meta" value="角色:R-USER|触发:P-EDIT / A-SUBMIT|取消:返回编辑|确认:S-SUBMITTED" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="3750" y="915" width="1240" height="30" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="label-p04" value="04 · FLOW-OVERVIEW · 端到端用户动线" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="0" y="1175" width="1200" height="40" as="geometry"/></mxCell>
|
||||
<mxCell id="p04-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="0" y="1230" width="1600" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p04-lane-user" value="R-USER 用户" style="swimlane;html=0;rounded=0;startSize=70;horizontal=0;fillColor=#EEF4FF;strokeColor=#9CBDF0;fontColor=#1768E8;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="70" y="1310" width="1460" height="330" as="geometry"/></mxCell>
|
||||
<mxCell id="p04-lane-system" value="R-SYSTEM 系统" style="swimlane;html=0;rounded=0;startSize=70;horizontal=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="70" y="1670" width="1460" height="330" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-start" value="开始" style="ellipse;whiteSpace=wrap;html=0;fillColor=#EEF4FF;strokeColor=#1768E8;fontColor=#1768E8;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="180" y="1430" width="120" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-list" value="P-LIST
任务列表" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#1768E8;fontColor=#10284B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="410" y="1415" width="220" height="100" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-edit" value="P-EDIT
创建任务" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#1768E8;fontColor=#10284B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="760" y="1415" width="220" height="100" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-confirm" value="P-CONFIRM
提交确认" style="rhombus;whiteSpace=wrap;html=0;fillColor=#FFF8E8;strokeColor=#D97706;fontColor=#604A16;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1110" y="1395" width="220" height="140" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-process" value="S-SUBMITTED
创建处理记录" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#EAF8F0;strokeColor=#89D3A8;fontColor=#18864B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;" vertex="1" parent="1"><mxGeometry x="760" y="1785" width="260" height="100" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-end" value="完成" style="ellipse;whiteSpace=wrap;html=0;fillColor=#EAF8F0;strokeColor=#18864B;fontColor=#18864B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1190" y="1800" width="120" height="70" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-e1" value="进入" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#1768E8;fontColor=#40516A;" edge="1" parent="1" source="flow-start" target="flow-list"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-e2" value="创建" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#1768E8;fontColor=#40516A;" edge="1" parent="1" source="flow-list" target="flow-edit"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-e3" value="提交" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#D97706;fontColor=#40516A;" edge="1" parent="1" source="flow-edit" target="flow-confirm"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-e4" value="确认" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#18864B;fontColor=#40516A;" edge="1" parent="1" source="flow-confirm" target="flow-process"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="flow-e5" value="处理成功" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#18864B;fontColor=#40516A;" edge="1" parent="1" source="flow-process" target="flow-end"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="label-p05" value="05 · SPEC-INDEX · 页面、状态与操作索引" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;" vertex="1" parent="1"><mxGeometry x="1720" y="1175" width="1200" height="40" as="geometry"/></mxCell>
|
||||
<mxCell id="p05-bg" value="" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;" vertex="1" parent="1"><mxGeometry x="1720" y="1230" width="1600" height="900" as="geometry"/></mxCell>
|
||||
<mxCell id="p05-title" value="页面与动作索引" style="text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;" vertex="1" parent="1"><mxGeometry x="1810" y="1310" width="700" height="50" as="geometry"/></mxCell>
|
||||
<mxCell id="p05-head" value="稳定 ID 页面/状态 角色 主操作 下一步" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#EAF0F8;strokeColor=#D9E2EF;fontColor=#263750;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1400" width="1420" height="56" as="geometry"/></mxCell>
|
||||
<mxCell id="p05-row1" value="P-LIST 任务列表 R-USER A-CREATE P-EDIT" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1456" width="1420" height="64" as="geometry"/></mxCell>
|
||||
<mxCell id="p05-row2" value="P-EDIT 创建任务 R-USER A-SUBMIT P-CONFIRM" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#F7F9FC;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1520" width="1420" height="64" as="geometry"/></mxCell>
|
||||
<mxCell id="p05-row3" value="P-CONFIRM 提交确认 R-USER A-CONFIRM S-SUBMITTED" style="rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;" vertex="1" parent="1"><mxGeometry x="1810" y="1584" width="1420" height="64" as="geometry"/></mxCell>
|
||||
<mxCell id="p05-note" value="规则:所有原型、动线、状态机与权限矩阵复用稳定 ID;显示序号只代表当前阅读顺序。" style="rounded=1;whiteSpace=wrap;html=0;fillColor=#FFF8E8;strokeColor=#E6BE65;fontColor=#604A16;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;arcSize=8;" vertex="1" parent="1"><mxGeometry x="1810" y="1730" width="1420" height="80" as="geometry"/></mxCell>
|
||||
|
||||
<mxCell id="cross-e1" value="创建任务" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#1768E8;fontColor=#40516A;" edge="1" parent="1" source="p01-new" target="p02-title"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="cross-e2" value="提交确认" style="edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;strokeWidth=2;strokeColor=#18864B;fontColor=#40516A;" edge="1" parent="1" source="p02-submit" target="p03-title"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"title": "产品名称",
|
||||
"columns": 3,
|
||||
"transitions": [
|
||||
{
|
||||
"from": "P-LIST",
|
||||
"to": "P-EDIT",
|
||||
"label": "创建任务",
|
||||
"kind": "primary"
|
||||
},
|
||||
{
|
||||
"from": "P-EDIT",
|
||||
"to": "P-LIST",
|
||||
"label": "提交成功",
|
||||
"kind": "success"
|
||||
}
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"name": "00-总览",
|
||||
"kind": "flow",
|
||||
"frames": [
|
||||
{
|
||||
"id": "FLOW-OVERVIEW",
|
||||
"title": "端到端用户动线",
|
||||
"role": "ALL",
|
||||
"state": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "10-核心页面",
|
||||
"kind": "screen",
|
||||
"frames": [
|
||||
{
|
||||
"id": "P-LIST",
|
||||
"title": "任务列表",
|
||||
"role": "R-USER",
|
||||
"state": "S-DEFAULT"
|
||||
},
|
||||
{
|
||||
"id": "P-EDIT",
|
||||
"title": "创建任务",
|
||||
"role": "R-USER",
|
||||
"state": "S-EDITING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "40-规格",
|
||||
"kind": "spec",
|
||||
"frames": [
|
||||
{
|
||||
"id": "SPEC-PAGE-INDEX",
|
||||
"title": "页面与动作索引",
|
||||
"role": "ALL",
|
||||
"state": ""
|
||||
},
|
||||
{
|
||||
"id": "SPEC-PERMISSION",
|
||||
"title": "角色权限矩阵",
|
||||
"role": "ALL",
|
||||
"state": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# 低保真产品说明板设计模式
|
||||
|
||||
## 1. 三层交付结构
|
||||
|
||||
一份可落地的产品原型同时包含三层:
|
||||
|
||||
1. **页面层**:用户看见什么、能做什么,包括页面、抽屉、弹窗和关键状态。
|
||||
2. **动线层**:谁在什么条件下执行什么动作,从哪里到哪里,失败后如何恢复。
|
||||
3. **规则层**:状态、权限、字段值域、联动、统计口径以及操作反馈。
|
||||
|
||||
只画页面会遗漏行为,只画流程会缺少界面上下文,只画规则又难以评审。三层必须复用同一套稳定 ID。
|
||||
|
||||
## 2. 画板与信息层级
|
||||
|
||||
默认桌面端画板为 1600×900;移动端改为统一设备宽高,但同一文件不要混用多个无说明尺寸。
|
||||
|
||||
每个画板从上到下包含:
|
||||
|
||||
- 外部编号标题:`03 · P-REQUEST-EDIT · 编辑申请 · 草稿`;
|
||||
- 应用外壳:产品名、当前角色、导航和环境信息;
|
||||
- 页面标题与一句任务说明;
|
||||
- 主要内容区;
|
||||
- 主次操作;
|
||||
- 底部元信息:角色、入口、关键规则和下一步稳定 ID。
|
||||
|
||||
画板之间保留足够留白,让跨画板连线走在间隙中。弹窗状态可单独复制一张触发页面并叠加 20%~30% 遮罩,避免脱离上下文的孤立弹窗。
|
||||
|
||||
## 3. 视觉语法
|
||||
|
||||
除非用户提供品牌规范,使用以下克制的基线:
|
||||
|
||||
| 语义 | 建议色 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 深色外壳 | `#061B3A` | 侧边栏、顶栏、产品框架 |
|
||||
| 主操作 | `#1768E8` | 主要按钮、普通导航连线 |
|
||||
| 成功 | `#18864B` | 成功反馈、通过、完成 |
|
||||
| 警告 | `#D97706` | 可恢复异常、未保存、待处理 |
|
||||
| 危险 | `#D92D20` | 删除、废弃、不可逆动作 |
|
||||
| 辅助跳转 | `#5B6ABF` | 预览、详情、跨域辅助流程 |
|
||||
| 主文字 | `#10284B` | 标题与正文重点 |
|
||||
| 次文字 | `#718198` | 说明、占位和元信息 |
|
||||
| 页面背景 | `#F4F7FB` | 内容画布 |
|
||||
| 边框 | `#D9E2EF` | 卡片、表格、输入框 |
|
||||
|
||||
低保真仍应有层级,但不要添加装饰性插画、复杂阴影、渐变或像素级视觉润色。圆角统一为 8~10,正文 12~14,区块标题 18,页面标题 28~30。中文优先使用环境常见无衬线字体并提供回退。
|
||||
|
||||
## 4. 组件表达
|
||||
|
||||
- **按钮**:独立图元,文字使用动词;危险动作不用模糊文案。
|
||||
- **输入框**:同时表达字段名、当前值/占位、必填和错误反馈,不能只画空矩形。
|
||||
- **表格**:表头、至少一条典型数据、空态与行操作;大表无需填满整页。
|
||||
- **状态标签**:颜色与文案双重编码,不能只靠颜色。
|
||||
- **反馈**:就近错误用于字段校验,全局消息用于操作结果,阻断性问题使用弹窗或整页状态。
|
||||
- **附件/媒体**:画出上传中、失败、预览、删除和权限差异中真正相关的状态。
|
||||
|
||||
## 5. 动线表达
|
||||
|
||||
总览使用角色或阶段泳道。页面节点使用稳定 ID;操作节点以动词开头;判断节点写可验证条件;开始和结束节点清楚区分。
|
||||
|
||||
连接线要求:
|
||||
|
||||
- 使用正交线并设置 `source`、`target`;
|
||||
- 线上的标签描述动作或结果,不重复节点名称;
|
||||
- 同一语义使用同一颜色,不给每条线随机配色;
|
||||
- 交叉多时拆分页面或增加局部子流程,不依赖长距离回折线;
|
||||
- 页面内按钮到目标页的连线与总览中的转移必须一致。
|
||||
|
||||
## 6. 页面与状态覆盖
|
||||
|
||||
按业务风险选择,不机械地给每页复制全部状态:
|
||||
|
||||
- 核心成功路径;
|
||||
- 表单校验与保存失败;
|
||||
- 加载、空数据和加载失败;
|
||||
- 无权限、账号异常或配置缺失;
|
||||
- 未保存离开;
|
||||
- 删除、撤回、废弃等不可逆确认;
|
||||
- 审批或库存等并发冲突;
|
||||
- 只读、已结束和审计视角。
|
||||
|
||||
## 7. 大型原型的默认优化
|
||||
|
||||
大型说明板容易出现两个问题:单页过大导致导航困难;显示序号变化导致页内引用失真。默认采用多页面结构与稳定 ID,显示序号只用于阅读顺序。
|
||||
|
||||
建议页签:
|
||||
|
||||
- `00-总览`:角色、核心任务、端到端动线、状态机;
|
||||
- `10-核心页面`:主流程页面;
|
||||
- `20-角色或子域`:按角色/业务域拆分;
|
||||
- `30-异常状态`:跨页面异常与恢复;
|
||||
- `40-规格`:索引、权限、字段与口径。
|
||||
|
||||
如果用户要求单页巨幅板,仍先在左上放导航索引,并限制每行画板数量。
|
||||
@@ -0,0 +1,71 @@
|
||||
# 产品模型与覆盖检查
|
||||
|
||||
复杂原型先用以下表格建立轻量 SSOT。可以放在工作笔记中,也可以作为 Draw.io 的规格页;不要让原型、动线和规则各自维护一套名称。
|
||||
|
||||
## 页面清单
|
||||
|
||||
| 稳定 ID | 页面/状态 | 角色 | 入口 | 用户目标 | 主操作 | 成功去向 | 关键异常 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| P-LOGIN | 登录 | 访客 | 未登录 | 进入系统 | 登录 | P-HOME | S-AUTH-FAIL |
|
||||
|
||||
检查:每个页面都有入口;非终态至少有一个去向;弹窗写明触发页面。
|
||||
|
||||
## 转移清单
|
||||
|
||||
| 动作 ID | 来源 | 操作者 | 前置条件 | 动作 | 成功目标 | 失败目标/反馈 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| A-SUBMIT | P-EDIT | 申请人 | 校验通过 | 提交 | P-DETAIL / S-PENDING | P-EDIT / 保存失败消息 |
|
||||
|
||||
检查:每个主按钮有一条转移;每条连线对应真实操作;失败后能重试、返回或安全结束。
|
||||
|
||||
## 状态机
|
||||
|
||||
| 当前状态 | 可执行动作 | 角色 | 下一状态 | 字段可编辑性 | 记录/通知 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| S-DRAFT | A-SUBMIT | 申请人 | S-PENDING | 全部可编辑 | 创建审批任务 |
|
||||
|
||||
检查:状态名称唯一;每次变化有触发者;终态不会意外回到可编辑状态;并发变化有刷新或冲突处理。
|
||||
|
||||
## 角色权限矩阵
|
||||
|
||||
| 页面/动作 | 申请人 | 审批人 | 管理员 | 无权限表现 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P-DETAIL / 查看 | 本人数据 | 分配给自己的数据 | 全部 | 403 或无入口 |
|
||||
| A-DELETE / 删除 | 仅草稿 | 无 | 按规则 | 隐藏按钮或阻断 |
|
||||
|
||||
检查:区分「看不见」「只读」「可操作」;数据范围和按钮权限同时表达;管理员操作注明审计要求。
|
||||
|
||||
## 字段规则
|
||||
|
||||
| 字段 ID | 页面 | 类型 | 必填 | 值域/格式 | 显示条件 | 联动 | 错误反馈 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| F-AMOUNT | P-EDIT | 金额 | 是 | > 0,2 位小数 | 始终 | 影响合计 | 请输入有效金额 |
|
||||
|
||||
检查:原型中的标签与字段表一致;依赖字段有清空/保留规则;日期、金额、附件和枚举给出边界。
|
||||
|
||||
## 异常与恢复
|
||||
|
||||
| 异常 ID | 触发点 | 用户看到什么 | 数据是否保留 | 恢复动作 | 记录 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| E-SAVE-TIMEOUT | A-SAVE | 保存超时消息 | 保留本地输入 | 重试 | 请求 ID |
|
||||
|
||||
检查:错误文案可行动;不可恢复问题给出返回路径;危险操作有二次确认;成功反馈不与实际状态矛盾。
|
||||
|
||||
## 端到端覆盖
|
||||
|
||||
每个核心任务至少验证一条成功路径和一条高风险分支:
|
||||
|
||||
```text
|
||||
角色 → 入口 → 页面 → 动作 → 状态变化 → 下一角色/系统处理 → 终态 → 可见反馈
|
||||
```
|
||||
|
||||
发布前抽查:
|
||||
|
||||
- 新用户第一次进入;
|
||||
- 正常创建并完成;
|
||||
- 中途保存和再次进入;
|
||||
- 校验失败或服务失败;
|
||||
- 无权限或配置缺失;
|
||||
- 不可逆动作;
|
||||
- 多角色交接;
|
||||
- 已结束对象的只读/审计视角。
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate an editable multi-page Draw.io prototype skeleton from JSON."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FRAME_W = 1600
|
||||
FRAME_H = 900
|
||||
GAP_X = 120
|
||||
GAP_Y = 260
|
||||
|
||||
STYLE = {
|
||||
"label": "text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=24;fontStyle=1;fontFamily=Microsoft YaHei;",
|
||||
"bg": "rounded=0;whiteSpace=wrap;html=0;fillColor=#F4F7FB;strokeColor=#D9E2EF;",
|
||||
"nav": "rounded=0;whiteSpace=wrap;html=0;fillColor=#061B3A;strokeColor=#061B3A;fontColor=#C8D6EA;fontSize=16;fontFamily=Microsoft YaHei;align=left;verticalAlign=top;spacingTop=32;spacingLeft=32;",
|
||||
"title": "text;html=0;strokeColor=none;fillColor=none;fontColor=#10284B;fontSize=30;fontStyle=1;fontFamily=Microsoft YaHei;align=left;",
|
||||
"card": "rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#718198;fontSize=16;fontFamily=Microsoft YaHei;arcSize=8;",
|
||||
"primary": "rounded=1;whiteSpace=wrap;html=0;fillColor=#1768E8;strokeColor=#1768E8;fontColor=#FFFFFF;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;",
|
||||
"secondary": "rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#AFC1DA;fontColor=#27405F;fontSize=16;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;",
|
||||
"meta": "text;html=0;strokeColor=none;fillColor=none;fontColor=#718198;fontSize=13;fontFamily=Microsoft YaHei;align=left;",
|
||||
"table_head": "rounded=0;whiteSpace=wrap;html=0;fillColor=#EAF0F8;strokeColor=#D9E2EF;fontColor=#263750;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;",
|
||||
"table_row": "rounded=0;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#D9E2EF;fontColor=#40516A;fontSize=14;fontFamily=Microsoft YaHei;align=left;spacingLeft=18;",
|
||||
}
|
||||
|
||||
EDGE_COLORS = {
|
||||
"primary": "#1768E8",
|
||||
"success": "#18864B",
|
||||
"warning": "#D97706",
|
||||
"danger": "#D92D20",
|
||||
"secondary": "#5B6ABF",
|
||||
}
|
||||
|
||||
|
||||
def args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("spec", type=Path, help="JSON scaffold specification")
|
||||
parser.add_argument("output", type=Path, help="Target .drawio path")
|
||||
parser.add_argument("--force", action="store_true", help="Overwrite an existing output file")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def safe_id(value: str) -> str:
|
||||
value = re.sub(r"[^A-Za-z0-9_-]+", "-", value).strip("-").lower()
|
||||
return value or "frame"
|
||||
|
||||
|
||||
def geometry(parent: ET.Element, x: int, y: int, width: int, height: int) -> None:
|
||||
ET.SubElement(parent, "mxGeometry", {
|
||||
"x": str(x), "y": str(y), "width": str(width), "height": str(height), "as": "geometry"
|
||||
})
|
||||
|
||||
|
||||
def cell(root: ET.Element, cell_id: str, value: str, style: str, x: int, y: int,
|
||||
width: int, height: int) -> ET.Element:
|
||||
node = ET.SubElement(root, "mxCell", {
|
||||
"id": cell_id, "value": value, "style": style, "vertex": "1", "parent": "1"
|
||||
})
|
||||
geometry(node, x, y, width, height)
|
||||
return node
|
||||
|
||||
|
||||
def edge(root: ET.Element, edge_id: str, value: str, source: str, target: str, kind: str) -> None:
|
||||
color = EDGE_COLORS.get(kind, EDGE_COLORS["primary"])
|
||||
node = ET.SubElement(root, "mxCell", {
|
||||
"id": edge_id,
|
||||
"value": value,
|
||||
"style": (
|
||||
"edgeStyle=orthogonalEdgeStyle;rounded=1;html=0;endArrow=classic;endFill=1;"
|
||||
f"strokeWidth=2;strokeColor={color};fontColor=#40516A;"
|
||||
),
|
||||
"edge": "1",
|
||||
"parent": "1",
|
||||
"source": source,
|
||||
"target": target,
|
||||
})
|
||||
ET.SubElement(node, "mxGeometry", {"relative": "1", "as": "geometry"})
|
||||
|
||||
|
||||
def screen_frame(root: ET.Element, prefix: str, number: int, frame: dict, x: int, y: int,
|
||||
product: str) -> None:
|
||||
stable_id = frame["id"]
|
||||
title = frame["title"]
|
||||
state = frame.get("state", "")
|
||||
role = frame.get("role", "")
|
||||
suffix = f" · {state}" if state else ""
|
||||
cell(root, f"{prefix}-label", f"{number:02d} · {stable_id} · {title}{suffix}", STYLE["label"], x, y, 1300, 40)
|
||||
top = y + 55
|
||||
cell(root, f"{prefix}-bg", "", STYLE["bg"], x, top, FRAME_W, FRAME_H)
|
||||
cell(root, f"{prefix}-nav", f"{product}\n工作台\n核心任务\n数据概览\n系统设置", STYLE["nav"], x, top, 260, FRAME_H)
|
||||
cell(root, f"{prefix}-title", title, STYLE["title"], x + 310, top + 50, 700, 50)
|
||||
cell(root, f"{prefix}-card", "在此绘制页面内容、字段、表格和反馈状态", STYLE["card"], x + 310, top + 150, 1240, 560)
|
||||
cell(root, f"{prefix}-secondary", "次要操作", STYLE["secondary"], x + 1060, top + 745, 200, 50)
|
||||
cell(root, f"{prefix}-primary", "主要操作", STYLE["primary"], x + 1310, top + 745, 240, 50)
|
||||
cell(root, f"{prefix}-meta", f"角色:{role or '待定义'}|状态:{state or '待定义'}|稳定 ID:{stable_id}|下一步:待定义", STYLE["meta"], x + 310, top + 845, 1240, 30)
|
||||
|
||||
|
||||
def flow_frame(root: ET.Element, prefix: str, number: int, frame: dict, x: int, y: int) -> None:
|
||||
stable_id, title = frame["id"], frame["title"]
|
||||
cell(root, f"{prefix}-label", f"{number:02d} · {stable_id} · {title}", STYLE["label"], x, y, 1300, 40)
|
||||
top = y + 55
|
||||
cell(root, f"{prefix}-bg", "", STYLE["bg"], x, top, FRAME_W, FRAME_H)
|
||||
cell(root, f"{prefix}-title", title, STYLE["title"], x + 80, top + 55, 900, 50)
|
||||
cell(root, f"{prefix}-card", "按角色或阶段添加泳道;使用带 source/target 的正交连接线表达动作与结果", STYLE["card"], x + 80, top + 150, 1440, 620)
|
||||
cell(root, f"{prefix}-meta", f"稳定 ID:{stable_id}|覆盖:入口、判断、跨角色交接、成功与失败终态", STYLE["meta"], x + 80, top + 835, 1440, 30)
|
||||
|
||||
|
||||
def flow_graph(root: ET.Element, prefix: str, transitions: list[dict], frames: dict[str, dict],
|
||||
x: int, y: int) -> None:
|
||||
ordered_ids: list[str] = []
|
||||
for transition in transitions:
|
||||
for stable_id in (transition["from"], transition["to"]):
|
||||
if stable_id not in ordered_ids:
|
||||
ordered_ids.append(stable_id)
|
||||
|
||||
node_ids: dict[str, str] = {}
|
||||
for index, stable_id in enumerate(ordered_ids):
|
||||
col, row = index % 5, index // 5
|
||||
node_id = f"{prefix}-node-{safe_id(stable_id)}"
|
||||
node_ids[stable_id] = node_id
|
||||
frame = frames[stable_id]
|
||||
cell(
|
||||
root,
|
||||
node_id,
|
||||
f"{stable_id}\n{frame['title']}",
|
||||
"rounded=1;whiteSpace=wrap;html=0;fillColor=#FFFFFF;strokeColor=#1768E8;"
|
||||
"fontColor=#10284B;fontSize=14;fontStyle=1;fontFamily=Microsoft YaHei;arcSize=10;",
|
||||
x + 110 + col * 280,
|
||||
y + 245 + row * 155,
|
||||
220,
|
||||
82,
|
||||
)
|
||||
|
||||
for index, transition in enumerate(transitions):
|
||||
edge(
|
||||
root,
|
||||
f"{prefix}-edge-{index}",
|
||||
transition["label"],
|
||||
node_ids[transition["from"]],
|
||||
node_ids[transition["to"]],
|
||||
transition.get("kind", "primary"),
|
||||
)
|
||||
|
||||
|
||||
def spec_frame(root: ET.Element, prefix: str, number: int, frame: dict, x: int, y: int) -> None:
|
||||
stable_id, title = frame["id"], frame["title"]
|
||||
cell(root, f"{prefix}-label", f"{number:02d} · {stable_id} · {title}", STYLE["label"], x, y, 1300, 40)
|
||||
top = y + 55
|
||||
cell(root, f"{prefix}-bg", "", STYLE["bg"], x, top, FRAME_W, FRAME_H)
|
||||
cell(root, f"{prefix}-title", title, STYLE["title"], x + 80, top + 55, 900, 50)
|
||||
cell(root, f"{prefix}-head", "稳定 ID 名称/条件 角色 动作/规则 结果", STYLE["table_head"], x + 80, top + 150, 1440, 58)
|
||||
for row in range(4):
|
||||
cell(root, f"{prefix}-row-{row}", "待填写 待填写 待填写 待填写 待填写", STYLE["table_row"], x + 80, top + 208 + row * 66, 1440, 66)
|
||||
cell(root, f"{prefix}-meta", f"规格 ID:{stable_id}|所有引用使用页面/状态/动作稳定 ID", STYLE["meta"], x + 80, top + 835, 1440, 30)
|
||||
|
||||
|
||||
def load_spec(path: Path) -> dict:
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise ValueError(f"Cannot read spec: {exc}") from exc
|
||||
if not isinstance(data.get("pages"), list) or not data["pages"]:
|
||||
raise ValueError("spec.pages must be a non-empty list")
|
||||
columns = data.get("columns", 3)
|
||||
if not isinstance(columns, int) or not 1 <= columns <= 5:
|
||||
raise ValueError("columns must be an integer from 1 to 5")
|
||||
seen: set[str] = set()
|
||||
for page in data["pages"]:
|
||||
if page.get("kind") not in {"screen", "flow", "spec"}:
|
||||
raise ValueError(f"Unsupported page kind: {page.get('kind')!r}")
|
||||
if not page.get("name") or not isinstance(page.get("frames"), list) or not page["frames"]:
|
||||
raise ValueError("each page needs name and a non-empty frames list")
|
||||
for frame in page["frames"]:
|
||||
if not frame.get("id") or not frame.get("title"):
|
||||
raise ValueError("each frame needs id and title")
|
||||
if frame["id"] in seen:
|
||||
raise ValueError(f"duplicate stable frame id: {frame['id']}")
|
||||
seen.add(frame["id"])
|
||||
transitions = data.get("transitions", [])
|
||||
if not isinstance(transitions, list):
|
||||
raise ValueError("transitions must be a list")
|
||||
for transition in transitions:
|
||||
if not all(transition.get(key) for key in ("from", "to", "label")):
|
||||
raise ValueError("each transition needs from, to and label")
|
||||
if transition["from"] not in seen or transition["to"] not in seen:
|
||||
raise ValueError(f"transition references unknown frame: {transition}")
|
||||
if transition.get("kind", "primary") not in EDGE_COLORS:
|
||||
raise ValueError(f"unsupported transition kind: {transition.get('kind')!r}")
|
||||
return data
|
||||
|
||||
|
||||
def build(spec: dict) -> ET.ElementTree:
|
||||
mxfile = ET.Element("mxfile", {
|
||||
"host": "app.diagrams.net", "agent": "draw-prototype-flow", "version": "24.7.17",
|
||||
"type": "device", "compressed": "false", "pages": str(len(spec["pages"]))
|
||||
})
|
||||
product = spec.get("title", "产品名称")
|
||||
columns = spec.get("columns", 3)
|
||||
frames_by_id = {
|
||||
frame["id"]: frame
|
||||
for page in spec["pages"]
|
||||
for frame in page["frames"]
|
||||
}
|
||||
number = 0
|
||||
for page_index, page in enumerate(spec["pages"]):
|
||||
diagram = ET.SubElement(mxfile, "diagram", {
|
||||
"id": f"page-{page_index}-{safe_id(page['name'])}", "name": page["name"]
|
||||
})
|
||||
model = ET.SubElement(diagram, "mxGraphModel", {
|
||||
"dx": "1200", "dy": "800", "grid": "1", "gridSize": "10", "guides": "1",
|
||||
"tooltips": "1", "connect": "1", "arrows": "1", "fold": "1", "page": "0",
|
||||
"pageScale": "1", "math": "0", "shadow": "0"
|
||||
})
|
||||
root = ET.SubElement(model, "root")
|
||||
ET.SubElement(root, "mxCell", {"id": "0"})
|
||||
ET.SubElement(root, "mxCell", {"id": "1", "parent": "0"})
|
||||
for frame_index, frame in enumerate(page["frames"]):
|
||||
number += 1
|
||||
col, row = frame_index % columns, frame_index // columns
|
||||
x, y = col * (FRAME_W + GAP_X), row * (FRAME_H + GAP_Y) + 15
|
||||
prefix = f"p{page_index}-f{frame_index}-{safe_id(frame['id'])}"
|
||||
if page["kind"] == "screen":
|
||||
screen_frame(root, prefix, number, frame, x, y, product)
|
||||
elif page["kind"] == "flow":
|
||||
flow_frame(root, prefix, number, frame, x, y)
|
||||
else:
|
||||
spec_frame(root, prefix, number, frame, x, y)
|
||||
if page["kind"] == "flow" and spec.get("transitions"):
|
||||
flow_graph(root, f"p{page_index}-auto-flow", spec["transitions"], frames_by_id, 0, 15)
|
||||
ET.indent(mxfile, space=" ")
|
||||
return ET.ElementTree(mxfile)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
options = args()
|
||||
if options.output.exists() and not options.force:
|
||||
print(f"Refusing to overwrite existing file: {options.output}", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
spec = load_spec(options.spec)
|
||||
tree = build(spec)
|
||||
options.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
tree.write(options.output, encoding="utf-8", xml_declaration=True)
|
||||
except (ValueError, OSError) as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
frame_count = sum(len(page["frames"]) for page in spec["pages"])
|
||||
print(f"Created {options.output}: {len(spec['pages'])} page(s), {frame_count} frame(s)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate the structure of an uncompressed Draw.io product prototype."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
import zlib
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FRAME_RE = re.compile(r"^(?:\d{2,3}|P-[A-Z0-9-]+)\s*(?:[·.\-—-]|\s)")
|
||||
NUMERIC_REF_RE = re.compile(r"(?:下一步|关联|进入|见|去向)[^|\n]{0,40}(?<!\d)\d{2,3}(?!\d)")
|
||||
FLOW_PAGE_RE = re.compile(r"(?:总览|流程|动线|flow)", re.I)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Issue:
|
||||
level: str
|
||||
code: str
|
||||
message: str
|
||||
page: str | None = None
|
||||
cell: str | None = None
|
||||
|
||||
|
||||
def label_text(value: str | None) -> str:
|
||||
value = html.unescape(value or "")
|
||||
value = re.sub(r"<br\s*/?>", " ", value, flags=re.I)
|
||||
value = re.sub(r"<[^>]+>", "", value)
|
||||
return " ".join(value.replace("\xa0", " ").split())
|
||||
|
||||
|
||||
def is_frame_title(cell: ET.Element) -> bool:
|
||||
"""Recognize large external frame headings, not numbered table content."""
|
||||
if not FRAME_RE.match(label_text(cell.get("value"))):
|
||||
return False
|
||||
geometry = cell.find("mxGeometry")
|
||||
if geometry is None:
|
||||
return False
|
||||
try:
|
||||
width = float(geometry.get("width", "0"))
|
||||
height = float(geometry.get("height", "0"))
|
||||
except ValueError:
|
||||
return False
|
||||
style = cell.get("style", "")
|
||||
size_match = re.search(r"(?:^|;)fontSize=(\d+(?:\.\d+)?)(?:;|$)", style)
|
||||
font_size = float(size_match.group(1)) if size_match else 0
|
||||
return width >= 500 and height <= 80 and font_size >= 20
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("file", type=Path, help="Uncompressed .drawio file")
|
||||
parser.add_argument("--min-frames", type=int, default=0)
|
||||
parser.add_argument("--min-edges", type=int, default=0)
|
||||
parser.add_argument("--strict", action="store_true", help="Treat warnings as failures")
|
||||
parser.add_argument("--json", action="store_true", dest="as_json")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def graph_model(diagram: ET.Element) -> tuple[ET.Element | None, bool]:
|
||||
"""Return a graph model and whether the page used Draw.io compression."""
|
||||
model = diagram.find("mxGraphModel")
|
||||
if model is not None:
|
||||
return model, False
|
||||
payload = (diagram.text or "").strip()
|
||||
if not payload:
|
||||
return None, False
|
||||
try:
|
||||
decoded = base64.b64decode(payload)
|
||||
xml_text = urllib.parse.unquote(zlib.decompress(decoded, -15).decode("utf-8"))
|
||||
model = ET.fromstring(xml_text)
|
||||
except (ValueError, UnicodeDecodeError, zlib.error, ET.ParseError):
|
||||
return None, True
|
||||
return (model if model.tag == "mxGraphModel" else None), True
|
||||
|
||||
|
||||
def validate(path: Path, min_frames: int, min_edges: int) -> tuple[dict, list[Issue]]:
|
||||
issues: list[Issue] = []
|
||||
try:
|
||||
root = ET.parse(path).getroot()
|
||||
except (OSError, ET.ParseError) as exc:
|
||||
return {"file": str(path), "pages": []}, [Issue("error", "xml", str(exc))]
|
||||
|
||||
if root.tag != "mxfile":
|
||||
issues.append(Issue("error", "root", f"Expected mxfile root, found {root.tag!r}"))
|
||||
|
||||
diagrams = root.findall("diagram")
|
||||
if not diagrams:
|
||||
issues.append(Issue("error", "pages", "No diagram pages found"))
|
||||
|
||||
names = [diagram.get("name", "") for diagram in diagrams]
|
||||
for name in sorted(set(names)):
|
||||
if name and names.count(name) > 1:
|
||||
issues.append(Issue("warning", "duplicate-page-name", f"Duplicate page name: {name}"))
|
||||
|
||||
page_reports = []
|
||||
total_frames = 0
|
||||
total_edges = 0
|
||||
|
||||
for index, diagram in enumerate(diagrams, start=1):
|
||||
page_name = diagram.get("name") or f"page-{index}"
|
||||
model, compressed = graph_model(diagram)
|
||||
if model is None:
|
||||
issues.append(Issue(
|
||||
"error",
|
||||
"compressed-page",
|
||||
"Page has no readable mxGraphModel",
|
||||
page_name,
|
||||
))
|
||||
page_reports.append({"name": page_name, "vertices": 0, "edges": 0, "frames": 0})
|
||||
continue
|
||||
if compressed:
|
||||
issues.append(Issue(
|
||||
"warning",
|
||||
"compressed-page",
|
||||
"Compressed page is readable, but agent-generated output should use compressed=false",
|
||||
page_name,
|
||||
))
|
||||
|
||||
graph_root = model.find("root")
|
||||
if graph_root is None:
|
||||
issues.append(Issue("error", "graph-root", "Missing graph root", page_name))
|
||||
continue
|
||||
|
||||
cells = graph_root.findall("mxCell")
|
||||
by_id: dict[str, ET.Element] = {}
|
||||
for cell in cells:
|
||||
cell_id = cell.get("id")
|
||||
if not cell_id:
|
||||
issues.append(Issue("error", "missing-id", "mxCell has no id", page_name))
|
||||
continue
|
||||
if cell_id in by_id:
|
||||
issues.append(Issue("error", "duplicate-id", f"Duplicate cell id: {cell_id}", page_name, cell_id))
|
||||
by_id[cell_id] = cell
|
||||
|
||||
vertices = [cell for cell in cells if cell.get("vertex") == "1"]
|
||||
edges = [cell for cell in cells if cell.get("edge") == "1"]
|
||||
frame_cells = [cell for cell in vertices if is_frame_title(cell)]
|
||||
|
||||
for cell in cells:
|
||||
cell_id = cell.get("id")
|
||||
parent = cell.get("parent")
|
||||
if parent and parent not in by_id:
|
||||
issues.append(Issue("error", "dangling-parent", f"Unknown parent {parent}", page_name, cell_id))
|
||||
if (cell.get("vertex") == "1" or cell.get("edge") == "1") and cell.find("mxGeometry") is None:
|
||||
issues.append(Issue("error", "geometry", "Vertex/edge has no mxGeometry", page_name, cell_id))
|
||||
if cell.get("vertex") == "1" and NUMERIC_REF_RE.search(label_text(cell.get("value"))):
|
||||
issues.append(Issue(
|
||||
"warning",
|
||||
"numeric-cross-reference",
|
||||
"Cross-reference appears to use a display number; use a stable page/state ID",
|
||||
page_name,
|
||||
cell_id,
|
||||
))
|
||||
|
||||
for edge in edges:
|
||||
edge_id = edge.get("id")
|
||||
source = edge.get("source")
|
||||
target = edge.get("target")
|
||||
if not source or not target:
|
||||
issues.append(Issue("error", "unbound-edge", "Edge needs both source and target", page_name, edge_id))
|
||||
continue
|
||||
if source not in by_id:
|
||||
issues.append(Issue("error", "dangling-source", f"Unknown source {source}", page_name, edge_id))
|
||||
if target not in by_id:
|
||||
issues.append(Issue("error", "dangling-target", f"Unknown target {target}", page_name, edge_id))
|
||||
if not label_text(edge.get("value")):
|
||||
issues.append(Issue("warning", "unlabelled-edge", "User-flow edge has no action/result label", page_name, edge_id))
|
||||
|
||||
if len(frame_cells) > 15:
|
||||
issues.append(Issue(
|
||||
"warning",
|
||||
"crowded-page",
|
||||
f"{len(frame_cells)} numbered frames on one page; consider splitting into multiple pages",
|
||||
page_name,
|
||||
))
|
||||
if FLOW_PAGE_RE.search(page_name) and not edges:
|
||||
issues.append(Issue("warning", "no-flow", "Flow/overview page has no linked user-flow edges", page_name))
|
||||
|
||||
total_frames += len(frame_cells)
|
||||
total_edges += len(edges)
|
||||
page_reports.append({
|
||||
"name": page_name,
|
||||
"cells": len(cells),
|
||||
"vertices": len(vertices),
|
||||
"edges": len(edges),
|
||||
"frames": len(frame_cells),
|
||||
})
|
||||
|
||||
if total_frames < min_frames:
|
||||
issues.append(Issue("error", "min-frames", f"Found {total_frames} frames; expected at least {min_frames}"))
|
||||
if total_edges < min_edges:
|
||||
issues.append(Issue("error", "min-edges", f"Found {total_edges} edges; expected at least {min_edges}"))
|
||||
|
||||
return {
|
||||
"file": str(path),
|
||||
"page_count": len(diagrams),
|
||||
"frame_count": total_frames,
|
||||
"edge_count": total_edges,
|
||||
"pages": page_reports,
|
||||
}, issues
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
report, issues = validate(args.file, args.min_frames, args.min_edges)
|
||||
errors = sum(issue.level == "error" for issue in issues)
|
||||
warnings = sum(issue.level == "warning" for issue in issues)
|
||||
|
||||
if args.as_json:
|
||||
print(json.dumps({**report, "issues": [asdict(issue) for issue in issues]}, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(f"{report['file']}: {report.get('page_count', 0)} page(s), "
|
||||
f"{report.get('frame_count', 0)} frame(s), {report.get('edge_count', 0)} edge(s)")
|
||||
for page in report.get("pages", []):
|
||||
print(f" {page['name']}: {page.get('frames', 0)} frames, "
|
||||
f"{page.get('vertices', 0)} vertices, {page.get('edges', 0)} edges")
|
||||
for issue in issues:
|
||||
where = " / ".join(value for value in (issue.page, issue.cell) if value)
|
||||
suffix = f" ({where})" if where else ""
|
||||
print(f" {issue.level.upper()} [{issue.code}] {issue.message}{suffix}")
|
||||
print(f"Result: {errors} error(s), {warnings} warning(s)")
|
||||
|
||||
return 1 if errors or (args.strict and warnings) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,13 +1,13 @@
|
||||
# manage-release
|
||||
|
||||
`manage-release` 帮助 Agent 管理从开发版本到发布的 Git 流程,包括 worktree、分支、
|
||||
版本号、PR/MR、合并和 release tag。它可以只处理其中一个阶段,也可以从准备开发目录
|
||||
一直执行到发布。
|
||||
`manage-release` 帮助 Agent 发布已经准备好的 Git commit,也能在发布前确有必要时管理
|
||||
版本文件、worktree、分支、PR/MR 和合并。普通发布优先走“更新说明 + annotated tag”的
|
||||
短路径;只有发布要求修改仓库内容时,才进入准备版本的分支流程。
|
||||
|
||||
## 使用前准备
|
||||
|
||||
- 项目已经是 Git 仓库,并配置了可访问的远端。
|
||||
- 项目已有版本文件或发布约定;没有时可以让 Agent 先给出版本建议。
|
||||
- 项目已有版本来源或 tag 发布约定;没有时可以让 Agent 先给出版本建议。
|
||||
- 创建或合并 PR/MR、创建 Forge Release 时,准备好 GitHub、GitLab、Gitea 或 Forgejo
|
||||
对应的已认证 CLI。只发布 Git tag 时不要求 Forge CLI,但 Git remote 必须可读写。
|
||||
- 请求里写清目标版本、基线分支,以及允许执行到哪一步。没有明确授权的远端写操作
|
||||
@@ -54,11 +54,21 @@ Agent 只会在仓库要求全部满足后合并,不会使用管理员权限
|
||||
### 为已合并版本发布 tag
|
||||
|
||||
```text
|
||||
使用 manage-release,为已经合并到 main 的 1.6.0 发布 v1.6.0 tag。确认 tag 指向包含该版本号的 merged commit,不要创建 Forge Release。
|
||||
使用 manage-release,为已经合并到 main 的 1.6.0 发布 v1.6.0 tag。根据 v1.5.0 到目标 commit 的实际变化生成更新说明,把说明写入 annotated tag,并验证远端 tag 的 commit 和说明。不要创建 Forge Release。
|
||||
```
|
||||
|
||||
适合依靠 Git tag 触发后续 CI 发布的项目。Agent 会分别验证版本文件、目标 commit 和
|
||||
远端 tag,不会给未合并分支打正式 tag。
|
||||
这是无需修改仓库文件时的默认短路径,适合依靠 Git tag 触发后续 CI 发布的项目。Agent
|
||||
会分别验证目标 commit、更新说明和远端 tag,不会创建 worktree、发布分支或 PR,也不会
|
||||
给未合并分支打正式 tag。
|
||||
|
||||
### 为已合并版本创建 Gitea Release
|
||||
|
||||
```text
|
||||
使用 manage-release,发布已经合并到 main 的 1.6.0:生成更新说明,创建并验证附带说明的 v1.6.0 annotated tag,然后在 Gitea 创建使用同一份说明的 Release。
|
||||
```
|
||||
|
||||
Agent 会先证明 tag 已存在、指向计划 commit 且包含更新说明,再创建 Gitea Release。
|
||||
Release 创建失败时保留正确的 tag,从 Release 阶段恢复。
|
||||
|
||||
### 完成端到端发布
|
||||
|
||||
@@ -66,8 +76,9 @@ Agent 只会在仓库要求全部满足后合并,不会使用管理员权限
|
||||
使用 manage-release 完成 1.6.0 发布:从 main 创建 worktree 和分支,完成版本更新和验证,创建 PR,检查通过后合并,推送 v1.6.0 tag,并创建 Forge Release。
|
||||
```
|
||||
|
||||
这条请求授权完整流程。遇到 review 未通过、CI 失败、版本冲突或已有同名 tag 时,
|
||||
Agent 会停止并说明卡在哪一步,不会绕过检查继续发布。
|
||||
只有版本文件或仓库内 CHANGELOG 必须随发布修改时才使用这条长路径。tag 仍必须附带更新
|
||||
说明,Forge Release 复用同一份说明。遇到 review 未通过、CI 失败、版本冲突或已有同名
|
||||
tag 时,Agent 会停止并说明卡在哪一步,不会绕过检查继续发布。
|
||||
|
||||
### 发布紧急修复版本
|
||||
|
||||
@@ -89,18 +100,19 @@ Skill 会从 Git 和代码托管平台重新判断状态,再继续缺失的步
|
||||
|
||||
## Agent 会做什么
|
||||
|
||||
Agent 会先发现项目自己的版本、分支和发布规则,再检查本地 Git 与远端状态。它只执行
|
||||
请求中明确授权的阶段,并在合并、tag 或 release 条件不满足时停止。完整流程结束后,
|
||||
结果中会分别列出 worktree、分支、版本、PR/MR、merged commit、tag 和 Forge Release
|
||||
状态。
|
||||
Agent 会先发现项目自己的版本和发布规则、锁定远端目标 commit,再判断发布是否要求修改
|
||||
仓库内容。不需要修改时直接生成更新说明并发布 annotated tag;需要修改时才准备分支,
|
||||
并只在隔离用户工作确有必要时创建 worktree。它只执行请求中明确授权的阶段,并在合并、
|
||||
tag 或 release 条件不满足时停止。
|
||||
|
||||
## 如何判断完成
|
||||
|
||||
- 只分析版本时,结果包含建议版本及依据。
|
||||
- 创建开发环境时,结果包含 worktree 路径、分支和基线 commit。
|
||||
- 创建或合并 PR/MR 时,结果包含 URL、检查状态和 merged commit。
|
||||
- 发布 tag 时,远端 tag 解引用后的 commit 与 merged commit 一致。
|
||||
- 创建 Forge Release 时,结果包含可访问的 release URL。
|
||||
- 发布 tag 时,远端 tag 解引用后的 commit 与目标 commit 一致,而且 tag object 包含经过
|
||||
核对的更新说明。
|
||||
- 创建 Forge Release 时,结果包含可访问的 release URL,正文与 tag 更新说明一致。
|
||||
- 中途停止时,结果说明停在哪一步、为什么停止,以及下次如何继续。
|
||||
|
||||
## 不适用的场景
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
---
|
||||
name: manage-release
|
||||
description: >-
|
||||
管理 Git 项目从开发版本到发布的完整生命周期:发现并遵循项目分支与版本策略,
|
||||
创建或复用隔离 worktree 和版本分支,确定并同步版本号,推送并创建、检查或合并
|
||||
PR/MR,在合并后的准确提交上创建并推送 release tag,并按需创建 Forge Release、
|
||||
恢复中断流程或安全清理。用户要求开始发布相关的新版本、为发布开 worktree 或分支、
|
||||
升版本、提交或合并发布 PR/MR、打 release tag、完成发版、处理 hotfix 或继续未完成
|
||||
发布时使用。只做普通编码、普通 worktree 或 PR/MR 操作、代码审查、构建或上传
|
||||
DEB/Docker 等产物、管理仓库权限时不使用。
|
||||
管理 Git 项目从已准备 commit 到版本发布的生命周期:发现并遵循项目版本与发布策略,
|
||||
优先为远端已验证 commit 生成更新说明并创建 annotated release tag,按需创建 Forge
|
||||
Release;只有必须修改版本文件或仓库内发布说明时,才创建或复用版本分支、worktree
|
||||
和 PR/MR。也支持恢复中断流程与安全清理。用户要求发布版本、打 release tag、创建
|
||||
平台 Release、准备版本变更、处理 hotfix 或继续未完成发布时使用。只做普通编码、普通
|
||||
worktree 或 PR/MR 操作、代码审查、构建或上传 DEB/Docker 等产物、管理仓库权限时不使用。
|
||||
---
|
||||
|
||||
# Manage Release
|
||||
@@ -40,9 +39,11 @@ description: >-
|
||||
项目策略只描述仓库期望的做法,不能替用户授予 push、创建或合并 PR/MR、推送 tag、
|
||||
创建 Forge Release、删除分支等外部写权限。
|
||||
|
||||
“完整处理并发布 v1.2.3”可以授权从准备到验证远端 tag 的流程,不要在每一步重复询问;
|
||||
它不自动包含 Forge Release 或分支清理。若版本、base、tag、远端或合并方式是推断所得,
|
||||
或者执行中目标发生变化,在第一次远端写操作前展示准确目标并获得确认。
|
||||
“发布 v1.2.3”允许为已经准备好的远端 commit 生成更新说明、创建并推送 annotated tag,
|
||||
再验证远端 tag 的 commit 和说明。“完整处理并发布 v1.2.3”还允许在仓库内容必须修改时
|
||||
走版本分支、PR/MR 和合并流程。两者都不自动包含 Forge Release 或分支清理。若版本、
|
||||
base、目标 commit、tag、远端或合并方式是推断所得,或者执行中目标发生变化,在第一次
|
||||
远端写操作前展示准确目标和更新说明并获得确认。
|
||||
|
||||
## 工作流
|
||||
|
||||
@@ -70,25 +71,47 @@ description: >-
|
||||
|
||||
在修改前确定并展示:
|
||||
|
||||
- base 分支及其远端 commit。
|
||||
- 当前版本、规范化后的目标版本、渲染出的分支名和 tag,以及升级依据。
|
||||
- 分支名与 worktree 路径。
|
||||
- base 分支、远端 commit,以及本次 tag 将指向的准确目标 commit。
|
||||
- 当前版本、规范化后的目标版本、tag,以及升级依据;需要准备仓库修改时再渲染分支名。
|
||||
- 从上一个稳定 tag 到目标 commit 的更新说明范围、来源和拟发布内容。
|
||||
- 是否需要修改仓库内容;若需要,再列出分支名与 worktree 路径。
|
||||
- 需要执行的本地验证和远端检查。
|
||||
- 合并方式、tag、是否创建 Forge Release。
|
||||
- 用户已授权的最远阶段和清理范围。
|
||||
|
||||
项目没有约定时,单一协调版本使用 SemVer、`release/v<version>` 分支、
|
||||
`v<version>` annotated tag。仓库只启用一种合并方式时使用该方式;存在多种方式且没有
|
||||
项目规则时默认 squash。默认不删除远端分支,不自动创建 Forge Release。
|
||||
项目没有约定时,单一协调版本使用 SemVer 和 `v<version>` annotated tag。只有发布前
|
||||
必须修改仓库内容时才使用 `release/v<version>` 分支;仓库只启用一种合并方式时使用该
|
||||
方式,存在多种方式且没有项目规则时默认 squash。默认不删除远端分支,不自动创建
|
||||
Forge Release。
|
||||
|
||||
把用户输入拆成“规范版本”和“展示名称”:默认 SemVer 的规范版本是无 `v` 前缀的
|
||||
`1.6.0`,版本文件和 `{version}` 都使用该值;分支与 tag 再分别按 pattern 渲染为
|
||||
`release/v1.6.0` 和 `v1.6.0`。用户输入 `v1.6.0` 时先规范化,不要产生双前缀。
|
||||
|
||||
### 3. 创建或复用隔离 worktree
|
||||
### 3. 选择直接发布或准备版本
|
||||
|
||||
优先使用当前环境提供的 worktree 管理器;项目仓库中的管理脚本只有在可信 base 已声明
|
||||
且经过检查时才能使用,没有时使用原生 Git。创建前验证:
|
||||
先计算发布所需的仓库内容差异,不要为了遵循固定模板而创建分支或 worktree。满足以下
|
||||
条件时走直接发布路径,跳过第 4 至 7 节:
|
||||
|
||||
- 目标 commit 已作为准确远端引用读取,并可从计划的 base 或项目允许的维护分支到达。
|
||||
- 项目权威版本文件若存在,目标 commit 中已经是目标版本;或项目明确以 tag 作为版本来源。
|
||||
- 项目不要求把本次 CHANGELOG 或发布说明提交回仓库。
|
||||
- 项目要求的构建、测试和发布检查已经通过。
|
||||
|
||||
直接发布不 checkout 目标 commit,不创建分支、worktree、版本提交或 PR/MR。更新说明是
|
||||
tag object 和可选 Forge Release 的内容,不因生成说明本身进入仓库修改路径。
|
||||
|
||||
只要版本文件、生成元数据或仓库内 CHANGELOG 必须修改,就进入准备版本路径。优先复用
|
||||
身份准确、未被占用且适合本次发布的现有分支;没有时才创建版本分支。优先复用安全且
|
||||
干净的现有 worktree;当前目录承载用户工作、目标分支已在别处使用或需要隔离时,才创建
|
||||
linked worktree。用户只授权了直接发布时,在展示必要差异后停止,不要擅自扩大到修改、
|
||||
分支或 PR/MR 流程。
|
||||
|
||||
### 4. 创建或复用准备版本的分支和 worktree
|
||||
|
||||
仅在第 3 节判定需要准备仓库修改时执行本节。优先使用当前环境提供的 worktree 管理器;
|
||||
项目仓库中的管理脚本只有在可信 base 已声明且经过检查时才能使用,没有时使用原生 Git。
|
||||
创建前验证:
|
||||
|
||||
- 目标分支和目录未被其他 worktree 占用。
|
||||
- 目标目录不是 `/`、用户主目录、仓库根目录或已有非空目录。
|
||||
@@ -105,7 +128,7 @@ worktree 占用的分支。
|
||||
相对计划 base 的提交,确认是同一次发布后用 `git worktree add <path> <branch>` 挂载;
|
||||
身份不匹配或来源不明时停止,不要用 `-b` 覆盖或另建同名分支。
|
||||
|
||||
### 4. 开发并同步版本
|
||||
### 5. 修改并同步版本
|
||||
|
||||
在目标 worktree 内完成用户要求的修改。根据项目规则更新所有权威版本来源和
|
||||
CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声明的格式化、测试、构建
|
||||
@@ -119,7 +142,7 @@ CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声
|
||||
提交前重新读取 HEAD 和完整工作区状态,只提交本次范围内的文件。不要在未合并分支上
|
||||
创建正式 release tag。
|
||||
|
||||
### 5. 推送并创建 PR/MR
|
||||
### 6. 推送并创建 PR/MR
|
||||
|
||||
执行远端写操作前再次确认 remote、base、head、版本、活动账号和授权范围,并重新读取
|
||||
唯一 fetch/push URL。Git 分支与 tag 通过已确认的 remote 读写;创建或操作 PR/MR 时使用
|
||||
@@ -130,7 +153,7 @@ CHANGELOG 或发布说明,保证它们进入同一个 PR/MR。运行项目声
|
||||
PR/MR 内容至少说明目标版本、变更摘要、验证命令和结果、发布后续动作。用户只要求开
|
||||
PR/MR 时,停在这里并返回 URL、head/base、当前检查状态和阻塞项。
|
||||
|
||||
### 6. 检查并合并
|
||||
### 7. 检查并合并
|
||||
|
||||
从托管平台重新读取 PR/MR 状态。仅在下列条件全部满足时合并:
|
||||
|
||||
@@ -143,29 +166,40 @@ PR/MR 时,停在这里并返回 URL、head/base、当前检查状态和阻塞
|
||||
不要使用管理员绕过、直接推送受保护 base,或为通过检查而修改保护规则。合并后获取
|
||||
平台确认的 merged commit,更新远端 base,并验证该 commit 可从远端 base 到达。
|
||||
|
||||
### 7. 创建并发布 tag
|
||||
### 8. 创建并发布带更新说明的 tag
|
||||
|
||||
在 merged commit 上核对目标版本后,再查询一次远端同名 tag。tag 不存在时创建
|
||||
annotated tag;项目要求签名时,必须先确认签名工具和密钥可用,创建 signed tag,并在
|
||||
本地验证签名成功后才允许 push。签名不可用或验证失败时停止,不得降级为 unsigned tag。
|
||||
显式指定 merged commit,并只推送这个 tag。
|
||||
准备版本路径使用平台确认且可从远端 base 到达的 merged commit;直接发布路径使用计划
|
||||
中锁定且重新验证过的远端目标 commit。两者统一记为 release commit。核对目标版本后,
|
||||
再查询一次远端同名 tag。
|
||||
|
||||
创建 tag 前,根据上一个稳定 tag 到 release commit 的实际差异生成更新说明;首次发布则
|
||||
使用项目声明的发布基线,没有声明时核对完整可达历史并明确标记首次发布。说明概括实际
|
||||
存在的新增、变更、修复、维护或文档变化;存在破坏性变化、迁移步骤或已知限制时必须明确
|
||||
列出。每项内容都应能追溯到本次提交、PR/MR 或项目 CHANGELOG。空说明、只重复版本号、
|
||||
模板占位文字或无法由实际变化支持的内容都不能发布。
|
||||
|
||||
tag 不存在时,使用完整更新说明创建 annotated tag;项目要求签名时,必须先确认签名
|
||||
工具和密钥可用,创建 signed annotated tag,并在本地验证签名成功后才允许 push。使用
|
||||
文件输入完整的多行说明,显式指定 release commit,并只推送这个 tag。禁止创建 lightweight
|
||||
release tag。签名不可用或验证失败时停止,不得降级为 unsigned tag。
|
||||
|
||||
同名远端 tag 已存在且指向其他 commit 时立即停止。不要覆盖、删除或移动已经发布的
|
||||
tag。推送后读取远端 tag,并将 annotated tag 解引用到 commit,确认它与 merged commit
|
||||
完全相同。
|
||||
tag。同名 tag 即使指向 release commit,只要它是 lightweight tag、缺少更新说明或说明与
|
||||
计划不一致,也视为身份不匹配并停止。推送后读取远端 tag object,确认它是 annotated
|
||||
tag,将它解引用到 commit 并核对完整更新说明;commit 和说明必须都与发布计划完全一致。
|
||||
|
||||
仅当用户本次请求明确要求时创建 Forge Release;项目策略只能说明创建方式,不能授予
|
||||
外部写权限。必须引用已经存在并验证过的 tag,禁止让平台从默认分支隐式创建 tag。
|
||||
发布说明中的每项变化都应能追溯到本次 PR/MR、提交或项目 CHANGELOG。
|
||||
Forge Release 正文复用 tag 中经过验证的更新说明,不维护第二份相互独立的发布内容。
|
||||
|
||||
### 8. 验证、恢复和清理
|
||||
### 9. 验证、恢复和清理
|
||||
|
||||
分别报告以下状态,不要用“发布成功”掩盖其中某一步未完成:
|
||||
|
||||
- 版本文件和本地验证。
|
||||
- PR/MR URL、合并状态和 merged commit。
|
||||
- 远端 tag 及其解引用后的 commit。
|
||||
- Forge Release URL 和可见性,若本次要求创建。
|
||||
- 远端 tag 的对象类型、完整更新说明及其解引用后的 commit。
|
||||
- Forge Release URL、可见性及正文一致性,若本次要求创建。
|
||||
- worktree、本地分支和远端分支是否保留。
|
||||
|
||||
清理时只移除干净且已确认不再使用的 linked worktree,不使用强制删除。删除本地或远端
|
||||
@@ -176,12 +210,15 @@ tag。推送后读取远端 tag,并将 annotated tag 解引用到 commit,确
|
||||
遇到以下任一情况时停止相应写操作并说明恢复路径:
|
||||
|
||||
- base、目标版本、版本来源、remote,或请求阶段所需的托管平台无法唯一确定。
|
||||
- 直接发布的目标 commit 无法从计划远端分支到达,或项目要求的发布检查未通过。
|
||||
- 项目策略包含未知字段、错误类型、仓库外路径或不安全 ref。
|
||||
- 多个权威版本来源不一致。
|
||||
- 目标分支、worktree、PR/MR、tag 或 release 已存在但身份不匹配。
|
||||
- required checks、review 或冲突状态不满足合并规则。
|
||||
- 合并结果的准确 commit 无法从平台确认,或无法从远端 base 到达。
|
||||
- 远端 tag 已指向其他 commit。
|
||||
- 更新说明为空、只有版本号、包含占位内容、无法追溯到实际变化,或远端 tag 不是包含
|
||||
计划说明的 annotated tag。
|
||||
- 请求的 PR/MR、合并或 Forge Release 阶段所需的平台 CLI 缺失、未认证或无法读取状态。
|
||||
- 项目要求 tag 签名,但签名能力不可用或本地签名验证失败。
|
||||
- 操作需要 force push、管理员绕过、移动已发布 tag 或覆盖用户工作。
|
||||
@@ -192,7 +229,9 @@ tag。推送后读取远端 tag,并将 annotated tag 解引用到 commit,确
|
||||
|
||||
## 完成标准
|
||||
|
||||
只分析时,给出当前状态、建议版本、依据和下一步。开始版本时,给出 worktree 路径、
|
||||
分支和基线 commit。创建 PR/MR 时,给出 URL 与检查状态。合并时,给出 merged commit。
|
||||
发布 tag 时,证明远端 tag 解引用到该 commit。创建 Forge Release 时,再给出 release URL
|
||||
与可见性。任何部分未完成都要标明阻塞阶段和可恢复动作。
|
||||
只分析时,给出当前状态、建议版本、依据和下一步。直接发布时,给出目标 commit 和更新
|
||||
说明,不得虚构 worktree 或分支步骤。开始准备版本时,给出所用 worktree、分支和基线
|
||||
commit;未新建 worktree 时明确说明复用了哪个安全工作区。创建 PR/MR 时,给出 URL 与
|
||||
检查状态。合并时,给出 merged commit。发布 tag 时,证明远端 tag 是包含计划更新说明的
|
||||
annotated tag,并解引用到 release commit。创建 Forge Release 时,再给出 release URL、
|
||||
可见性与正文一致性。任何部分未完成都要标明阻塞阶段和可恢复动作。
|
||||
|
||||
@@ -68,14 +68,16 @@ commit 已进入远端 base。只看到本地 merge commit 或分支关闭不足
|
||||
|
||||
## 创建 Forge Release
|
||||
|
||||
把 Git tag 和 Forge Release 当成两个独立状态。先创建、推送并验证 tag,再创建 release。
|
||||
把 Git tag 和 Forge Release 当成两个独立状态。先创建、推送并验证包含完整更新说明的
|
||||
annotated tag,再创建 release。Forge Release 正文复用已经冻结并验证的 tag 更新说明,
|
||||
不要重新生成另一份内容。
|
||||
|
||||
- GitHub 创建 release 时使用能够拒绝缺失 tag 的选项,例如当前 CLI 支持的
|
||||
`--verify-tag`。
|
||||
- GitLab、Gitea 或 Forgejo 创建 release 前,先用只读命令证明 tag 已存在并指向计划的
|
||||
commit;不要使用会顺便创建 tag 的默认行为。
|
||||
commit、tag object 包含计划的更新说明;不要使用会顺便创建 tag 的默认行为。
|
||||
- 预发布版本按项目规则标记 prerelease,不要自动把它标为 latest 或 stable。
|
||||
- 发布后重新读取 release URL、tag 和可见性。
|
||||
- 发布后重新读取 release URL、tag、正文和可见性,确认正文与 tag 更新说明一致。
|
||||
|
||||
Forge Release 创建失败但 tag 已成功推送时,保留 tag 并从 release 阶段恢复,不要重新
|
||||
合并或创建另一个 tag。
|
||||
|
||||
@@ -28,9 +28,11 @@
|
||||
|
||||
- `schema`:配置结构版本,当前为 `1`。
|
||||
- `base_branch`:发布 PR/MR 的目标分支。
|
||||
- `branch_pattern`:版本分支格式,支持无展示前缀的规范 `{version}`。
|
||||
- `branch_pattern`:需要准备仓库修改时使用的版本分支格式,支持无展示前缀的规范
|
||||
`{version}`;直接发布不因此创建分支。
|
||||
- `version.scheme`:`semver` 或项目已经使用的其他方案。
|
||||
- `version.sources`:构建和运行实际读取的权威版本文件。
|
||||
- `version.sources`:构建和运行实际读取的权威版本文件;项目明确从 Git tag 派生版本时
|
||||
可以省略,不要为了发布新增无消费方的版本文件。
|
||||
- `tag_pattern`:release tag 格式,支持无展示前缀的规范 `{version}`。
|
||||
- `merge_method`:`squash`、`merge` 或 `rebase`。
|
||||
- `forge_release`:项目是否建议在 tag 后创建 Forge Release;它不授予创建权限。
|
||||
@@ -54,18 +56,20 @@ symlink 后,目标必须仍在当前 worktree 内;权威来源通常还应
|
||||
|
||||
1. 从 remote HEAD 和平台信息确定默认 base,不能确定时询问。
|
||||
2. 从项目文档、CI 和构建入口找版本文件及更新方式。
|
||||
3. 从已有分支和已合并 PR/MR 识别命名与合并方式。
|
||||
4. 从稳定 tag 识别前缀和版本方案,只把 tag 当作交叉验证。
|
||||
5. 无项目约定时使用 `release/v<version>`、SemVer、`v<version>` annotated tag 和
|
||||
squash merge;默认不创建 Forge Release,不删除远端分支。
|
||||
3. 仅当发布需要修改仓库内容时,从已有分支和已合并 PR/MR 识别命名与合并方式。
|
||||
4. 从稳定 tag 识别前缀、版本方案和更新说明范围;项目由 tag 派生版本时,把实际构建
|
||||
入口和 tag 历史共同作为版本来源证据,否则只把 tag 当作交叉验证。
|
||||
5. 无项目约定时使用 SemVer 和 `v<version>` annotated tag;需要准备仓库修改时再使用
|
||||
`release/v<version>` 和 squash merge。默认不创建 Forge Release,不删除远端分支。
|
||||
|
||||
## 每次执行的计划快照
|
||||
|
||||
在第一次写操作前列出以下事实:
|
||||
|
||||
- base 分支和远端 commit。
|
||||
- base 分支、远端 commit 和 tag 将指向的目标 commit。
|
||||
- 当前版本、目标版本、版本来源和升级依据。
|
||||
- worktree 路径与分支名。
|
||||
- 更新说明的提交范围、来源和拟发布内容。
|
||||
- 是否需要修改仓库内容;需要时再列出 worktree 路径与分支名。
|
||||
- PR/MR 托管平台和合并方式。
|
||||
- tag 与 Forge Release 计划。
|
||||
- 用户授权的最远阶段和清理范围。
|
||||
|
||||
@@ -12,12 +12,15 @@ base、head、PR/MR、merged commit、tag 和 release,再执行唯一缺失的
|
||||
|
||||
| 当前状态 | 继续方式 | 禁止事项 |
|
||||
|---|---|---|
|
||||
| 远端 release commit 已准备,没有 tag | 核对版本与更新说明后直接创建 annotated tag | 不创建无必要的分支或 worktree |
|
||||
| 本地 annotated tag 已创建但未推送 | 核对对象类型、commit 和完整说明后只推送该 tag | 不因重入重复创建或改写 tag |
|
||||
| worktree 已创建,无改动 | 继续开发,或经授权移除干净 worktree | 不使用强制删除 |
|
||||
| 本地发布分支已存在,没有 worktree | 核对版本身份、tip、upstream 和 base 后挂载现有分支 | 不用 `-b` 覆盖分支 |
|
||||
| 分支已推送,没有 PR/MR | 确认 head/base 后创建一次 PR/MR | 不重复推送新分支 |
|
||||
| PR/MR 已存在,未合并 | 复用 URL,刷新 checks、review 和冲突状态 | 不创建第二个 PR/MR |
|
||||
| PR/MR 已合并,没有 tag | 获取 merged commit,在该 commit 上继续发布 | 不重新合并 |
|
||||
| tag 已推送,没有 release | 验证 tag 后创建 Forge Release | 不创建替代 tag |
|
||||
| PR/MR 已合并,没有 tag | 获取 merged commit,生成并确认更新说明后继续发布 | 不重新合并 |
|
||||
| tag 已推送,没有 release | 验证 tag 的 commit 和更新说明后创建 Forge Release | 不创建替代 tag |
|
||||
| tag 指向正确 commit 但为 lightweight 或说明不符 | 停止并交由维护者决定撤销或发布修订版本 | 不移动、覆盖或补写远端 tag |
|
||||
| release 已创建,验证未完成 | 读回 release、tag 和可见性 | 不直接声称发布完成 |
|
||||
| 同名资源身份不匹配 | 停止并报告差异 | 不覆盖、关闭或删除未知资源 |
|
||||
|
||||
@@ -35,7 +38,7 @@ merge queue 和 auto-merge。任何 force push 都是硬停止条件。
|
||||
- 未推送的本地版本提交可以在用户授权下修改或放弃,但不要覆盖其他工作。
|
||||
- 已推送但未合并的 PR/MR 可以关闭,分支默认保留。
|
||||
- 已合并变更通过新的 revert PR/MR 回滚,不重写 base 历史。
|
||||
- 已发布 tag 默认不可变。tag 错误时停止,由维护者决定撤销发布或发布新版本。
|
||||
- 已发布 tag 默认不可变。commit 或更新说明错误时停止,由维护者决定撤销发布或发布新版本。
|
||||
- Forge Release 失败不回滚已经正确推送的 tag;从 release 阶段恢复。
|
||||
|
||||
## 清理规则
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
按项目实际构建链路寻找版本来源,不要遍历到一个看起来像版本号的字符串就修改。优先级:
|
||||
|
||||
1. 项目发布文档或 `.release-policy.yaml` 明确声明的文件。
|
||||
2. 构建、打包或运行入口直接读取的清单,例如 `VERSION`、`package.json`、
|
||||
2. 项目明确由 Git tag 或 VCS metadata 派生构建版本时,以 tag 规则作为发布版本来源,
|
||||
不要为了发布凭空新增或修改版本文件。
|
||||
3. 构建、打包或运行入口直接读取的清单,例如 `VERSION`、`package.json`、
|
||||
`pyproject.toml`、`Cargo.toml` 或语言工具链的版本配置。
|
||||
3. 由权威文件生成的镜像文件、锁文件或发布元数据。
|
||||
4. 最近稳定 tag,只用于验证当前版本和发布历史。
|
||||
4. 由权威文件生成的镜像文件、锁文件或发布元数据。
|
||||
5. 最近稳定 tag,用于确定当前已发布版本、更新说明范围和发布历史。
|
||||
|
||||
记录每个权威文件的当前值和更新方式。多个权威来源不一致时停止,不要选择修改时间最新
|
||||
的文件,也不要只改其中一个。
|
||||
|
||||
配置声明的版本文件必须使用仓库相对路径。拒绝绝对路径、`..` 和解析后逃出目标
|
||||
配置声明了版本文件时,它们必须使用仓库相对路径。拒绝绝对路径、`..` 和解析后逃出目标
|
||||
worktree 的 symlink;修改前确认规范化后的准确路径。不要用 glob 或模糊搜索结果执行
|
||||
批量替换。
|
||||
|
||||
@@ -45,10 +47,19 @@ worktree 的 symlink;修改前确认规范化后的准确路径。不要用 gl
|
||||
项目采用自定义前缀或非 SemVer 时,以项目版本来源定义规范值,以 branch/tag pattern
|
||||
定义展示形式。规范化结果无法唯一确定时停止并让用户确认。
|
||||
|
||||
## 判断是否需要同步版本
|
||||
|
||||
直接读取计划 release commit 中的权威版本来源。版本文件已经是目标值,或者项目明确由
|
||||
tag 派生版本,且发布说明不要求写回仓库时,不产生仓库修改,直接进入 tag 发布。
|
||||
|
||||
只有权威版本文件、生成文件或仓库内 CHANGELOG 必须变化时,才进入准备版本流程并使用
|
||||
分支/PR。不要为了制造发布提交而触碰与构建、运行或项目发布规则无关的文件。
|
||||
|
||||
## 同步版本
|
||||
|
||||
在发布分支中一次性更新所有权威版本来源、生成文件和项目要求的 CHANGELOG。运行项目
|
||||
自己的版本更新工具时,先检查它会修改哪些文件,避免隐式发布、提交或上传。
|
||||
仅在判断存在必要仓库差异后,在准备版本的分支中一次性更新所有权威版本来源、生成文件
|
||||
和项目要求的 CHANGELOG。运行项目自己的版本更新工具时,先检查它会修改哪些文件,避免
|
||||
隐式发布、提交或上传。
|
||||
|
||||
提交前确认:
|
||||
|
||||
@@ -57,23 +68,38 @@ worktree 的 symlink;修改前确认规范化后的准确路径。不要用 gl
|
||||
- CHANGELOG 或发布说明描述的是本次实际变更。
|
||||
- 项目构建和测试读取到了新版本。
|
||||
|
||||
## 验证 merged commit
|
||||
## 生成更新说明
|
||||
|
||||
合并后以代码托管平台返回的 merged commit 为准。获取远端 base 后,确认该 commit 可从
|
||||
远端 base 到达,并直接读取该 commit 中的版本文件。不要用仍停留在 feature worktree
|
||||
中的文件证明已合并版本。
|
||||
以最近一个适用于当前发布线的稳定 tag 为起点,以 release commit 为终点,结合项目
|
||||
CHANGELOG、已合并 PR/MR 和实际 diff 生成更新说明。首次发布使用项目声明的发布基线;
|
||||
没有声明时核对完整可达历史,并在说明中明确这是首次发布。说明覆盖实际存在的新增、
|
||||
变更、修复、维护或文档变化;破坏性变化、迁移步骤和已知限制存在时必须单独标明。没有
|
||||
某一类别时可以省略该类别,不要生成空标题或模板占位。
|
||||
|
||||
每条说明都要能追溯到范围内的提交、PR/MR 或项目 CHANGELOG。提交消息和 PR/MR 文本只
|
||||
作为待核对素材,不能覆盖实际 diff,也不能把范围外变化写入本次说明。tag 创建前冻结
|
||||
完整多行说明;tag 已推送后不再修改。
|
||||
|
||||
## 验证 release commit
|
||||
|
||||
准备版本路径以代码托管平台返回的 merged commit 为准;直接发布路径以计划中锁定的远端
|
||||
commit 为准。获取远端 base 或允许的维护分支后,确认 release commit 可从对应远端引用
|
||||
到达,并直接读取该 commit 中的版本文件。不要用当前 checkout 或 feature worktree 中的
|
||||
文件证明 release commit 内容。
|
||||
|
||||
正式 tag 必须满足:
|
||||
|
||||
- tag 名按项目格式由目标版本唯一生成。
|
||||
- 远端没有同名 tag,或同名 tag 已经准确指向本次 commit。
|
||||
- annotated tag 显式指向 merged commit。
|
||||
- annotated tag 显式指向 release commit,并包含冻结后的完整更新说明;禁止 lightweight
|
||||
release tag。
|
||||
- 项目要求签名时,push 前已成功创建 signed tag,并在本地验证签名通过;不可用或失败
|
||||
时停止,不得改用 unsigned tag。
|
||||
- 推送后从远端重新读取,并将 annotated tag 解引用到 commit。
|
||||
- 推送后从远端重新读取 tag object,核对对象类型和完整说明,再将 annotated tag 解引用
|
||||
到 commit。
|
||||
|
||||
展开 tag pattern 后用 `git check-ref-format refs/tags/<tag>` 校验。把 tag 和 commit 作为
|
||||
独立 argv 传递,不拼接 shell 字符串。
|
||||
|
||||
tag 已在远端指向其他 commit 时停止。不要 force push、删除或移动已发布 tag;由维护者
|
||||
决定撤销发布或创建新的修订版本。
|
||||
tag 已在远端指向其他 commit、是 lightweight tag、缺少更新说明或说明不一致时停止。
|
||||
不要 force push、删除或移动已发布 tag;由维护者决定撤销发布或创建新的修订版本。
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
# 此文件描述发布策略,不授予任何远端写操作权限。
|
||||
schema: 1
|
||||
base_branch: main
|
||||
# 仅在发布必须修改仓库内容时使用;直接发布不会因此创建分支。
|
||||
# {version} 是规范版本,例如 1.6.0;前缀由 pattern 添加。
|
||||
branch_pattern: release/v{version}
|
||||
version:
|
||||
scheme: semver
|
||||
# 项目明确从 Git tag 派生版本时可以省略 sources。
|
||||
sources:
|
||||
- VERSION
|
||||
tag_pattern: v{version}
|
||||
|
||||
Reference in New Issue
Block a user