新增 AutoRDO 需求清洗工作台与消息中枢,迭代 OneOS V2 设计规范及租赁合同/工作台/车辆等原型,同步云效技能与导航注册;并归档一批 legacy 原型快照。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-28 15:50:35 +08:00
parent b14425da5a
commit 5d51a6bf7a
1393 changed files with 488042 additions and 10046 deletions

View File

@@ -0,0 +1,113 @@
---
name: YunxiaoPM
description: >-
产品经理云效Projex自动化记录需求压缩点选 1a2b3a4d类型/项目/优先级/标签)、
实时点选云效项目、推进 待处理→已确认→分析中→设计中→设计完成→待开发,
交付树【交付】ASSOCIATED /【分析】【设计】TASK_SUB无单快轨与编号直推交棒何斐
创建迭代并挂【交付】(不挂需求)。用户说 YunxiaoPM、YunxiaoPMapp、需求任务、记录需求、受理确认、开始分析、
开始设计、设计完成、交棒开发、快轨待开发、编号直推、创建迭代 时使用。
不建【开发】/【测试】。凡写云效先 Plan 确认再一口气 apply禁止对齐
yunxiao-requirement-lifecycle。
---
# 需求任务YunxiaoPM
产品部云效自动化。斜杠调起 **`/YunxiaoPM`**;对外中文名 **需求任务**(原名 YunxiaoPMapp。本 Skill **自洽成篇****禁止** fork / include / 「对齐」`yunxiao-requirement-lifecycle`
产品经理会话**不要**同时挂载旧 lifecycle Skill避免双建任务。
## Plan 模式门禁(强制 · 凡写云效)
凡会改云效的操作(建单、改状态、建/改任务、打标、传附件、改负责人、建迭代等Agent **第一步**必须:
1. `SwitchMode`**plan**(说明:先对齐参数与执行清单,确认后再一口气 apply
2. **新建**依赖项目空间时:先按 [references/project-selection.md](references/project-selection.md) **实时拉项目列表并点选(门禁 PJ**;禁止静默使用 `runtime-ids.json` 默认 `spaceIdentifier`。口令/选项**无法对应**时:**自动重拉项目列表一次**再匹配;仍失败则停请用户重选(禁止第 3 次空转拉取)。
3. Plan 写清:**已选项目名 + spaceId**、目标需求/任务**编号**、将改状态、将建/复用的交付·分析·设计编号策略、迭代版本类型若适用、§0.1① 占位风险勾选(若适用)、**不会做的事**(不建【开发】/【测试】、不按标题查重)。**记录需求**须按 [compact-select.md](references/compact-select.md) 给出 14 题字母表,接受压缩答复如 `1a2b3a4d`
4. **用户确认 / 批准 Plan / 「执行」之前**:禁止 apply项目未点选同禁。压缩串须先解析回显再等「执行」。
5. 确认后切回 Agent**同一轮按清单一口气执行到底**,再一次性校验回报;中途缺参才停下。
**例外(可读可不进 Plan** 仅查状态 / 为什么没流转 / 给我方案。
**禁止:** 以「参数已齐」「速度路径」「用户很熟」跳过 Plan「批准计划」若清单未点齐关键参数**PJ 项目**),仍视为未完成门禁。
## 真相源模型
```text
需求状态 = 阶段看板唯一真相
【交付】 = 每需求最多 1 条容器ASSOCIATED→需求
【分析】/【设计】 = TASK_SUB→交付交付「子项」必可见与 ASSOCIATED 同 create 互斥)
【开发】/【测试】 = 不进本 Skill
查重/复用唯一渠道 = 任务编号ONEOS-xx禁止按标题
```
编号权威:需求描述 `## 工作项编号(系统)`(见 [references/workitem-ids.md](references/workitem-ids.md))。
操作顺序:口令显式编号 > 读该区块 > ASSOCIATED/SUB 校验;冲突则停。
## 外置调用(禁止本 Skill 内嵌对方全文)
| 时机 | 调用 |
|---|---|
| 入库清洗聊天/录音 | **`$AutoRDO`**(独立 Skill路径 `AutoRDO/SKILL.md`;不内嵌清洗细则) |
| 设计完成 PRD + 对象存储链接 + 回填【交付】 | **`$oneos-autoprd`AutoPRD**;创建【交付】仍占位,设计完成才灌 MD |
| 人员 / 状态 / 字段 ID项目 catalog 仅缓存 | [assets/runtime-ids.json](assets/runtime-ids.json) |
| **PJ 云效项目点选**(新建必选;实时列表) | [references/project-selection.md](references/project-selection.md) · [scripts/list_projects.py](scripts/list_projects.py) |
| **压缩点选 `1a2b3a4d`**(类型/项目/优先级/标签) | [references/compact-select.md](references/compact-select.md) · [scripts/list_tags.py](scripts/list_tags.py) |
| 阶段日历工时 | [references/work-hours.md](references/work-hours.md) + [assets/cn-workday-calendar.json](assets/cn-workday-calendar.json) + [scripts/workday_hours.py](scripts/workday_hours.py) |
## 路由(按需完整阅读)
| 场景 | 模块 |
|---|---|
| 交付树、关联约定、禁止项 | [references/model.md](references/model.md) |
| 描述双段 · AutoRDO / 占位 / AutoPRD | [references/description-split.md](references/description-split.md) |
| 步骤 05 标准路径 | [references/stage-flow.md](references/stage-flow.md) |
| 无单快轨到待开发 | [references/fast-track.md](references/fast-track.md) |
| 编号直推交棒 | [references/number-push.md](references/number-push.md) |
| 交棒门禁 · 回退最小集 | [references/handoff-and-rollback.md](references/handoff-and-rollback.md) |
| 计划开始/完成 · 阶段日历工时 | [references/work-hours.md](references/work-hours.md) |
| Make 导出 ZIP + 复制截图 | [references/make-export-attach.md](references/make-export-attach.md) |
| 创建迭代 · V主.副.子 · 只挂交付 | [references/sprint.md](references/sprint.md) |
| 口令面 | [references/commands.md](references/commands.md) |
| 记录需求元字段(优先级/标签/提交部门/提交人) | [references/record-meta-fields.md](references/record-meta-fields.md) |
| 云效项目点选 PJ | [references/project-selection.md](references/project-selection.md) |
| 验收清单 · 回报模板 | [references/acceptance.md](references/acceptance.md) |
| 交接契约(开发 Skill 入口) | [references/handoff-contract.md](references/handoff-contract.md) |
| 已验证实写 API · 极速建单 | [references/live-api.md](references/live-api.md) · [scripts/live_create_fast.py](scripts/live_create_fast.py) |
| 2026-07-23 复盘与耗时对比 | [references/live-perf-2026-07-23.md](references/live-perf-2026-07-23.md) |
## 口令速查
```text
记录需求:…;项目=Plan 点选,勿默认);优先级=紧急|高|中|低;标签=…;提交部门=…;提交人=…;推进至=暂不推进|已确认|分析中|设计中|设计完成|待开发|待开发(快轨)
受理确认ONEOS-xx
开始分析ONEOS-xx
开始设计ONEOS-xx交付任务=…;分析任务=…
设计完成ONEOS-xx设计任务=…;原型=…
交棒开发ONEOS-xx交付任务=…
快轨待开发ONEOS-xx
编号直推:分析任务=ONEOS-b / 设计任务=ONEOS-c / 交付任务=ONEOS-a
创建迭代:版本类型=主|副|子;交付任务=ONEOS-a,ONEOS-b,…;名称前缀=…
```
**PJ 项目**:新建前必须从云效实时列表点选(见 [project-selection.md](references/project-selection.md));口令带项目名仅作预填建议。
**压缩点选**:记录需求 Plan 展示 `1.类型 2.项目 3.优先级 4.标签` 字母表;你可回 `1a2b3a4d`(见 [compact-select.md](references/compact-select.md))。标签未命中会自动重拉一次标签候选并重生选项。
**记录需求**元字段(优先级/标签/提交部门/提交人):与压缩点选并用;缺项用字母表补齐。见 [references/record-meta-fields.md](references/record-meta-fields.md)。
后续口令**优先带任务编号**;未带则读「工作项编号(系统)」;仍无则询问;**禁止按标题补全**。
## 本 Skill 终点
交棒完成(需求=待开发;【交付】负责人=何斐)后结束。可一句:「请技术经理使用开发 Skill」。
例外:交棒后「创建迭代并关联交付」仍属 YunxiaoPM。
**明确不做:** 创建【开发】/【测试】、挂仓库、开分支、提测、写用例。
## §0.1 五条补齐(摘要)
1. **交棒占位**:标准路径下交付仍为 `等待设计任务完成后自动填入` 时**允许**交棒,但 Plan 必须勾选风险,回报首行标红。**快轨**有手工/原型时禁止占位(见 [fast-track.md](references/fast-track.md))。
2. **预计工时**:标准路径 = **阶段日历工时**工作日×8**快轨待开发**需求默认预计/实际各 **2**
3. **编号真相源**在需求「工作项编号(系统)」;新建后立即 PATCH 该区块。
4. **无单快轨**:【设计】描述同步需求;计划起止=当日TASK_SUB→交付后补 ASSOCIATED→需求【交付】描述手工同步或 AutoPRD交付/设计与需求同标签;设计当日完成态。
5. **描述双段**不互相覆盖;迭代**只挂【交付】**(需求不挂迭代);回退重做设计则**新开设计编号**,交付计划开始不改。
细则见各 references。

View File

@@ -0,0 +1,49 @@
# 验收清单与回报
## apply 后必须自检
| # | 项 | 通过标准 |
|---|---|---|
| 1 | 需求编号 | 回报含 ONEOS-xx |
| 2 | 任务编号 | 交付/分析/设计凡新建或操作均回报编号;禁止只报标题 |
| 3 | 编号区块 | 需求「工作项编号(系统)」与实际一致 |
| 4 | ASSOCIATED | **仅交付**详情关联项可见需求(`createWorkitemRelationInfo=ASSOCIATED`;禁止 PARENT 冒充) |
| 5 | SUB | **必过**:分析/设计 create 用 `TASK_SUB→交付`(含 `parent`+`parentIdentifier`交付「子项」tab 须可见。与 ASSOCIATED 同 create 互斥,阶段任务关联项可空(见 live-api.md |
| 6 | 计划开始 | 未误覆盖已有值 |
| 7 | 阶段日历工时 | 有计划完成时已写入;脚注存在;回报用语正确 |
| 8 | 交棒 | 需求=待开发;交付负责人=何斐(交棒场景) |
| 9 | 占位风险 | 交付仍占位时首行标红 |
| 10 | 负向 | 本轮**无**【开发】/【测试】任务;未按标题查重 |
设计完成额外AutoPRD 段已写交付非占位除非失败停下ZIP+截图已挂或已列出缺项。
迭代额外:只挂【交付】(需求不挂);版本号符合递增规则。
## 回报模板(精简)
```text
【YunxiaoPMapp】
风险:(若有占位交棒则首行标红)
需求ONEOS-xx | 状态=…
交付ONEOS-a | 负责人=…
分析ONEOS-b | …(无则写无)
设计ONEOS-c | …(无则写无)
阶段日历工时:分析 Hh / 设计 Hh若本轮写入
附件:…(若本轮)
迭代:…(若本轮)
下一步:请技术经理使用开发 Skill交棒后
```
## 适合全自动 vs 人工门禁
| 适合全自动 | 建议半自动/人工门禁 |
|---|---|
| 建单、打标签、挂迭代、写描述、建交付树、改状态、交棒负责人、幂等查重 | 受理(已确认)、是否快轨、设计完成是否真可开发、跨需求优先级与迭代容量、回退与取消 |
## 实写性能2026-07-23 复盘)
| 项 | 要求 |
|---|---|
| 改状态 | 只用 `status/transit`(见 [live-api.md](live-api.md) |
| 改【交付】负责人 | 只用 `PATCH …/{id}` + `propertyKey=assignedTo` |
| 极速复测 | `scripts/live_create_fast.py`;默认不开浏览器 |

View File

@@ -0,0 +1,4 @@
interface:
display_name: "需求任务"
short_description: "云效:压缩点选记录需求→分析/设计→交棒待开发→挂迭代"
default_prompt: "按需求任务(/YunxiaoPM处理先 Plan 出压缩点选字母表并确认项目,用户回 1a2b3a4d 与「执行」后再 apply。"

View File

@@ -0,0 +1,95 @@
{
"schema_version": 1,
"timezone": "Asia/Shanghai",
"note": "法定放假日 holidays国务院公布的周末调休补班日 workdays_on_weekend。缺年则禁止静默按仅去周末计算。",
"source": {
"2025": "国办发明电202412号 https://www.gov.cn/zhengce/zhengceku/202411/content_6986383.htm",
"2026": "国办发明电20257号 https://www.gov.cn/zhengce/content/202511/content_7047090.htm"
},
"years": {
"2025": {
"holidays": [
"2025-01-01",
"2025-01-28",
"2025-01-29",
"2025-01-30",
"2025-01-31",
"2025-02-01",
"2025-02-02",
"2025-02-03",
"2025-02-04",
"2025-04-04",
"2025-04-05",
"2025-04-06",
"2025-05-01",
"2025-05-02",
"2025-05-03",
"2025-05-04",
"2025-05-05",
"2025-05-31",
"2025-06-01",
"2025-06-02",
"2025-10-01",
"2025-10-02",
"2025-10-03",
"2025-10-04",
"2025-10-05",
"2025-10-06",
"2025-10-07",
"2025-10-08"
],
"workdays_on_weekend": [
"2025-01-26",
"2025-02-08",
"2025-04-27",
"2025-09-28",
"2025-10-11"
]
},
"2026": {
"holidays": [
"2026-01-01",
"2026-01-02",
"2026-01-03",
"2026-02-15",
"2026-02-16",
"2026-02-17",
"2026-02-18",
"2026-02-19",
"2026-02-20",
"2026-02-21",
"2026-02-22",
"2026-02-23",
"2026-04-04",
"2026-04-05",
"2026-04-06",
"2026-05-01",
"2026-05-02",
"2026-05-03",
"2026-05-04",
"2026-05-05",
"2026-06-19",
"2026-06-20",
"2026-06-21",
"2026-09-25",
"2026-09-26",
"2026-09-27",
"2026-10-01",
"2026-10-02",
"2026-10-03",
"2026-10-04",
"2026-10-05",
"2026-10-06",
"2026-10-07"
],
"workdays_on_weekend": [
"2026-01-04",
"2026-02-14",
"2026-02-28",
"2026-05-09",
"2026-09-20",
"2026-10-10"
]
}
}
}

View File

@@ -0,0 +1,221 @@
{
"schema_version": 1,
"skill": "YunxiaoPM",
"verified_at": "2026-07-25",
"note": "YunxiaoPM constants. Project space MUST be user-selected (PJ gate); project.spaceIdentifier is last-known cache only — never auto-apply. See references/project-selection.md.",
"project": {
"selection_mode": "user_pick_required",
"name": null,
"name_aliases": [
"01_ONEOS",
"ONEOS",
"统一运营管理平台",
"统一运营管理平台PC端",
"统一运营管理平台 PC 端"
],
"spaceIdentifier": null,
"customCode": null,
"spaceType": "Project",
"organizationIdentifier": "697c54a19df7fdfa65466405",
"default_sprint_name_prefix": "统一运营管理平台PC端",
"list_api": "GET /projex/api/workspace/project/search/list",
"last_selected": {
"name": "01_ONEOS",
"spaceIdentifier": "1280be963a5a2cc126a4118dca",
"customCode": "ONEOS",
"note": "历史常用项;仅作预填建议,禁止未点选即使用"
},
"refreshed_at": "2026-07-25"
},
"projects_catalog": [
{
"name": "05_羚牛碳资产平台",
"identifier": "ff104a3bce09463da136a97999",
"customCode": "CARBON"
},
{
"name": "06_对外客户项目",
"identifier": "baca8b4d676c5af67e475caec0",
"customCode": "CUST"
},
{
"name": "07_LNBOX",
"identifier": "35e1e915a052379bbdfb993aac",
"customCode": "LNBOX"
},
{
"name": "04_AI应用",
"identifier": "d9002ea72c6c3a1a97b02be750",
"customCode": "AIAPP"
},
{
"name": "03_数据中台",
"identifier": "a801cef5c9a68fa051c07432c7",
"customCode": "DATA"
},
{
"name": "02_小羚羚APP",
"identifier": "db771a2cca07bed43b369af077",
"customCode": "XLLAPP"
},
{
"name": "01_ONEOS",
"identifier": "1280be963a5a2cc126a4118dca",
"customCode": "ONEOS"
},
{
"name": "敏捷研发示例项目",
"identifier": "65eca0c2e16a23939081e19e14",
"customCode": "DEMO"
}
],
"people": {
"wangmian": {
"displayName": "王冕",
"identifier": "6811df000601d2fea60144a9",
"role": "产品创建人/默认负责人"
},
"hefei": {
"displayName": "何斐",
"identifier": "695f0400562f09713f9c3a93",
"role": "待开发交棒【交付】负责人"
}
},
"tags": {
"故障管理": "ceb526a7343995577645317e9a",
"还车应结款": "4204960ce658c94b17abb7c5f6",
"工作台": "b62d21beac55c0b43389915eab",
"交车管理": "b6947b5aca82a8c759612ba039",
"合同管理": "23873be81931cb0dc1bf87a456",
"安全培训": "76988b5f73ef515de5930d59b9",
"证照管理": "11f4c2ec65901a8f3199c81706",
"还车管理": "1e0fce2d6929ff4ac13b310e97"
},
"status": {
"req": {
"待处理": "100005",
"已确认": "32",
"分析中": "154395",
"设计中": "156603",
"设计完成": "307012",
"待开发": "1582fc929d429111b925309493"
},
"task": {
"待处理": "100005",
"已完成": "100014"
},
"transit_api": "POST /projex/api/workitem/workitem/{id}/status/transit",
"fast_handoff_hops": [
"设计完成",
"待开发"
],
"note": "见 references/live-api.md禁止再用 updateStatus"
},
"workitem_types": {
"product_req": {
"name": "产品类需求",
"category": "Req",
"identifier": "9uy29901re573f561d69jn40"
},
"task": {
"name": "任务",
"category": "Task",
"identifier": "ba102e46bc6a8483d9b7f25c"
}
},
"priority": {
"紧急": "646004e97f54bb77fec7b455df",
"高": "95b89e0a524d9693e1f335ffe5",
"中": "fa155d1214f9f8db222d39db3b",
"低": "92924feff9c1085891e7511872"
},
"fields": {
"plan_start": {
"fieldIdentifier": "79",
"name": "计划开始时间",
"value_shape": "YYYY-MM-DD HH:mm:ss China wall time preferred; epoch ms string also accepted",
"example": "2026-07-27 12:00:00",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON"
},
"plan_end": {
"fieldIdentifier": "80",
"name": "计划完成时间",
"value_shape": "YYYY-MM-DD HH:mm:ss or epoch ms string",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON"
},
"estimated_hours": {
"fieldIdentifier": "101586",
"name": "预计工时",
"note": "不可直接改字段;须工时预估登记",
"create_api": "POST /projex/api/workitem/workitem/time/estimate body spentTime,type,recordUserIdentifier,workitemIdentifier",
"delete_api": "DELETE /projex/api/workitem/workitem/time/estimate/{workitemId}/{estimateId}",
"list_api": "GET /projex/api/workitem/workitem/time/estimate/list?workitemIdentifier="
},
"actual_hours": {
"name": "实际工时",
"note": "快轨待开发默认 2",
"create_api": "POST /projex/api/workitem/workitem/time body actualTime,type,recordUserIdentifier,workitemIdentifier,gmtStart,gmtEnd (epoch ms string)",
"list_api": "GET /projex/api/workitem/workitem/time/list?workitemIdentifier=",
"fast_track_default": 2,
"verified_at": "2026-07-27",
"verified_on": "ONEOS-293"
},
"fast_track": {
"req_estimated_hours": 2,
"req_actual_hours": 2,
"design_plan_start_end_same_day": true,
"design_description": "copy_req_document",
"delivery_description": "manual_sync_or_autoprd",
"delivery_design_same_tags_as_req": true,
"design_associated_to_req_after_task_sub": true,
"document_update_api": "PATCH /projex/api/workitem/workitem/{id}/document {content,formatType:RICHTEXT}"
},
"submit_department": {
"fieldIdentifier": "3132597a9718d1c282b7ba5a0c",
"name": "提交部门",
"format": "string input",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON",
"verified_at": "2026-07-27",
"verified_on": "ONEOS-293"
},
"submitter": {
"fieldIdentifier": "9e01269e96f91fbb97d36bf5b3",
"name": "提交人",
"format": "string input",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON",
"verified_at": "2026-07-27",
"verified_on": "ONEOS-293"
},
"tag": {
"fieldIdentifier": "tag",
"name": "标签",
"propertyKey": "tag"
}
},
"assignee_rules": {
"待开发_交付任务": "hefei",
"分析中_交付与分析": "creator",
"设计中_设计": "creator"
},
"relation": {
"delivery_to_req": "ASSOCIATED",
"stage_to_delivery": "TASK_SUB",
"create_field": "createWorkitemRelationInfo",
"forbid": [
"PARENT_as_关联项",
"ASSOCIATED_only_for_stage_tasks_without_TASK_SUB"
],
"note": "交付必须 ASSOCIATED→需求。分析/设计默认 TASK_SUB→交付子项 tab。二者同 create 互斥。见 references/live-api.md"
},
"delivery_placeholder": "等待设计任务完成后自动填入",
"task_title_prefixes": {
"delivery": "【交付】",
"analysis": "【分析】",
"design": "【设计】"
},
"publish_url": {
"shape": "{baseUrl}/{prototype-id}/index.html",
"forbid_prototypes_prefix": true,
"require_index_html": true
}
}

View File

@@ -0,0 +1,317 @@
# YunxiaoPMapp 实现原理说明
> 供**开发部门**设计 / 制作「开发侧 Skill」暂称 **YunxiaoDevapp**)时对齐契约。
> 本文描述产品侧 Skill **YunxiaoPMapp** 的模型、边界、数据契约与交棒接口;**不是**对 `yunxiao-requirement-lifecycle` 的兼容说明。
> 技能包https://github.com/15810879921-coder/oneos-pm-skills · `skills/YunxiaoPMapp/`
> 文档版本2026-07-24
---
## 1. 为什么拆成两个 Skill
| | YunxiaoPMapp产品 | 开发 Skill待建 |
|---|---|---|
| 职责 | 需求从「待处理」到「待开发」交棒 | 从「待开发」到开发完成 / 提测前 |
| 任务类型 | 【交付】【分析】【设计】 | 【开发】(可多条)、可选挂仓库/分支 |
| 负责人交接 | 交棒时【交付】→ **何斐** | 拆【开发】子任务并指派研发 |
| 明确不做 | 建【开发】/【测试】、开分支、提测 | 不建【分析】/【设计】、不改 AutoRDO 段 |
**硬规则:** 两个 Skill **禁止互相 include / fork 全文**;只认本文第 6 章「交棒契约」。产品会话与开发会话不要同时挂载旧 `yunxiao-requirement-lifecycle`,避免双建任务树。
---
## 2. 核心真相源(必须先统一)
```text
需求状态 = 阶段看板唯一真相(分析中 / 设计中 / 待开发 …)
【交付】任务 = 每需求最多 1 条「交付容器」
【分析】【设计】 = 【交付】下的阶段子项(产品侧建)
【开发】【测试】 = 开发 / 测试 Skill 建(产品侧永不创建)
查重 / 复用 = 只认云效任务编号 ONEOS-xx禁止按标题
```
### 2.1 关系模型(云效)
```mermaid
flowchart TB
Req["产品类需求 ONEOS-R"]
DEL["【交付】ONEOS-a"]
AN["【分析】ONEOS-b"]
DE["【设计】ONEOS-c"]
DEV["【开发】… 开发 Skill"]
TEST["【测试】… 测试 Skill"]
DEL -->|"ASSOCIATED 关联项"| REQ
AN -->|"TASK_SUB 子项"| DEL
DE -->|"TASK_SUB 子项"| DEL
DEV -.->|"建议SUB→交付 或 ASSOCIATED→需求+交付"| DEL
TEST -.->|"测试 Skill"| REQ
```
| 关系 | 谁写 | 验收 |
|---|---|---|
| `ASSOCIATED` | **仅【交付】→ 需求** | 交付详情「关联项」可见需求 |
| `TASK_SUB` | 【分析】/【设计】→【交付】 | 交付详情「子项」可见阶段任务 |
| (开发侧建议) | 【开发】→【交付】或需求 | 由开发 Skill 自定,但须可编号查重 |
**踩坑(已复现):** 对分析/设计只写 `parentIdentifier` + `ASSOCIATED→需求`交付「子项」会为空。正确create 时 `createWorkitemRelationInfo=TASK_SUB` + `parent`/`parentIdentifier`=交付。同一 create **只能**带一条关系ASSOCIATED 与 TASK_SUB 互斥。
### 2.2 标题只是展示
- 形式:`【交付】`/`【分析】`/`【设计】` + 需求标题
- **不参与查重**;改标题不影响幂等
- 开发侧【开发】标题建议 `【开发】` + 范围简述,同样**只认编号**
---
## 3. 编号权威源
需求描述固定区块(机器可解析):
```markdown
## 工作项编号(系统)
- 交付ONEOS-a
- 分析ONEOS-b无则写无
- 设计ONEOS-c无则写无
```
| 规则 | 说明 |
|---|---|
| 新建后立即 PATCH | 只改本区块,不动 AutoRDO / AutoPRD |
| 操作顺序 | 口令显式编号 > 读本区块 > ASSOCIATED/SUB 反查 |
| 冲突 | 停下人工;**禁止按标题猜** |
| 多交付编号 | 停止并列号请人合并 |
**开发 Skill 入口建议:** 口令必须带 `需求=ONEOS-R` + `交付任务=ONEOS-a`;缺则读本区块;仍无则询问。
---
## 4. 需求描述双段(产品写 · 开发只读)
```markdown
## 原始诉求AutoRDO
(聊天/录音清洗稿;设计完成也不删)
## 产品说明AutoPRD
(设计完成才灌满;含对象存储预览链接)
## 工作项编号(系统)
- 交付 / 分析 / 设计
```
| 段 | 谁写 | 开发侧用法 |
|---|---|---|
| AutoRDO | `$AutoRDO` → 产品记需求时 | 占位交棒时**唯一可信**业务原文 |
| AutoPRD | `$oneos-autoprd` → 设计完成时 | 正式需求说明;缺则有权要求产品补设计完成 |
| 编号区块 | YunxiaoPMapp | 定位交付树 |
**【交付】描述:** 设计完成前固定占位文案 `等待设计任务完成后自动填入`;设计完成后替换为 AutoPRD 正文。
**允许占位交棒**,但产品回报必须标红风险;开发 Skill 应检测占位并提示「材料不齐,可凭 AutoRDO 开工或退回补设计」。
对象存储预览 URL 形态:`{baseUrl}/{prototype-id}/index.html`(禁止加 `prototypes/` 前缀、禁止去掉 `index.html`)。
---
## 5. 产品侧阶段状态机0→5
```text
0 创建需求(待处理) ─AutoRDO→ 仅需求,不建任务
1 受理确认(已确认) 仍不建任务
2 分析中 建【交付】+【分析】
3 设计中 建【设计】;收口【分析】计划完成
4 设计完成 收口【设计】AutoPRD+附件;灌【交付】描述
5 待开发交棒 ★ 需求→待开发;【交付】负责人→何斐
★ = YunxiaoPMapp 终点 / 开发 Skill 起点
```
### 5.1 各步要点(开发需知道的副作用)
| 步骤 | 需求状态 | 任务侧关键动作 |
|---|---|---|
| 2 分析中 | 分析中 | 新建【交付】ASSOCIATED→需求描述=占位;计划开始仅空写当日且**此后不可改**新建【分析】TASK_SUB→交付 |
| 3 设计中 | 设计中 | 新建【设计】;【分析】计划完成=当日 + 阶段日历工时 |
| 4 设计完成 | 设计完成 | 【设计】完成态;需求+交付挂 ZIP/截图;交付描述换正式说明 |
| 5 交棒 | **待开发** | **只改交付负责人→何斐**;不新建分析/设计;不写交付计划完成 |
### 5.2 两条旁路(仍交到同一终点)
| 路径 | 前提 | 行为 |
|---|---|---|
| **无单快轨** | 尚无分析路径 / 明确跳过分析 | 待开发 + 新建交付+设计(设计当日收口);**不建分析**;默认可不跑 AutoPRD |
| **编号直推** | 已有分析/设计**编号** | 收口空计划完成 → 待开发 + 交付交棒何斐;不新建冗余单 |
---
## 6. 交棒契约(开发 Skill 必须实现)
### 6.1 入口条件(产品已完成)
```text
需求状态 = 待开发
【交付】任务编号 = ONEOS-a唯一
【交付】负责人 = 何斐
【交付】ASSOCIATED → 该需求
可选:分析/设计编号在「工作项编号(系统)」中
```
### 6.2 开发 Skill 建议入口口令
```text
接手开发:需求=ONEOS-R交付任务=ONEOS-a
拆分开发:交付任务=ONEOS-a范围=前端|后端|…;负责人=…
开始开发:开发任务=ONEOS-d
开发完成:开发任务=ONEOS-d
```
### 6.3 开发 Skill 建议职责边界
| 应做 | 不应做 |
|---|---|
| 在【交付】下建一条或多条【开发】(编号幂等) | 再建【分析】/【设计】或第二套【交付】 |
| 正式关联:建议 SUB→交付或同时 ASSOCIATED→需求 | 只按标题「【开发】xxx」查重 |
| 挂仓库 / 分支 / MR按你们 Codeup 规范) | 覆盖需求 AutoRDO 段 |
| 改【开发】状态与负责人 | 擅自把需求从待开发退回(除非产品授权回退口令) |
| 检测交付描述是否仍为占位并提示 | 假装 AutoPRD 已齐全 |
| 全部【开发】完成后通知测试 Skill / 建【测试】 | 在产品 Skill 会话里混跑 |
### 6.4 共享常量(可引用,勿整包加载)
路径均在 `skills/YunxiaoPMapp/assets/`
| 文件 | 内容 |
|---|---|
| `runtime-ids.json` | 项目 spaceId、何斐 ID、工作项类型、状态 transit、字段 79/80 |
| `cn-workday-calendar.json` | 法定节假日 / 调休(阶段日历工时) |
开发侧可复制一份到自己的 `assets/`,或只读引用短路径;**不要**把 YunxiaoPMapp 的 references 全文 include 进开发 Skill。
---
## 7. 计划时间与「阶段日历工时」
产品侧口径(开发侧若写计划时间建议对齐语义):
| 对象 | 计划开始 | 计划完成 | 预计工时 |
|---|---|---|---|
| 【交付】 | 首次分析中空则写当日,**永不覆盖** | 产品侧不写(留给上线) | 一般不推全周期 |
| 【分析】【设计】 | 创建时空则写当日,**永不覆盖** | 阶段收口日 | `工作日×8` |
```text
预计工时 = 阶段日历工时Lead Time≠ 人力投入人天
算法起止日含首尾扣周末与法定假计入调休补班×8 小时
禁止:(结束−开始+1×8 的自然日算法
```
脚注固定:`【系统】预计工时=阶段日历工时工作日×8非人力投入预估`
脚本:`scripts/workday_hours.py`
**建议:** 开发 Skill 若给【开发】写预计工时,在文档中明确是「人力投入预估」还是「阶段日历工时」,避免与产品侧混称。
---
## 8. Agent 运行时原理(制作 Skill 时照抄模式)
### 8.1 Plan 门禁
凡写云效:`SwitchMode → plan` → 用户确认 → 一口气 apply → 一次校验回报。
禁止用「参数已齐 / 速度路径」跳过 Plan。
### 8.2 模块化路由
`SKILL.md` 只做路由与门禁;细则在 `references/*.md`;常量在 `assets/`;脚本在 `scripts/`
外置能力AutoRDO、AutoPRD**调用对方 Skill**,不内嵌对方全文。
### 8.3 幂等
```text
幂等键 = 项目 + 需求编号 + 任务类型角色(交付/分析/设计/开发…)+ 已登记任务编号
命中编号 → 复用并补缺字段;禁止新建第二条同角色主容器(交付唯一)
```
### 8.4 验收与回报
每次 apply 后自检(产品侧清单见 `references/acceptance.md`)。开发侧建议至少回报:
```text
【YunxiaoDevapp】
需求ONEOS-R | 状态=待开发|开发中|…
交付ONEOS-a | 负责人=…
开发ONEOS-d1, ONEOS-d2 | …
仓库/分支:…
下一步:…
```
### 8.5 已验证写路径(可复用思路)
| 动作 | 约定(见 live-api.md |
|---|---|
| 改状态 | `POST …/status/transit`(勿用错误 updateStatus |
| 改负责人 | `PATCH …/{id}` + `propertyKey=assignedTo` |
| 建单关系 | create 带 `createWorkitemRelationInfo` |
| 极速复测 | `scripts/live_create_fast.py`(可参考,开发侧另写自己的脚本) |
---
## 9. 回退最小集(开发需配合)
| 场景 | 产品侧规则 | 开发侧注意 |
|---|---|---|
| 待开发 → 退回设计中 | 需求回退;交付负责人可改回产品;**交付计划开始不改**;重做设计则**新开设计编号** | 已建【开发】是否取消 / 暂停由开发 Skill 定义;勿静默删编号 |
| 设计完成后需求大变 | Plan 问是否回退;更新 AutoPRDAutoRDO 保留+变更纪要 | 勿覆盖 AutoRDO |
| 取消需求 | 任务标取消;不删编号区块 | 同步取消未完成【开发】 |
---
## 10. 建议的「YunxiaoDevapp」目录骨架
```text
YunxiaoDevapp/
├── SKILL.md # 门禁 + 路由 + 边界(引用本文契约,不 include PMapp
├── assets/
│ └── runtime-ids.json # 可从 PMapp 复制/裁剪
├── references/
│ ├── model.md # 【开发】与交付/需求的关系约定
│ ├── handoff-intake.md # 认交付编号 + 占位检测
│ ├── split-dev-tasks.md # 拆前端/后端多【开发】
│ ├── codeup.md # 分支 / MR / 关联
│ ├── commands.md # 口令面
│ └── acceptance.md
└── scripts/ # 可选极速 API
```
`SKILL.md` description 建议写明:触发词「接手开发 / 拆分开发 / 开始开发」;**Does NOT create 【分析】/【设计】**;入口条件需求=待开发。
---
## 11. 对照检查表(开发 Skill 评审用)
- [ ] 是否只认「需求编号 + 交付任务编号」,禁止标题查重
- [ ] 是否拒绝创建第二套【交付】或【分析】【设计】
- [ ] 占位交棒时是否提示风险且不假装 PRD 齐全
- [ ] 【开发】是否可编号幂等、可挂到交付树
- [ ] 是否与 YunxiaoPMapp **会话隔离**(不同 Skill、不同口令
- [ ] 计划开始字段是否遵守「已有值不覆盖」(若沿用同一字段)
- [ ] 提测 / 【测试】是否交给测试 Skill本包边界清晰
---
## 12. 相关文档索引
| 主题 | 路径(仓库内) |
|---|---|
| Skill 入口 | `skills/YunxiaoPMapp/SKILL.md` |
| 交付树模型 | `skills/YunxiaoPMapp/references/model.md` |
| 交棒契约 | `skills/YunxiaoPMapp/references/handoff-contract.md` |
| 标准路径 05 | `skills/YunxiaoPMapp/references/stage-flow.md` |
| 交棒门禁 / 回退 | `skills/YunxiaoPMapp/references/handoff-and-rollback.md` |
| 描述双段 | `skills/YunxiaoPMapp/references/description-split.md` |
| 编号区块 | `skills/YunxiaoPMapp/references/workitem-ids.md` |
| 快轨 / 编号直推 | `references/fast-track.md` · `number-push.md` |
| 工时算法 | `references/work-hours.md` |
| 实写 API | `references/live-api.md` |
安装产品 Skill
```bash
npx skills add 15810879921-coder/oneos-pm-skills --skill YunxiaoPMapp -a cursor -g -y
```

View File

@@ -0,0 +1,179 @@
# 产品部流转YunxiaoPMapp到交棒为止
> 用途:先把**产品部**在云效上的工作流转说清楚;开发 / 测试 / 发版不在本 Skill 内,仅在文末标出交接点。
> 依据:`references/stage-flow.md` · `model.md` · `fast-track.md` · `handoff-contract.md`
> 日期2026-07-24
---
## 1. 产品部管到哪里
```text
产品部YunxiaoPMapp终点 = 需求状态「待开发」+【交付】负责人「何斐」
之后 = 请技术经理使用开发 Skill不建【开发】/【测试】)
```
| 谁 | 做什么 | 不做什么 |
|---|---|---|
| 产品 / YunxiaoPMapp | 建需求、打标签、建【交付】【分析】【设计】、推进状态、设计完成灌 AutoPRD、交棒 | 建【开发】【测试】、开分支、提测、发版 |
| 技术经理(接手) | 从「待开发」起拆开发 | 不改写 AutoRDO不新建第二套【交付】 |
---
## 2. 总览流程图(标准路径)
```mermaid
flowchart TB
subgraph inputs [材料入口]
Chat[聊天/录音/口述]
AutoRDO["$AutoRDO 清洗"]
Proto[原型页 Make]
end
subgraph pm [产品部 · YunxiaoPMapp]
S0["0 创建需求\n状态=待处理\n只写 AutoRDO\n不建任务"]
S1["1 受理确认\n状态=已确认\n仍不建任务"]
S2["2 分析中\n建【交付】+【分析】"]
S3["3 设计中\n建【设计】\n收口【分析】"]
S4["4 设计完成\n收口【设计】\nAutoPRD+附件\n灌【交付】描述"]
S5["5 待开发交棒\n交付负责人→何斐\n★ 产品终点"]
end
subgraph after [产品之后 · 不在本 Skill]
Dev["开发 Skill\n拆【开发】…"]
Test["测试 Skill"]
Rel["发版"]
end
Chat --> AutoRDO --> S0
Proto -.-> S4
S0 --> S1 --> S2 --> S3 --> S4 --> S5
S5 -->|"交棒契约:需求编号+交付编号"| Dev
Dev --> Test --> Rel
```
---
## 3. 各步说明(产品侧)
| 步骤 | 需求状态 | 云效任务动作 | 描述写什么 | 口令示例 |
|---|---|---|---|---|
| 0 创建 | 待处理 | 无 | `## 原始诉求AutoRDO`;编号区块待建 | `记录需求:…;推进至=暂不推进` |
| 1 受理 | 已确认 | 仍无 | 不动 | `受理确认ONEOS-xx` |
| 2 分析中 | 分析中 | 新建【交付】ASSOCIATED→需求新建【分析】TASK_SUB→交付 | 交付=占位文案 | `开始分析ONEOS-xx` |
| 3 设计中 | 设计中 | 新建【设计】TASK_SUB→交付分析计划完成=当日+阶段日历工时 | — | `开始设计ONEOS-xx交付=…;分析=…` |
| 4 设计完成 | 设计完成 | 设计完成态;需求+交付挂 ZIP/截图;交付描述换 AutoPRD | `$oneos-autoprd``## 产品说明` | `设计完成ONEOS-xx设计=…;原型=…` |
| 5 交棒 | **待开发** | **只**把【交付】负责人改为何斐 | 若仍占位须标红风险 | `交棒开发ONEOS-xx交付=…` |
**编号权威:** 需求描述 `## 工作项编号(系统)`(交付 / 分析 / 设计)。后续口令优先带 `ONEOS-xx`,禁止按标题查重。
---
## 4. 交付树(产品建出来的结构)
```mermaid
flowchart TB
REQ["产品类需求 ONEOS-R\n状态=阶段真相"]
DEL["【交付】ONEOS-a\nASSOCIATED→需求"]
AN["【分析】ONEOS-b"]
DE["【设计】ONEOS-c"]
DEL -->|"关联项 ASSOCIATED"| REQ
AN -->|"子项 TASK_SUB"| DEL
DE -->|"子项 TASK_SUB"| DEL
```
验收口诀:
- 打开【交付】→「关联项」能看到需求
- 打开【交付】→「子项」能看到分析/设计(必须 `TASK_SUB`,不能只写 parentIdentifier
---
## 5. 两条旁路(仍交到同一终点)
```mermaid
flowchart LR
subgraph standard [标准]
A1[分析中] --> A2[设计中] --> A3[设计完成] --> A4[待开发]
end
subgraph fast [无单快轨]
B1[待处理/已确认等] --> B2["待开发\n建交付+设计当日收口\n不建分析"]
end
subgraph push [编号直推]
C1[已有分析/设计编号] --> C2[收口空计划完成] --> C3[待开发+交棒何斐]
end
```
| 路径 | 何时用 | 注意 |
|---|---|---|
| 标准 0→5 | 正常产品节奏 | 设计完成才灌 AutoPRD |
| 快轨 | 明确跳过分析、急交棒 | 默认可不跑 AutoPRD交付常占位→回报标红 |
| 编号直推 | 树上已有阶段任务编号 | 不新建冗余单;只认编号 |
---
## 6. 产品部人机边界Plan 门禁)
```text
凡写云效Plan 对齐 → 人确认 → 一口气 apply → 一次校验回报
```
| 适合全自动 | 建议人点一下 |
|---|---|
| 建单、打标、建树、改状态、交棒负责人、幂等复用 | 优先级、标签、是否快轨、设计是否真可开发、占位是否接受交棒 |
---
## 7. 交棒给下游时交出什么
产品回报至少包含:
```text
【YunxiaoPMapp】
风险:(占位交棒则首行标红)
需求ONEOS-R | 状态=待开发
交付ONEOS-a | 负责人=何斐
分析ONEOS-b 或 无
设计ONEOS-c 或 快轨占位说明
下一步:请技术经理使用开发 Skill
```
下游入口条件见 `references/handoff-contract.md`:认 **需求编号 + 交付任务编号**
---
## 8. 和全链路的关系(本文件边界)
```mermaid
flowchart LR
PM[产品部本文] --> Handoff[待开发交棒]
Handoff --> Dev[开发]
Dev --> QA[测试]
QA --> Rel[发版]
```
全链路(含谢佳伟 / 时生亮)见仓库原型:
`src/prototypes/yunxiao-pipeline-handbook/`
产品 Skill 对接开发说明:
`docs-YunxiaoPMapp-实现原理-开发Skill对接.md`
---
## 9. 相关 references
| 主题 | 路径 |
|---|---|
| 标准 05 | `references/stage-flow.md` |
| 交付树 / 子项 | `references/model.md` |
| 快轨 | `references/fast-track.md` |
| 编号直推 | `references/number-push.md` |
| 交棒门禁 | `references/handoff-and-rollback.md` |
| 交棒契约 | `references/handoff-contract.md` |
| 口令 | `references/commands.md` |
---
## 延伸阅读
- 四角色泳道(产品/开发/测试/发版):`docs-四角色泳道流程图.md`
- 开发侧草案:`docs-开发侧流转草案.md`

View File

@@ -0,0 +1,318 @@
# 全流程分步说明:产品 → 开发 → 测试 → 发版
> 把整条链路拆成**按顺序执行的步骤**,每步说清:谁做、做什么、云效上变成什么、下一步谁接。
> 读完应能照着口令/动作走完一轮(标准路径)。
> 日期2026-07-24
---
## 先记住三件事
1. **需求状态 = 阶段真相**
看需求状态就知道现在卡在哪一段(分析中 / 待开发 / 开发中 / 待测试…)。
2. **每条需求只有 1 个【交付】容器**
分析、设计、开发、测试都挂在这个交付下面(子项),不要再建第二个【交付】。
3. **编号说话,不靠标题查**
口令尽量带 `ONEOS-xx`;改标题不影响关联。
```text
需求(状态主轴)
└─【交付】(容器,关联到需求)
├─【分析】【设计】 ← 产品建
├─【开发】… ← 开发建
└─【测试】 ← 开发提测时建
【发版】另建,挂迭代 + 范围内需求
```
---
## 总步骤一览(共 18 步)
| 阶段 | 步骤 | 一句话 |
|---|---|---|
| **产品** | 16 | 从记诉求到交棒给何斐 |
| **开发** | 712 | 从拆任务到提测 |
| **测试** | 1315 | 从接测到测试完成 |
| **发版** | 1617 | 从建发版到发布结果 |
| **收口** | 18 | 产品验收关闭 |
---
## 阶段 A产品步骤 16
### 步骤 1记录需求
| | |
|---|---|
| **谁** | 产品 / AIYunxiaoPMapp |
| **做什么** | 把聊天、录音、口述先洗成 AutoRDO再建一条**产品类需求** |
| **需求状态** | **待处理** |
| **云效任务** | 不建任何任务 |
| **描述里写** | `## 原始诉求AutoRDO`;编号区先空着 |
| **口令例** | `记录需求:…;推进至=暂不推进` |
| **为什么** | 先落盘,避免口头需求丢;还没确认值不值得做 |
---
### 步骤 2受理确认
| | |
|---|---|
| **谁** | 产品 |
| **做什么** | 确认要做:定优先级、打标签(从云效标签里点选) |
| **需求状态** | **已确认** |
| **云效任务** | 仍不建任务 |
| **口令例** | `受理确认ONEOS-xx` |
| **为什么** | 和「还没想清楚」的待处理分开;确认后才进分析/设计 |
---
### 步骤 3开始分析
| | |
|---|---|
| **谁** | 产品 / AI |
| **做什么** | ① 建 **【交付】**(整条需求的唯一容器,关联到需求)② 建 **【分析】**(挂在交付**子项**下) |
| **需求状态** | **分析中** |
| **云效任务** | 【交付】+【分析】 |
| **口令例** | `开始分析ONEOS-xx` |
| **验收** | 打开【交付】→「关联项」有需求;「子项」有【分析】 |
| **为什么** | 分析要有落点;后续设计/开发/测试都挂同一棵树 |
---
### 步骤 4开始设计
| | |
|---|---|
| **谁** | 产品 / AI |
| **做什么** | 建 **【设计】**(子项挂交付);收口【分析】计划时间 |
| **需求状态** | **设计中** |
| **云效任务** | 新增【设计】 |
| **口令例** | `开始设计ONEOS-xx交付=…;分析=…` |
| **为什么** | 进入原型/方案设计;分析阶段收口 |
---
### 步骤 5设计完成
| | |
|---|---|
| **谁** | 产品 / AI |
| **做什么** | 收口【设计】;跑 AutoPRD把**产品说明**灌进【交付】描述;挂原型 ZIP/截图等附件 |
| **需求状态** | **设计完成** |
| **口令例** | `设计完成ONEOS-xx设计=…;原型=…` |
| **为什么** | 开发接手时看的是【交付】里的产品说明,不是聊天记录 |
---
### 步骤 6交棒开发产品终点
| | |
|---|---|
| **谁** | 产品 / AI |
| **做什么** | 需求改为待开发;**【交付】负责人改为何斐**(不新建开发/测试任务) |
| **需求状态** | **待开发** ★ |
| **交出什么** | 需求编号 +【交付】编号 |
| **口令例** | `交棒开发ONEOS-xx交付=…` |
| **为什么** | 产品部到此结束;技术经理从这里拆开发 |
> 旁路:急单可用**快轨**(跳过分析、直接待开发),仍交到同一终点「待开发 + 交付→何斐」。
---
## 阶段 B开发步骤 712
### 步骤 7分配开发任务
| | |
|---|---|
| **谁** | 开发主管何斐 / 开发 Skill |
| **做什么** | 输入**交付任务编号** → 自动建 **【开发】**;标题=`【开发】`+需求标题;**描述人工补**;指定开发人;自动写计划时间 |
| **关联** | 子项挂【交付】;关联项挂需求 |
| **需求状态** | 仍为 **待开发**(确认即可,一般不用再改) |
| **口令例** | `分配任务:交付=ONEOS-a开发人=张三` |
| **为什么** | 一条需求可拆多个开发任务;都挂在同一交付下 |
---
### 步骤 8开始开发
| | |
|---|---|
| **谁** | 开发人员 |
| **做什么** | 口令带上**开发任务编号** |
| **开发任务** | → **处理中** |
| **需求状态** | → **开发中** |
| **口令例** | `开始开发ONEOS-d` |
| **为什么** | 看板一眼能看出「正在写代码」 |
---
### 步骤 9写代码 / 提 MR
| | |
|---|---|
| **谁** | 开发人员 |
| **做什么** | 按【交付】里的产品说明实现;分支/MR 建议挂上开发任务编号与需求编号 |
| **需求状态** | 仍为 **开发中** |
| **为什么** | 代码可追溯到哪条开发任务、哪条需求 |
---
### 步骤 10AI 自测门禁(开发侧草案)
| | |
|---|---|
| **谁** | AI开发完成后触发 |
| **做什么** | 找与**需求同标题**的测试计划 → 按用例自测 → 不过则自动提缺陷(标题含开发任务编号)并尝试修复,直到本计划用例全过 |
| **需求状态** | 仍为 **开发中**(未全过前) |
| **为什么** | 在正式提测前先挡住明显问题(属草案,规则可再收紧) |
---
### 步骤 11开发任务完成
| | |
|---|---|
| **谁** | 系统 / AI用例全过之后 |
| **做什么** | 该条【开发】标为**已完成** |
| **需求状态** | → **开发完成**(若有多条开发,建议等**全部**完成再改,避免过早) |
| **为什么** | 标记「代码+门禁」这一段结束 |
---
### 步骤 12完成开发 · 正式提测
| | |
|---|---|
| **谁** | 开发人员(自测也完成) |
| **做什么** | 口令「完成开发」→ 自动建 **【测试】**(标题=`【测试】`+任务名);子项挂【交付】;建议再关联需求;指派测试(如谢佳伟) |
| **需求状态** | → **待测试** ★ |
| **口令例** | `完成开发ONEOS-d` |
| **为什么** | 开发交棒给测试;测试侧开始接单 |
---
## 阶段 C测试步骤 1315
### 步骤 13接测试任务
| | |
|---|---|
| **谁** | 测试(谢佳伟等) |
| **做什么** | 打开【测试】任务;确认关联的需求与交付;准备/核对测试计划与用例包(建议按**需求编号**分包,比「同标题」更稳) |
| **需求状态** | **待测试** → 开始执行后改为 **测试中** |
| **为什么** | 正式测试以计划用例为准,不靠口头「测过了」 |
---
### 步骤 14执行用例 · 缺陷回流
| | |
|---|---|
| **谁** | 测试执行;失败时回流开发修 |
| **做什么** | 按用例执行;失败建缺陷 → 开发修复 → 再测 |
| **需求状态** | **测试中** |
| **为什么** | 质量关口;未绿不能进发版 |
---
### 步骤 15测试完成
| | |
|---|---|
| **谁** | 测试 / AI 辅助判定 |
| **做什么** | **本需求**相关用例全绿 →【测试】收口;通知发版对接人 |
| **需求状态** | → **测试完成** ★ |
| **注意** | 只卡本需求,不卡同迭代里别的需求 |
| **为什么** | 发版范围以「测试完成」的需求为准 |
---
## 阶段 D发版步骤 1617
### 步骤 16建发版任务
| | |
|---|---|
| **谁** | 发版对接人(时生亮)· 手工或 AI |
| **做什么** | 建 **【发版】**;关联**迭代** + 本期内要上的需求(建议再挂交付/测试);汇总更新说明 |
| **需求状态** | 进入发布流程时 → **发布中** |
| **为什么** | 一次发版可能含多条需求;要有明确范围清单 |
---
### 步骤 17发布结果回写
| | |
|---|---|
| **谁** | 发版对接人 / 流水线 |
| **做什么** | 执行发布 |
| **结果** | 成功 → **发布完成**;失败 → **发布失败**(可重试再进发布中) |
| **为什么** | 成败要回写云效,避免「以为发了其实没发」 |
---
## 阶段 E收口步骤 18
### 步骤 18业务验收 · 关闭
| | |
|---|---|
| **谁** | 产品 |
| **做什么** | 业务验收通过后,关闭需求,并收口【交付】 |
| **需求状态** | → **已关闭** |
| **为什么** | 全链路结束;树上任务与需求一致收口 |
---
## 状态主链(串起来看)
```text
待处理 → 已确认 → 分析中 → 设计中 → 设计完成
→ 待开发 ← 产品交棒给何斐
→ 开发中 → 开发完成
→ 待测试 → 测试中 → 测试完成
→ 发布中 → 发布完成(或发布失败可重试)
→ 已关闭 ← 产品验收关单
```
---
## 四个交棒口(最重要)
| 交棒 | 交出人 | 接棒人 | 交出物 | 状态落点 |
|---|---|---|---|---|
| ① 产品→开发 | 产品 | 何斐 | 需求号 + 交付号 | 待开发 |
| ② 开发→测试 | 开发人 | 测试 | 【测试】任务已建好 | 待测试 |
| ③ 测试→发版 | 测试 | 发版 | 本需求用例全绿 | 测试完成 |
| ④ 发版→产品 | 发版 | 产品 | 发布完成 / 更新说明 | 发布完成 → 已关闭 |
---
## 谁在什么时候动(对照表)
| 步骤 | 产品 | 开发主管 | 开发人 | 测试 | 发版 |
|---|---|---|---|---|---|
| 12 记需求/确认 | ● | | | | |
| 35 分析设计 | ● | | | | |
| 6 交棒 | ● | 接 | | | |
| 7 分配任务 | | ● | | | |
| 89 开始写码 | | | ● | | |
| 1011 AI门禁/完成任务 | | | ●/AI | | |
| 12 提测 | | | ● | 接 | |
| 1315 测试 | | | 修缺陷 | ● | |
| 1617 发版 | | | | | ● |
| 18 关闭 | ● | | | | |
---
## 相关文档
| 文档 | 用途 |
|---|---|
| `docs-四角色泳道流程图.md` | 泳道图总览 |
| `docs-产品部流转流程图.md` | 产品 16 细规 |
| `docs-开发侧流转草案.md` | 开发 712 细规与待拍板 |
| `yunxiao-pipeline-handbook` | 云效规则与配置 |

View File

@@ -0,0 +1,181 @@
# 四角色泳道图:产品 · 开发 · 测试 · 发版
> 用途:一张图看清从建需求到发版关闭,四条泳道各自做什么、在哪交棒。
> 依据:产品部流转 · 开发侧草案 · 生产线手册(测试/发版)
> 日期2026-07-24
> 说明开发泳道含「AI 用例门禁」草案;测试泳道为正式提测后的人/计划执行。
---
## 1. 泳道总览(推荐主图)
```mermaid
flowchart TB
subgraph PM["泳道① 产品"]
direction TB
P0["建需求 · 待处理<br/>只写 AutoRDO"]
P1["受理确认 · 已确认"]
P2["分析中<br/>建【交付】+【分析】"]
P3["设计中<br/>建【设计】"]
P4["设计完成<br/>AutoPRD 灌交付"]
P5["交棒 · 待开发<br/>交付负责人→何斐"]
P6["业务验收后<br/>关闭需求+交付"]
P0 --> P1 --> P2 --> P3 --> P4 --> P5
end
subgraph DEV["泳道② 开发"]
direction TB
D1["何斐:分配任务<br/>建【开发】SUB→交付"]
D2["开发人:开始开发<br/>任务处理中 · 需求开发中"]
D3["写代码 / 提 MR"]
D4["AI 按同标题测试计划自测<br/>不过→提缺陷并修"]
D5["开发任务已完成<br/>需求→开发完成"]
D6["完成开发(含自测)<br/>建【测试】· 需求待测试"]
D1 --> D2 --> D3 --> D4 --> D5 --> D6
end
subgraph QA["泳道③ 测试"]
direction TB
T1["接【测试】任务<br/>需求=待测试"]
T2["建/挂测试计划与用例<br/>(建议按需求编号分包)"]
T3["测试中 · 执行用例"]
T4["缺陷回流开发修复"]
T5["本需求用例全绿<br/>需求→测试完成"]
T1 --> T2 --> T3
T3 -.->|失败| T4
T4 -.->|再测| T3
T3 -->|通过| T5
end
subgraph REL["泳道④ 发版"]
direction TB
R1["建【发版】任务<br/>挂迭代+范围内需求"]
R2["汇总更新说明"]
R3["发布中"]
R4{"发布结果"}
R5["发布完成"]
R6["发布失败 · 可重试"]
R1 --> R2 --> R3 --> R4
R4 -->|成功| R5
R4 -->|失败| R6
R6 -.-> R3
end
P5 -->|"交棒契约<br/>需求编号+交付编号"| D1
D6 -->|"提测<br/>【测试】已挂交付"| T1
T5 -->|"交棒发版"| R1
R5 -->|"业务可验收"| P6
```
---
## 2. 横向时间轴版(谁在哪一阶段动)
从左到右是需求状态主链;上下是四个角色。空格表示该角色此阶段无主动动作。
```mermaid
flowchart LR
subgraph S_PM["产品阶段"]
direction TB
sp0["待处理→已确认"]
sp1["分析/设计/设计完成"]
sp2["待开发交棒"]
sp3["已关闭"]
end
subgraph S_DEV["开发阶段"]
direction TB
sd0["拆【开发】"]
sd1["开发中"]
sd2["AI门禁→开发完成"]
sd3["建【测试】→待测试"]
end
subgraph S_QA["测试阶段"]
direction TB
sq0["待测试"]
sq1["测试中"]
sq2["测试完成"]
end
subgraph S_REL["发版阶段"]
direction TB
sr0["建【发版】"]
sr1["发布中"]
sr2["完成/失败"]
end
sp0 --> sp1 --> sp2
sp2 --> sd0 --> sd1 --> sd2 --> sd3
sd3 --> sq0 --> sq1 --> sq2
sq2 --> sr0 --> sr1 --> sr2
sr2 --> sp3
```
---
## 3. 交接点一览
| # | 从 → 到 | 交出什么 | 需求状态落点 |
|---|---|---|---|
| 1 | 产品 → 开发 | 需求编号 +【交付】编号;交付负责人=何斐 | **待开发** |
| 2 | 开发 → 测试 | 【测试】任务(子项挂交付);用例计划可先备好 | **待测试** |
| 3 | 测试 → 发版 | 本需求用例全绿;范围内需求清单 | **测试完成** |
| 4 | 发版 → 产品 | 发布完成证据 / 更新说明 | **发布完成** → 产品关单 **已关闭** |
---
## 4. 各泳道职责边界(一句话)
| 角色 | 管什么 | 不管什么 |
|---|---|---|
| **产品** | 建需求、【交付】【分析】【设计】、AutoPRD、交棒待开发、验收关闭 | 不建【开发】【测试】【发版】、不开分支 |
| **开发** | 拆【开发】、写码、AI 自测门禁、建【测试】提测 | 不改写产品 AutoRDO不建第二套【交付】 |
| **测试** | 接【测试】、跑计划用例、缺陷回流、判测试完成 | 不擅自改需求状态到开发中;不建【发版】(可催) |
| **发版** | 建【发版】、挂迭代与范围、发布与回写成败 | 不替代测试判绿;不关闭需求(交给产品) |
---
## 5. 云效工作项树(跨角色)
```mermaid
flowchart TB
REQ["需求 · 状态=阶段真相"]
DEL["【交付】ASSOCIATED→需求"]
AN["【分析】"]
DE["【设计】"]
DV["【开发】"]
TE["【测试】"]
RE["【发版】ASSOCIATED→迭代/需求"]
DEL --> REQ
AN -->|TASK_SUB| DEL
DE -->|TASK_SUB| DEL
DV -->|TASK_SUB| DEL
TE -->|TASK_SUB| DEL
DV -.->|ASSOCIATED| REQ
TE -.->|ASSOCIATED| REQ
RE -.->|关联范围| REQ
```
---
## 6. 典型负责人(手册约定,可按项目改)
| 泳道 | 典型对接人 |
|---|---|
| 产品 | 王冕 · YunxiaoPMapp / AI |
| 开发 | 何斐(拆任务)→ 王雨昊 / 李振等(执行) |
| 测试 | 谢佳伟 |
| 发版 | 时生亮 |
---
## 相关文档
| 文档 | 说明 |
|---|---|
| `docs-全流程分步说明.md` | **整条链路逐步说明(推荐先读)** |
| `docs-产品部流转流程图.md` | 产品 0→5 细图 |
| `docs-开发侧流转草案.md` | 开发 D1D5 细图 |
| `yunxiao-pipeline-handbook` | 全链路配置与规则 |

View File

@@ -0,0 +1,230 @@
# 开发侧流转草案(产品交棒之后)
> 来源产品经理口述规则整理2026-07-24
> 上游终点:产品部 `docs-产品部流转流程图.md` → 需求=**待开发**,【交付】负责人=**何斐**
> 本文件目标:把「何斐拆任务 → 开发执行 → AI 用例门禁 → 提测」画清,供后续 **YunxiaoDevapp** Skill 定稿
> 状态:**草案**(文末有待拍板项)
---
## 1. 总览流程图
```mermaid
flowchart TB
subgraph handoff [产品已交棒]
Ready["需求=待开发\n【交付】负责人=何斐"]
end
subgraph d1 [D1 分配开发]
Assign["何斐:分配任务 + 交付任务编号"]
CreateDev["自动建【开发】+需求标题\n描述人工补\n指定开发人\n写计划时间"]
Link["子项 TASK_SUB→交付\n关联项 ASSOCIATED→需求"]
StReady["需求保持/确认为 待开发"]
end
subgraph d2 [D2 开始开发]
Start["开发人:开始开发 + 开发任务编号"]
DevDoing["开发任务→处理中"]
ReqDoing["需求→开发中"]
end
subgraph d3 [D3 AI 用例门禁]
CodeDone["代码开发完成触发"]
FindPlan["按需求同标题找测试计划"]
AITest["AI 按用例自测"]
Fail["未通过→自动提缺陷\n标题含开发任务编号\nAI 写标题描述"]
Fix["自动修复→再测"]
PassGate["用例全部通过"]
end
subgraph d4 [D4 开发任务完成]
DevDone["该条【开发】→已完成"]
ReqDevDone["需求→开发完成"]
end
subgraph d5 [D5 完成开发·提测]
Finish["开发人:完成开发\n含自测完成"]
CreateTest["自动建【测试】+任务名\n子项→交付"]
PendingTest["需求→待测试"]
end
Ready --> Assign --> CreateDev --> Link --> StReady
StReady --> Start --> DevDoing --> ReqDoing
ReqDoing --> CodeDone --> FindPlan --> AITest
AITest -->|未通过| Fail --> Fix --> AITest
AITest -->|全通过| PassGate --> DevDone --> ReqDevDone
ReqDevDone --> Finish --> CreateTest --> PendingTest
```
---
## 2. 分步说明(按你的 5 条)
### D1何斐分配任务
| 项 | 规则 |
|---|---|
| 触发 | Skill 口令:`分配任务` + **交付任务编号** |
| 新建 | 【开发】任务;标题 = `【开发】` + **需求标题** |
| 描述 | **人工添加**Skill 不代写正文,或只留空模板) |
| 负责人 | 口令指定开发人员 |
| 计划时间 | **自动创建**计划开始/计划完成(算法待定:阶段日历工时 or 人力预估,见待拍板) |
| 关联 | **子项** `TASK_SUB` →【交付】;**关联项** `ASSOCIATED` → 需求 |
| 需求状态 | 改为 / 确认为 **待开发**(见待拍板:与产品交棒是否重复) |
### D2开始开发
| 项 | 规则 |
|---|---|
| 触发 | `开始开发` + **开发任务编号** |
| 开发任务 | → **处理中** |
| 需求 | → **开发中**(有 ≥1 条开发进入处理中即可,或本条触发即改,见待拍板) |
### D3代码完成后 · AI 按测试计划自测
| 项 | 规则 |
|---|---|
| 触发 | 「代码开发完成」MR 合并 / 口令 / Webhook触发源待定 |
| 找计划 | **自动寻找与需求同标题的测试计划** |
| 执行 | AI 按该计划中用例自行测试 |
| 失败 | 自动提缺陷:标题含 **开发任务编号** + AI 生成标题/描述;再自动修复,循环直到用例全过 |
### D4用例全通过
| 项 | 规则 |
|---|---|
| 该条【开发】 | → **已完成** |
| 需求 | → **开发完成** |
### D5开发人「完成开发」自测也完成
| 项 | 规则 |
|---|---|
| 触发 | `完成开发` +(建议带)开发任务编号 |
| 新建 | 【测试】任务;标题 = `【测试】` + 任务名称(建议=需求标题) |
| 关联 | 子项 →【交付】(建议同时 ASSOCIATED→需求 |
| 需求 | → **待测试** |
---
## 3. 与产品部的衔接
```mermaid
flowchart LR
PM["产品 YunxiaoPMapp\n待开发 + 交付→何斐"] --> DevSkill["开发 Skill 草案\n本文 D1D5"]
DevSkill --> QA["测试执行人/Skill\n待测试之后"]
```
| 上游已具备 | 开发侧依赖 |
|---|---|
| 需求编号 +【交付】编号 | D1 入口只认编号,不按标题拆第二套交付 |
| 交付 ASSOCIATED→需求 | D1 开发任务再 ASSOCIATED→需求、SUB→交付 |
| 产品不建【开发】【测试】 | 仅开发 Skill 建这两类 |
---
## 4. 待拍板(建议你先定这几条)
### 4.1 状态顺序是否自洽
你现在的顺序是:
```text
D3 AI 用例全过 → 开发任务已完成 + 需求「开发完成」
D5 完成开发 → 建【测试】+ 需求「待测试」
```
与现有手册常见顺序对比:
```text
手册常见:开发人完成自测 → 待测试 →(测试执行)→ 测试完成
你草案: AI 先跑测试计划用例过关 → 开发完成 → 人再点完成开发 → 待测试
```
需要确认:
1. D3 的「测试计划」是 **开发自测门禁**,正式【测试】任务仍留给谢佳伟?
2. 还是 D3 已等同正式测试,那 D5 再建【测试】是否重复?
### 4.2 「需求→待开发」写在 D1
产品交棒时需求**已经是待开发**。D1 再改一次通常多余。建议改为:
- D1 **校验**需求已是待开发;若不是则停下或只允许从设计完成快轨补交棒
- 有开发任务进入处理中时,需求才变为 **开发中**(即你的 D2
### 4.3 「与需求同标题的测试计划」
标题易改、易撞名。更稳的是:
- 测试计划关联 **需求编号 / 迭代**,或
- 用例包命名 `CP-{需求编号}-…`
否则同名需求/改标题会导致找错计划。
### 4.4 AI 自动提缺陷并自动修复
风险高:无上限循环、误报、改回旧逻辑。建议门禁:
- 最多 N 轮;失败转人工
- 缺陷必须关联 **开发任务编号 + 需求编号 + 用例 ID**
- 与「防回归」清单联动,禁止静默改无关模块
### 4.5 多条【开发】时需求状态
| 场景 | 建议 |
|---|---|
| 任一条开始开发 | 需求 → 开发中 |
| 需求 → 开发完成 | **全部**未取消【开发】均为已完成(与手册 Y21 一致) |
| 需求 → 待测试 | 全部【开发】完成后,由末位「完成开发」或系统自动建【测试】 |
你原文 D4 写「该条」通过就把需求改成开发完成——多开发人并行时会过早。建议改成「全员开发任务完成」。
### 4.6 关联写法(与产品侧一致)
同一 create 只能带一条 `createWorkitemRelationInfo`
- 优先保证【开发】/【测试】**子项挂在交付**(交付详情「子项」可见)
- ASSOCIATED→需求若同 create 互斥,需约定:先 SUB再补 ASSOCIATED或 OpenAPI 双挂
---
## 5. 建议口令面(草案)
```text
分配任务:交付任务=ONEOS-a开发人=张三,李四;计划开始=…;计划完成=…
开始开发:开发任务=ONEOS-d
代码完成:开发任务=ONEOS-d → 触发 AI 用例门禁(或 Webhook
完成开发:开发任务=ONEOS-d → 建【测试】;需求→待测试
```
---
## 6. 推荐定稿后的状态主链(供你勾选)
**方案 A贴近你原文AI 测作开发门禁)**
```text
待开发 → 开发中 →AI 用例门禁)→ 开发完成 → 待测试 → 测试中 → 测试完成 → …
```
**方案 B贴近现有生产线手册**
```text
待开发 → 开发中 → 开发完成(人自测+MR→ 待测试 → 测试中(人/AI 跑计划)→ 测试完成 → …
```
AI 跑测试计划放在 **待测试之后**,与【测试】任务负责人(如谢佳伟)对齐。
---
## 7. 下一步
1. 你拍板:**方案 A 或 B**,以及多【开发】时「开发完成 / 待测试」的聚合规则
2. 我按定稿输出 `YunxiaoDevapp``SKILL.md` 骨架 + 与产品交棒契约对齐段落
3. 再补一版「仅开发侧」验收清单10 条以内)
---
## 相关文档
| 文档 | 路径 |
|---|---|
| 产品部流转 | `docs-产品部流转流程图.md` |
| 产品↔开发契约 | `docs-YunxiaoPMapp-实现原理-开发Skill对接.md` |
| 全链路手册 | `src/prototypes/yunxiao-pipeline-handbook/` |

View File

@@ -0,0 +1,47 @@
# 无单快轨到待开发
适用:口令明确「快轨」或「推进至待开发」且当前需求尚无标准分析路径任务(或用户确认跳过分析)。
## 动作表
| 对象 | 动作 |
|---|---|
| 需求 | 状态 → **待开发****预计工时=2**、**实际工时=2**(覆盖同日 8h 日历工时默认);标签按口令;更新编号区块 |
| 【交付】 | 无则新建ASSOCIATED→需求勿用 PARENT负责人→何斐**计划开始=创建当日**`79`**不写**计划完成;**描述双路径**(见下);**标签与需求相同**;更新编号区块 |
| 【设计】 | **必须新建****TASK_SUB→交付**(交付「子项」须可见);**描述=复制需求当前描述正文**;计划开始/完成均=当日create 后 `field/value``79`+`80`);任务→完成态;**标签与需求相同**;建后再尝试 **ASSOCIATED→原始需求**Cookie 常失败,见 live-api失败须标红**子项优先于关联项**);更新编号区块 |
| 【分析】 | **禁止创建** |
## 交付描述双路径A1
| 口令/材料 | 【交付】描述 |
|---|---|
| **手工描述**(无指定原型) | 同步需求当前手工/AutoRDO 正文(与设计一致可复制 document |
| **指定原型页面** | 调用 `$oneos-autoprd`,从原型生成 AutoPRD「产品说明」写入【交付】 |
| 既无手工可同步正文、又无原型 | 才允许占位 `等待设计任务完成后自动填入`Plan 勾选交棒占位风险,回报首行标红 |
有手工或原型时**禁止**交棒占位。
## AutoPRD
- 快轨默认可不跑 AutoPRD**例外**:口令指定原型 → 交付走 AutoPRD 路径(上表)。
- 口令同时「设计完成 + 原型」时仍按步骤 4 灌需求产品说明段。
## 查重
只认任务编号。已有分析单不得因「快轨」被删除。
## 回报必含
- 需求 / 交付 / 设计编号
- 「快轨:未建分析;设计已同步需求描述并当日收口」
- 交付描述来源:手工同步 / AutoPRD / 占位(若占位则首行标红)
- 需求预计工时=2、实际工时=2
- 设计关联项含需求编号;交付子项含设计编号
## 与标准 / 编号直推对比
```text
标准:分析中→交付+分析 → 设计中→设计+收口分析 → 设计完成→收口设计 → 待开发交棒
快轨(无既有阶段单):→ 待开发:新建交付+设计+交棒何斐(无分析;设计当日收口;需求工时 2+2
编号直推:已有分析/设计编号 → 收口空计划完成 → 待开发+交付交棒(不新建冗余单)
```

View File

@@ -0,0 +1,242 @@
# 云效实写 APIYunxiaoPMapp 已验证)
`verified_at`: 2026-07-25 · **项目须门禁 PJ 点选**(见 [project-selection.md](project-selection.md));历史验证样本项目为 `01_ONEOS` / 原「统一运营管理平台」(`last_selected.spaceIdentifier``assets/runtime-ids.json`,禁止未点选即使用)。
本文件只记**已跑通**的写法;禁止再盲试 `updateStatus` / 错误 `updateFieldValue` POST。
## 认证
- CookieChrome 域 `.aliyun.com` / `devops.aliyun.com``browser_cookie3` 或 Playwright storage
- Header`x-xsrf-token` = cookie `XSRF-TOKEN`URL 解码后)
- `Origin` / `Referer``https://devops.aliyun.com`
## 建单
`POST|PUT /projex/api/workitem/workitem?_input_charset=utf-8`
创建响应 `result.identifier` / `serialNumber` 即编号真相;**禁止按标题查重**。
## 改负责人(已通)
```http
PATCH /projex/api/workitem/workitem/{id}?_input_charset=utf-8
{"propertyKey":"assignedTo","propertyValue":"<userId>","operateType":"COVER"}
```
交棒:【交付】`propertyValue` = 何斐 ID。
## 打标签(已通)
```http
PATCH /projex/api/workitem/workitem/{id}?_input_charset=utf-8
{"workitemIdentifier":"{id}","propertyKey":"tag","propertyValue":"<tagId>[,<tagId>]","operateType":"COVER"}
```
## 改状态(已通 · 唯一推荐)
```http
POST /projex/api/workitem/workitem/{id}/status/transit?_input_charset=utf-8
{"fromStatus":"<status.identifier>","toStatus":"<status.identifier>"}
```
成功:`code=200``result=true`。失败时 `errorMsg` 含「不能流转」。
### 需求状态 ID本项目
| 显示名 | identifier |
|---|---|
| 待处理 | `100005` |
| 已确认 | `32` |
| 分析中 | `154395` |
| 设计中 | `156603` |
| 设计完成 | `307012` |
| 待开发 | `1582fc929d429111b925309493` |
### 任务状态 ID
| 显示名 | identifier |
|---|---|
| 待处理 | `100005` |
| 已完成 | `100014` |
### 极速交棒跳转(工作流允许)
从「待处理」菜单可见直达「设计完成」;推荐最少跳:
```text
待处理 → 设计完成 → 待开发
```
标准路径若需看板留痕,可走完整链:已确认→分析中→设计中→设计完成→待开发(仍用本 API勿开 UI
### 禁止(已证伪)
| 写法 | 结果 |
|---|---|
| `PATCH …/updateStatus` + `statusIdentifier` | `400 不能为空` |
| `PATCH …/{id}` + `propertyKey=status` | `property not found` |
| Playwright 点左侧/列表上的状态色块(`x<1100` | 假成功、状态不落库 |
仅当 `status/transit` 不可用时,才用 UI右侧详情状态钮`getBoundingClientRect().x > 1100`+ `.next-menu-item`
## 计划开始/完成 · 提交部门/人 · 预计工时2026-07-27 修订)
**通用字段写入(已通):**
```http
POST /projex/api/workitem/workitem/field/value/{workitemId}?_input_charset=utf-8
Content-Type: application/x-www-form-urlencoded
fieldValueList=[{"fieldIdentifier":"79","value":"2026-07-27 12:00:00"},{"fieldIdentifier":"3132597a9718d1c282b7ba5a0c","value":""},{"fieldIdentifier":"9e01269e96f91fbb97d36bf5b3","value":""}]
```
| 字段 | fieldIdentifier | value |
|---|---|---|
| 计划开始 | `79` | `YYYY-MM-DD HH:mm:ss`(推荐正午)或 epoch ms 字符串 |
| 计划完成 | `80` | 同上 |
| 提交部门 | `3132597a9718d1c282b7ba5a0c` | 纯文本 |
| 提交人 | `9e01269e96f91fbb97d36bf5b3` | 纯文本 |
**预计工时 `101586`:禁止直接改字段**(报「不可直接修改」)。须登记:
```http
POST /projex/api/workitem/workitem/time/estimate?_input_charset=utf-8
{"workitemIdentifier":"<id>","spentTime":8,"type":"develop","description":"","recordUserIdentifier":"<userId>","forCreate":false,"containsRestDay":false}
```
删除多余预估:`DELETE /projex/api/workitem/workitem/time/estimate/{workitemId}/{estimateId}`
列表:`GET …/time/estimate/list?workitemIdentifier=`
旧写法 `PATCH …/updateWorkitemFieldValue` 对上述自定义字段常 `400 不能为空`,勿再优先使用。
## 父子 / 子项 / 关联项(已通 · 2026-07-23 修订 · 子项优先)
### 关联项(【交付】强制 · ASSOCIATED
任务详情「关联项」只认 `ASSOCIATED`。**仅【交付】**建单时 `createWorkitemRelationInfo` 必须指向**需求**
```json
{
"createWorkitemRelationInfo": {
"relatedWorkitemIdentifier": "<需求id>",
"relatedToRelationIdentifier": "ASSOCIATED"
}
}
```
校验:
```http
GET /projex/api/workitem/v2/workitem/{id}/relation/workitem/list/by-relation-category?category=ASSOCIATED&isForward=true
```
`result` 含该需求即通过。
### 禁止(关联项)
| 写法 | 结果 |
|---|---|
| `relatedToRelationIdentifier=PARENT` 把交付挂需求 | 详情可能有 parent**关联项仍为空** |
| 分析/设计只用 `ASSOCIATED→需求` + `parentIdentifier` | 关联项可能有,**交付子项仍为空**ONEOS-246/247 |
| 建后再 `POST …/relation/record` 补关系 | Cookie 路径下常报「不能关联相同的工作项」 |
| `createWorkitemRelationList` | 不落 ASSOCIATED |
### 子项(【分析】/【设计】强制 · TASK_SUB
「子项」页读 `PARENT_SUB` / `TASK_SUB`,分析/设计**必须**
```json
{
"parent": "<交付id>",
"parentIdentifier": "<交付id>",
"createWorkitemRelationInfo": {
"relatedWorkitemIdentifier": "<交付id>",
"relatedToRelationIdentifier": "TASK_SUB"
}
}
```
同一 create 只能带一条 `createWorkitemRelationInfo`
`ASSOCIATED→需求``TASK_SUB→交付` **不能同时写**
**产品优先级交付「子项」tab > 阶段任务「关联项」。**
交付本身仍必须 `ASSOCIATED→需求`。标准路径下分析/设计的「关联项」允许为空。
**无单快轨例外(设计双挂):** create 用 `TASK_SUB→交付` 后,须再补 **ASSOCIATED→原始需求**,使设计详情「关联项」可见需求。
```http
POST /projex/api/workitem/workitem/{id}/relation/record?_input_charset=utf-8
{"relationIdentifier":"ASSOCIATED","toWorkitemIdentifier":"<id>"}
```
**已证伪Cookie · 2026-07-27** 凡工作项**已创建**后再 `relation/record` 补挂(含 ASSOCIATED / TASK_SUB常报「不能关联相同的工作项」与是否已有父项无关。`createWorkitemRelationList` 亦不落 ASSOCIATED。
**可行路径:**
| 目标 | 做法 |
|---|---|
| 交付「子项」可见设计(优先) | create 带 `TASK_SUB→交付` |
| 设计「关联项」可见需求 | create 带 `ASSOCIATED→需求`(与上互斥,同 create 只能一条) |
| 双挂 | 需个人 `x-yunxiao-token` OpenAPICookie 路径**不得**声称成功 |
产品默认:**子项优先**;关联项失败须在回报中标红并列出缺项。
校验子项:
```http
GET /projex/api/workitem/v2/workitem/{id}/relation/workitem/list/by-relation-category?category=PARENT_SUB&isForward=true
```
结果须含对应分析/设计 identifier。
校验设计关联项:
```http
GET /projex/api/workitem/v2/workitem/{id}/relation/workitem/list/by-relation-category?category=ASSOCIATED&isForward=true
```
`result` 须含原始需求 identifier。
### 无单快轨字段默认2026-07-27
| 对象 | 规则 |
|---|---|
| 【设计】描述 | 复制需求 document HTML |
| 【设计】`79`/`80` | 当日 `12:00:00` / `23:59:59`create 后 `field/value`(勿 create 同时带 79+80 |
| 【交付】描述 | 手工同步需求正文或原型→AutoPRD禁止无故占位 |
| 【交付】`79` | 创建当日;不写 `80` |
| 【交付】/【设计】标签 | 与需求相同,`PATCH propertyKey=tag` |
| 需求预计工时 | `time/estimate` **spentTime=2**(先删多余预估) |
| 需求实际工时 | `POST …/workitem/time`body 用 **`actualTime`**(非 spentTime+ `gmtStart`/`gmtEnd` epoch ms 字符串;见 `runtime-ids.json` `fields.actual_hours` |
| 描述更新 | `PATCH …/workitem/{id}/document``{"content":"<html>","formatType":"RICHTEXT"}` |
## 迭代挂接(已通 · 2026-07-27 · 只挂交付)
创建迭代:`POST /projex/api/workspace/sprint`(必填 `staffIds`;可写 `capacityHours`)。
挂【交付】到迭代:
```http
PATCH /projex/api/workitem/workitem/{id}?_input_charset=utf-8
{"workitemIdentifier":"{id}","propertyKey":"sprint","propertyValue":"{sprintId}","operateType":"COVER"}
```
清空误挂(如需求):`propertyValue:""` + `operateType:"COVER"`
**校验**必须读 `/extra`(详情主接口常不含 sprint 字段,禁止据此判失败):
```http
GET /projex/api/workitem/workitem/{id}/extra?_input_charset=utf-8
result.sprint[].identifier / name
```
产品规则:**只挂【交付】**;需求 / 分析 / 设计默认不挂(除非口令显式)。
### 极速建单注意
1. Cookie 只刷一次;全程纯 HTTP默认**不开浏览器**。
2. 交付建完后,【分析】与【设计】**并行**创建(均 TASK_SUB→交付标准/快轨两树可并行。
3. 状态用 `transit` + **本地追踪 fromStatus**(禁止每次 GET负责人在交棒场景下**创建时即何斐**。
4. 建单 `fieldValueList` 可带计划开始 `79`**不要**在 create 同时写 `79+80`(同日会 400
5. 标签必须 PATCHcreate 带 tag 不落库);可与建子任务重叠;快轨交付/设计须与需求同标签。
6. `requests.Session` keep-alive**禁止**对共享 opener 加全局锁。
7. 脚本入口:`scripts/live_create_fast.py`v5快轨描述/计划/标签/工时 2+2/设计 ASSOCIATED 补挂)。

View File

@@ -0,0 +1,660 @@
#!/usr/bin/env python3
"""YunxiaoPM 极速真实建单 v5快轨描述/计划/标签/工时 2+2/设计 ASSOCIATED 补挂。"""
from __future__ import annotations
import json
import time
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
try:
import browser_cookie3
except ImportError: # pragma: no cover
browser_cookie3 = None
import requests
ROOT = Path(__file__).resolve().parents[1]
RUNTIME = json.loads((ROOT / "assets" / "runtime-ids.json").read_text())
STATUS = RUNTIME["status"]["req"]
TASK_STATUS = RUNTIME["status"]["task"]
FAST = RUNTIME.get("fields", {}).get("fast_track") or {}
SPACE = (
RUNTIME.get("project", {}).get("spaceIdentifier")
or (RUNTIME.get("project", {}).get("last_selected") or {}).get("spaceIdentifier")
)
if not SPACE:
raise RuntimeError(
"未选定项目 spaceIdentifier须先走门禁 PJ 点选,或设置 runtime project.last_selected"
)
REQ_TYPE = RUNTIME["workitem_types"]["product_req"]["identifier"]
TASK_TYPE = RUNTIME["workitem_types"]["task"]["identifier"]
HEFEI = RUNTIME["people"]["hefei"]["identifier"]
WANG = RUNTIME["people"].get("wangmian", {}).get("identifier", "6811df000601d2fea60144a9")
PRI = RUNTIME["priority"][""]
TAG_FAULT = RUNTIME.get("tags", {}).get("故障管理", "ceb526a7343995577645317e9a")
PLACEHOLDER = RUNTIME["delivery_placeholder"]
PROTO = "https://prototype.lnoneos.com/vehicle-fault-handling/index.html"
TZ = timezone(timedelta(hours=8))
TODAY = datetime.now(TZ).strftime("%Y-%m-%d")
NOON = f"{TODAY} 12:00:00"
EOD = f"{TODAY} 23:59:59"
NOON_MS = str(
int(datetime.now(TZ).replace(hour=12, minute=0, second=0, microsecond=0).timestamp() * 1000)
)
START_MS = str(
int(datetime.now(TZ).replace(hour=9, minute=0, second=0, microsecond=0).timestamp() * 1000)
)
END_MS = str(
int(datetime.now(TZ).replace(hour=11, minute=0, second=0, microsecond=0).timestamp() * 1000)
)
FAST_EST = int(FAST.get("req_estimated_hours", 2))
FAST_ACT = int(FAST.get("req_actual_hours", 2))
PENDING = STATUS["待处理"]
DESIGN_DONE = STATUS["设计完成"]
PENDING_DEV = STATUS["待开发"]
TASK_DONE = TASK_STATUS["已完成"]
_COOKIE = ""
_XSRF = ""
def load_auth() -> None:
global _COOKIE, _XSRF
jar: dict[str, str] = {}
if browser_cookie3:
for domain in (".aliyun.com", "devops.aliyun.com", ".devops.aliyun.com"):
try:
for c in browser_cookie3.chrome(domain_name=domain):
jar[c.name] = c.value
except Exception:
pass
if not jar:
p = Path("/tmp/yunxiao_cookies.json")
if p.exists():
raw = json.loads(p.read_text())
jar = (
raw
if isinstance(raw, dict) and "XSRF-TOKEN" in raw
else {c["name"]: c["value"] for c in raw.get("cookies", [])}
)
_COOKIE = "; ".join(f"{k}={v}" for k, v in jar.items())
x = jar.get("XSRF-TOKEN", "")
_XSRF = urllib.parse.unquote(x) if "%" in x else x
if not _XSRF:
raise RuntimeError("缺少 XSRF-TOKEN请先在 Chrome 登录 devops.aliyun.com")
def session() -> requests.Session:
s = requests.Session()
s.headers.update(
{
"Content-Type": "application/json",
"Cookie": _COOKIE,
"x-xsrf-token": _XSRF,
"X-XSRF-TOKEN": _XSRF,
"Origin": "https://devops.aliyun.com",
"Referer": f"https://devops.aliyun.com/projex/project/{SPACE}/req",
"accept": "application/json",
"User-Agent": "YunxiaoPM-live_create_fast/5.0",
"Connection": "keep-alive",
}
)
return s
def api(s: requests.Session, method: str, url: str, body: Any = None) -> dict:
r = s.request(method, url, json=body, timeout=60)
try:
return r.json()
except Exception:
r.raise_for_status()
raise
def create(s: requests.Session, payload: dict) -> dict:
url = "https://devops.aliyun.com/projex/api/workitem/workitem?_input_charset=utf-8"
j = api(s, "POST", url, payload)
r = j.get("result") or {}
if j.get("code") == 200 and isinstance(r, dict) and r.get("identifier"):
return r
j = api(s, "PUT", url, payload)
r = j.get("result") or {}
if j.get("code") == 200 and isinstance(r, dict) and r.get("identifier"):
return r
raise RuntimeError(f"create failed: {j}")
def get(s: requests.Session, wid: str) -> dict:
return api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}?_input_charset=utf-8",
)["result"]
def apply_tag(s: requests.Session, wid: str, tag_id: str = TAG_FAULT) -> None:
j = api(
s,
"PATCH",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}?_input_charset=utf-8",
{
"workitemIdentifier": wid,
"propertyKey": "tag",
"propertyValue": tag_id,
"operateType": "COVER",
},
)
if j.get("code") != 200:
raise RuntimeError(f"tag failed {wid}: {j}")
def set_document(s: requests.Session, wid: str, html: str) -> None:
j = api(
s,
"PATCH",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}/document?_input_charset=utf-8",
{"content": html, "formatType": "RICHTEXT"},
)
if j.get("code") != 200 or j.get("errorMsg"):
raise RuntimeError(f"document failed {wid}: {j}")
def set_fields(s: requests.Session, wid: str, pairs: list[tuple[str, str]]) -> None:
data = urllib.parse.urlencode(
{
"fieldValueList": json.dumps(
[{"fieldIdentifier": k, "value": v} for k, v in pairs], ensure_ascii=False
)
}
)
r = s.post(
f"https://devops.aliyun.com/projex/api/workitem/workitem/field/value/{wid}?_input_charset=utf-8",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=60,
)
j = r.json()
if j.get("code") != 200:
raise RuntimeError(f"field/value failed {wid}: {j}")
def set_estimate_hours(s: requests.Session, wid: str, hours: int, user: str = WANG) -> None:
est = api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/workitem/time/estimate/list?workitemIdentifier={wid}",
).get("result") or []
for row in est:
eid = row.get("identifier")
if eid:
api(
s,
"DELETE",
f"https://devops.aliyun.com/projex/api/workitem/workitem/time/estimate/{wid}/{eid}",
)
j = api(
s,
"POST",
"https://devops.aliyun.com/projex/api/workitem/workitem/time/estimate?_input_charset=utf-8",
{
"workitemIdentifier": wid,
"spentTime": hours,
"type": "develop",
"description": "快轨默认预计工时",
"recordUserIdentifier": user,
"forCreate": False,
"containsRestDay": False,
},
)
if j.get("code") != 200:
raise RuntimeError(f"estimate failed {wid}: {j}")
def set_actual_hours(s: requests.Session, wid: str, hours: int, user: str = WANG) -> None:
j = api(
s,
"POST",
"https://devops.aliyun.com/projex/api/workitem/workitem/time?_input_charset=utf-8",
{
"workitemIdentifier": wid,
"actualTime": hours,
"type": "develop",
"description": "快轨默认实际工时",
"recordUserIdentifier": user,
"gmtStart": START_MS,
"gmtEnd": END_MS,
},
)
if j.get("code") != 200 or not j.get("result"):
raise RuntimeError(f"actual time failed {wid}: {j}")
def try_associate_to_req(s: requests.Session, stage_id: str, req_id: str) -> bool:
"""建后补 ASSOCIATED→需求。Cookie 下常失败,成功返回 True。"""
bodies = [
{"relationIdentifier": "ASSOCIATED", "toWorkitemIdentifier": req_id},
{
"relationIdentifier": "ASSOCIATED",
"fromWorkitemIdentifier": stage_id,
"toWorkitemIdentifier": req_id,
},
]
for body in bodies:
for url in (
f"https://devops.aliyun.com/projex/api/workitem/workitem/{stage_id}/relation/record?_input_charset=utf-8",
f"https://devops.aliyun.com/projex/api/workitem/v2/workitem/{stage_id}/relation/record?_input_charset=utf-8",
):
j = api(s, "POST", url, body)
if j.get("code") == 200 and j.get("result") not in (None, False):
if not (isinstance(j.get("result"), dict) and j["result"].get("status") in (404, 405)):
rows = list_associated(s, stage_id)
if req_id in {r.get("identifier") for r in rows}:
return True
return False
def transit(s: requests.Session, wid: str, from_status: str, to_status: str) -> None:
if from_status == to_status:
return
j = api(
s,
"POST",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}/status/transit?_input_charset=utf-8",
{"fromStatus": from_status, "toStatus": to_status},
)
if not (j.get("code") == 200 and j.get("result") is True):
raise RuntimeError(f"transit {wid} {from_status}->{to_status}: {j}")
def md_to_html(md: str) -> str:
parts = []
for line in md.splitlines():
if line.startswith("## "):
parts.append(f"<h2>{line[3:]}</h2>")
elif line.startswith("### "):
parts.append(f"<h3>{line[4:]}</h3>")
elif line.startswith("- "):
parts.append(f"<p>• {line[2:]}</p>")
elif line.strip():
parts.append(f"<p>{line}</p>")
return "".join(parts)
def req_document_html(s: requests.Session, rid: str) -> str:
w = get(s, rid)
return ((w.get("document") or {}).get("content") or w.get("description") or "").strip()
def req_payload(subject: str, html: str) -> dict:
return {
"subject": subject,
"description": html,
"formatType": "RICHTEXT",
"document": {"content": html, "formatType": "RICHTEXT"},
"spaceIdentifier": SPACE,
"space": SPACE,
"spaceType": "Project",
"workitemTypeIdentifier": REQ_TYPE,
"workitemType": REQ_TYPE,
"categoryIdentifier": "Req",
"category": "Req",
"assignedTo": WANG,
"fieldValueList": [
{"fieldIdentifier": "priority", "value": PRI},
{"fieldIdentifier": "assignedTo", "value": WANG},
],
"attachmentIdList": [],
"cloneFrom": None,
"createWorkitemRelationList": [],
}
def task_payload(
subject: str,
html: str,
assignee: str,
*,
plan_start: bool = True,
associated_req: str | None = None,
parent_delivery: str | None = None,
) -> dict:
fvl = [
{"fieldIdentifier": "priority", "value": PRI},
{"fieldIdentifier": "assignedTo", "value": assignee},
]
if plan_start:
fvl.append({"fieldIdentifier": "79", "value": NOON_MS})
payload: dict[str, Any] = {
"subject": subject,
"description": html,
"formatType": "RICHTEXT",
"spaceIdentifier": SPACE,
"space": SPACE,
"spaceType": "Project",
"workitemTypeIdentifier": TASK_TYPE,
"workitemType": TASK_TYPE,
"categoryIdentifier": "Task",
"category": "Task",
"assignedTo": assignee,
"fieldValueList": fvl,
"attachmentIdList": [],
"cloneFrom": None,
}
if parent_delivery:
payload["parent"] = parent_delivery
payload["parentIdentifier"] = parent_delivery
payload["createWorkitemRelationInfo"] = {
"relatedWorkitemIdentifier": parent_delivery,
"relatedToRelationIdentifier": "TASK_SUB",
}
else:
if not associated_req:
raise ValueError("associated_req required for delivery: ASSOCIATED→需求")
payload["createWorkitemRelationInfo"] = {
"relatedWorkitemIdentifier": associated_req,
"relatedToRelationIdentifier": "ASSOCIATED",
}
return payload
def list_associated(s: requests.Session, wid: str) -> list[dict]:
j = api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/v2/workitem/{wid}/relation/workitem/list/by-relation-category?category=ASSOCIATED&isForward=true",
)
return j.get("result") or []
def assert_associated_to_req(s: requests.Session, wid: str, req_id: str, label: str) -> None:
rows = list_associated(s, wid)
ids = {r.get("identifier") for r in rows}
if req_id not in ids:
raise RuntimeError(
f"{label} 关联项未挂需求:期望 {req_id},实际 {[r.get('serialNumber') for r in rows]}"
)
AUTO_RDO = """## 原始诉求AutoRDO
运维需在故障处置页承接机器人上报的故障,完成处置、挂起与归档,并保留证据链;工作台相关统计口径需与处置页一致
待确认:
- 本期是否含真实短信/邮件通道(现口径一般为演示模板)
## 工作项编号(系统)
- 交付:待建
- 分析:待建
- 设计:待建
"""
def summarize_from_create(w: dict, *, status: str, assignee_name: str) -> dict:
return {
"serial": w.get("serialNumber"),
"id": w.get("identifier"),
"subject": w.get("subject"),
"status": status,
"assignee": assignee_name,
"parent": w.get("parentIdentifier"),
}
def build_normal() -> dict:
t0 = time.perf_counter()
s = session()
title = "【新增】故障处置YunxiaoPMapp标准·极速v2"
req = create(s, req_payload(title, md_to_html(AUTO_RDO + f"\n原型:{PROTO}\n")))
rid = req["identifier"]
with ThreadPoolExecutor(max_workers=2) as pool:
f_tag_r = pool.submit(apply_tag, session(), rid)
f_deliv = pool.submit(
create,
session(),
task_payload(
f"【交付】{title}",
f"<p>{PLACEHOLDER}</p>",
HEFEI,
associated_req=rid,
),
)
deliv = f_deliv.result()
f_tag_r.result()
did = deliv["identifier"]
with ThreadPoolExecutor(max_workers=3) as pool:
f_tag_d = pool.submit(apply_tag, session(), did)
f_ana = pool.submit(
create,
session(),
task_payload(
f"【分析】{title}",
"<p>分析阶段:故障处置台账、挂起归档与证据链。</p>",
WANG,
associated_req=rid,
parent_delivery=did,
),
)
f_des = pool.submit(
create,
session(),
task_payload(
f"【设计】{title}",
f"<p>设计阶段:对齐原型 {PROTO}</p>",
WANG,
associated_req=rid,
parent_delivery=did,
),
)
ana = f_ana.result()
des = f_des.result()
f_tag_d.result()
with ThreadPoolExecutor(max_workers=3) as pool:
list(
as_completed(
[
pool.submit(transit, session(), rid, PENDING, DESIGN_DONE),
pool.submit(transit, session(), ana["identifier"], PENDING, TASK_DONE),
pool.submit(transit, session(), des["identifier"], PENDING, TASK_DONE),
]
)
)
transit(s, rid, DESIGN_DONE, PENDING_DEV)
return {
"path": "normal",
"elapsed_s": round(time.perf_counter() - t0, 3),
"req": summarize_from_create(req, status="待开发", assignee_name="王冕"),
"delivery": summarize_from_create(deliv, status="待处理", assignee_name="何斐"),
"analysis": summarize_from_create(ana, status="已完成", assignee_name="王冕"),
"design": summarize_from_create(des, status="已完成", assignee_name="王冕"),
}
def build_fast(
*,
tag_id: str = TAG_FAULT,
has_prototype: bool = True,
delivery_html: str | None = None,
) -> dict:
"""无单快轨。
- 设计描述 = 需求 document
- 交付描述 = 手工同步需求正文(无原型)或传入 AutoPRD HTML禁止无故占位
- 设计 79/80=当日;交付 79=当日
- 需求/交付/设计同标签
- 需求预计/实际工时各 2
- 设计 TASK_SUB→交付后尝试 ASSOCIATED→需求
"""
t0 = time.perf_counter()
s = session()
title = "【新增】故障处置YunxiaoPM快轨·极速v5"
req_html = md_to_html(AUTO_RDO + (f"\n原型:{PROTO}\n" if has_prototype else "\n"))
req = create(s, req_payload(title, req_html))
rid = req["identifier"]
# 以落库 document 为准(与手动建需后读需求一致)
req_html = req_document_html(s, rid) or req_html
if delivery_html:
deliv_html = delivery_html
desc_source = "autoprd"
elif req_html.strip():
deliv_html = req_html
desc_source = "manual_sync"
else:
deliv_html = f"<p>{PLACEHOLDER}</p>"
desc_source = "placeholder"
with ThreadPoolExecutor(max_workers=2) as pool:
f_tag_r = pool.submit(apply_tag, session(), rid, tag_id)
f_deliv = pool.submit(
create,
session(),
task_payload(
f"【交付】{title}",
deliv_html,
HEFEI,
associated_req=rid,
),
)
deliv = f_deliv.result()
f_tag_r.result()
did = deliv["identifier"]
with ThreadPoolExecutor(max_workers=2) as pool:
f_tag_d = pool.submit(apply_tag, session(), did, tag_id)
f_des = pool.submit(
create,
session(),
task_payload(
f"【设计】{title}",
req_html,
WANG,
parent_delivery=did,
),
)
des = f_des.result()
f_tag_d.result()
des_id = des["identifier"]
apply_tag(s, des_id, tag_id)
# 计划时间设计起止当日交付开始当日create 已带 79再 field/value 加固)
set_fields(s, des_id, [("79", NOON), ("80", EOD)])
set_fields(s, did, [("79", NOON)])
# 设计关联项补挂需求(可能失败,回报 risk
design_assoc_ok = try_associate_to_req(s, des_id, rid)
with ThreadPoolExecutor(max_workers=2) as pool:
list(
as_completed(
[
pool.submit(transit, session(), rid, PENDING, DESIGN_DONE),
pool.submit(transit, session(), des_id, PENDING, TASK_DONE),
]
)
)
transit(s, rid, DESIGN_DONE, PENDING_DEV)
set_estimate_hours(s, rid, FAST_EST)
set_actual_hours(s, rid, FAST_ACT)
risk = None
if desc_source == "placeholder":
risk = "交付描述仍为占位"
if not design_assoc_ok:
risk = (risk + "" if risk else "") + "设计 ASSOCIATED→需求补挂失败Cookie须 UI/OpenAPI 兜底"
return {
"path": "fast",
"elapsed_s": round(time.perf_counter() - t0, 3),
"req": summarize_from_create(req, status="待开发", assignee_name="王冕"),
"delivery": summarize_from_create(deliv, status="待处理", assignee_name="何斐"),
"design": summarize_from_create(des, status="已完成", assignee_name="王冕"),
"analysis": None,
"delivery_desc_source": desc_source,
"design_associated_ok": design_assoc_ok,
"risk": risk,
}
def main() -> None:
auth0 = time.perf_counter()
load_auth()
auth_s = round(time.perf_counter() - auth0, 3)
wall0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as pool:
f_fast = pool.submit(build_fast)
f_normal = pool.submit(build_normal)
fast = f_fast.result()
normal = f_normal.result()
build_s = round(time.perf_counter() - wall0, 3)
s = session()
assert_associated_to_req(s, normal["delivery"]["id"], normal["req"]["id"], "normal.delivery")
assert_associated_to_req(s, fast["delivery"]["id"], fast["req"]["id"], "fast.delivery")
def assert_sub(delivery_id: str, child_id: str, label: str) -> None:
rows = api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/v2/workitem/{delivery_id}/relation/workitem/list/by-relation-category?category=PARENT_SUB&isForward=true",
).get("result") or []
ids = {r.get("identifier") for r in rows}
if child_id not in ids:
raise RuntimeError(
f"{label} 未出现在交付子项:期望 {child_id},实际 {[r.get('serialNumber') for r in rows]}"
)
assert_sub(normal["delivery"]["id"], normal["analysis"]["id"], "normal.analysis")
assert_sub(normal["delivery"]["id"], normal["design"]["id"], "normal.design")
assert_sub(fast["delivery"]["id"], fast["design"]["id"], "fast.design")
verify = {
"normal_req": get(s, normal["req"]["id"])["status"]["displayName"],
"fast_req": get(s, fast["req"]["id"])["status"]["displayName"],
"normal_delivery_assignee": (
get(s, normal["delivery"]["id"]).get("assignedTo") or {}
).get("displayName"),
"fast_delivery_assignee": (get(s, fast["delivery"]["id"]).get("assignedTo") or {}).get(
"displayName"
),
"delivery_associated_ok": True,
"stage_tasks_as_sub_ok": True,
"fast_design_associated_ok": fast.get("design_associated_ok"),
}
out = {
"normal": normal,
"fast": fast,
"verify": verify,
"auth_elapsed_s": auth_s,
"wall_elapsed_s": build_s,
"created_at": datetime.now(TZ).isoformat(),
"mode": "live_create_fast_v5_fast_track_rules",
"opts": {
"create_includes_plan_start": True,
"skip_plan_end_on_create": True,
"tracked_transit_no_get": True,
"delivery_assignee_hefei_at_create": True,
"overlap_tag_with_create": True,
"fast_req_hours": f"{FAST_EST}+{FAST_ACT}",
"relation": "delivery ASSOCIATED→req; analysis/design TASK_SUB→delivery; design post ASSOCIATED→req",
"http": "requests.Session keep-alive",
},
}
Path("/tmp/yunxiao_pmapp_fast_v2_result.json").write_text(
json.dumps(out, ensure_ascii=False, indent=2)
)
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,60 @@
# 交付树模型与关联约定
## 真相源
```text
需求状态 = 阶段看板唯一真相(分析中/设计中/待开发…)
【交付】任务 = 该需求的交付容器(每需求最多 1 条)
【分析】/【设计】 = YunxiaoPMapp 在交付下建的阶段子项
【开发】/【测试】 = 另属开发 Skill / 测试 Skill不进本包
ASSOCIATED = 横向挂钩(【交付】 ↔ 需求)
SUB / TASK_SUB = 纵向拆解(交付 → 分析/设计)
```
## 关系验收
| 关系 | 用在哪里 | 验收 | 优先级 |
|---|---|---|---|
| ASSOCIATED | **仅【交付】** ↔ 需求 | 交付详情「关联项」能看到需求(**强制** | 交付必过 |
| TASK_SUB | 【分析】/【设计】 → 【交付】 | 交付详情「子项」能看到阶段任务(**强制** | 阶段任务必过 |
建单规则Cookie 路径 · 同一 create 只能一条 `createWorkitemRelationInfo`
| 对象 | `createWorkitemRelationInfo` | 另写字段 |
|---|---|---|
| 【交付】 | `ASSOCIATED` → 需求 | — |
| 【分析】/【设计】 | `TASK_SUB` → 交付 | `parent` + `parentIdentifier` = 交付 |
**禁止**对分析/设计只写 `parentIdentifier` + `ASSOCIATED→需求`关联项可能有但交付「子项」仍为空ONEOS-246/247 已复现)。
勿用 `PARENT` 冒充关联项。细则见 [live-api.md](live-api.md)。
## 禁止
| 禁止 | 说明 |
|---|---|
| 创建或描述【开发】/【测试】 | 口令与回报均不出现 |
| 分析/设计未挂成交付子项(无 TASK_SUB | 交付「子项」为 0交棒验收失败 |
| 用任务名称/标题做唯一性或查重 | 不得 `subject` 匹配复用 |
| 同一需求认定多个交付任务编号 | 列出编号请人合并后再继续 |
| 功能模块标签用「分析/设计/交付」 | 模块标签打在需求(可选交付)上 |
## 任务标题(仅展示)
- 【交付】+ 需求标题
- 【分析】+ 需求标题
- 【设计】+ 需求标题
标题可改,**不参与查重**。
## 任务编号唯一性
**唯一允许的查重/复用渠道:云效任务编号**(如 `ONEOS-99` / `serialNumber`)。
| 场景 | 正确 | 错误 |
|---|---|---|
| 首次创建【交付】 | 建单成功后回报并写入「工作项编号(系统)」 | 下次用标题搜 |
| 复用【交付】 | 口令或编号区块带 `交付任务=ONEOS-xx` | list 后按 subject 相等 |
| 判断是否已有交付 | 编号区块或交付 ASSOCIATED 编号集合 | 数【交付】开头标题 |
| 分析/设计幂等 | 仅已登记编号 | 按【分析】+需求标题 |
按编号命中则复用;**计划开始已有值禁止改写**。

View File

@@ -0,0 +1,49 @@
# 验收清单与回报
## apply 后必须自检
| # | 项 | 通过标准 |
|---|---|---|
| 1 | 需求编号 | 回报含 ONEOS-xx |
| 2 | 任务编号 | 交付/分析/设计凡新建或操作均回报编号;禁止只报标题 |
| 3 | 编号区块 | 需求「工作项编号(系统)」与实际一致 |
| 4 | ASSOCIATED | **仅交付**详情关联项可见需求(`createWorkitemRelationInfo=ASSOCIATED`;禁止 PARENT 冒充) |
| 5 | SUB | **必过**:分析/设计 create 用 `TASK_SUB→交付`(含 `parent`+`parentIdentifier`交付「子项」tab 须可见。与 ASSOCIATED 同 create 互斥,阶段任务关联项可空(见 live-api.md |
| 6 | 计划开始 | 未误覆盖已有值 |
| 7 | 阶段日历工时 | 有计划完成时已写入;脚注存在;回报用语正确 |
| 8 | 交棒 | 需求=待开发;交付负责人=何斐(交棒场景) |
| 9 | 占位风险 | 交付仍占位时首行标红 |
| 10 | 负向 | 本轮**无**【开发】/【测试】任务;未按标题查重 |
设计完成额外AutoPRD 段已写交付非占位除非失败停下ZIP+截图已挂或已列出缺项。
迭代额外:只挂【交付】(需求不挂);版本号符合递增规则。
## 回报模板(精简)
```text
【YunxiaoPM】
风险:(若有占位交棒则首行标红)
需求ONEOS-xx | 状态=…
交付ONEOS-a | 负责人=…
分析ONEOS-b | …(无则写无)
设计ONEOS-c | …(无则写无)
阶段日历工时:分析 Hh / 设计 Hh若本轮写入
附件:…(若本轮)
迭代:…(若本轮)
下一步:请技术经理使用开发 Skill交棒后
```
## 适合全自动 vs 人工门禁
| 适合全自动 | 建议半自动/人工门禁 |
|---|---|
| 建单、打标签、挂迭代、写描述、建交付树、改状态、交棒负责人、幂等查重 | **PJ 云效项目点选**、受理(已确认)、是否快轨、设计完成是否真可开发、跨需求优先级与迭代容量、回退与取消 |
## 实写性能2026-07-23 复盘)
| 项 | 要求 |
|---|---|
| 改状态 | 只用 `status/transit`(见 [live-api.md](live-api.md) |
| 改【交付】负责人 | 只用 `PATCH …/{id}` + `propertyKey=assignedTo` |
| 极速复测 | `scripts/live_create_fast.py`;默认不开浏览器 |

View File

@@ -0,0 +1,22 @@
# 口令面
```text
记录需求:…;项目=必选·Plan 点选);优先级=紧急|高|中|低;标签=…;提交部门=…;提交人=…;推进至=暂不推进|已确认|分析中|设计中|设计完成|待开发|待开发(快轨)
受理确认ONEOS-xx
开始分析ONEOS-xx → 【交付】+【分析】并回报编号
开始设计ONEOS-xx交付任务=…;分析任务=… → 【设计】+收口分析
设计完成ONEOS-xx设计任务=…;原型=…
交棒开发ONEOS-xx交付任务=… → 待开发 + 该编号负责人=何斐
快轨待开发ONEOS-xx → 待开发 +【交付】+【设计】(无【分析】)+ 交付负责人=何斐
编号直推:分析任务=ONEOS-b / 设计任务=ONEOS-c → 收口未完成计划完成 + 需求待开发 + 交付交棒何斐
创建迭代:版本类型=主|副|子;交付任务=ONEOS-a,ONEOS-b,…;名称前缀=…
AutoRDO粘贴聊天/附录音)→ 再记录需求
```
版本自动规则:取同前缀下最大 `Vx.y.z`(旧 `Vx.y` 视为 `Vx.y.0`)再按主/副/子递增;迭代名=`{前缀}{新版本}`
**PJ 项目**:新建需求/迭代前须点选云效项目(实时列表);禁止默认直指。见 [project-selection.md](project-selection.md)。
后续推进口令**优先显式带任务编号**;未带则读「工作项编号(系统)」;仍无则询问;**禁止按标题补全**。
YunxiaoPM 回报止于交棒;可一句「请技术经理使用开发 Skill」。

View File

@@ -0,0 +1,95 @@
# 压缩点选(`1a2b3a4d`
记录需求 Plan **默认**用编号题 + 字母选项;用户可用一行压缩答复确认。
## Plan 展示模板
```text
请回复压缩点选1a2b3a4d
(数字=题号,字母=选项;大小写不敏感;可写 1A2B3A4D
1. 类型
A. 新增
B. 优化
2. 项目(云效实时列表;建议项可标★)
A. 01_ONEOSONEOS
B. 02_小羚羚APPXLLAPP
C. …
3. 优先级
A. 紧急
B. 高
C. 中
D. 低
4. 标签(云效候选;建议项可标★)
A. 故障管理★
B. 还车应结款
C. …
```
可选续题(口令已齐则可写入清单、压缩串可不含):
```text
5. 推进至 …
6. 迭代 …
```
## 解析规则
| 规则 | 说明 |
|---|---|
| 形态 | `(题号)(字母)` 连续拼接,如 `1a2b3a4d` |
| 题号 | `1`=类型 `2`=项目 `3`=优先级 `4`=标签;可扩展 `5` `6` |
| 字母 | `a`→第 1 项 … `z`→第 26 项;超出选项数 → 非法,停 |
| 缺题 | 该题若口令已唯一预填且列表唯一命中,可用预填;否则停并追问缺题 |
| 多答同题 | 后写覆盖先写 |
| 非法字符 | 停,重贴模板 |
解析成功后 Plan 回显人话确认一行,例如:
```text
已解析 1a2b3a4d → 类型=新增;项目=02_小羚羚APP优先级=紧急;标签=D 项名)
确认后回复「执行」
```
用户再回「执行」才 apply仍守 Plan 门禁)。
## 与口令预填
口令已写 `类型新增项目01_ONEOS优先级标签故障管理` 时:
- 对应选项标 ★,并给出**建议压缩串**(如 `1a2a3b4a`),用户可直接改字母或整段重答。
- **不得**因有预填而跳过展示字母表;压缩确认(或逐题点选)仍是门禁。
## 标签未命中 → 自动重拉一次候选并重生选项
「标签无法对应」:口令标签名在**当前标签候选**中 0 命中(或多命中无法唯一)。
| 步骤 | 动作 |
|---|---|
| 1 | **自动重拉一次**标签候选(见下「标签候选来源」;强制刷新,不用旧缓存) |
| 2 | 用新列表**重新生成 4.A/B/C…** 选择项 |
| 3 | 回报:`已自动重拉标签列表(因无法对应)`;请用户重答 `4x` 或整串 |
| 4 | 仍无法对应 → 停;**禁止**第 3 次空转;**禁止**猜 tagId |
> 说明:此处重拉的是**标签候选列表**,不是项目列表。项目无法对应仍走 [project-selection.md](project-selection.md) 的「自动重拉一次」。
### 标签候选来源(当前已验证路径)
1. `runtime-ids.json``tags`
2. 目标项目(或预填项目)下近期工作项 `tag` 字段聚合去重
3. 重拉 = 再请求工作项列表聚合 + 合并 runtime命中后可回写 `tags` 缓存
(专用 `tag/list` Cookie API 尚未稳定;有稳定端点后改写入 `live-api.md` 并切换。)
## 项目列表顺序
生成 2. 选项时:口令预填/建议项目置 **A** 并标 ★,其余按云效返回顺序接 B/C/…。
## Agent 义务
1. 记录需求进 Plan 时必须输出本模板(至少 14 题)。
2. 收到压缩串先解析回显,再等「执行」。
3. 标签/项目无法对应:各自动重拉**一次**并刷新对应题选项。

View File

@@ -0,0 +1,69 @@
# 需求描述 vs 交付描述
两类描述职责不同;禁止混写;禁止建【交付】时把 AutoPRD 六大块提前塞进交付描述。
## A. 需求描述 · AutoRDO 清洗
| 项 | 规则 |
|---|---|
| 触发 | 对话建需求 / 碎片材料入库 |
| 调用 | **必须**使用独立 Skill「**`$AutoRDO`**」(`AutoRDO/SKILL.md`;本 Skill 不内嵌清洗细则) |
| 输入 | 聊天记录、录音、口述材料 |
| 处理 | 保留原意;书面化;去口头禅;**去除结尾句号**(细则在 AutoRDO `references/rules.md` |
| 输出 | 写入 `## 原始诉求AutoRDO` |
| 不做 | 本阶段不写 AutoPRD 六大块;不要求已有原型;不覆盖已有「产品说明」 |
口令:`AutoRDO…` → 确认后 `记录需求:【新增】标题;描述=整理稿;推进至=…`
## B. 交付描述
### B1. 标准路径(分析中起建交付)
| 时机 | 【交付】描述 |
|---|---|
| 首次创建起至设计完成前 | 固定文案:`等待设计任务完成后自动填入` |
| 设计完成时 | 用 AutoPRD「产品说明」正文替换占位 |
### B2. 无单快轨建交付(覆盖 B1
| 口令/材料 | 【交付】描述 |
|---|---|
| 手工描述(无指定原型) | **同步需求**当前手工/AutoRDO 正文;禁止占位 |
| 指定原型页面 | `$oneos-autoprd` 从原型生成写入;禁止占位 |
| 既无正文又无原型 | 才允许占位并标红风险 |
快轨【设计】描述始终复制需求当前描述正文(见 [fast-track.md](fast-track.md))。
## C. 需求描述双段模板(不可互相覆盖)
```markdown
## 原始诉求AutoRDO
(清洗稿;设计完成也不删除)
## 产品说明AutoPRD
(六大块+对象存储链接;未设计完成前可无此节或写「待设计完成后填入」)
## 工作项编号(系统)
- 交付:…
- 分析:…
- 设计:…
```
## D. 设计完成 · AutoPRD + 附件
前提:设计任务完成且已关联对应原型页。
1. 调用 **`$oneos-autoprd`AutoPRD**,产出并落盘 `.spec/requirements-prd.md` + 标注同步:
- 对象存储预览链接:`{baseUrl}/{prototype-id}/index.html`(禁止加 `prototypes/` 前缀、禁止去掉 `index.html`
- 产品说明 Markdown总览/角色/流程/状态/风险/交付等,见 AutoPRD 模板)
2. 写入需求 `## 产品说明AutoPRD`**禁止覆盖** `## 原始诉求AutoRDO` / `## 工作项编号(系统)`
3. 【交付】描述:按**交付任务编号**用产品说明正文**替换**占位(细则见 AutoPRD `references/yunxiao-delivery-sync.md`);可附「原始诉求见需求描述」。**创建【交付】时不得提前灌 MD。**
4. 附件(需求 +【交付】均挂;失败则不得声称成功):
- Make「导出 HTML含源码」ZIP → [make-export-attach.md](make-export-attach.md)
- Make「复制截图」全交互页 → 同上
缺原型 / AutoPRD 失败 / 导出或截图失败 → **不得**报到设计完成并宣称附件齐全;停下并列出缺项。
执行顺序:先 AutoPRD 落盘与附件就绪 → 再改需求/交付描述与设计计划完成 → 最后改需求状态。
**禁止**加载 `yunxiao-requirement-lifecycle`;阶段任务树只由本 SkillYunxiaoPMapp创建。

View File

@@ -0,0 +1,47 @@
# 无单快轨到待开发
适用:口令明确「快轨」或「推进至待开发」且当前需求尚无标准分析路径任务(或用户确认跳过分析)。
## 动作表
| 对象 | 动作 |
|---|---|
| 需求 | 状态 → **待开发****预计工时=2**、**实际工时=2**(覆盖同日 8h 日历工时默认);标签按口令;更新编号区块 |
| 【交付】 | 无则新建ASSOCIATED→需求勿用 PARENT负责人→何斐**计划开始=创建当日**`79`**不写**计划完成;**描述双路径**(见下);**标签与需求相同**;更新编号区块 |
| 【设计】 | **必须新建****TASK_SUB→交付**(交付「子项」须可见);**描述=复制需求当前描述正文**;计划开始/完成均=当日create 后 `field/value``79`+`80`);任务→完成态;**标签与需求相同**;建后再尝试 **ASSOCIATED→原始需求**Cookie 常失败,见 live-api失败须标红**子项优先于关联项**);更新编号区块 |
| 【分析】 | **禁止创建** |
## 交付描述双路径A1
| 口令/材料 | 【交付】描述 |
|---|---|
| **手工描述**(无指定原型) | 同步需求当前手工/AutoRDO 正文(与设计一致可复制 document |
| **指定原型页面** | 调用 `$oneos-autoprd`,从原型生成 AutoPRD「产品说明」写入【交付】 |
| 既无手工可同步正文、又无原型 | 才允许占位 `等待设计任务完成后自动填入`Plan 勾选交棒占位风险,回报首行标红 |
有手工或原型时**禁止**交棒占位。
## AutoPRD
- 快轨默认可不跑 AutoPRD**例外**:口令指定原型 → 交付走 AutoPRD 路径(上表)。
- 口令同时「设计完成 + 原型」时仍按步骤 4 灌需求产品说明段。
## 查重
只认任务编号。已有分析单不得因「快轨」被删除。
## 回报必含
- 需求 / 交付 / 设计编号
- 「快轨:未建分析;设计已同步需求描述并当日收口」
- 交付描述来源:手工同步 / AutoPRD / 占位(若占位则首行标红)
- 需求预计工时=2、实际工时=2
- 设计关联项含需求编号;交付子项含设计编号
## 与标准 / 编号直推对比
```text
标准:分析中→交付+分析 → 设计中→设计+收口分析 → 设计完成→收口设计 → 待开发交棒
快轨(无既有阶段单):→ 待开发:新建交付+设计+交棒何斐(无分析;设计当日收口;需求工时 2+2
编号直推:已有分析/设计编号 → 收口空计划完成 → 待开发+交付交棒(不新建冗余单)
```

View File

@@ -0,0 +1,20 @@
# 交棒门禁与回退最小集
## 交棒门禁(待开发)
| 情形 | 规则 |
|---|---|
| 【交付】描述已是 AutoPRD 正式说明(非占位) | 允许交棒Plan 正常列交付任务编号 + 负责人→何斐 |
| 【交付】描述仍为 `等待设计任务完成后自动填入` | **仍允许交棒**,但 Plan **必须**勾选风险项「交付描述仍为占位,技术侧仅可凭需求 AutoRDO 稿理解」;回报**首行标红**该风险;**禁止**静默交棒假装材料齐全 |
| 口令同时带原型且要求设计完成 | 先走步骤 4AutoPRD+附件)成功,再交棒,不再勾占位风险 |
## 回退 / 变更(最小集 · 兼容计划开始不篡改)
| 场景 | 规则 |
|---|---|
| 待开发 → 退回设计中 | 需求状态回退;【交付】负责人可改回创建人/产品;**交付计划开始不改**;若需重做设计 → **新开**设计任务编号(旧设计保持已收口,不改其计划开始);更新「工作项编号(系统)」中的设计编号为新号 |
| 设计完成后需求大变 | 不自动改状态Plan 询问是否回退设计中并新开设计编号;更新 AutoPRD 段AutoRDO 段保留并追加「变更纪要」 |
| 取消需求 | 关联任务标取消/废止;不删编号区块;不改已写计划开始 |
| 度量含义 | 交付计划开始保留 = 全周期仍从首次开工起算;回退重做会拉长日历工时——回报注明「含回退重做」 |
凡写云效的回退仍须走 Plan 门禁。

View File

@@ -0,0 +1,14 @@
# 交接契约(开发 Skill 入口)
YunxiaoPMapp 与开发 Skill **不要**互相 include 全文;仅认下列契约。
```text
PM 完成交棒 → 需求=待开发;【交付】任务编号=ONEOS-xx负责人=何斐ASSOCIATED 需求
若交付描述仍为占位 → 回报已标红风险
开发 Skill 入口 → 认「需求编号 + 交付任务编号」
占位时只可信需求「原始诉求AutoRDO」并有权要求产品补设计完成
```
共享常量(项目 ID、何斐 ID、节假日日历可引用本 Skill 的 `assets/` 短路径,勿加载整份对方规则。
测试 Skill 另开;本契约不覆盖提测/缺陷。

View File

@@ -0,0 +1,242 @@
# 云效实写 APIYunxiaoPMapp 已验证)
`verified_at`: 2026-07-25 · **项目须门禁 PJ 点选**(见 [project-selection.md](project-selection.md));历史验证样本项目为 `01_ONEOS` / 原「统一运营管理平台」(`last_selected.spaceIdentifier``assets/runtime-ids.json`,禁止未点选即使用)。
本文件只记**已跑通**的写法;禁止再盲试 `updateStatus` / 错误 `updateFieldValue` POST。
## 认证
- CookieChrome 域 `.aliyun.com` / `devops.aliyun.com``browser_cookie3` 或 Playwright storage
- Header`x-xsrf-token` = cookie `XSRF-TOKEN`URL 解码后)
- `Origin` / `Referer``https://devops.aliyun.com`
## 建单
`POST|PUT /projex/api/workitem/workitem?_input_charset=utf-8`
创建响应 `result.identifier` / `serialNumber` 即编号真相;**禁止按标题查重**。
## 改负责人(已通)
```http
PATCH /projex/api/workitem/workitem/{id}?_input_charset=utf-8
{"propertyKey":"assignedTo","propertyValue":"<userId>","operateType":"COVER"}
```
交棒:【交付】`propertyValue` = 何斐 ID。
## 打标签(已通)
```http
PATCH /projex/api/workitem/workitem/{id}?_input_charset=utf-8
{"workitemIdentifier":"{id}","propertyKey":"tag","propertyValue":"<tagId>[,<tagId>]","operateType":"COVER"}
```
## 改状态(已通 · 唯一推荐)
```http
POST /projex/api/workitem/workitem/{id}/status/transit?_input_charset=utf-8
{"fromStatus":"<status.identifier>","toStatus":"<status.identifier>"}
```
成功:`code=200``result=true`。失败时 `errorMsg` 含「不能流转」。
### 需求状态 ID本项目
| 显示名 | identifier |
|---|---|
| 待处理 | `100005` |
| 已确认 | `32` |
| 分析中 | `154395` |
| 设计中 | `156603` |
| 设计完成 | `307012` |
| 待开发 | `1582fc929d429111b925309493` |
### 任务状态 ID
| 显示名 | identifier |
|---|---|
| 待处理 | `100005` |
| 已完成 | `100014` |
### 极速交棒跳转(工作流允许)
从「待处理」菜单可见直达「设计完成」;推荐最少跳:
```text
待处理 → 设计完成 → 待开发
```
标准路径若需看板留痕,可走完整链:已确认→分析中→设计中→设计完成→待开发(仍用本 API勿开 UI
### 禁止(已证伪)
| 写法 | 结果 |
|---|---|
| `PATCH …/updateStatus` + `statusIdentifier` | `400 不能为空` |
| `PATCH …/{id}` + `propertyKey=status` | `property not found` |
| Playwright 点左侧/列表上的状态色块(`x<1100` | 假成功、状态不落库 |
仅当 `status/transit` 不可用时,才用 UI右侧详情状态钮`getBoundingClientRect().x > 1100`+ `.next-menu-item`
## 计划开始/完成 · 提交部门/人 · 预计工时2026-07-27 修订)
**通用字段写入(已通):**
```http
POST /projex/api/workitem/workitem/field/value/{workitemId}?_input_charset=utf-8
Content-Type: application/x-www-form-urlencoded
fieldValueList=[{"fieldIdentifier":"79","value":"2026-07-27 12:00:00"},{"fieldIdentifier":"3132597a9718d1c282b7ba5a0c","value":""},{"fieldIdentifier":"9e01269e96f91fbb97d36bf5b3","value":""}]
```
| 字段 | fieldIdentifier | value |
|---|---|---|
| 计划开始 | `79` | `YYYY-MM-DD HH:mm:ss`(推荐正午)或 epoch ms 字符串 |
| 计划完成 | `80` | 同上 |
| 提交部门 | `3132597a9718d1c282b7ba5a0c` | 纯文本 |
| 提交人 | `9e01269e96f91fbb97d36bf5b3` | 纯文本 |
**预计工时 `101586`:禁止直接改字段**(报「不可直接修改」)。须登记:
```http
POST /projex/api/workitem/workitem/time/estimate?_input_charset=utf-8
{"workitemIdentifier":"<id>","spentTime":8,"type":"develop","description":"","recordUserIdentifier":"<userId>","forCreate":false,"containsRestDay":false}
```
删除多余预估:`DELETE /projex/api/workitem/workitem/time/estimate/{workitemId}/{estimateId}`
列表:`GET …/time/estimate/list?workitemIdentifier=`
旧写法 `PATCH …/updateWorkitemFieldValue` 对上述自定义字段常 `400 不能为空`,勿再优先使用。
## 父子 / 子项 / 关联项(已通 · 2026-07-23 修订 · 子项优先)
### 关联项(【交付】强制 · ASSOCIATED
任务详情「关联项」只认 `ASSOCIATED`。**仅【交付】**建单时 `createWorkitemRelationInfo` 必须指向**需求**
```json
{
"createWorkitemRelationInfo": {
"relatedWorkitemIdentifier": "<需求id>",
"relatedToRelationIdentifier": "ASSOCIATED"
}
}
```
校验:
```http
GET /projex/api/workitem/v2/workitem/{id}/relation/workitem/list/by-relation-category?category=ASSOCIATED&isForward=true
```
`result` 含该需求即通过。
### 禁止(关联项)
| 写法 | 结果 |
|---|---|
| `relatedToRelationIdentifier=PARENT` 把交付挂需求 | 详情可能有 parent**关联项仍为空** |
| 分析/设计只用 `ASSOCIATED→需求` + `parentIdentifier` | 关联项可能有,**交付子项仍为空**ONEOS-246/247 |
| 建后再 `POST …/relation/record` 补关系 | Cookie 路径下常报「不能关联相同的工作项」 |
| `createWorkitemRelationList` | 不落 ASSOCIATED |
### 子项(【分析】/【设计】强制 · TASK_SUB
「子项」页读 `PARENT_SUB` / `TASK_SUB`,分析/设计**必须**
```json
{
"parent": "<交付id>",
"parentIdentifier": "<交付id>",
"createWorkitemRelationInfo": {
"relatedWorkitemIdentifier": "<交付id>",
"relatedToRelationIdentifier": "TASK_SUB"
}
}
```
同一 create 只能带一条 `createWorkitemRelationInfo`
`ASSOCIATED→需求``TASK_SUB→交付` **不能同时写**
**产品优先级交付「子项」tab > 阶段任务「关联项」。**
交付本身仍必须 `ASSOCIATED→需求`。标准路径下分析/设计的「关联项」允许为空。
**无单快轨例外(设计双挂):** create 用 `TASK_SUB→交付` 后,须再补 **ASSOCIATED→原始需求**,使设计详情「关联项」可见需求。
```http
POST /projex/api/workitem/workitem/{id}/relation/record?_input_charset=utf-8
{"relationIdentifier":"ASSOCIATED","toWorkitemIdentifier":"<id>"}
```
**已证伪Cookie · 2026-07-27** 凡工作项**已创建**后再 `relation/record` 补挂(含 ASSOCIATED / TASK_SUB常报「不能关联相同的工作项」与是否已有父项无关。`createWorkitemRelationList` 亦不落 ASSOCIATED。
**可行路径:**
| 目标 | 做法 |
|---|---|
| 交付「子项」可见设计(优先) | create 带 `TASK_SUB→交付` |
| 设计「关联项」可见需求 | create 带 `ASSOCIATED→需求`(与上互斥,同 create 只能一条) |
| 双挂 | 需个人 `x-yunxiao-token` OpenAPICookie 路径**不得**声称成功 |
产品默认:**子项优先**;关联项失败须在回报中标红并列出缺项。
校验子项:
```http
GET /projex/api/workitem/v2/workitem/{id}/relation/workitem/list/by-relation-category?category=PARENT_SUB&isForward=true
```
结果须含对应分析/设计 identifier。
校验设计关联项:
```http
GET /projex/api/workitem/v2/workitem/{id}/relation/workitem/list/by-relation-category?category=ASSOCIATED&isForward=true
```
`result` 须含原始需求 identifier。
### 无单快轨字段默认2026-07-27
| 对象 | 规则 |
|---|---|
| 【设计】描述 | 复制需求 document HTML |
| 【设计】`79`/`80` | 当日 `12:00:00` / `23:59:59`create 后 `field/value`(勿 create 同时带 79+80 |
| 【交付】描述 | 手工同步需求正文或原型→AutoPRD禁止无故占位 |
| 【交付】`79` | 创建当日;不写 `80` |
| 【交付】/【设计】标签 | 与需求相同,`PATCH propertyKey=tag` |
| 需求预计工时 | `time/estimate` **spentTime=2**(先删多余预估) |
| 需求实际工时 | `POST …/workitem/time`body 用 **`actualTime`**(非 spentTime+ `gmtStart`/`gmtEnd` epoch ms 字符串;见 `runtime-ids.json` `fields.actual_hours` |
| 描述更新 | `PATCH …/workitem/{id}/document``{"content":"<html>","formatType":"RICHTEXT"}` |
## 迭代挂接(已通 · 2026-07-27 · 只挂交付)
创建迭代:`POST /projex/api/workspace/sprint`(必填 `staffIds`;可写 `capacityHours`)。
挂【交付】到迭代:
```http
PATCH /projex/api/workitem/workitem/{id}?_input_charset=utf-8
{"workitemIdentifier":"{id}","propertyKey":"sprint","propertyValue":"{sprintId}","operateType":"COVER"}
```
清空误挂(如需求):`propertyValue:""` + `operateType:"COVER"`
**校验**必须读 `/extra`(详情主接口常不含 sprint 字段,禁止据此判失败):
```http
GET /projex/api/workitem/workitem/{id}/extra?_input_charset=utf-8
result.sprint[].identifier / name
```
产品规则:**只挂【交付】**;需求 / 分析 / 设计默认不挂(除非口令显式)。
### 极速建单注意
1. Cookie 只刷一次;全程纯 HTTP默认**不开浏览器**。
2. 交付建完后,【分析】与【设计】**并行**创建(均 TASK_SUB→交付标准/快轨两树可并行。
3. 状态用 `transit` + **本地追踪 fromStatus**(禁止每次 GET负责人在交棒场景下**创建时即何斐**。
4. 建单 `fieldValueList` 可带计划开始 `79`**不要**在 create 同时写 `79+80`(同日会 400
5. 标签必须 PATCHcreate 带 tag 不落库);可与建子任务重叠;快轨交付/设计须与需求同标签。
6. `requests.Session` keep-alive**禁止**对共享 opener 加全局锁。
7. 脚本入口:`scripts/live_create_fast.py`v5快轨描述/计划/标签/工时 2+2/设计 ASSOCIATED 补挂)。

View File

@@ -0,0 +1,59 @@
# 2026-07-23 真实建单复盘与极速优化
## 测试结论
标准 + 快轨均可建到「待开发」交棒;交付树、标签「故障管理」、快轨无【分析】均正确。
| 轮次 | 编号 |
|---|---|
| 第一轮(探测+UI | ONEOS-141147 |
| 极速 v1 | ONEOS-148154 |
| 极速 v2 | ONEOS-164177含中间探针 |
## 过程问题(已修)
| # | 问题 | 根因 | 修复 |
|---|---|---|---|
| 1 | 状态改不动 / 假成功 | 误用 `updateStatus`UI 点列表区 | `POST …/status/transit` |
| 2 | 负责人改不成 | `updateFieldValue` 错 | `PATCH …/{id}` + `assignedTo` |
| 3 | 状态 ID 不全 | 未抓网络 | 写入 `runtime-ids.json` |
| 4 | 极慢 | Playwright + 串行探测 | 纯 HTTP + 并行 |
| 5 | v1 仍偏慢 | 多余 GET、串行 PATCH 计划、交棒再改负责人 | 见下节 v2 |
## 性能对比
| 口径 | 优化前(第一轮) | 极速 v1 | 极速 v2本轮 |
|---|---|---|---|
| 会话墙钟 | ≈ **17.9 分钟** | — | — |
| 两路径并行建单墙钟 | ≈4.1 分钟自动化 | **4.4 s** | **2.2 s** |
| 标准单路径 | 失败反复 | 3.7 s | **2.2 s** |
| 快轨单路径 | 失败反复 | 3.7 s | **2.1 s** |
| 浏览器 | 多次 | 无 | 无 |
相对第一轮会话 ≈ **490×**;相对 v1 再快约 **2×**
## v2 压榨点(已落地)
1. `status/transit` **本地追踪** `fromStatus`,跳过每次 GET。
2. 建单 `fieldValueList` 带**计划开始(79)**;计划完成不在 create 写(同日会 400
3. 【交付】创建时负责人直接 **何斐**(省 PATCH
4. `requests.Session` keep-alive**去掉全局锁**(否则并行失效)。
5. 打标签与建子任务 **重叠**;两路径并行。
6. 交棒后汇总用创建响应,校验 GET 移出主路径计时。
## 探针结论(未采用)
| 尝试 | 结果 |
|---|---|
| create `fieldValueList` 带 tag | 建单成功但标签不落库 → 仍须 PATCH |
| create 同时写 79+80 | `400 计划…转化异常` |
| urllib 全局 lock + 单 opener | 并行被串行化,单路径 ≈4 s |
## 仍可再压(收益变小)
1. 标签 API 若将来支持 create 落库,可再少 24 次 PATCH。
2. HTTP/2 或连接预热DNS/TLS 复用到进程级)。
3. 业务允许「建单即待开发」且平台支持初始状态 → 少 2 次 transit。
4. 计划完成改为交棒后异步补写(当前故意跳过)。
脚本:`scripts/live_create_fast.py`mode=`live_create_fast_v2`)。

View File

@@ -0,0 +1,53 @@
# Make 导出与截图附件
设计完成(步骤 4且已关联原型页时需求与【交付】均挂附件。任一侧失败 → 不得声称设计完成附件齐全。
## 1. 导出 HTML含源码ZIP
与 Make「发布 → 导出 HTML含源码」对齐。
优先本地 Make Admin
```http
GET {adminOrigin}/api/export-html?path={prototypePath}&projectId={projectId}&includeSource=true
```
| 参数 | 要求 |
|---|---|
| `path` | 原型路径(如 `prototypes/oneos-h5-vehicle-assets` |
| `includeSource` | **必须** `true` |
| 响应 | ZIP`PK` 头);文件名建议 `{prototype-id}-html-source.zip` |
失败时:提示用户在 Make 客户端对该原型执行「发布 → 导出 HTML含源码把 ZIP 路径发回。
**禁止**用「仅对象存储链接」冒充已附带源码包。
## 2. 复制截图(全交互页)
1. Make「发布 → **复制截图**」(或等价:导出该原型主界面及所有交互页截图)。
2. 上传到需求附件与【交付】附件。
3. 缺页/失败 → 列出缺项,不得报成功。
## 3. 挂载范围
同一 ZIP / 同一批截图:
1. 上传到需求 identifier
2. 上传到【交付】任务 identifier
优先 API未知 upload 端点时用已登录浏览器在详情页「附件」上传。
## 4. 与对象存储的关系
| 产物 | 用途 |
|---|---|
| `{baseUrl}/{id}/index.html` | AutoPRD 描述内可点预览链接 |
| 导出 ZIP含源码 | 附件,供开发离线打开 |
| 交互页截图 | 附件,供评审/开发对照 |
三者职责不同,不可互相替代。
## 负向
- 不得导出错误原型页(须 Plan 确认原型路径/名称)。
- 不得把别的需求的旧 ZIP 复用到新需求。
- 快轨交棒默认不跑本附件流程(除非口令同时设计完成+原型)。

View File

@@ -0,0 +1,60 @@
# 交付树模型与关联约定
## 真相源
```text
需求状态 = 阶段看板唯一真相(分析中/设计中/待开发…)
【交付】任务 = 该需求的交付容器(每需求最多 1 条)
【分析】/【设计】 = YunxiaoPMapp 在交付下建的阶段子项
【开发】/【测试】 = 另属开发 Skill / 测试 Skill不进本包
ASSOCIATED = 横向挂钩(【交付】 ↔ 需求)
SUB / TASK_SUB = 纵向拆解(交付 → 分析/设计)
```
## 关系验收
| 关系 | 用在哪里 | 验收 | 优先级 |
|---|---|---|---|
| ASSOCIATED | **仅【交付】** ↔ 需求 | 交付详情「关联项」能看到需求(**强制** | 交付必过 |
| TASK_SUB | 【分析】/【设计】 → 【交付】 | 交付详情「子项」能看到阶段任务(**强制** | 阶段任务必过 |
建单规则Cookie 路径 · 同一 create 只能一条 `createWorkitemRelationInfo`
| 对象 | `createWorkitemRelationInfo` | 另写字段 |
|---|---|---|
| 【交付】 | `ASSOCIATED` → 需求 | — |
| 【分析】/【设计】 | `TASK_SUB` → 交付 | `parent` + `parentIdentifier` = 交付 |
**禁止**对分析/设计只写 `parentIdentifier` + `ASSOCIATED→需求`关联项可能有但交付「子项」仍为空ONEOS-246/247 已复现)。
勿用 `PARENT` 冒充关联项。细则见 [live-api.md](live-api.md)。
## 禁止
| 禁止 | 说明 |
|---|---|
| 创建或描述【开发】/【测试】 | 口令与回报均不出现 |
| 分析/设计未挂成交付子项(无 TASK_SUB | 交付「子项」为 0交棒验收失败 |
| 用任务名称/标题做唯一性或查重 | 不得 `subject` 匹配复用 |
| 同一需求认定多个交付任务编号 | 列出编号请人合并后再继续 |
| 功能模块标签用「分析/设计/交付」 | 模块标签打在需求(可选交付)上 |
## 任务标题(仅展示)
- 【交付】+ 需求标题
- 【分析】+ 需求标题
- 【设计】+ 需求标题
标题可改,**不参与查重**。
## 任务编号唯一性
**唯一允许的查重/复用渠道:云效任务编号**(如 `ONEOS-99` / `serialNumber`)。
| 场景 | 正确 | 错误 |
|---|---|---|
| 首次创建【交付】 | 建单成功后回报并写入「工作项编号(系统)」 | 下次用标题搜 |
| 复用【交付】 | 口令或编号区块带 `交付任务=ONEOS-xx` | list 后按 subject 相等 |
| 判断是否已有交付 | 编号区块或交付 ASSOCIATED 编号集合 | 数【交付】开头标题 |
| 分析/设计幂等 | 仅已登记编号 | 按【分析】+需求标题 |
按编号命中则复用;**计划开始已有值禁止改写**。

View File

@@ -0,0 +1,35 @@
# 编号直推到待开发
适用:标准路径已建过【分析】和/或【设计】,用**任务编号**一口气推到待开发交棒。
## 入口(只认编号)
```text
交棒开发:需求=ONEOS-xx交付任务=ONEOS-a分析任务=ONEOS-b设计任务=ONEOS-c
交棒开发:分析任务=ONEOS-b → 由分析 ASSOCIATED/SUB 反查需求与交付
交棒开发:设计任务=ONEOS-c → 同上反查
```
禁止用标题猜。编号对不上关联树 → 停下列出关联,不瞎改。
## 动作表
| 对象 | 动作 |
|---|---|
| 需求 | 状态 → **待开发**(按云效连跳) |
| 【交付】 | **必须**用交付任务编号定位;负责人→何斐;不改计划开始;不写计划完成;描述仍占位则保持(除非同时设计完成+原型+AutoPRD |
| 【分析】 | 若有编号:计划完成若为空 → 写当日并推工时;**不新建**第二条 |
| 【设计】 | 若有编号:计划完成若为空 → 写当日并推工时;**不新建**第二条 |
| 仅有分析、尚无设计 | **不强制补建【设计】**(与无单快轨区分);回报注明「无设计任务编号」 |
| 仅有设计、无分析 | 收口设计 + 交棒;**不补建分析** |
## 与无单快轨区别
| | 无单快轨 | 编号直推 |
|---|---|---|
| 前提 | 尚无分析/设计阶段单(或明确跳过分析) | 已有分析和/或设计**任务编号** |
| 【分析】 | 禁止创建 | 有编号则收口,无则不建 |
| 【设计】 | 必须新建且当日收口 | 有编号则收口;仅分析无设计时不强制新建 |
| 定位 | 新建后登记编号 | **仅任务编号**定位/反查 |
仍不创建【开发】/【测试】。交棒占位风险见 [handoff-and-rollback.md](handoff-and-rollback.md)。

View File

@@ -0,0 +1,73 @@
# 门禁 PJ · 云效项目选择(强制点选)
**新建需求 / 新建任务树 / 创建迭代 / 其它写入依赖 `spaceIdentifier`** 的操作,项目必须由用户从云效实时列表**点选**。禁止静默使用 `runtime-ids.json` 里的默认 `project.spaceIdentifier`
## 何时触发
| 场景 | 是否必须选项目 |
|---|---|
| 记录需求 / 无单快轨新建 | **必须** |
| 创建迭代(新挂项目空间) | **必须**(与需求同项目时沿用已锁定项,仍须在 Plan 写出项目名+ID |
| 仅推进已有编号(受理确认、开始分析…) | **不必重选**以该编号所在项目为准Plan 写出项目名 |
| 只读查询 | 不强制 |
## 执行顺序(建单前)
1. **实时拉取**云效项目列表Cookie + XSRF
```http
GET /projex/api/workspace/project/search/list
?category=Project&scope=all&toPage=1&pageSize=100
&conditions={"conditionGroups":[[]]}
&extraConditions= + public
&orderBy={"fieldIdentifier":"gmtCreate","format":"input","order":"desc","className":"date"}
&_input_charset=utf-8
```
2. 将结果整理为单选列表:`{name}{customCode}`;选项 id 用 `identifier`spaceId
3. **AskQuestion / Plan 单选**「PJ. 云效项目」;未点选 → **禁止 create**
4. 「批准 Plan」≠ 已选项目Plan 里仍是空选项或写「默认 01_ONEOS」而未点选 → 停。
5. 用户点选后:本轮 apply 全程使用该 `spaceIdentifier`;回报写清项目名 + customCode + spaceId。
6. 拉取成功后可回写 `assets/runtime-ids.json``projects_catalog`(缓存);**缓存不得当作已选项**。
## 无法对应 → 自动重拉一次
「无法对应」任一成立即触发:
| # | 情形 |
|---|---|
| 1 | 口令/预填的项目名、customCode、别名在**当前列表**中 0 命中 |
| 2 | 口令给出的 spaceId / identifier 不在当前列表 |
| 3 | 用户点选的选项 id 在 apply 前校验时已不在列表(列表过期) |
| 4 | 仅命中 `projects_catalog` 缓存、与本次实时列表不一致 |
| 5 | 首次拉取失败后改用了缓存,用户按缓存点选后 create 报项目/空间无效 |
**动作(固定一次):**
1. **立即再调一次**同一 `project/search/list`(强制网络,不用缓存)。
2. 用新列表重新做匹配 / 刷新 Plan 单选选项;可回写 `projects_catalog`
3. 回报注明:`已自动重拉项目列表(因无法对应)`
4. **仍无法对应** → 停,展示最新列表请用户重选;**禁止**再自动拉第 3 次;**禁止**猜一个 spaceId 继续建单。
匹配规则(名/码):忽略大小写;`name` 全等或包含;`customCode` 全等;`name_aliases` 全等。多命中视为无法唯一对应 → 重拉后仍多命中则请用户点选,不自动选定。
## 禁止
- 未询问就写入 `project.spaceIdentifier`(即便 catalog 标了 `suggested`
- 按口令里的「统一运营管理平台」字符串静默映射而不展示列表点选(口令仅作**预填建议**,仍须用户确认点选)
- API 失败时擅自沿用上次默认;应展示 `projects_catalog` 缓存并标明「离线缓存,请确认」,仍须点选;用户确认后若仍无法对应 → 走上一节「自动重拉一次」
- 多项目并行建单却共用一个未确认的 spaceId
- 「无法对应」时循环重拉超过 1 次,或跳过点选直接建单
## 口令预填
```text
记录需求:…;项目=01_ONEOS
```
若口令已含项目名/编号前缀且在实时列表中**唯一命中**Plan 可预勾该选项但仍须用户确认0 命中或多命中 → **先自动重拉一次**再匹配;仍 0/多 → 不预勾,只展示最新列表。
## 脚本
`scripts/list_projects.py`stdout JSON `projects[]`
带查询时:`python3 scripts/list_projects.py --match '01_ONEOS'` —— 0 命中/多命中则自动重拉一次再匹配(`refetched: true`)。

View File

@@ -0,0 +1,58 @@
# 记录需求 · 元字段(优先级 / 标签 / 提交部门 / 提交人)
建单前消费网页 / AutoRDO / 口令中的四类元信息。细则与 `assets/runtime-ids.json` 对齐;**禁止臆造未验证的 fieldIdentifier**。
## 口令形态
```text
记录需求:…;优先级=紧急|高|中|低;标签=…;提交部门=…;提交人=…;推进至=…
```
四字段均可选出现;网页或 AutoRDO 提示里已带的值与口令等价。
## Plan 是否追问
记录需求 **一律**走 [compact-select.md](compact-select.md) 字母表(类型/项目/优先级/标签),即使用户口令已写齐;给出建议压缩串,等用户回 `1a2b3a4d`(或改字母)并「执行」。
| 条件 | 行为 |
|---|---|
| 口令已含类型/项目/优先级/标签 | 对应选项标 ★ + 建议压缩串;**仍须**压缩确认或显式点选 |
| 标签名 0 命中 / 多命中 | **自动重拉一次**标签候选并重生 4.A/B/C…仍失败则停 |
| 仅缺提交部门/提交人 | 压缩 14 确认后,缺则追问或写入描述页脚 |
| 仅缺「推进至」等 | 按既有口令规则;可作 5. 题字母表 |
## 写入策略
### 优先级(已验证)
- 映射:`runtime-ids.json``priority`(紧急 / 高 / 中 / 低)。
- create 时写入 `fieldIdentifier: "priority"`(与 [live-api.md](live-api.md) / `live_create_fast.py` 一致)。
- 未给优先级时Plan 追问;脚本默认「中」仅作既有兜底,口令/网页有值时以用户值为准。
### 标签(已验证)
- 映射:`runtime-ids.json``tags`(按显示名取 tagId
- 标签须 **PATCH** `propertyKey: "tag"`create 带 tag 不落库)。
- 口令/网页给出的标签名若不在候选:按 [compact-select.md](compact-select.md) **自动重拉一次**标签列表并重生选项;仍无则停在 Plan勿猜 ID。
### 提交部门 / 提交人(已验证 · 2026-07-27 · ONEOS-293
| 字段 | fieldIdentifier |
|---|---|
| 提交部门 | `3132597a9718d1c282b7ba5a0c` |
| 提交人 | `9e01269e96f91fbb97d36bf5b3` |
| 规则 | 说明 |
|---|---|
| **写入** | `POST /projex/api/workitem/workitem/field/value/{workitemId}``Content-Type: application/x-www-form-urlencoded`,参数 `fieldValueList` = JSON 数组字符串 `[{"fieldIdentifier","value"}]` |
| **形态** | 均为普通文本 input非人员选择器口令值原样写入 |
| **页脚** | 仍可在描述页脚重复一份,便于检索;**不能替代**字段写入 |
| **计划开始** | 字段 `79`,同一 `field/value` API推荐 `YYYY-MM-DD 12:00:00`。快轨:【交付】/【设计】创建当日写入;【设计】另写计划完成 `80`=`当日 23:59:59` |
| **预计工时** | 字段 `101586` **禁止**直接 PATCH`POST …/time/estimate``spentTime`)登记;删多余用 `DELETE …/time/estimate/{workitemId}/{estimateId}` |
| **实际工时** | `POST …/workitem/time`body **`actualTime`**(非 spentTime+ `gmtStart`/`gmtEnd`epoch ms 字符串);快轨待开发需求默认 **2** |
| **描述更新** | `PATCH …/workitem/{id}/document``{"content","formatType":"RICHTEXT"}` |
| **快轨标签** | 【交付】【设计】建单后 `PATCH propertyKey=tag``propertyValue` 与需求 tagId 一致(多标签逗号拼接) |
## 与描述双段的关系
页脚元信息写在「原始诉求」段末或整篇描述末尾均可,**不得覆盖** `## 产品说明AutoPRD` / `## 工作项编号(系统)` 区块(见 [description-split.md](description-split.md))。

View File

@@ -0,0 +1,63 @@
# 创建迭代并关联交付
产品经理可将已交棒(或已有编号)的多条【交付】任务打进同一云效迭代。
## 口令
```text
创建迭代:版本类型=副;交付任务=ONEOS-a,ONEOS-b,ONEOS-c名称前缀=统一运营管理平台PC端
创建迭代:版本类型=子;交付任务=ONEOS-99名称前缀=统一运营管理平台PC端
```
| 参数 | 规则 |
|---|---|
| 交付任务 | **1..N 个任务编号**(逗号分隔);禁止用标题凑数 |
| 版本类型 | 主 / 副 / 子;未点选则停下询问,禁止默认猜 |
| 名称前缀 | 可选;缺省 `统一运营管理平台PC端`。最终名=`{前缀}{版本号}` |
## 版本号 `V{主}.{副}.{子}`
| 版本类型 | 含义 | 递增 |
|---|---|---|
| **主** | 功能重制 | 主+1副→0子→0V1.3.2→V2.0.0 |
| **副** | 新功能上线 | 副+1子→0V1.3.2→V1.4.0 |
| **子** | bug/优化 | 子+1V1.3.2→V1.3.3 |
### 取基线再递增
1. 拉取当前项目云效迭代列表。
2. 从迭代**名称**解析 `V数字.数字.数字`;旧名 `V数字.数字` 视为 `.0`
3. 同名称前缀内取**最大**版本为基线(主→副→子比较);避免 PC 与小程序串号。
4. 按用户点选类型递增,写入新迭代名称。
5. 无一可解析版本:基线 `V0.0.0` 再递增主→V1.0.0副→V0.1.0子→V0.0.1);或口令显式给起始版本。
禁止:手填与规则冲突的版本号却声称自动生成;禁止用交付标题推断版本类型。
## 创建与关联
| 步骤 | 动作 |
|---|---|
| 1 | 算出新版本 → 拼迭代全名 |
| 2 | 创建迭代(起止日期口令未给则询问或用项目默认,禁止瞎填) |
| 3 | **只挂【交付】**N 个交付任务挂迭代;交付失败不得报完成 |
| 4 | 幂等:同名迭代已存在 → 不新建,补挂尚未关联的交付,回报「复用迭代」 |
| 5 | 回报迭代名、版本号、identifier、已关联交付编号、失败编号 |
**禁止**把需求挂进迭代(需求侧「迭代」字段保持空)。不挂【分析】/【设计】除非口令显式点名。
**不**在本步创建【开发】/【测试】。仍须 Plan 门禁。
### 挂接 API已通 · 校验走 `/extra`
```http
PATCH /projex/api/workitem/workitem/{id}?_input_charset=utf-8
{"workitemIdentifier":"{id}","propertyKey":"sprint","propertyValue":"{sprintId}","operateType":"COVER"}
```
校验(详情主接口**不**含 sprint勿用其判空
```http
GET /projex/api/workitem/workitem/{id}/extra?_input_charset=utf-8
result.sprint[].identifier
```
清空误挂的需求迭代:同上 PATCH`propertyValue:""` + `operateType:"COVER"`(或 `CLEAR`)。

View File

@@ -0,0 +1,63 @@
# 标准路径:待处理 → 待开发交棒
人员/状态等常量见 [../assets/runtime-ids.json](../assets/runtime-ids.json)。**项目空间**须按 [project-selection.md](project-selection.md) 点选,禁止默认直指。查重只用任务编号。
## 步骤 0创建需求
| 云效动作 | 落地 |
|---|---|
| 新建产品类需求 | POST 建单;标题 `【新增】`/`【优化】` |
| 需求描述 | **先** AutoRDO 清洗再写入 `## 原始诉求AutoRDO`;本步不写 AutoPRD |
| 默认状态 | **待处理**;不建任务、不改负责人 |
| 可选推进 | 口令点选目标状态;未选则停在待处理 |
## 步骤 1受理 → 已确认
| 云效动作 | 落地 |
|---|---|
| 状态 | 待处理 → **已确认** |
| 任务 | **仍不建**交付/分析/设计 |
若用户只「创建」未受理:只提示「受理后请推进至已确认」,不自动跳。
## 步骤 2分析中
| 对象 | 动作 |
|---|---|
| 【交付】 | 无则新建;标题 `【交付】`+需求标题;**ASSOCIATED→需求**(勿用 PARENT负责人=创建人/产品;**计划开始仅空时写当日**;不写计划完成;描述=`等待设计任务完成后自动填入`;更新编号区块 |
| 【分析】 | 新建;**TASK_SUB→交付**`parent`+`parentIdentifier`+`createWorkitemRelationInfo=TASK_SUB`);交付「子项」须可见;计划开始=当日(仅空时);负责人默认可=创建人;更新编号区块 |
| 需求 | 状态=`分析中` |
## 步骤 3设计中
| 对象 | 动作 |
|---|---|
| 【交付】 | 已存在则按编号复用;没有则补建;**不改交付计划开始**;不写交付计划完成 |
| 【设计】 | 新建;**TASK_SUB→交付**(同分析);计划开始=当日(仅空时);更新编号区块 |
| 【分析】 | **计划完成**=状态更新日;推算阶段日历工时(见 work-hours |
| 需求 | 状态=`设计中` |
## 步骤 4设计完成
| 对象 | 动作 |
|---|---|
| 【设计】 | 计划完成=当日;推算工时;任务→完成态 |
| 需求 | AutoPRD 写入 `## 产品说明`;挂 ZIP+截图;状态=`设计完成` |
| 【交付】 | 不换负责人;不改计划开始;不写计划完成;描述改为 AutoPRD 正文;同样挂附件 |
顺序与失败门禁见 [description-split.md](description-split.md)。
## 步骤 5待开发交棒本 Skill 终点 · 标准路径)
| 对象 | 动作 |
|---|---|
| 需求 | 状态=`待开发` |
| 【交付】 | 按**任务编号**定位;**负责人→何斐**;不改计划开始;不写计划完成 |
| 【分析】/【设计】 | 不新建;不改已有计划开始;前序未收口按步骤 3/4 处理 |
执行 [handoff-and-rollback.md](handoff-and-rollback.md) §交棒门禁。
**不做:** 【开发】/【测试】、仓库、分支、提测。
快轨 / 编号直推见专文。创建迭代见 [sprint.md](sprint.md)。

View File

@@ -0,0 +1,73 @@
# 计划时间与阶段日历工时
适用于【交付】/【分析】/【设计】统一口径。
## 用途
| 对象 | 计划开始→计划完成 |
|---|---|
| 【交付】 | 需求开工→上线全周期(本 Skill **不**写交付计划完成;上线由后续段) |
| 【分析】 | 分析阶段时长 |
| 【设计】 | 设计阶段时长 |
## 计划开始(不可篡改)
| 对象 | 首次写入 | 之后 |
|---|---|---|
| 【交付】 | 首次进入分析中且字段为空 → 当日 | **禁止覆盖** |
| 【分析】 | 首次创建且为空 → 当日 | **禁止覆盖** |
| 【设计】 | 首次创建且为空 → 当日 | **禁止覆盖** |
日期写入形态:中国时区正午 epoch ms 字符串,见 [../assets/runtime-ids.json](../assets/runtime-ids.json) `fields.plan_start`
## 计划完成
| 环节 | 写计划完成的对象 |
|---|---|
| 推进到设计中 | 【分析】= 状态更新日 |
| 推进到设计完成 | 【设计】= 状态更新日 |
| 无单快轨建设计 | 【设计】= 当日(同轮收口) |
| 编号直推收口 | 口令涉及的分析/设计若计划完成为空 → 当日 |
| 上线/关闭 | 【交付】计划完成(**本 Skill 暂不写** |
## 预计工时 = 阶段日历工时Lead Time
有计划完成且已有计划开始时,写入该对象云效「预计工时」:
```text
workdays = 0
从计划开始日到计划完成日(含首尾)逐日:
周六或周日 → 跳过(除非该日是调休补班)
法定放假日 → 跳过
调休补班日(含周末上班)→ 计入
其余周一~周五 → 计入
预计工时(小时) = workdays × 8
```
**禁止**用「(结束日 开始日 + 1× 8」日历天数算法。
### 语义与脚注(强制)
- 正式名称:**阶段日历工时****不是**投入人天/排期负荷。
- 任务描述末尾固定脚注:
`【系统】预计工时=阶段日历工时工作日×8非人力投入预估`
- 回报写「阶段日历工时 Hh」**禁止**写「需投入 Hh」。
- 本 Skill **不自动写**「人力投入预估」。
### 日历数据源
- 使用 [../assets/cn-workday-calendar.json](../assets/cn-workday-calendar.json)(按自然年;跨年须覆盖起止两侧年份)。
- 缺当年日历 → **停下**提示补日历;**禁止**静默按「仅去周末」算完并报成功。
- 计算脚本:[../scripts/workday_hours.py](../scripts/workday_hours.py)
```bash
python3 scripts/workday_hours.py 2026-03-06 2026-03-09
```
缺计划开始则只写结束、不推工时,回报提示。
## 示例
- 开始=周五、结束=下周一(无节假日)→ 五+一 → 16h
- 区间全在法定放假内 → 0h
- 某周日为调休补班且落在区间内 → 该日计入 8h

View File

@@ -0,0 +1,22 @@
# 工作项编号(系统)
## 权威源
需求描述固定区块(机器可解析;人工只读即可):
```markdown
## 工作项编号(系统)
- 交付ONEOS-xx
- 分析ONEOS-yy无则写无
- 设计ONEOS-zz无则写无
```
## 规则
1. **禁止**仅靠本地会话映射当唯一真相。
2. 每次新建/登记交付·分析·设计编号后,**立即 PATCH 更新该区块**(只改本区块,不动 AutoRDO / AutoPRD 正文)。
3. 操作顺序:口令显式编号 > 读本区块 > ASSOCIATED/SUB 反查校验。
4. 三者冲突 → 停下人工;**仍禁止按标题猜**。
5. 本地缓存仅加速,启动以云效该区块为准。
6. 区块出现两个交付编号 → 停止并列号请人合并。
7. 取消需求:关联任务标取消/废止;**不删**本区块(留痕)。

View File

@@ -0,0 +1,221 @@
{
"schema_version": 1,
"skill": "YunxiaoPM",
"verified_at": "2026-07-25",
"note": "YunxiaoPM constants. Project space MUST be user-selected (PJ gate); project.spaceIdentifier is last-known cache only — never auto-apply. See references/project-selection.md.",
"project": {
"selection_mode": "user_pick_required",
"name": null,
"name_aliases": [
"01_ONEOS",
"ONEOS",
"统一运营管理平台",
"统一运营管理平台PC端",
"统一运营管理平台 PC 端"
],
"spaceIdentifier": null,
"customCode": null,
"spaceType": "Project",
"organizationIdentifier": "697c54a19df7fdfa65466405",
"default_sprint_name_prefix": "统一运营管理平台PC端",
"list_api": "GET /projex/api/workspace/project/search/list",
"last_selected": {
"name": "01_ONEOS",
"spaceIdentifier": "1280be963a5a2cc126a4118dca",
"customCode": "ONEOS",
"note": "历史常用项;仅作预填建议,禁止未点选即使用"
},
"refreshed_at": "2026-07-25"
},
"projects_catalog": [
{
"name": "05_羚牛碳资产平台",
"identifier": "ff104a3bce09463da136a97999",
"customCode": "CARBON"
},
{
"name": "06_对外客户项目",
"identifier": "baca8b4d676c5af67e475caec0",
"customCode": "CUST"
},
{
"name": "07_LNBOX",
"identifier": "35e1e915a052379bbdfb993aac",
"customCode": "LNBOX"
},
{
"name": "04_AI应用",
"identifier": "d9002ea72c6c3a1a97b02be750",
"customCode": "AIAPP"
},
{
"name": "03_数据中台",
"identifier": "a801cef5c9a68fa051c07432c7",
"customCode": "DATA"
},
{
"name": "02_小羚羚APP",
"identifier": "db771a2cca07bed43b369af077",
"customCode": "XLLAPP"
},
{
"name": "01_ONEOS",
"identifier": "1280be963a5a2cc126a4118dca",
"customCode": "ONEOS"
},
{
"name": "敏捷研发示例项目",
"identifier": "65eca0c2e16a23939081e19e14",
"customCode": "DEMO"
}
],
"people": {
"wangmian": {
"displayName": "王冕",
"identifier": "6811df000601d2fea60144a9",
"role": "产品创建人/默认负责人"
},
"hefei": {
"displayName": "何斐",
"identifier": "695f0400562f09713f9c3a93",
"role": "待开发交棒【交付】负责人"
}
},
"tags": {
"故障管理": "ceb526a7343995577645317e9a",
"还车应结款": "4204960ce658c94b17abb7c5f6",
"工作台": "b62d21beac55c0b43389915eab",
"交车管理": "b6947b5aca82a8c759612ba039",
"合同管理": "23873be81931cb0dc1bf87a456",
"安全培训": "76988b5f73ef515de5930d59b9",
"证照管理": "11f4c2ec65901a8f3199c81706",
"还车管理": "1e0fce2d6929ff4ac13b310e97"
},
"status": {
"req": {
"待处理": "100005",
"已确认": "32",
"分析中": "154395",
"设计中": "156603",
"设计完成": "307012",
"待开发": "1582fc929d429111b925309493"
},
"task": {
"待处理": "100005",
"已完成": "100014"
},
"transit_api": "POST /projex/api/workitem/workitem/{id}/status/transit",
"fast_handoff_hops": [
"设计完成",
"待开发"
],
"note": "见 references/live-api.md禁止再用 updateStatus"
},
"workitem_types": {
"product_req": {
"name": "产品类需求",
"category": "Req",
"identifier": "9uy29901re573f561d69jn40"
},
"task": {
"name": "任务",
"category": "Task",
"identifier": "ba102e46bc6a8483d9b7f25c"
}
},
"priority": {
"紧急": "646004e97f54bb77fec7b455df",
"高": "95b89e0a524d9693e1f335ffe5",
"中": "fa155d1214f9f8db222d39db3b",
"低": "92924feff9c1085891e7511872"
},
"fields": {
"plan_start": {
"fieldIdentifier": "79",
"name": "计划开始时间",
"value_shape": "YYYY-MM-DD HH:mm:ss China wall time preferred; epoch ms string also accepted",
"example": "2026-07-27 12:00:00",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON"
},
"plan_end": {
"fieldIdentifier": "80",
"name": "计划完成时间",
"value_shape": "YYYY-MM-DD HH:mm:ss or epoch ms string",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON"
},
"estimated_hours": {
"fieldIdentifier": "101586",
"name": "预计工时",
"note": "不可直接改字段;须工时预估登记",
"create_api": "POST /projex/api/workitem/workitem/time/estimate body spentTime,type,recordUserIdentifier,workitemIdentifier",
"delete_api": "DELETE /projex/api/workitem/workitem/time/estimate/{workitemId}/{estimateId}",
"list_api": "GET /projex/api/workitem/workitem/time/estimate/list?workitemIdentifier="
},
"actual_hours": {
"name": "实际工时",
"note": "快轨待开发默认 2",
"create_api": "POST /projex/api/workitem/workitem/time body actualTime,type,recordUserIdentifier,workitemIdentifier,gmtStart,gmtEnd (epoch ms string)",
"list_api": "GET /projex/api/workitem/workitem/time/list?workitemIdentifier=",
"fast_track_default": 2,
"verified_at": "2026-07-27",
"verified_on": "ONEOS-293"
},
"fast_track": {
"req_estimated_hours": 2,
"req_actual_hours": 2,
"design_plan_start_end_same_day": true,
"design_description": "copy_req_document",
"delivery_description": "manual_sync_or_autoprd",
"delivery_design_same_tags_as_req": true,
"design_associated_to_req_after_task_sub": true,
"document_update_api": "PATCH /projex/api/workitem/workitem/{id}/document {content,formatType:RICHTEXT}"
},
"submit_department": {
"fieldIdentifier": "3132597a9718d1c282b7ba5a0c",
"name": "提交部门",
"format": "string input",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON",
"verified_at": "2026-07-27",
"verified_on": "ONEOS-293"
},
"submitter": {
"fieldIdentifier": "9e01269e96f91fbb97d36bf5b3",
"name": "提交人",
"format": "string input",
"update_api": "POST /projex/api/workitem/workitem/field/value/{workitemId} form fieldValueList=JSON",
"verified_at": "2026-07-27",
"verified_on": "ONEOS-293"
},
"tag": {
"fieldIdentifier": "tag",
"name": "标签",
"propertyKey": "tag"
}
},
"assignee_rules": {
"待开发_交付任务": "hefei",
"分析中_交付与分析": "creator",
"设计中_设计": "creator"
},
"relation": {
"delivery_to_req": "ASSOCIATED",
"stage_to_delivery": "TASK_SUB",
"create_field": "createWorkitemRelationInfo",
"forbid": [
"PARENT_as_关联项",
"ASSOCIATED_only_for_stage_tasks_without_TASK_SUB"
],
"note": "交付必须 ASSOCIATED→需求。分析/设计默认 TASK_SUB→交付子项 tab。二者同 create 互斥。见 references/live-api.md"
},
"delivery_placeholder": "等待设计任务完成后自动填入",
"task_title_prefixes": {
"delivery": "【交付】",
"analysis": "【分析】",
"design": "【设计】"
},
"publish_url": {
"shape": "{baseUrl}/{prototype-id}/index.html",
"forbid_prototypes_prefix": true,
"require_index_html": true
}
}

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""拉取云效项目列表(供 YunxiaoPMapp 门禁 PJ 点选。stdout = JSON。
无法对应时自动重拉一次:
python3 scripts/list_projects.py --match '01_ONEOS'
python3 scripts/list_projects.py --match-id 1280be963a5a2cc126a4118dca
"""
from __future__ import annotations
import argparse
import json
import urllib.parse
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
import requests
ROOT = Path(__file__).resolve().parents[1]
RUNTIME = json.loads((ROOT / "assets" / "runtime-ids.json").read_text())
ORG = RUNTIME.get("project", {}).get("organizationIdentifier", "697c54a19df7fdfa65466405")
WANG = RUNTIME.get("people", {}).get("wangmian", {}).get("identifier", "6811df000601d2fea60144a9")
ALIASES = [a.lower() for a in (RUNTIME.get("project", {}).get("name_aliases") or [])]
TZ = timezone(timedelta(hours=8))
def load_jar() -> dict[str, str]:
jar: dict[str, str] = {}
try:
import browser_cookie3
for domain in (".aliyun.com", "devops.aliyun.com", ".devops.aliyun.com"):
try:
for c in browser_cookie3.chrome(domain_name=domain):
jar[c.name] = c.value
except Exception:
pass
except Exception:
pass
if not jar.get("XSRF-TOKEN"):
p = Path("/tmp/yunxiao_cookies.json")
if p.exists():
raw = json.loads(p.read_text())
jar = (
raw
if isinstance(raw, dict) and "XSRF-TOKEN" in raw
else {c["name"]: c["value"] for c in raw.get("cookies", [])}
)
if not jar.get("XSRF-TOKEN"):
raise RuntimeError("缺少 XSRF-TOKEN请先在 Chrome 登录 devops.aliyun.com")
return jar
def fetch_projects() -> list[dict[str, Any]]:
jar = load_jar()
x = jar.get("XSRF-TOKEN", "")
xsrf = urllib.parse.unquote(x) if "%" in x else x
cookie = "; ".join(f"{k}={v}" for k, v in jar.items())
headers = {
"Cookie": cookie,
"x-xsrf-token": xsrf,
"X-XSRF-TOKEN": xsrf,
"Origin": "https://devops.aliyun.com",
"Referer": "https://devops.aliyun.com/projex",
"accept": "application/json",
}
extra = json.dumps(
{
"conditionGroups": [
[
{
"className": "user",
"fieldIdentifier": "users",
"format": "multiList",
"operator": "CONTAINS",
"value": [WANG],
}
],
[
{
"className": "string",
"fieldIdentifier": "scope",
"format": "list",
"operator": "CONTAINS",
"value": ["public"],
}
],
]
},
separators=(",", ":"),
)
params = {
"extraConditions": extra,
"conditions": json.dumps({"conditionGroups": [[]]}, separators=(",", ":")),
"orderBy": json.dumps(
{
"fieldIdentifier": "gmtCreate",
"format": "input",
"order": "desc",
"className": "date",
},
separators=(",", ":"),
),
"scope": "all",
"category": "Project",
"toPage": 1,
"pageSize": 100,
"_input_charset": "utf-8",
}
r = requests.get(
"https://devops.aliyun.com/projex/api/workspace/project/search/list",
headers=headers,
params=params,
timeout=30,
)
r.raise_for_status()
rows = r.json().get("result") or []
return [
{
"name": p.get("name"),
"identifier": p.get("identifier"),
"customCode": p.get("customCode"),
"logicalStatus": p.get("logicalStatus"),
"status": (p.get("status") or {}).get("displayName"),
"scope": p.get("scope"),
}
for p in rows
]
def match_projects(
projects: list[dict[str, Any]],
*,
query: str | None = None,
space_id: str | None = None,
) -> list[dict[str, Any]]:
if space_id:
sid = space_id.strip()
return [p for p in projects if p.get("identifier") == sid]
if not query:
return []
q = query.strip().lower()
hits: list[dict[str, Any]] = []
for p in projects:
name = (p.get("name") or "").lower()
code = (p.get("customCode") or "").lower()
if q == name or q == code or q in name or name in q:
hits.append(p)
continue
if q in ALIASES and (code == "oneos" or "oneos" in name or "运营" in name):
hits.append(p)
# de-dupe by identifier
seen: set[str] = set()
out: list[dict[str, Any]] = []
for p in hits:
i = p.get("identifier") or ""
if i in seen:
continue
seen.add(i)
out.append(p)
return out
def list_projects(
*,
match: str | None = None,
match_id: str | None = None,
) -> dict:
projects = fetch_projects()
payload: dict[str, Any] = {
"fetched_at": datetime.now(TZ).isoformat(),
"organizationIdentifier": ORG,
"count": len(projects),
"projects": projects,
"selection_required": True,
"refetched": False,
"note": "禁止默认直指;须 AskQuestion/Plan 点选后再写入 spaceIdentifier",
}
if not match and not match_id:
return payload
hits = match_projects(projects, query=match, space_id=match_id)
if len(hits) == 1:
payload["matches"] = hits
payload["match_status"] = "unique"
return payload
# 无法对应0 或多)→ 自动重拉一次
projects2 = fetch_projects()
hits2 = match_projects(projects2, query=match, space_id=match_id)
payload["projects"] = projects2
payload["count"] = len(projects2)
payload["refetched"] = True
payload["refetch_reason"] = "无法对应" if len(hits) != 1 else "unexpected"
if len(hits) == 0:
payload["refetch_reason"] = "首次0命中"
elif len(hits) > 1:
payload["refetch_reason"] = "首次多命中"
payload["matches"] = hits2
if len(hits2) == 1:
payload["match_status"] = "unique_after_refetch"
elif len(hits2) == 0:
payload["match_status"] = "none_after_refetch"
payload["note"] = "已自动重拉一次仍无法对应请用户从最新列表点选禁止再拉第3次"
else:
payload["match_status"] = "ambiguous_after_refetch"
payload["note"] = "已自动重拉一次仍多命中;请用户点选;禁止自动选定"
payload["fetched_at"] = datetime.now(TZ).isoformat()
return payload
def main() -> None:
ap = argparse.ArgumentParser(description="YunxiaoPMapp 项目列表 / 匹配(无法对应则重拉一次)")
ap.add_argument("--match", help="按项目名或 customCode 匹配")
ap.add_argument("--match-id", help="按 spaceIdentifier 匹配")
args = ap.parse_args()
print(
json.dumps(
list_projects(match=args.match, match_id=args.match_id),
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""标签候选列表(聚合工作项 tag + runtime未命中可 --match 后自动重拉一次。"""
from __future__ import annotations
import argparse
import json
import urllib.parse
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
import requests
ROOT = Path(__file__).resolve().parents[1]
RUNTIME = json.loads((ROOT / "assets" / "runtime-ids.json").read_text())
TZ = timezone(timedelta(hours=8))
def load_jar() -> dict[str, str]:
jar: dict[str, str] = {}
try:
import browser_cookie3
for domain in (".aliyun.com", "devops.aliyun.com", ".devops.aliyun.com"):
try:
for c in browser_cookie3.chrome(domain_name=domain):
jar[c.name] = c.value
except Exception:
pass
except Exception:
pass
if not jar.get("XSRF-TOKEN"):
p = Path("/tmp/yunxiao_cookies.json")
if p.exists():
raw = json.loads(p.read_text())
jar = (
raw
if isinstance(raw, dict) and "XSRF-TOKEN" in raw
else {c["name"]: c["value"] for c in raw.get("cookies", [])}
)
if not jar.get("XSRF-TOKEN"):
raise RuntimeError("缺少 XSRF-TOKEN")
return jar
def session() -> requests.Session:
jar = load_jar()
x = jar.get("XSRF-TOKEN", "")
xsrf = urllib.parse.unquote(x) if "%" in x else x
s = requests.Session()
s.headers.update(
{
"Cookie": "; ".join(f"{k}={v}" for k, v in jar.items()),
"x-xsrf-token": xsrf,
"X-XSRF-TOKEN": xsrf,
"Origin": "https://devops.aliyun.com",
"Referer": "https://devops.aliyun.com/projex",
"accept": "application/json",
"Content-Type": "application/json",
}
)
return s
def fetch_tags(space_id: str) -> list[dict[str, str]]:
s = session()
by_id: dict[str, str] = {}
for name, tid in (RUNTIME.get("tags") or {}).items():
by_id[tid] = name
body = {
"spaceIdentifier": space_id,
"spaceType": "Project",
"category": "Req",
"toPage": 1,
"pageSize": 100,
"conditions": json.dumps({"conditionGroups": [[]]}),
}
rows = (
s.post(
"https://devops.aliyun.com/projex/api/workitem/workitem/list?_input_charset=utf-8",
json=body,
timeout=30,
)
.json()
.get("result")
or []
)
for row in rows:
for t in row.get("tag") or []:
tid = t.get("identifier")
name = t.get("name") or t.get("displayName")
if tid and name:
by_id[tid] = name
# Task 再扫一页,补标签
body["category"] = "Task"
rows = (
s.post(
"https://devops.aliyun.com/projex/api/workitem/workitem/list?_input_charset=utf-8",
json=body,
timeout=30,
)
.json()
.get("result")
or []
)
for row in rows:
for t in row.get("tag") or []:
tid = t.get("identifier")
name = t.get("name") or t.get("displayName")
if tid and name:
by_id[tid] = name
return [{"name": n, "identifier": i} for i, n in sorted(by_id.items(), key=lambda x: x[1])]
def match_tags(tags: list[dict[str, str]], query: str) -> list[dict[str, str]]:
q = query.strip().lower()
hits = []
for t in tags:
name = (t.get("name") or "").lower()
if q == name or q in name or name in q:
hits.append(t)
return hits
def list_tags(*, space_id: str, match: str | None = None, prefer: str | None = None) -> dict[str, Any]:
tags = fetch_tags(space_id)
# prefer 置顶
if prefer:
prefer_l = prefer.strip().lower()
tags = sorted(
tags,
key=lambda t: (0 if (t.get("name") or "").lower() == prefer_l else 1, t.get("name") or ""),
)
out: dict[str, Any] = {
"fetched_at": datetime.now(TZ).isoformat(),
"spaceIdentifier": space_id,
"count": len(tags),
"tags": tags,
"refetched": False,
"note": "标签未命中须自动重拉一次并重生 4.A/B/C 选项;见 compact-select.md",
}
if not match:
return out
hits = match_tags(tags, match)
if len(hits) == 1:
out["matches"] = hits
out["match_status"] = "unique"
return out
# 无法对应 → 重拉一次
tags2 = fetch_tags(space_id)
if prefer:
prefer_l = prefer.strip().lower()
tags2 = sorted(
tags2,
key=lambda t: (0 if (t.get("name") or "").lower() == prefer_l else 1, t.get("name") or ""),
)
hits2 = match_tags(tags2, match)
out["tags"] = tags2
out["count"] = len(tags2)
out["refetched"] = True
out["refetch_reason"] = "首次0命中" if len(hits) == 0 else "首次多命中"
out["matches"] = hits2
if len(hits2) == 1:
out["match_status"] = "unique_after_refetch"
elif len(hits2) == 0:
out["match_status"] = "none_after_refetch"
out["note"] = "已自动重拉标签列表仍无法对应;请用户点选 4.x禁止再拉第3次"
else:
out["match_status"] = "ambiguous_after_refetch"
out["fetched_at"] = datetime.now(TZ).isoformat()
return out
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--space", required=True, help="spaceIdentifier")
ap.add_argument("--match", help="标签名")
ap.add_argument("--prefer", help="置顶标签名")
args = ap.parse_args()
print(
json.dumps(
list_tags(space_id=args.space, match=args.match, prefer=args.prefer),
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,660 @@
#!/usr/bin/env python3
"""YunxiaoPM 极速真实建单 v5快轨描述/计划/标签/工时 2+2/设计 ASSOCIATED 补挂。"""
from __future__ import annotations
import json
import time
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
try:
import browser_cookie3
except ImportError: # pragma: no cover
browser_cookie3 = None
import requests
ROOT = Path(__file__).resolve().parents[1]
RUNTIME = json.loads((ROOT / "assets" / "runtime-ids.json").read_text())
STATUS = RUNTIME["status"]["req"]
TASK_STATUS = RUNTIME["status"]["task"]
FAST = RUNTIME.get("fields", {}).get("fast_track") or {}
SPACE = (
RUNTIME.get("project", {}).get("spaceIdentifier")
or (RUNTIME.get("project", {}).get("last_selected") or {}).get("spaceIdentifier")
)
if not SPACE:
raise RuntimeError(
"未选定项目 spaceIdentifier须先走门禁 PJ 点选,或设置 runtime project.last_selected"
)
REQ_TYPE = RUNTIME["workitem_types"]["product_req"]["identifier"]
TASK_TYPE = RUNTIME["workitem_types"]["task"]["identifier"]
HEFEI = RUNTIME["people"]["hefei"]["identifier"]
WANG = RUNTIME["people"].get("wangmian", {}).get("identifier", "6811df000601d2fea60144a9")
PRI = RUNTIME["priority"][""]
TAG_FAULT = RUNTIME.get("tags", {}).get("故障管理", "ceb526a7343995577645317e9a")
PLACEHOLDER = RUNTIME["delivery_placeholder"]
PROTO = "https://prototype.lnoneos.com/vehicle-fault-handling/index.html"
TZ = timezone(timedelta(hours=8))
TODAY = datetime.now(TZ).strftime("%Y-%m-%d")
NOON = f"{TODAY} 12:00:00"
EOD = f"{TODAY} 23:59:59"
NOON_MS = str(
int(datetime.now(TZ).replace(hour=12, minute=0, second=0, microsecond=0).timestamp() * 1000)
)
START_MS = str(
int(datetime.now(TZ).replace(hour=9, minute=0, second=0, microsecond=0).timestamp() * 1000)
)
END_MS = str(
int(datetime.now(TZ).replace(hour=11, minute=0, second=0, microsecond=0).timestamp() * 1000)
)
FAST_EST = int(FAST.get("req_estimated_hours", 2))
FAST_ACT = int(FAST.get("req_actual_hours", 2))
PENDING = STATUS["待处理"]
DESIGN_DONE = STATUS["设计完成"]
PENDING_DEV = STATUS["待开发"]
TASK_DONE = TASK_STATUS["已完成"]
_COOKIE = ""
_XSRF = ""
def load_auth() -> None:
global _COOKIE, _XSRF
jar: dict[str, str] = {}
if browser_cookie3:
for domain in (".aliyun.com", "devops.aliyun.com", ".devops.aliyun.com"):
try:
for c in browser_cookie3.chrome(domain_name=domain):
jar[c.name] = c.value
except Exception:
pass
if not jar:
p = Path("/tmp/yunxiao_cookies.json")
if p.exists():
raw = json.loads(p.read_text())
jar = (
raw
if isinstance(raw, dict) and "XSRF-TOKEN" in raw
else {c["name"]: c["value"] for c in raw.get("cookies", [])}
)
_COOKIE = "; ".join(f"{k}={v}" for k, v in jar.items())
x = jar.get("XSRF-TOKEN", "")
_XSRF = urllib.parse.unquote(x) if "%" in x else x
if not _XSRF:
raise RuntimeError("缺少 XSRF-TOKEN请先在 Chrome 登录 devops.aliyun.com")
def session() -> requests.Session:
s = requests.Session()
s.headers.update(
{
"Content-Type": "application/json",
"Cookie": _COOKIE,
"x-xsrf-token": _XSRF,
"X-XSRF-TOKEN": _XSRF,
"Origin": "https://devops.aliyun.com",
"Referer": f"https://devops.aliyun.com/projex/project/{SPACE}/req",
"accept": "application/json",
"User-Agent": "YunxiaoPM-live_create_fast/5.0",
"Connection": "keep-alive",
}
)
return s
def api(s: requests.Session, method: str, url: str, body: Any = None) -> dict:
r = s.request(method, url, json=body, timeout=60)
try:
return r.json()
except Exception:
r.raise_for_status()
raise
def create(s: requests.Session, payload: dict) -> dict:
url = "https://devops.aliyun.com/projex/api/workitem/workitem?_input_charset=utf-8"
j = api(s, "POST", url, payload)
r = j.get("result") or {}
if j.get("code") == 200 and isinstance(r, dict) and r.get("identifier"):
return r
j = api(s, "PUT", url, payload)
r = j.get("result") or {}
if j.get("code") == 200 and isinstance(r, dict) and r.get("identifier"):
return r
raise RuntimeError(f"create failed: {j}")
def get(s: requests.Session, wid: str) -> dict:
return api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}?_input_charset=utf-8",
)["result"]
def apply_tag(s: requests.Session, wid: str, tag_id: str = TAG_FAULT) -> None:
j = api(
s,
"PATCH",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}?_input_charset=utf-8",
{
"workitemIdentifier": wid,
"propertyKey": "tag",
"propertyValue": tag_id,
"operateType": "COVER",
},
)
if j.get("code") != 200:
raise RuntimeError(f"tag failed {wid}: {j}")
def set_document(s: requests.Session, wid: str, html: str) -> None:
j = api(
s,
"PATCH",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}/document?_input_charset=utf-8",
{"content": html, "formatType": "RICHTEXT"},
)
if j.get("code") != 200 or j.get("errorMsg"):
raise RuntimeError(f"document failed {wid}: {j}")
def set_fields(s: requests.Session, wid: str, pairs: list[tuple[str, str]]) -> None:
data = urllib.parse.urlencode(
{
"fieldValueList": json.dumps(
[{"fieldIdentifier": k, "value": v} for k, v in pairs], ensure_ascii=False
)
}
)
r = s.post(
f"https://devops.aliyun.com/projex/api/workitem/workitem/field/value/{wid}?_input_charset=utf-8",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=60,
)
j = r.json()
if j.get("code") != 200:
raise RuntimeError(f"field/value failed {wid}: {j}")
def set_estimate_hours(s: requests.Session, wid: str, hours: int, user: str = WANG) -> None:
est = api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/workitem/time/estimate/list?workitemIdentifier={wid}",
).get("result") or []
for row in est:
eid = row.get("identifier")
if eid:
api(
s,
"DELETE",
f"https://devops.aliyun.com/projex/api/workitem/workitem/time/estimate/{wid}/{eid}",
)
j = api(
s,
"POST",
"https://devops.aliyun.com/projex/api/workitem/workitem/time/estimate?_input_charset=utf-8",
{
"workitemIdentifier": wid,
"spentTime": hours,
"type": "develop",
"description": "快轨默认预计工时",
"recordUserIdentifier": user,
"forCreate": False,
"containsRestDay": False,
},
)
if j.get("code") != 200:
raise RuntimeError(f"estimate failed {wid}: {j}")
def set_actual_hours(s: requests.Session, wid: str, hours: int, user: str = WANG) -> None:
j = api(
s,
"POST",
"https://devops.aliyun.com/projex/api/workitem/workitem/time?_input_charset=utf-8",
{
"workitemIdentifier": wid,
"actualTime": hours,
"type": "develop",
"description": "快轨默认实际工时",
"recordUserIdentifier": user,
"gmtStart": START_MS,
"gmtEnd": END_MS,
},
)
if j.get("code") != 200 or not j.get("result"):
raise RuntimeError(f"actual time failed {wid}: {j}")
def try_associate_to_req(s: requests.Session, stage_id: str, req_id: str) -> bool:
"""建后补 ASSOCIATED→需求。Cookie 下常失败,成功返回 True。"""
bodies = [
{"relationIdentifier": "ASSOCIATED", "toWorkitemIdentifier": req_id},
{
"relationIdentifier": "ASSOCIATED",
"fromWorkitemIdentifier": stage_id,
"toWorkitemIdentifier": req_id,
},
]
for body in bodies:
for url in (
f"https://devops.aliyun.com/projex/api/workitem/workitem/{stage_id}/relation/record?_input_charset=utf-8",
f"https://devops.aliyun.com/projex/api/workitem/v2/workitem/{stage_id}/relation/record?_input_charset=utf-8",
):
j = api(s, "POST", url, body)
if j.get("code") == 200 and j.get("result") not in (None, False):
if not (isinstance(j.get("result"), dict) and j["result"].get("status") in (404, 405)):
rows = list_associated(s, stage_id)
if req_id in {r.get("identifier") for r in rows}:
return True
return False
def transit(s: requests.Session, wid: str, from_status: str, to_status: str) -> None:
if from_status == to_status:
return
j = api(
s,
"POST",
f"https://devops.aliyun.com/projex/api/workitem/workitem/{wid}/status/transit?_input_charset=utf-8",
{"fromStatus": from_status, "toStatus": to_status},
)
if not (j.get("code") == 200 and j.get("result") is True):
raise RuntimeError(f"transit {wid} {from_status}->{to_status}: {j}")
def md_to_html(md: str) -> str:
parts = []
for line in md.splitlines():
if line.startswith("## "):
parts.append(f"<h2>{line[3:]}</h2>")
elif line.startswith("### "):
parts.append(f"<h3>{line[4:]}</h3>")
elif line.startswith("- "):
parts.append(f"<p>• {line[2:]}</p>")
elif line.strip():
parts.append(f"<p>{line}</p>")
return "".join(parts)
def req_document_html(s: requests.Session, rid: str) -> str:
w = get(s, rid)
return ((w.get("document") or {}).get("content") or w.get("description") or "").strip()
def req_payload(subject: str, html: str) -> dict:
return {
"subject": subject,
"description": html,
"formatType": "RICHTEXT",
"document": {"content": html, "formatType": "RICHTEXT"},
"spaceIdentifier": SPACE,
"space": SPACE,
"spaceType": "Project",
"workitemTypeIdentifier": REQ_TYPE,
"workitemType": REQ_TYPE,
"categoryIdentifier": "Req",
"category": "Req",
"assignedTo": WANG,
"fieldValueList": [
{"fieldIdentifier": "priority", "value": PRI},
{"fieldIdentifier": "assignedTo", "value": WANG},
],
"attachmentIdList": [],
"cloneFrom": None,
"createWorkitemRelationList": [],
}
def task_payload(
subject: str,
html: str,
assignee: str,
*,
plan_start: bool = True,
associated_req: str | None = None,
parent_delivery: str | None = None,
) -> dict:
fvl = [
{"fieldIdentifier": "priority", "value": PRI},
{"fieldIdentifier": "assignedTo", "value": assignee},
]
if plan_start:
fvl.append({"fieldIdentifier": "79", "value": NOON_MS})
payload: dict[str, Any] = {
"subject": subject,
"description": html,
"formatType": "RICHTEXT",
"spaceIdentifier": SPACE,
"space": SPACE,
"spaceType": "Project",
"workitemTypeIdentifier": TASK_TYPE,
"workitemType": TASK_TYPE,
"categoryIdentifier": "Task",
"category": "Task",
"assignedTo": assignee,
"fieldValueList": fvl,
"attachmentIdList": [],
"cloneFrom": None,
}
if parent_delivery:
payload["parent"] = parent_delivery
payload["parentIdentifier"] = parent_delivery
payload["createWorkitemRelationInfo"] = {
"relatedWorkitemIdentifier": parent_delivery,
"relatedToRelationIdentifier": "TASK_SUB",
}
else:
if not associated_req:
raise ValueError("associated_req required for delivery: ASSOCIATED→需求")
payload["createWorkitemRelationInfo"] = {
"relatedWorkitemIdentifier": associated_req,
"relatedToRelationIdentifier": "ASSOCIATED",
}
return payload
def list_associated(s: requests.Session, wid: str) -> list[dict]:
j = api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/v2/workitem/{wid}/relation/workitem/list/by-relation-category?category=ASSOCIATED&isForward=true",
)
return j.get("result") or []
def assert_associated_to_req(s: requests.Session, wid: str, req_id: str, label: str) -> None:
rows = list_associated(s, wid)
ids = {r.get("identifier") for r in rows}
if req_id not in ids:
raise RuntimeError(
f"{label} 关联项未挂需求:期望 {req_id},实际 {[r.get('serialNumber') for r in rows]}"
)
AUTO_RDO = """## 原始诉求AutoRDO
运维需在故障处置页承接机器人上报的故障,完成处置、挂起与归档,并保留证据链;工作台相关统计口径需与处置页一致
待确认:
- 本期是否含真实短信/邮件通道(现口径一般为演示模板)
## 工作项编号(系统)
- 交付:待建
- 分析:待建
- 设计:待建
"""
def summarize_from_create(w: dict, *, status: str, assignee_name: str) -> dict:
return {
"serial": w.get("serialNumber"),
"id": w.get("identifier"),
"subject": w.get("subject"),
"status": status,
"assignee": assignee_name,
"parent": w.get("parentIdentifier"),
}
def build_normal() -> dict:
t0 = time.perf_counter()
s = session()
title = "【新增】故障处置YunxiaoPMapp标准·极速v2"
req = create(s, req_payload(title, md_to_html(AUTO_RDO + f"\n原型:{PROTO}\n")))
rid = req["identifier"]
with ThreadPoolExecutor(max_workers=2) as pool:
f_tag_r = pool.submit(apply_tag, session(), rid)
f_deliv = pool.submit(
create,
session(),
task_payload(
f"【交付】{title}",
f"<p>{PLACEHOLDER}</p>",
HEFEI,
associated_req=rid,
),
)
deliv = f_deliv.result()
f_tag_r.result()
did = deliv["identifier"]
with ThreadPoolExecutor(max_workers=3) as pool:
f_tag_d = pool.submit(apply_tag, session(), did)
f_ana = pool.submit(
create,
session(),
task_payload(
f"【分析】{title}",
"<p>分析阶段:故障处置台账、挂起归档与证据链。</p>",
WANG,
associated_req=rid,
parent_delivery=did,
),
)
f_des = pool.submit(
create,
session(),
task_payload(
f"【设计】{title}",
f"<p>设计阶段:对齐原型 {PROTO}</p>",
WANG,
associated_req=rid,
parent_delivery=did,
),
)
ana = f_ana.result()
des = f_des.result()
f_tag_d.result()
with ThreadPoolExecutor(max_workers=3) as pool:
list(
as_completed(
[
pool.submit(transit, session(), rid, PENDING, DESIGN_DONE),
pool.submit(transit, session(), ana["identifier"], PENDING, TASK_DONE),
pool.submit(transit, session(), des["identifier"], PENDING, TASK_DONE),
]
)
)
transit(s, rid, DESIGN_DONE, PENDING_DEV)
return {
"path": "normal",
"elapsed_s": round(time.perf_counter() - t0, 3),
"req": summarize_from_create(req, status="待开发", assignee_name="王冕"),
"delivery": summarize_from_create(deliv, status="待处理", assignee_name="何斐"),
"analysis": summarize_from_create(ana, status="已完成", assignee_name="王冕"),
"design": summarize_from_create(des, status="已完成", assignee_name="王冕"),
}
def build_fast(
*,
tag_id: str = TAG_FAULT,
has_prototype: bool = True,
delivery_html: str | None = None,
) -> dict:
"""无单快轨。
- 设计描述 = 需求 document
- 交付描述 = 手工同步需求正文(无原型)或传入 AutoPRD HTML禁止无故占位
- 设计 79/80=当日;交付 79=当日
- 需求/交付/设计同标签
- 需求预计/实际工时各 2
- 设计 TASK_SUB→交付后尝试 ASSOCIATED→需求
"""
t0 = time.perf_counter()
s = session()
title = "【新增】故障处置YunxiaoPM快轨·极速v5"
req_html = md_to_html(AUTO_RDO + (f"\n原型:{PROTO}\n" if has_prototype else "\n"))
req = create(s, req_payload(title, req_html))
rid = req["identifier"]
# 以落库 document 为准(与手动建需后读需求一致)
req_html = req_document_html(s, rid) or req_html
if delivery_html:
deliv_html = delivery_html
desc_source = "autoprd"
elif req_html.strip():
deliv_html = req_html
desc_source = "manual_sync"
else:
deliv_html = f"<p>{PLACEHOLDER}</p>"
desc_source = "placeholder"
with ThreadPoolExecutor(max_workers=2) as pool:
f_tag_r = pool.submit(apply_tag, session(), rid, tag_id)
f_deliv = pool.submit(
create,
session(),
task_payload(
f"【交付】{title}",
deliv_html,
HEFEI,
associated_req=rid,
),
)
deliv = f_deliv.result()
f_tag_r.result()
did = deliv["identifier"]
with ThreadPoolExecutor(max_workers=2) as pool:
f_tag_d = pool.submit(apply_tag, session(), did, tag_id)
f_des = pool.submit(
create,
session(),
task_payload(
f"【设计】{title}",
req_html,
WANG,
parent_delivery=did,
),
)
des = f_des.result()
f_tag_d.result()
des_id = des["identifier"]
apply_tag(s, des_id, tag_id)
# 计划时间设计起止当日交付开始当日create 已带 79再 field/value 加固)
set_fields(s, des_id, [("79", NOON), ("80", EOD)])
set_fields(s, did, [("79", NOON)])
# 设计关联项补挂需求(可能失败,回报 risk
design_assoc_ok = try_associate_to_req(s, des_id, rid)
with ThreadPoolExecutor(max_workers=2) as pool:
list(
as_completed(
[
pool.submit(transit, session(), rid, PENDING, DESIGN_DONE),
pool.submit(transit, session(), des_id, PENDING, TASK_DONE),
]
)
)
transit(s, rid, DESIGN_DONE, PENDING_DEV)
set_estimate_hours(s, rid, FAST_EST)
set_actual_hours(s, rid, FAST_ACT)
risk = None
if desc_source == "placeholder":
risk = "交付描述仍为占位"
if not design_assoc_ok:
risk = (risk + "" if risk else "") + "设计 ASSOCIATED→需求补挂失败Cookie须 UI/OpenAPI 兜底"
return {
"path": "fast",
"elapsed_s": round(time.perf_counter() - t0, 3),
"req": summarize_from_create(req, status="待开发", assignee_name="王冕"),
"delivery": summarize_from_create(deliv, status="待处理", assignee_name="何斐"),
"design": summarize_from_create(des, status="已完成", assignee_name="王冕"),
"analysis": None,
"delivery_desc_source": desc_source,
"design_associated_ok": design_assoc_ok,
"risk": risk,
}
def main() -> None:
auth0 = time.perf_counter()
load_auth()
auth_s = round(time.perf_counter() - auth0, 3)
wall0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as pool:
f_fast = pool.submit(build_fast)
f_normal = pool.submit(build_normal)
fast = f_fast.result()
normal = f_normal.result()
build_s = round(time.perf_counter() - wall0, 3)
s = session()
assert_associated_to_req(s, normal["delivery"]["id"], normal["req"]["id"], "normal.delivery")
assert_associated_to_req(s, fast["delivery"]["id"], fast["req"]["id"], "fast.delivery")
def assert_sub(delivery_id: str, child_id: str, label: str) -> None:
rows = api(
s,
"GET",
f"https://devops.aliyun.com/projex/api/workitem/v2/workitem/{delivery_id}/relation/workitem/list/by-relation-category?category=PARENT_SUB&isForward=true",
).get("result") or []
ids = {r.get("identifier") for r in rows}
if child_id not in ids:
raise RuntimeError(
f"{label} 未出现在交付子项:期望 {child_id},实际 {[r.get('serialNumber') for r in rows]}"
)
assert_sub(normal["delivery"]["id"], normal["analysis"]["id"], "normal.analysis")
assert_sub(normal["delivery"]["id"], normal["design"]["id"], "normal.design")
assert_sub(fast["delivery"]["id"], fast["design"]["id"], "fast.design")
verify = {
"normal_req": get(s, normal["req"]["id"])["status"]["displayName"],
"fast_req": get(s, fast["req"]["id"])["status"]["displayName"],
"normal_delivery_assignee": (
get(s, normal["delivery"]["id"]).get("assignedTo") or {}
).get("displayName"),
"fast_delivery_assignee": (get(s, fast["delivery"]["id"]).get("assignedTo") or {}).get(
"displayName"
),
"delivery_associated_ok": True,
"stage_tasks_as_sub_ok": True,
"fast_design_associated_ok": fast.get("design_associated_ok"),
}
out = {
"normal": normal,
"fast": fast,
"verify": verify,
"auth_elapsed_s": auth_s,
"wall_elapsed_s": build_s,
"created_at": datetime.now(TZ).isoformat(),
"mode": "live_create_fast_v5_fast_track_rules",
"opts": {
"create_includes_plan_start": True,
"skip_plan_end_on_create": True,
"tracked_transit_no_get": True,
"delivery_assignee_hefei_at_create": True,
"overlap_tag_with_create": True,
"fast_req_hours": f"{FAST_EST}+{FAST_ACT}",
"relation": "delivery ASSOCIATED→req; analysis/design TASK_SUB→delivery; design post ASSOCIATED→req",
"http": "requests.Session keep-alive",
},
}
Path("/tmp/yunxiao_pmapp_fast_v2_result.json").write_text(
json.dumps(out, ensure_ascii=False, indent=2)
)
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Compute YunxiaoPMapp stage calendar hours = workdays × 8.
Usage:
python3 workday_hours.py YYYY-MM-DD YYYY-MM-DD
python3 workday_hours.py YYYY-MM-DD YYYY-MM-DD --calendar /path/to/cn-workday-calendar.json
"""
from __future__ import annotations
import argparse
import json
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
def parse_day(s: str) -> date:
return datetime.strptime(s, "%Y-%m-%d").date()
def load_calendar(path: Path) -> dict:
data = json.loads(path.read_text(encoding="utf-8"))
years = data.get("years") or {}
holidays: set[str] = set()
makeup: set[str] = set()
for _y, block in years.items():
holidays.update(block.get("holidays") or [])
makeup.update(block.get("workdays_on_weekend") or [])
return {"holidays": holidays, "makeup": makeup, "years": set(years.keys())}
def ensure_years_covered(start: date, end: date, years: set[str]) -> None:
y = start.year
while y <= end.year:
if str(y) not in years:
raise SystemExit(
f"缺少 {y} 年日历:请在 assets/cn-workday-calendar.json 补全后再计算。"
"禁止仅去周末静默估算。"
)
y += 1
def is_workday(d: date, holidays: set[str], makeup: set[str]) -> bool:
key = d.isoformat()
if key in holidays:
return False
if key in makeup:
return True
return d.weekday() < 5 # Mon=0 .. Fri=4
def count_workdays(start: date, end: date, holidays: set[str], makeup: set[str]) -> int:
if end < start:
raise SystemExit("结束日早于开始日")
n = 0
cur = start
while cur <= end:
if is_workday(cur, holidays, makeup):
n += 1
cur += timedelta(days=1)
return n
def main() -> None:
parser = argparse.ArgumentParser(description="阶段日历工时 = 工作日×8")
parser.add_argument("start", help="计划开始 YYYY-MM-DD")
parser.add_argument("end", help="计划完成 YYYY-MM-DD")
parser.add_argument(
"--calendar",
default=str(Path(__file__).resolve().parent.parent / "assets" / "cn-workday-calendar.json"),
help="日历 JSON 路径",
)
args = parser.parse_args()
start = parse_day(args.start)
end = parse_day(args.end)
cal = load_calendar(Path(args.calendar))
ensure_years_covered(start, end, cal["years"])
days = count_workdays(start, end, cal["holidays"], cal["makeup"])
hours = days * 8
out = {
"start": start.isoformat(),
"end": end.isoformat(),
"workdays": days,
"stage_calendar_hours": hours,
"label": "阶段日历工时工作日×8非人力投入预估",
}
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,63 @@
# 标准路径:待处理 → 待开发交棒
项目常量见 [../assets/runtime-ids.json](../assets/runtime-ids.json)。查重只用任务编号。
## 步骤 0创建需求
| 云效动作 | 落地 |
|---|---|
| 新建产品类需求 | POST 建单;标题 `【新增】`/`【优化】` |
| 需求描述 | **先** AutoRDO 清洗再写入 `## 原始诉求AutoRDO`;本步不写 AutoPRD |
| 默认状态 | **待处理**;不建任务、不改负责人 |
| 可选推进 | 口令点选目标状态;未选则停在待处理 |
## 步骤 1受理 → 已确认
| 云效动作 | 落地 |
|---|---|
| 状态 | 待处理 → **已确认** |
| 任务 | **仍不建**交付/分析/设计 |
若用户只「创建」未受理:只提示「受理后请推进至已确认」,不自动跳。
## 步骤 2分析中
| 对象 | 动作 |
|---|---|
| 【交付】 | 无则新建;标题 `【交付】`+需求标题;**ASSOCIATED→需求**(勿用 PARENT负责人=创建人/产品;**计划开始仅空时写当日**;不写计划完成;描述=`等待设计任务完成后自动填入`;更新编号区块 |
| 【分析】 | 新建;**TASK_SUB→交付**`parent`+`parentIdentifier`+`createWorkitemRelationInfo=TASK_SUB`);交付「子项」须可见;计划开始=当日(仅空时);负责人默认可=创建人;更新编号区块 |
| 需求 | 状态=`分析中` |
## 步骤 3设计中
| 对象 | 动作 |
|---|---|
| 【交付】 | 已存在则按编号复用;没有则补建;**不改交付计划开始**;不写交付计划完成 |
| 【设计】 | 新建;**TASK_SUB→交付**(同分析);计划开始=当日(仅空时);更新编号区块 |
| 【分析】 | **计划完成**=状态更新日;推算阶段日历工时(见 work-hours |
| 需求 | 状态=`设计中` |
## 步骤 4设计完成
| 对象 | 动作 |
|---|---|
| 【设计】 | 计划完成=当日;推算工时;任务→完成态 |
| 需求 | AutoPRD 写入 `## 产品说明`;挂 ZIP+截图;状态=`设计完成` |
| 【交付】 | 不换负责人;不改计划开始;不写计划完成;描述改为 AutoPRD 正文;同样挂附件 |
顺序与失败门禁见 [description-split.md](description-split.md)。
## 步骤 5待开发交棒本 Skill 终点 · 标准路径)
| 对象 | 动作 |
|---|---|
| 需求 | 状态=`待开发` |
| 【交付】 | 按**任务编号**定位;**负责人→何斐**;不改计划开始;不写计划完成 |
| 【分析】/【设计】 | 不新建;不改已有计划开始;前序未收口按步骤 3/4 处理 |
执行 [handoff-and-rollback.md](handoff-and-rollback.md) §交棒门禁。
**不做:** 【开发】/【测试】、仓库、分支、提测。
快轨 / 编号直推见专文。创建迭代见 [sprint.md](sprint.md)。