• {line[2:]}
") + elif line.strip(): + parts.append(f"{line}
") + 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"{PLACEHOLDER}
", + 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}", + "分析阶段:故障处置台账、挂起归档与证据链。
", + WANG, + associated_req=rid, + parent_delivery=did, + ), + ) + f_des = pool.submit( + create, + session(), + task_payload( + f"【设计】{title}", + f"设计阶段:对齐原型 {PROTO}
", + 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"{PLACEHOLDER}
" + 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() diff --git a/.claude/skills/YunxiaoPM/model.md b/.claude/skills/YunxiaoPM/model.md new file mode 100644 index 0000000..a59f700 --- /dev/null +++ b/.claude/skills/YunxiaoPM/model.md @@ -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 编号集合 | 数【交付】开头标题 | +| 分析/设计幂等 | 仅已登记编号 | 按【分析】+需求标题 | + +按编号命中则复用;**计划开始已有值禁止改写**。 diff --git a/.claude/skills/YunxiaoPM/references/acceptance.md b/.claude/skills/YunxiaoPM/references/acceptance.md new file mode 100644 index 0000000..d677730 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/acceptance.md @@ -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`;默认不开浏览器 | diff --git a/.claude/skills/YunxiaoPM/references/commands.md b/.claude/skills/YunxiaoPM/references/commands.md new file mode 100644 index 0000000..571bd06 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/commands.md @@ -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」。 diff --git a/.claude/skills/YunxiaoPM/references/compact-select.md b/.claude/skills/YunxiaoPM/references/compact-select.md new file mode 100644 index 0000000..03756b8 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/compact-select.md @@ -0,0 +1,95 @@ +# 压缩点选(`1a2b3a4d`) + +记录需求 Plan **默认**用编号题 + 字母选项;用户可用一行压缩答复确认。 + +## Plan 展示模板 + +```text +请回复压缩点选,例:1a2b3a4d +(数字=题号,字母=选项;大小写不敏感;可写 1A2B3A4D) + +1. 类型 + A. 新增 + B. 优化 + +2. 项目(云效实时列表;建议项可标★) + A. 01_ONEOS(ONEOS)★ + B. 02_小羚羚APP(XLLAPP) + 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 时必须输出本模板(至少 1–4 题)。 +2. 收到压缩串先解析回显,再等「执行」。 +3. 标签/项目无法对应:各自动重拉**一次**并刷新对应题选项。 diff --git a/.claude/skills/YunxiaoPM/references/description-split.md b/.claude/skills/YunxiaoPM/references/description-split.md new file mode 100644 index 0000000..6683bb5 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/description-split.md @@ -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`;阶段任务树只由本 Skill(YunxiaoPMapp)创建。 diff --git a/.claude/skills/YunxiaoPM/references/fast-track.md b/.claude/skills/YunxiaoPM/references/fast-track.md new file mode 100644 index 0000000..4ae6a81 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/fast-track.md @@ -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) +编号直推:已有分析/设计编号 → 收口空计划完成 → 待开发+交付交棒(不新建冗余单) +``` diff --git a/.claude/skills/YunxiaoPM/references/handoff-and-rollback.md b/.claude/skills/YunxiaoPM/references/handoff-and-rollback.md new file mode 100644 index 0000000..44edcf6 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/handoff-and-rollback.md @@ -0,0 +1,20 @@ +# 交棒门禁与回退最小集 + +## 交棒门禁(待开发) + +| 情形 | 规则 | +|---|---| +| 【交付】描述已是 AutoPRD 正式说明(非占位) | 允许交棒;Plan 正常列交付任务编号 + 负责人→何斐 | +| 【交付】描述仍为 `等待设计任务完成后自动填入` | **仍允许交棒**,但 Plan **必须**勾选风险项「交付描述仍为占位,技术侧仅可凭需求 AutoRDO 稿理解」;回报**首行标红**该风险;**禁止**静默交棒假装材料齐全 | +| 口令同时带原型且要求设计完成 | 先走步骤 4(AutoPRD+附件)成功,再交棒,不再勾占位风险 | + +## 回退 / 变更(最小集 · 兼容计划开始不篡改) + +| 场景 | 规则 | +|---|---| +| 待开发 → 退回设计中 | 需求状态回退;【交付】负责人可改回创建人/产品;**交付计划开始不改**;若需重做设计 → **新开**设计任务编号(旧设计保持已收口,不改其计划开始);更新「工作项编号(系统)」中的设计编号为新号 | +| 设计完成后需求大变 | 不自动改状态;Plan 询问是否回退设计中并新开设计编号;更新 AutoPRD 段;AutoRDO 段保留并追加「变更纪要」 | +| 取消需求 | 关联任务标取消/废止;不删编号区块;不改已写计划开始 | +| 度量含义 | 交付计划开始保留 = 全周期仍从首次开工起算;回退重做会拉长日历工时——回报注明「含回退重做」 | + +凡写云效的回退仍须走 Plan 门禁。 diff --git a/.claude/skills/YunxiaoPM/references/handoff-contract.md b/.claude/skills/YunxiaoPM/references/handoff-contract.md new file mode 100644 index 0000000..ab9c712 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/handoff-contract.md @@ -0,0 +1,14 @@ +# 交接契约(开发 Skill 入口) + +YunxiaoPMapp 与开发 Skill **不要**互相 include 全文;仅认下列契约。 + +```text +PM 完成交棒 → 需求=待开发;【交付】任务编号=ONEOS-xx;负责人=何斐;ASSOCIATED 需求 +若交付描述仍为占位 → 回报已标红风险 +开发 Skill 入口 → 认「需求编号 + 交付任务编号」 +占位时只可信需求「原始诉求(AutoRDO)」并有权要求产品补设计完成 +``` + +共享常量(项目 ID、何斐 ID、节假日日历)可引用本 Skill 的 `assets/` 短路径,勿加载整份对方规则。 + +测试 Skill 另开;本契约不覆盖提测/缺陷。 diff --git a/.claude/skills/YunxiaoPM/references/live-api.md b/.claude/skills/YunxiaoPM/references/live-api.md new file mode 100644 index 0000000..f2bab69 --- /dev/null +++ b/.claude/skills/YunxiaoPM/references/live-api.md @@ -0,0 +1,242 @@ +# 云效实写 API(YunxiaoPMapp 已验证) + +`verified_at`: 2026-07-25 · **项目须门禁 PJ 点选**(见 [project-selection.md](project-selection.md));历史验证样本项目为 `01_ONEOS` / 原「统一运营管理平台」(`last_selected.spaceIdentifier` 见 `assets/runtime-ids.json`,禁止未点选即使用)。 + +本文件只记**已跑通**的写法;禁止再盲试 `updateStatus` / 错误 `updateFieldValue` POST。 + +## 认证 + +- Cookie:Chrome 域 `.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":"• {line[2:]}
") + elif line.strip(): + parts.append(f"{line}
") + 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"{PLACEHOLDER}
", + 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}", + "分析阶段:故障处置台账、挂起归档与证据链。
", + WANG, + associated_req=rid, + parent_delivery=did, + ), + ) + f_des = pool.submit( + create, + session(), + task_payload( + f"【设计】{title}", + f"设计阶段:对齐原型 {PROTO}
", + 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"{PLACEHOLDER}
" + 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() diff --git a/.claude/skills/YunxiaoPM/scripts/workday_hours.py b/.claude/skills/YunxiaoPM/scripts/workday_hours.py new file mode 100755 index 0000000..289b867 --- /dev/null +++ b/.claude/skills/YunxiaoPM/scripts/workday_hours.py @@ -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() diff --git a/.claude/skills/YunxiaoPM/stage-flow.md b/.claude/skills/YunxiaoPM/stage-flow.md new file mode 100644 index 0000000..3c97b2f --- /dev/null +++ b/.claude/skills/YunxiaoPM/stage-flow.md @@ -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)。 diff --git a/.claude/skills/oneos-autoprd/SKILL.md b/.claude/skills/oneos-autoprd/SKILL.md index a10d84f..6668282 100644 --- a/.claude/skills/oneos-autoprd/SKILL.md +++ b/.claude/skills/oneos-autoprd/SKILL.md @@ -1,146 +1,108 @@ --- name: oneos-autoprd description: >- - Generates and keeps in sync OneOS AutoPRD (PM-facing requirements) plus Axhub - Make annotation directory Markdown/PRD; on 需求定稿/定稿/确认定稿/本次定稿 appends - functional release changelog below the PRD since last baseline. When a Yunxiao - requirement advances to 分析中/设计中/待开发, creates a same-titled linked task - (tag+create-time follow the requirement; 分析中/设计中 assignee=creator, 待开发 - assignee=何斐). Use eagerly for prototypes, AutoPRD, or Yunxiao requirement - description/更新内容/stage advance—do not skip annotation sync, 定稿 changelog, - or stage-task creation. + OneOS AutoPRD: generates PM-facing requirements (总览/目标/边界/角色/用户故事 + 起点→运作→闭环/故事点/正逆向/流程图/关键逻辑/状态/风险/交付) and syncs Axhub Make + annotation PRD. On 需求定稿/本轮定稿 appends functional changelog with auto + V主.副.子 (user 主/副/子 override wins). With YunxiaoPMapp design-complete, + writes Markdown into 【交付】 by task number (not at create; placeholder until then). + Use for AutoPRD, prototypes, 产品说明, 本轮定稿. Never create same-titled stage + tasks; never load yunxiao-requirement-lifecycle—cloud combo is YunxiaoPMapp only. --- -# OneOS AutoPRD +# AutoPRD(oneos-autoprd) 为 OneOS 业务模块生成**产品经理可读、可评审、可排期**的需求说明,并**自动挂到 Axhub Make 标注工具 → 原型目录**。 -另支持:**需求定稿**时汇总功能/逻辑变更记录;与云效组合时提供「需求说明 + 更新内容」;需求进入**分析中 / 设计中 / 待开发**时**自动创建与需求同名的关联任务**(规则见下,写在本 Skill,不改云效 Skill)。 +另支持:**本轮定稿 / 需求定稿**时汇总功能变更并**自动递增 PRD 版本号**;与 **`$YunxiaoPMapp`** 组合时,在**设计完成**(或显式同步交付)把 Markdown 写入【交付】任务描述。 -颗粒度对齐「保险采购」全模块 PRD:讲清做什么、谁用、故事点、正逆向、流程图与关键业务逻辑;**不写**表结构、接口、字段代码名、文件路径、实现清单。 +**禁止加载** `yunxiao-requirement-lifecycle`。云效状态机与【交付】/【分析】/【设计】树**只**由 YunxiaoPMapp 负责;本 Skill **不**建同名无前缀阶段任务。 + +颗粒度:讲清做什么、谁用、故事点、正逆向、流程图与关键业务逻辑;**不写**表结构、接口、字段代码名、文件路径、实现清单。 ## 何时使用(含自动触发) -**主动调用时** +**主动调用** -- AutoPRD、OneOS 需求说明、整模块 PRD、故事点 + 流程图 +- AutoPRD、OneOS 需求说明、整模块 PRD、故事点 + 流程图、产品说明 -**改原型时必须自动跟进(全局规则)** +**改原型时必须自动跟进** -- 正在修改 `src/prototypes/